dodil/erp-method

scripts/build_index.py

#!/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")