mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
3529 invalid comments in 528 files, grandfathered in while the linter only saw the working-tree diff. Removal ran through the linter's own collectors, so detection and deletion cannot drift: a comment whose opener line is blank before it and whose closer line is blank after it loses all its lines, anything else loses only the comment region, and a line left holding a bare delimiter goes with it. Five files keep their comments behind a nocheck marker instead. Four are upstream configs whose inline docs are upstream's own (mautrix, systemd journald, nextcloud-oidc-login, MediaWiki). In littlejs the commented-out entries ARE the deactivation mechanism for an example tile, so deleting them would delete configuration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
352 lines
11 KiB
Python
352 lines
11 KiB
Python
"""CLI inspector for the role-file mapping SPOT.
|
|
|
|
Renders :data:`utils.roles.mapping.ROLE_FILES` in a human-readable form
|
|
so contributors can see at a glance which role files apply to which
|
|
role types, whether each is mandatory, allowed, or a type marker, and
|
|
which dotted-path entries are required inside each file.
|
|
|
|
Subcommands
|
|
-----------
|
|
|
|
``files`` : list every role file with its per-type policy.
|
|
``types`` : list every role type with the files it MUST / MAY ship.
|
|
``markers`` : list the marker entries that drive
|
|
:func:`utils.roles.type.get_role_types` (which dotted
|
|
path in which file decides each type).
|
|
|
|
Filtering
|
|
---------
|
|
|
|
``--type <ROLE_TYPE>`` narrows every subcommand to a single role type.
|
|
The default lists every type.
|
|
|
|
Output
|
|
------
|
|
|
|
``--format text`` (default) renders coloured / icon-prefixed plain
|
|
text suitable for terminals. ``--format json`` emits a stable
|
|
machine-readable structure for piping into ``jq`` or similar.
|
|
|
|
Examples::
|
|
|
|
python -m cli.meta.roles.mapping files
|
|
python -m cli.meta.roles.mapping files --type application
|
|
python -m cli.meta.roles.mapping types --type system-service
|
|
python -m cli.meta.roles.mapping markers --format json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.roles.mapping import (
|
|
ROLE_FILES,
|
|
ROLE_TYPE_ALL,
|
|
ROLE_TYPES,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable, Iterator
|
|
|
|
_FORMATS = ("text", "json")
|
|
|
|
|
|
# === Resolution helpers =====================================================
|
|
|
|
|
|
def _entries_for_type(file_entry: dict, role_type: str) -> dict | None:
|
|
"""Return the type-scoping entry that applies to *role_type* in
|
|
*file_entry*. Concrete-type entries override the wildcard fallback;
|
|
when neither matches the file is implicitly forbidden.
|
|
"""
|
|
types = file_entry.get("types") or []
|
|
wildcard: dict | None = None
|
|
for entry in types:
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
t = entry.get("type")
|
|
if t == role_type:
|
|
return entry
|
|
if t == ROLE_TYPE_ALL and wildcard is None:
|
|
wildcard = entry
|
|
return wildcard
|
|
|
|
|
|
def _is_allowed(type_entry: dict | None) -> bool:
|
|
if type_entry is None:
|
|
return False
|
|
return bool(type_entry.get("allowed", True))
|
|
|
|
|
|
def _is_mandatory(type_entry: dict | None) -> bool:
|
|
if type_entry is None:
|
|
return False
|
|
return bool(type_entry.get("mandatory", False))
|
|
|
|
|
|
def _entries(type_entry: dict | None) -> list[dict]:
|
|
if type_entry is None:
|
|
return []
|
|
raw = type_entry.get("entries") or []
|
|
return [e for e in raw if isinstance(e, dict)]
|
|
|
|
|
|
def _resolved_types(role_type_filter: str | None) -> tuple[str, ...]:
|
|
if role_type_filter is None:
|
|
return ROLE_TYPES
|
|
if role_type_filter not in ROLE_TYPES:
|
|
raise SystemExit(
|
|
f"--type: unknown role type {role_type_filter!r}; "
|
|
f"valid types: {', '.join(ROLE_TYPES)}"
|
|
)
|
|
return (role_type_filter,)
|
|
|
|
|
|
# === Render: text ===========================================================
|
|
|
|
|
|
def _policy_label(allowed: bool, mandatory: bool) -> str:
|
|
if not allowed:
|
|
return "FORBIDDEN"
|
|
if mandatory:
|
|
return "MUST"
|
|
return "MAY"
|
|
|
|
|
|
def _entry_label(allowed: bool, mandatory: bool, marker: bool) -> str:
|
|
parts = [_policy_label(allowed, mandatory)]
|
|
if marker:
|
|
parts.append("marker")
|
|
return ", ".join(parts)
|
|
|
|
|
|
def _render_files_text(role_type_filter: str | None) -> Iterator[str]:
|
|
types = _resolved_types(role_type_filter)
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
yield f"📄 {file_path}"
|
|
description = str(file_entry.get("description", "")).strip()
|
|
if description:
|
|
yield f" {description}"
|
|
for role_type in types:
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
allowed = _is_allowed(type_entry)
|
|
mandatory = _is_mandatory(type_entry)
|
|
label = _policy_label(allowed, mandatory)
|
|
yield f" • {role_type:<16} {label}"
|
|
for sub in _entries(type_entry):
|
|
path = sub.get("path", "?")
|
|
sub_label = _entry_label(
|
|
bool(sub.get("allowed", True)),
|
|
bool(sub.get("mandatory", False)),
|
|
bool(sub.get("marker", False)),
|
|
)
|
|
yield f" └─ {path} ({sub_label})"
|
|
yield ""
|
|
|
|
|
|
def _render_types_text(role_type_filter: str | None) -> Iterator[str]:
|
|
types = _resolved_types(role_type_filter)
|
|
for role_type in types:
|
|
yield f"🧩 {role_type}"
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
allowed = _is_allowed(type_entry)
|
|
mandatory = _is_mandatory(type_entry)
|
|
if not allowed:
|
|
continue
|
|
label = _policy_label(allowed, mandatory)
|
|
yield f" • {file_path:<32} {label}"
|
|
for sub in _entries(type_entry):
|
|
path = sub.get("path", "?")
|
|
sub_label = _entry_label(
|
|
bool(sub.get("allowed", True)),
|
|
bool(sub.get("mandatory", False)),
|
|
bool(sub.get("marker", False)),
|
|
)
|
|
yield f" └─ {path} ({sub_label})"
|
|
yield ""
|
|
|
|
|
|
def _render_markers_text(role_type_filter: str | None) -> Iterator[str]:
|
|
types = _resolved_types(role_type_filter)
|
|
yield "🎯 Type markers (file.path that decides each role type)"
|
|
yield ""
|
|
any_marker = False
|
|
for role_type in types:
|
|
listed = False
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
for sub in _entries(type_entry):
|
|
if not bool(sub.get("marker", False)):
|
|
continue
|
|
if not listed:
|
|
yield f" {role_type}"
|
|
listed = True
|
|
any_marker = True
|
|
yield f" └─ {file_path} → {sub.get('path', '?')}"
|
|
if listed:
|
|
yield ""
|
|
if not any_marker:
|
|
yield " (no markers declared for the selected type)"
|
|
|
|
|
|
# === Render: json ===========================================================
|
|
|
|
|
|
def _files_json(role_type_filter: str | None) -> dict:
|
|
types = _resolved_types(role_type_filter)
|
|
out: dict[str, dict] = {}
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
per_type: dict[str, dict] = {}
|
|
for role_type in types:
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
per_type[role_type] = {
|
|
"allowed": _is_allowed(type_entry),
|
|
"mandatory": _is_mandatory(type_entry),
|
|
"entries": [
|
|
{
|
|
"path": str(sub.get("path", "")),
|
|
"allowed": bool(sub.get("allowed", True)),
|
|
"mandatory": bool(sub.get("mandatory", False)),
|
|
"marker": bool(sub.get("marker", False)),
|
|
}
|
|
for sub in _entries(type_entry)
|
|
],
|
|
}
|
|
out[file_path] = {
|
|
"description": str(file_entry.get("description", "")),
|
|
"types": per_type,
|
|
}
|
|
return out
|
|
|
|
|
|
def _types_json(role_type_filter: str | None) -> dict:
|
|
types = _resolved_types(role_type_filter)
|
|
out: dict[str, dict] = {}
|
|
for role_type in types:
|
|
files: dict[str, dict] = {}
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
allowed = _is_allowed(type_entry)
|
|
if not allowed:
|
|
continue
|
|
files[file_path] = {
|
|
"allowed": allowed,
|
|
"mandatory": _is_mandatory(type_entry),
|
|
"entries": [
|
|
{
|
|
"path": str(sub.get("path", "")),
|
|
"allowed": bool(sub.get("allowed", True)),
|
|
"mandatory": bool(sub.get("mandatory", False)),
|
|
"marker": bool(sub.get("marker", False)),
|
|
}
|
|
for sub in _entries(type_entry)
|
|
],
|
|
}
|
|
out[role_type] = files
|
|
return out
|
|
|
|
|
|
def _markers_json(role_type_filter: str | None) -> dict:
|
|
types = _resolved_types(role_type_filter)
|
|
out: dict[str, list[dict]] = {}
|
|
for role_type in types:
|
|
markers: list[dict] = []
|
|
for file_path, file_entry in ROLE_FILES.items():
|
|
type_entry = _entries_for_type(file_entry, role_type)
|
|
for sub in _entries(type_entry):
|
|
if not bool(sub.get("marker", False)):
|
|
continue
|
|
markers.append(
|
|
{
|
|
"file": file_path,
|
|
"path": str(sub.get("path", "")),
|
|
}
|
|
)
|
|
out[role_type] = markers
|
|
return out
|
|
|
|
|
|
# === Argparse plumbing ======================================================
|
|
|
|
|
|
def _print_lines(lines: Iterable[str]) -> None:
|
|
for line in lines:
|
|
print(line)
|
|
|
|
|
|
def _add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"--type",
|
|
dest="role_type",
|
|
default=None,
|
|
choices=ROLE_TYPES,
|
|
help="Limit output to a single role type (default: every type).",
|
|
)
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=_FORMATS,
|
|
default="text",
|
|
help="Output format (default: text).",
|
|
)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="python -m cli.meta.roles.mapping",
|
|
description=("Inspect the role-file mapping SPOT (utils/roles/mapping.py)."),
|
|
)
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
files_parser = sub.add_parser(
|
|
"files",
|
|
help="List every role file with its per-type policy.",
|
|
)
|
|
_add_common_args(files_parser)
|
|
|
|
types_parser = sub.add_parser(
|
|
"types",
|
|
help="List every role type with the files it MAY / MUST ship.",
|
|
)
|
|
_add_common_args(types_parser)
|
|
|
|
markers_parser = sub.add_parser(
|
|
"markers",
|
|
help="List the type markers that decide each role type.",
|
|
)
|
|
_add_common_args(markers_parser)
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.format == "json":
|
|
if args.command == "files":
|
|
payload = _files_json(args.role_type)
|
|
elif args.command == "types":
|
|
payload = _types_json(args.role_type)
|
|
elif args.command == "markers":
|
|
payload = _markers_json(args.role_type)
|
|
else: # pragma: no cover - argparse already enforces required
|
|
parser.error(f"unknown command {args.command!r}")
|
|
return 2
|
|
json.dump(payload, sys.stdout, indent=2, sort_keys=True)
|
|
sys.stdout.write("\n")
|
|
return 0
|
|
|
|
if args.command == "files":
|
|
_print_lines(_render_files_text(args.role_type))
|
|
elif args.command == "types":
|
|
_print_lines(_render_types_text(args.role_type))
|
|
elif args.command == "markers":
|
|
_print_lines(_render_markers_text(args.role_type))
|
|
else: # pragma: no cover
|
|
parser.error(f"unknown command {args.command!r}")
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
raise SystemExit(main())
|