Investment Plans workspace
Open raw ↗
# -*- coding: utf-8 -*-
"""build_all.py — regenerate every deliverable from 01_System/project_data.py.

Order matters: diagrams and the model feed the study and the deck; everything feeds the
transfer pack; the checker suite runs last and is the gate (GOV-F9.10).

GOV-F4.2: no step may depend on a path that exists only on the machine that built it. The two
office-automation steps below are OPTIONAL and are skipped with a warning where the tool is
absent, rather than killing the build for the next operator (DEF-016).
"""
import os, shutil, subprocess, sys
HERE = os.path.dirname(os.path.abspath(__file__))
XLSX = os.path.join("..", "05_Outputs", "NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx")
DOCX = os.path.join("..", "05_Outputs", "NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx")

def _find(*candidates):
    """Return the first candidate that exists, or None. Overridable by environment variable."""
    for c in candidates:
        if not c:
            continue
        if os.path.isabs(c) and os.path.exists(c):
            return c
        w = shutil.which(c)
        if w:
            return w
    return None

def _glob_first(*patterns):
    """Skill directories are re-keyed by the host between sessions, so a hard-coded path to a
    helper script stops resolving without anything in this project having changed. Search for the
    script by name instead of by address (DEF-016 taught this the first time; the path moved
    again on 2026-09-07, which is why this now globs)."""
    import glob
    for pat in patterns:
        hits = sorted(g for g in glob.glob(pat, recursive=True) if "/.trash/" not in g)
        if hits:
            return hits[0]
    return None

RECALC = _find(os.environ.get("NDIS_RECALC")) or _glob_first(
    "/mnt/skills/**/xlsx/scripts/recalc.py",
    "/root/.claude/skills/**/xlsx/scripts/recalc.py")
SOFFICE = _find(os.environ.get("NDIS_SOFFICE")) or _glob_first(
    "/mnt/skills/**/docx/scripts/office/soffice.py",
    "/root/.claude/skills/**/docx/scripts/office/soffice.py") or _find("soffice", "libreoffice")

STEPS = [
    ("Governance projections (txt + core)", [sys.executable, "build_governance.py"], True),
    ("Registers  (xlsx + csv projections)", [sys.executable, "build_registers.py"], True),
    ("Diagrams   (D1-D10)",                 [sys.executable, "build_diagrams.py"], True),
    ("Financial model",                     [sys.executable, "build_financial_model.py"], True),
    ("Recalculate the workbook",
     ([sys.executable, RECALC, XLSX, "120"] if RECALC else None), False),
    ("Pre-build check run (gives the study a real log to cite)",
     [sys.executable, os.path.join("checkers", "run_checks.py")], False),
    ("Study      (docx)",                   [sys.executable, "build_study.py"], True),
    ("Model export for the deck (model_export.json)",
     [sys.executable, "export_model_json.py"], True),
    ("Decision pack (pptx)",                (["node", "build_deck.js"] if shutil.which("node") else None), False),
    ("Study PDF render",
     ([sys.executable, SOFFICE, "--headless", "--convert-to", "pdf",
       "--outdir", os.path.join("..", "05_Outputs"), DOCX]
      if (SOFFICE and SOFFICE.endswith(".py")) else
      ([SOFFICE, "--headless", "--convert-to", "pdf",
        "--outdir", os.path.join("..", "05_Outputs"), DOCX] if SOFFICE else None)), False),
    ("System breakdown (GOV-B4.10)", [sys.executable, "build_system_breakdown.py"], True),
    ("Business plan twin (business_plan.json)",
     [sys.executable, "build_business_plan_json.py"], True),
    ("Business plan A3 page (validated, then generated)",
     [sys.executable, os.path.join("portable", "templates", "business_plan",
                                   "build_business_plan.py")], True),
    ("Dashboard and Help Hub",              [sys.executable, "build_web.py"], True),
    ("README, threat model, transfer pack", [sys.executable, "build_handover.py"], True),
]

def main():
    skipped = []
    for label, cmd, required in STEPS:
        if cmd is None:
            print("\n=== %s  -- SKIPPED, tool not found on this machine" % label)
            skipped.append(label)
            continue
        print("\n=== %s" % label)
        r = subprocess.run(cmd, cwd=HERE)
        if r.returncode != 0:
            if required:
                print("BUILD FAILED at: %s" % label); sys.exit(r.returncode)
            print("   (optional step failed; continuing)")
            skipped.append(label)
    if skipped:
        print("\nOptional steps skipped or failed: %s" % ", ".join(skipped))
        print("Everything else was regenerated. The delivered .xlsx keeps its previously cached values"
              " and the .pdf keeps its previous render.")
    print("\n=== Placement sweep (GOV-F3.10 — debris archived with a hash manifest, never deleted)")
    subprocess.run([sys.executable, "tidy.py", "--ensure", "--sweep"], cwd=HERE)

    print("\n=== Source manifest gate (GOV-D1.13 — hash every source of truth)")
    subprocess.run([sys.executable, "build_source_manifest.py", "--gate"], cwd=HERE)

    print("\n=== Checker suite (the gate)")
    r = subprocess.run([sys.executable, os.path.join("checkers", "run_checks.py")], cwd=HERE)
    sys.exit(r.returncode)

if __name__ == "__main__":
    main()