Files
kevinveenbirkenbachandClaude Opus 4.7 286408fa91 refactor(roles): role-mapping SPOT under utils/roles + cli inspector + generalist lint
Move utils/roles/applications/files.py to utils/roles/mapping.py and add a
data-driven role-type detector in utils/roles/type.py. Each entry in
ROLE_FILES carries per-role-type allowed/mandatory flags plus marker entries
that drive get_role_types(); a role MAY belong to several types (e.g. an
application that also ships a system service).

Add cli/meta/roles/mapping with files / types / markers subcommands so
contributors can query the schema in text or JSON. Move
cli/meta/applications/ to cli/meta/roles/applications/ and adjust PROJECT_ROOT
parents indices accordingly. Move docs/contributing/design/services/ to
docs/contributing/design/role/services/ and document the mapping in
docs/contributing/design/role/mapping.md per documentation.md.

Replace the narrow test_app_only_files with a generalist
tests/integration/roles/meta/test_mapping.py that walks every role and every
ROLE_FILES entry and reports forbidden files, missing mandatory files,
forbidden entries, and missing mandatory entries in one structured fail
report. meta/info.yml and meta/users.yml are now allowed across every role
type. Create the seven mandatory files surfaced by the generalist test
(README.md for desk-keepassxc, desk-obs, web-svc-cdn following readme_md.md,
plus tasks/main.yml stubs for svc-db-memcached, svc-db-redis, sys-svc-proxy,
web-app-socialhome).

Audit and rewrite touched markdown for documentation.md compliance: replace
em-dashes with fluent sentences, drop SPOT self-labels, fix all broken
relative links introduced by the services/ move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:21:22 +02:00

65 lines
2.0 KiB
Python

from __future__ import annotations
from .resolver import CombinedResolver
from .role_introspection import require_role_exists
def print_tree(start_role: str) -> None:
"""
Print an ASCII tree showing [run_after] and [dependencies] for each role.
Avoid infinite loops by:
- marking already-expanded nodes
- detecting cycles on the current stack
"""
require_role_exists(start_role)
resolver = CombinedResolver()
expanded: set[str] = set()
stack: list[str] = []
def show_node(node: str, prefix: str, is_last: bool) -> None:
connector = "└── " if is_last else "├── "
if prefix == "":
print(node)
else:
print(prefix + connector + node)
if node in stack:
print(prefix + (" " if is_last else "│ ") + "↩︎ (cycle)")
return
if node in expanded:
print(prefix + (" " if is_last else "│ ") + "… (already shown)")
return
expanded.add(node)
stack.append(node)
edges = resolver.edges_for(node)
groups: list[tuple[str, list[str]]] = []
if edges.run_after:
groups.append(("run_after", edges.run_after))
if edges.dependencies:
groups.append(("dependencies", edges.dependencies))
if edges.services:
groups.append(("services", edges.services))
base_indent = prefix + (" " if is_last else "│ ")
for gi, (gname, children) in enumerate(groups):
g_last = gi == (len(groups) - 1)
g_connector = "└── " if g_last else "├── "
print(base_indent + g_connector + f"[{gname}]")
child_prefix = base_indent + (" " if g_last else "│ ")
for ci, child in enumerate(children):
c_last = ci == (len(children) - 1)
show_node(child, child_prefix, c_last)
stack.pop()
show_node(start_role, prefix="", is_last=True)