#!/usr/bin/env python3
"""Write index.json: what this release of erp-method holds. The DODIL CLI and the blog read it.
Deterministic (no timestamps), so `git diff` shows only real changes. Run after any edit:
python3 scripts/build_index.py
"""
import json
import pathlib
import sys
import yaml
ROOT = pathlib.Path(__file__).resolve().parent.parent
PHASE_ORDER = ["frame", "discover", "show", "fit", "signoff", "build", "test", "deploy", "operate", "change"]
SKIP = {".git", "__pycache__", ".venv", "node_modules"}
def title(path: pathlib.Path) -> str:
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith("# "):
return line[2:].strip()
return path.stem
def files() -> list:
out = []
for p in sorted(ROOT.rglob("*")):
if p.is_file() and not (set(p.relative_to(ROOT).parts) & SKIP) and p.name != "index.json":
out.append(p.relative_to(ROOT).as_posix())
return out
def phases() -> list:
found = {p.stem: p for p in (ROOT / "erp" / "phase").glob("*.md")}
names = [n for n in PHASE_ORDER if n in found] + sorted(set(found) - set(PHASE_ORDER))
return [{"name": n, "title": title(found[n]), "path": f"erp/phase/{n}.md", "uri": f"dodil://erp/phase/{n}"}
for n in names]
def modules() -> list:
out = []
for f in sorted((ROOT / "modules").glob("*/module.yaml")):
m = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
repos = (m.get("reference") or {}).get("repos") or []
out.append({
"id": m.get("id", f.parent.name),
"name": m.get("name"),
"maturity": m.get("maturity"),
"spec": f"modules/{f.parent.name}/module.yaml",
"repos": [{k: r.get(k) for k in ("name", "role", "tag", "https", "ssh")} for r in repos],
})
return out
def pending() -> list:
cat = yaml.safe_load((ROOT / "modules" / "catalog.yaml").read_text(encoding="utf-8")) or {}
return [{"id": p.get("id"), "name": p.get("name"), "maturity": p.get("maturity", "planned"),
"start_from": p.get("start_from", [])} for p in cat.get("pending") or []]
def brick_entries() -> dict:
shared = yaml.safe_load((ROOT / "modules" / "shared.yaml").read_text(encoding="utf-8")) or {}
# process_view: "none: <reason>" — a master-data brick's waiver, written into compose.yaml by `erp-compose init`
# (erp-platform 0.8.2; without it 0.8's check refuses every composition with bp: wave-4 end-to-end, 2026-10-18)
return {repo: {"name": e["name"], "brick": e["brick"], **({"process_view": e["process_view"]} if e.get("process_view")
else {})} for repo, e in sorted((shared.get("brick_entries") or {}).items())}
def build() -> dict:
return {
"schema": "dodil.erp-method.index/v1",
"repo": "https://git.dodil.io/dodil/erp-method.git",
"version": (ROOT / "VERSION").read_text().strip(),
# Readers (the CLI's dodil://erp/*, the blog) fetch index.json at `main` to learn the current release,
# then read every file AT THAT TAG, which is immutable and cacheable. Bump VERSION and tag v<VERSION> together.
"latest_release": "v" + (ROOT / "VERSION").read_text().strip(),
"map": {"title": title(ROOT / "erp" / "build.md"), "path": "erp/build.md", "uri": "dodil://erp/build"},
"phases": phases(),
"method": {k: f"modules/{v}" for k, v in {
"lifecycle": "lifecycle.md", "pattern": "pattern.md", "template": "template.md",
"module_template": "module.template.yaml", "catalog": "catalog.yaml", "shared": "shared.yaml",
"waivers": "waivers.yaml", "test_composition": "test_composition.py"}.items()},
"modules": modules(),
# repo -> its compose.yaml entry; `erp-compose init` writes compose.yaml from these.
"brick_entries": brick_entries(),
"code_entry": (yaml.safe_load((ROOT / "modules" / "shared.yaml").read_text(encoding="utf-8")) or {}).get("code_entry"),
"pending": pending(),
"files": files(),
}
if __name__ == "__main__":
text = json.dumps(build(), indent=2, ensure_ascii=False) + "\n"
if "--check" in sys.argv:
current = (ROOT / "index.json").read_text(encoding="utf-8") if (ROOT / "index.json").exists() else ""
if current != text:
print("index.json is stale: run python3 scripts/build_index.py", file=sys.stderr)
sys.exit(1)
print("index.json up to date")
else:
(ROOT / "index.json").write_text(text, encoding="utf-8")
print(f"index.json: {len(build()['modules'])} modules, {len(build()['phases'])} phases")