Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 39s
T03 writes a rulings overlay and a readable conflict list. T04 starts relevance: five superseded, five live conflicts, estate and activity-core ADRs marked publish-after-prefix. The Coulomb estate map is now a published architecture document.
303 lines
11 KiB
Python
303 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the ADR review ledger from source-inventory.json and source files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import Any
|
|
|
|
from render import split_frontmatter
|
|
from source_inventory import _repo_paths, _read_json
|
|
|
|
|
|
BARE_ADR = re.compile(r"(?i)\bADR-0*(\d+)\b")
|
|
REQUIRED = ("id", "title", "status", "owner")
|
|
FIRST_PUBLISH = {
|
|
("policy-nexus", "docs/adr/ADR-0001-addressing-and-permanence.md"),
|
|
*(
|
|
(
|
|
"railiance-master",
|
|
f"docs/adr/ADR-000{n}-{slug}.md",
|
|
)
|
|
for n, slug in (
|
|
(1, "repository-prefix-architecture"),
|
|
(2, "rail-kubernetes-wave-1-boundary"),
|
|
(3, "rapp-first-wave-selection"),
|
|
(4, "first-wave-reef-rollout"),
|
|
(5, "derived-rail-composition"),
|
|
(6, "reef-production-admission"),
|
|
(7, "rapp-declaration-contract"),
|
|
(8, "private-by-default-exposure"),
|
|
)
|
|
),
|
|
}
|
|
SPECIALS = {
|
|
("policy-nexus", "docs/adr/ADR-0001-addressing-and-permanence.md"): (
|
|
"First publish set. This repo's addressing contract."
|
|
),
|
|
("the-custodian", "canon/architecture/adr-008-multi-tenancy-model.md"): (
|
|
"Relocated to net-kingdom Tenancy Posture. Duplicate-claim candidate (kind 6)."
|
|
),
|
|
}
|
|
for n, slug in (
|
|
(1, "repository-prefix-architecture"),
|
|
(2, "rail-kubernetes-wave-1-boundary"),
|
|
(3, "rapp-first-wave-selection"),
|
|
(4, "first-wave-reef-rollout"),
|
|
(5, "derived-rail-composition"),
|
|
(6, "reef-production-admission"),
|
|
(7, "rapp-declaration-contract"),
|
|
(8, "private-by-default-exposure"),
|
|
):
|
|
SPECIALS[("railiance-master", f"docs/adr/ADR-000{n}-{slug}.md")] = (
|
|
"First publish set. Publication-ready front-matter."
|
|
)
|
|
|
|
|
|
def _bare_adr(source_path: str, meta_id: str) -> str:
|
|
for text in (meta_id, Path(source_path).name):
|
|
match = BARE_ADR.search(text)
|
|
if match:
|
|
return f"ADR-{int(match.group(1)):04d}"
|
|
return ""
|
|
|
|
|
|
def _missing(meta: dict[str, str]) -> list[str]:
|
|
missing = [field for field in REQUIRED if not meta.get(field)]
|
|
if not (meta.get("revision") or meta.get("version")):
|
|
missing.append("revision|version")
|
|
if not ((meta.get("last_reviewed") or meta.get("updated")) and meta.get("review_interval")):
|
|
missing.append("review")
|
|
return missing
|
|
|
|
|
|
def _frontmatter(path: Path) -> dict[str, str]:
|
|
if not path.is_file():
|
|
return {}
|
|
meta, _body = split_frontmatter(path.read_text(encoding="utf-8", errors="replace"))
|
|
return {key: value for key, value in meta.items() if value}
|
|
|
|
|
|
def _load_rulings(path: Path | None) -> dict[tuple[str, str], dict[str, Any]]:
|
|
if path is None or not path.exists():
|
|
return {}
|
|
payload = _read_json(path)
|
|
rulings: dict[tuple[str, str], dict[str, Any]] = {}
|
|
for ruling in payload.get("rulings", []):
|
|
key = (ruling["source_repo"], ruling["source_path"])
|
|
if key in rulings:
|
|
raise ValueError(f"duplicate ruling: {key[0]}/{key[1]}")
|
|
rulings[key] = ruling
|
|
return rulings
|
|
|
|
|
|
def build_ledger(
|
|
inventory_path: Path,
|
|
config_path: Path,
|
|
*,
|
|
policy_root: Path,
|
|
source_root: Path,
|
|
rulings_path: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
inventory = _read_json(inventory_path)
|
|
config = _read_json(config_path)
|
|
rulings = _load_rulings(rulings_path)
|
|
paths = _repo_paths(config, policy_root=policy_root, source_root=source_root)
|
|
rows: list[dict[str, Any]] = []
|
|
for source in inventory.get("sources", []):
|
|
key = (source["source_repo"], source["source_path"])
|
|
repo = paths.get(source["source_repo"])
|
|
file_path = (repo / source["source_path"]) if repo is not None else None
|
|
present = bool(file_path and file_path.is_file())
|
|
meta = _frontmatter(file_path) if present and file_path is not None else {}
|
|
rows.append(
|
|
{
|
|
"source_repo": source["source_repo"],
|
|
"source_path": source["source_path"],
|
|
"inventory_disposition": source.get("disposition", ""),
|
|
"inventory_reason": source.get("reason", ""),
|
|
"file_present": present,
|
|
"frontmatter": {
|
|
field: meta.get(field, "")
|
|
for field in (
|
|
"id",
|
|
"title",
|
|
"status",
|
|
"owner",
|
|
"revision",
|
|
"version",
|
|
"last_reviewed",
|
|
"updated",
|
|
"review_interval",
|
|
)
|
|
},
|
|
"missing_fields": _missing(meta) if present else ["file-missing"],
|
|
"bare_adr": _bare_adr(source["source_path"], meta.get("id", "")),
|
|
"id_collisions": [],
|
|
"bare_adr_collisions": [],
|
|
"notes": SPECIALS.get(key, ""),
|
|
"proposed_disposition": "publish" if key in FIRST_PUBLISH else "unreviewed",
|
|
"conflict_kinds": [],
|
|
"successor": "",
|
|
"review_notes": "",
|
|
}
|
|
)
|
|
|
|
by_id: dict[str, list[str]] = {}
|
|
by_bare: dict[str, list[str]] = {}
|
|
for row in rows:
|
|
label = f"{row['source_repo']}/{row['source_path']}"
|
|
meta_id = row["frontmatter"]["id"]
|
|
if meta_id:
|
|
by_id.setdefault(meta_id, []).append(label)
|
|
if row["bare_adr"]:
|
|
by_bare.setdefault(row["bare_adr"], []).append(label)
|
|
for row in rows:
|
|
label = f"{row['source_repo']}/{row['source_path']}"
|
|
meta_id = row["frontmatter"]["id"]
|
|
if meta_id:
|
|
row["id_collisions"] = [other for other in by_id.get(meta_id, []) if other != label]
|
|
if row["bare_adr"]:
|
|
row["bare_adr_collisions"] = [
|
|
other for other in by_bare.get(row["bare_adr"], []) if other != label
|
|
]
|
|
key = (row["source_repo"], row["source_path"])
|
|
ruling = rulings.get(key)
|
|
if ruling:
|
|
for field in (
|
|
"proposed_disposition",
|
|
"conflict_kinds",
|
|
"successor",
|
|
"review_notes",
|
|
):
|
|
if field in ruling:
|
|
row[field] = ruling[field]
|
|
if row["id_collisions"] and 1 not in row["conflict_kinds"]:
|
|
row["conflict_kinds"] = [*row["conflict_kinds"], 1]
|
|
if not row["review_notes"]:
|
|
row["review_notes"] = (
|
|
f"Front-matter id {row['frontmatter']['id']!r} is shared; "
|
|
"pick a repo-prefixed publication id before publish. "
|
|
"Who rules: owning repo."
|
|
)
|
|
|
|
return {
|
|
"schema_version": 1,
|
|
"protocol": "docs/adr-review/protocol.md",
|
|
"source_inventory": "source-inventory.json",
|
|
"rulings": str(rulings_path) if rulings_path else "",
|
|
"rows": rows,
|
|
}
|
|
|
|
|
|
def _summary(ledger: dict[str, Any]) -> str:
|
|
rows = ledger["rows"]
|
|
dispositions = {}
|
|
inventory = {}
|
|
for row in rows:
|
|
dispositions[row["proposed_disposition"]] = (
|
|
dispositions.get(row["proposed_disposition"], 0) + 1
|
|
)
|
|
inventory[row["inventory_disposition"]] = inventory.get(row["inventory_disposition"], 0) + 1
|
|
missing_files = [row for row in rows if not row["file_present"]]
|
|
id_hits = [row for row in rows if row["id_collisions"]]
|
|
bare_hits = [row for row in rows if row["bare_adr_collisions"]]
|
|
lines = [
|
|
"# ADR review ledger summary",
|
|
"",
|
|
f"Rows: {len(rows)}",
|
|
"",
|
|
"## Inventory dispositions",
|
|
"",
|
|
]
|
|
for name, count in sorted(inventory.items()):
|
|
lines.append(f"- `{name}`: {count}")
|
|
lines.extend(["", "## Proposed dispositions", ""])
|
|
for name, count in sorted(dispositions.items()):
|
|
lines.append(f"- `{name}`: {count}")
|
|
lines.extend(["", "## Front-matter `id` collisions", ""])
|
|
if not id_hits:
|
|
lines.append("None.")
|
|
else:
|
|
seen = set()
|
|
for row in id_hits:
|
|
key = row["frontmatter"]["id"]
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
others = ", ".join([f"{row['source_repo']}/{row['source_path']}", *row["id_collisions"]])
|
|
lines.append(f"- `{key}`: {others}")
|
|
lines.extend(["", "## Bare ADR-NNNN collisions", ""])
|
|
if not bare_hits:
|
|
lines.append("None.")
|
|
else:
|
|
seen = set()
|
|
for row in bare_hits:
|
|
key = row["bare_adr"]
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
others = ", ".join(
|
|
[f"{row['source_repo']}/{row['source_path']}", *row["bare_adr_collisions"]]
|
|
)
|
|
lines.append(f"- `{key}`: {others}")
|
|
conflicts = [row for row in rows if row["proposed_disposition"] == "conflict"]
|
|
lines.extend(["", "## Conflict rows", ""])
|
|
if not conflicts:
|
|
lines.append("None.")
|
|
else:
|
|
for row in conflicts:
|
|
kinds = ",".join(str(kind) for kind in row["conflict_kinds"]) or "-"
|
|
lines.append(
|
|
f"- `{row['source_repo']}/{row['source_path']}` "
|
|
f"(kinds {kinds}): {row['review_notes']}"
|
|
)
|
|
lines.extend(["", "## Missing files", ""])
|
|
if not missing_files:
|
|
lines.append("None.")
|
|
else:
|
|
for row in missing_files:
|
|
lines.append(f"- `{row['source_repo']}/{row['source_path']}`")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--config", type=Path, default=Path("source-inventory.config.json"))
|
|
parser.add_argument("--inventory", type=Path, default=Path("source-inventory.json"))
|
|
parser.add_argument("--output", type=Path, default=Path("docs/adr-review/ledger.json"))
|
|
parser.add_argument("--summary", type=Path, default=Path("docs/adr-review/SUMMARY.md"))
|
|
parser.add_argument("--rulings", type=Path, default=Path("docs/adr-review/rulings.json"))
|
|
parser.add_argument("--policy-root", type=Path)
|
|
parser.add_argument("--source-root", type=Path)
|
|
args = parser.parse_args(argv)
|
|
config_path = args.config.resolve()
|
|
policy_root = (args.policy_root or config_path.parent).resolve()
|
|
source_root = (args.source_root or policy_root.parent).resolve()
|
|
try:
|
|
ledger = build_ledger(
|
|
args.inventory.resolve(),
|
|
config_path,
|
|
policy_root=policy_root,
|
|
source_root=source_root,
|
|
rulings_path=args.rulings.resolve() if args.rulings else None,
|
|
)
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(
|
|
json.dumps(ledger, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
)
|
|
args.summary.write_text(_summary(ledger), encoding="utf-8")
|
|
print(f"{args.output}: {len(ledger['rows'])} row(s)")
|
|
return 0
|
|
except (OSError, ValueError) as exc:
|
|
print(f"adr review ledger failed: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|