-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgen_architecture.py
More file actions
140 lines (113 loc) · 5.32 KB
/
Copy pathgen_architecture.py
File metadata and controls
140 lines (113 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python3
"""gen_architecture.py - generate the tool -> codes -> floor inventory block of ARCHITECTURE.md.
METHOD-Kit family G10 / METHOD v0.42. The architecture map's *inventory*
(tool -> codes -> floor) is GENERATED from the modules' CODES/FLOOR registries,
never hand-transcribed: the living source is the registry, the published
block is a regenerated projection - the same single-source rule as
extract_roadmap -> diagrams. Only the surrounding prose (the layered model, the
why, the gate topology) is authored by hand.
Discovery is REUSED from check_floor_coverage (its discover_tools): the inventory
and the coverage gate must see the exact same set of tools, or the published map
would drift from what is actually gated. One discovery, two consumers.
The block lives between markers in ARCHITECTURE.md::
<!-- ARCH:INVENTORY:BEGIN (generated by scripts/gen_architecture.py - do not edit) -->
...generated table...
<!-- ARCH:INVENTORY:END -->
Modes::
python scripts/gen_architecture.py # print the block to stdout
python scripts/gen_architecture.py --write <file> # fill the block in <file> in place
python scripts/gen_architecture.py --check <file> # exit 1 if <file>'s block drifted
--check is the live drift guard (committed block == freshly generated); it runs
in the quality gate, beside doc_drift. Tools are sorted by path and codes are
sorted, so regeneration is diff-stable; no timestamp lives inside the block (it
would flash a false drift on every run).
Exit 0 = success (printed / written / block matches);
exit 1 = markers missing, or (under --check) the block drifted.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
# Reuse the floor-coverage discovery so the inventory and the coverage gate never
# diverge on which tools exist. The two scripts ship side by side under scripts/.
_HERE = Path(__file__).resolve().parent
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
import check_floor_coverage as cfc # noqa: E402
BEGIN = "<!-- ARCH:INVENTORY:BEGIN (generated by scripts/gen_architecture.py - do not edit) -->"
END = "<!-- ARCH:INVENTORY:END -->"
def render_block(root: Path) -> str:
"""Render the full marked inventory block for the repo rooted at `root`."""
tools = cfc.discover_tools(root) # already sorted by path
body: list[str] = []
declared_codes = 0
for tool in tools:
name = tool.path.relative_to(root).as_posix()
declared_codes += len(tool.codes)
codes = ", ".join(f"`{c}`" for c in sorted(tool.codes)) if tool.codes else "(no codes)"
floor = f"`{tool.floor}`" if tool.floor else "(none)"
body.append(f"| `{name}` | {codes} | {floor} |")
summary = (
f"_{len(tools)} tool(s), {declared_codes} declared code(s) "
f"- regenerated by `scripts/gen_architecture.py` from the `CODES`/`FLOOR` registries._"
)
lines = [BEGIN, "", "| Tool | Codes | Floor |", "|---|---|---|", *body, "", summary, "", END]
return "\n".join(lines)
def extract_block(text: str) -> str | None:
"""The marked block (markers inclusive) found in `text`, or None if absent."""
if BEGIN not in text or END not in text:
return None
start = text.index(BEGIN)
end = text.index(END) + len(END)
return text[start:end]
def write_block(path: Path, root: Path) -> int:
text = path.read_text(encoding="utf-8")
if BEGIN not in text or END not in text:
print(f"ERROR: inventory markers not found in {path}", file=sys.stderr)
return 1
start = text.index(BEGIN)
end = text.index(END) + len(END)
new_text = text[:start] + render_block(root) + text[end:]
path.write_text(new_text, encoding="utf-8")
print(f"OK: wrote the inventory block into {path}")
return 0
def check_block(path: Path, root: Path) -> int:
have = extract_block(path.read_text(encoding="utf-8"))
want = render_block(root)
if have is None:
print(f"DRIFT: inventory markers missing in {path}", file=sys.stderr)
return 1
if have.strip() != want.strip():
print(
f"DRIFT: the inventory block in {path} no longer matches the registries.\n"
f" Run: python scripts/gen_architecture.py --write {path}",
file=sys.stderr,
)
return 1
print(f"OK: the inventory block in {path} matches the CODES/FLOOR registries")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Generate / verify the tool->codes->floor inventory block of ARCHITECTURE.md."
)
parser.add_argument(
"--root", default=".", help="Repo root to scan for registries (default: cwd)."
)
group = parser.add_mutually_exclusive_group()
group.add_argument("--write", metavar="FILE", help="Fill the marked block in FILE in place.")
group.add_argument(
"--check", metavar="FILE", help="Exit 1 if FILE's block drifted from the registries."
)
args = parser.parse_args(argv)
root = Path(args.root).resolve()
if args.write:
return write_block(Path(args.write), root)
if args.check:
return check_block(Path(args.check), root)
print(render_block(root))
return 0
if __name__ == "__main__":
sys.exit(main())
# --- floor-coverage registry ---
CODES = frozenset()
FLOOR = "test_gen_architecture.py"