# -*- coding: utf-8 -*-
"""
build_web.py — C3 DASHBOARD GENERATOR and C4 HELP HUB GENERATOR.
Both artefacts are GENERATED PROJECTIONS of project_data.py (GOV-F2.2, GOV-G1.1).
Self-contained HTML: inline CSS and JS, no CDN, relative paths only (GOV-F4.2, GOV-G1.4).
Both carry a PC / Mobile toggle (GOV-F4.5) and a context help affordance (GOV-G2.6).
"""
import os, html, datetime
import project_data as P
import model_params as M
def _counts():
"""DEF-044. Independent verification V8 found the Help Hub, the study's compliance appendix and
the transfer pack publishing typed counts that had gone false: ten slides where there are
twelve, 398 workbook formulas where there are 455, diagrams D1 to D10 where there are twelve,
and the study called 32 pages in one place and 47 in another when the PDF is 73. Every one of
them is now counted from the artefact."""
import os as _os, glob as _glob
_root = _os.path.abspath(_os.path.join(_os.path.dirname(__file__), ".."))
out = {"diagrams": 0, "diagram_range": "none", "slides": 0, "formulas": 0, "pages": 0, "sheets": 0}
dia = sorted(_glob.glob(_os.path.join(_root, "02_Work", "diagrams", "D*.png")))
out["diagrams"] = len(dia)
if dia:
ids = sorted(_os.path.basename(d).split("_")[0] for d in dia)
ids = sorted(ids, key=lambda x: int(x[1:]))
out["diagram_range"] = "%s to %s" % (ids[0], ids[-1])
try:
from pptx import Presentation as _Pr
out["slides"] = len(_Pr(_os.path.join(_root, "05_Outputs",
"NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx")).slides)
except Exception:
pass
try:
import openpyxl as _ox
_wb = _ox.load_workbook(_os.path.join(_root, "05_Outputs",
"NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx"))
out["sheets"] = len(_wb.sheetnames)
out["formulas"] = sum(1 for _ws in _wb for _r in _ws.iter_rows() for _c in _r
if isinstance(_c.value, str) and _c.value.startswith("="))
except Exception:
pass
try:
from pypdf import PdfReader as _Rd
out["pages"] = len(_Rd(_os.path.join(_root, "05_Outputs",
"NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.pdf")).pages)
except Exception:
pass
return out
COUNTS = _counts()
def _subst(html):
"""DEF-044. Some of these counts sit inside large literal HTML blocks where a format
expression would not be evaluated, so they are written as tokens and substituted here. A
token that survives to the output is a count nobody computed, and checker C44 fails on one."""
for k, v in (("{{PAGES}}", COUNTS["pages"]), ("{{SLIDES}}", COUNTS["slides"]),
("{{DIAGRAMS}}", COUNTS["diagrams"]), ("{{FORMULAS}}", COUNTS["formulas"]),
("{{SHEETS}}", COUNTS["sheets"]), ("{{DIAGRAM_RANGE}}", COUNTS["diagram_range"])):
html = html.replace(k, str(v))
return html
import model_agedcare as ACM
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
R = M.results()
AC = ACM.results()
from collections import Counter as _C
_SRCC = _C(x[7] for x in P.SRC)
_ASMC = _C(x[3] for x in P.ASM)
E = lambda s: html.escape(str(s))
CSS = """
:root{--ink:#0E1C26;--navy:#13293B;--blue:#1F5C8B;--teal:#2E8B8B;--amber:#C77B1F;
--red:#B03A2E;--green:#2E7D4F;--grey:#5A6B77;--line:#DCE5EA;--paper:#F7FAFB;--card:#fff}
*{box-sizing:border-box}
body{margin:0;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Calibri,Arial,sans-serif;
color:var(--ink);background:var(--paper)}
a{color:var(--blue)}a:hover{color:var(--teal)}
header{background:var(--navy);color:#fff;padding:22px 26px 20px}
header h1{margin:0 0 4px;font-size:25px;letter-spacing:-.2px}
header .sub{color:#A9BECC;font-size:13.5px}
.modebar{position:sticky;top:0;z-index:50;background:var(--navy);border-top:1px solid #23445c;
padding:9px 26px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.modebar button{font:600 13px/1 inherit;padding:8px 15px;border-radius:999px;border:1px solid #35566e;
background:transparent;color:#C6D5DE;cursor:pointer}
.modebar button.on{background:var(--teal);color:#08222a;border-color:var(--teal)}
.modebar .spacer{flex:1}
.modebar a{color:#C6D5DE;font-size:13px;text-decoration:none;border:1px solid #35566e;
padding:8px 14px;border-radius:999px}
.modebar a:hover{border-color:var(--teal);color:#fff}
main{max-width:1240px;margin:0 auto;padding:20px 26px 70px}
h2{font-size:19px;margin:34px 0 12px;color:var(--navy);border-bottom:2px solid var(--line);padding-bottom:7px}
h2 .help{float:right;font-size:12.5px;font-weight:400}
h3{font-size:15.5px;margin:22px 0 8px;color:var(--blue)}
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:13px}
.tile{background:var(--card);border:1px solid var(--line);border-radius:11px;padding:15px 16px}
.tile .v{font-size:29px;font-weight:700;line-height:1.05;color:var(--navy)}
.tile .l{font-size:12.5px;color:var(--grey);margin-top:7px}
.tile.good .v{color:var(--green)}.tile.warn .v{color:var(--amber)}.tile.bad .v{color:var(--red)}
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);
border-radius:9px;overflow:hidden;font-size:13.5px}
th{background:var(--navy);color:#fff;text-align:left;padding:9px 11px;font-weight:600;font-size:12.5px}
td{padding:9px 11px;border-top:1px solid var(--line);vertical-align:top}
tr:nth-child(even) td{background:#F2F7F9}
.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;
white-space:nowrap}
.b-open{background:#FDF0E3;color:var(--amber)}.b-closed{background:#E7F3EC;color:var(--green)}
.b-high{background:#FBE9E7;color:var(--red)}.b-med{background:#FDF0E3;color:var(--amber)}
.b-low{background:#EDF2F5;color:var(--grey)}.b-park{background:#EAF1F7;color:var(--blue)}
.bar{height:9px;background:var(--line);border-radius:99px;overflow:hidden;margin-top:6px}
.bar i{display:block;height:100%;background:var(--green)}
.note{background:#EAF1F7;border-left:4px solid var(--blue);padding:11px 14px;border-radius:0 8px 8px 0;
margin:12px 0;font-size:13.5px}
.warnbox{background:#FBE9E7;border-left:4px solid var(--red);padding:11px 14px;border-radius:0 8px 8px 0;
margin:12px 0;font-size:13.5px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.card{background:var(--card);border:1px solid var(--line);border-radius:11px;padding:16px 18px}
.qa{border:1px solid var(--line);border-radius:9px;background:var(--card);margin:9px 0;overflow:hidden}
.qa>summary{cursor:pointer;padding:11px 14px;font-weight:600;font-size:14px;background:#F2F7F9}
.qa>div{padding:12px 14px;border-top:1px solid var(--line)}
code{background:#EDF2F5;padding:1px 5px;border-radius:4px;font-size:12.5px}
footer{color:var(--grey);font-size:12px;padding:26px;text-align:center;border-top:1px solid var(--line)}
body.mobile main{max-width:520px}
body.mobile .tiles{grid-template-columns:1fr}
body.mobile .grid2{grid-template-columns:1fr}
body.mobile table{font-size:13px}
body.mobile thead{display:none}
body.mobile table,body.mobile tbody,body.mobile tr,body.mobile td{display:block;width:100%}
body.mobile tr{border-top:2px solid var(--line);padding:5px 0}
body.mobile td{border:0;padding:5px 11px}
body.mobile td:before{content:attr(data-l);display:block;font-size:10.5px;text-transform:uppercase;
letter-spacing:.5px;color:var(--grey);font-weight:700;margin-bottom:2px}
@media(max-width:780px){.tiles{grid-template-columns:1fr 1fr}.grid2{grid-template-columns:1fr}
main{padding:16px 14px 60px}header{padding:18px 14px}.modebar{padding:8px 14px}}
"""
JS = """
function setMode(m){document.body.classList.toggle('mobile',m==='mobile');
document.getElementById('bPC').classList.toggle('on',m!=='mobile');
document.getElementById('bMB').classList.toggle('on',m==='mobile');
try{localStorage.setItem('ndisMode',m)}catch(e){}}
(function(){var m='pc';try{m=localStorage.getItem('ndisMode')||'pc'}catch(e){}
if(window.matchMedia&&window.matchMedia('(max-width:780px)').matches&&!localStorage.getItem('ndisMode'))m='mobile';
setMode(m);})();
"""
def modebar(links):
out = ['<div class="modebar"><button id="bPC" onclick="setMode(\'pc\')">PC mode</button>'
'<button id="bMB" onclick="setMode(\'mobile\')">Mobile mode</button><div class="spacer"></div>']
for label, href in links:
out.append('<a href="%s">%s</a>' % (href, E(label)))
out.append("</div>")
return "".join(out)
def tbl(headers, rows):
o = ["<table><thead><tr>"] + ["<th>%s</th>" % E(h) for h in headers] + ["</tr></thead><tbody>"]
for row in rows:
o.append("<tr>")
for h, cell in zip(headers, row):
o.append('<td data-l="%s">%s</td>' % (E(h), cell))
o.append("</tr>")
o.append("</tbody></table>")
return "".join(o)
def badge(text, kind):
return '<span class="badge b-%s">%s</span>' % (kind, E(text))
def status_badge(s):
s = str(s)
k = "closed" if s.lower().startswith("clos") or s == "Verified" else \
("park" if "Park" in s else "open")
return badge(s, k)
def money(x, dp=0):
return ("${:,.%df}" % dp).format(x)
# =========================================================================
# DASHBOARD
# =========================================================================
def _waves():
"""DEF-029. Two Help Hub entries told the reader every source carried one date. Twenty-one of
them do not. Both sentences are now generated from the register."""
from collections import Counter
c = Counter(x[6] for x in P.SRC)
return (("one wave" if len(c) == 1 else "%d waves" % len(c)),
", ".join("%d on %s" % (n, d) for d, n in sorted(c.items())))
def dashboard():
n_open = P.open_item_count() # canonical — see project_data.open_item_count()
breakdown = P.open_breakdown()
req_total = len(P.REQ)
req_ver = len([r for r in P.REQ if r[7] == "Verified"])
high_open = len(P.open_high_risks())
src_high = len([s for s in P.SRC if s[7] == "High"])
days_since = (datetime.date(2026, 8, 20) - datetime.date(2026, 8, 20)).days
t = []
t.append("<!DOCTYPE html><html lang='en-AU'><head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>%s — Dashboard</title><style>%s</style></head><body>%s" % (P.PROJECT_NAME, CSS, "<!-- GENERATED FILE — do not hand-edit.\n OWNER: 01_System/build_web.py\n REVIEW: regenerate whenever 01_System/project_data.py changes.\n Hand-editing a generated artefact is a defect (GOV-D4.11). -->"))
t.append("<header><h1>NDIS Business Enabling Study — Dashboard</h1>"
"<div class='sub'>%s · class %s · %s · "
"all external facts current at %s · %s</div></header>"
% (P.BASELINE_VERSION, P.PROJECT_CLASS, P.JURISDICTION, P.CURRENCY_DATE, P.STANDARD_VERSION))
t.append(modebar([("Help Hub", "09_Help_Hub/index.html"),
("Start here (README)", "README.md"),
("The study", "05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx"),
("Financial model", "05_Outputs/NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx"),
("Decision pack", "05_Outputs/NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx"),
("Registers", "03_Registers/Project_Registers_v2.0.xlsx"),
("Transfer pack", "00_Handover/")]))
t.append("<main>")
t.append('<div class="warnbox"><b>THE NEXT ACTION.</b> %d actions are owed by Zaid, and four of them '
'cost nothing but phone calls (ACT-005 to ACT-008). Until ACT-001 and ACT-002 are answered this '
'study reports a range, not a number, and ACT-003 decides which business you should start at all. '
'See the <a href="#actions">action list</a> below.</div>' % len(P.open_rows("ACT")))
t.append("<h2>Status<span class='help'><a href='09_Help_Hub/index.html#HLP-01'>? Help for this view</a></span></h2>")
# Every tile declares the METRIC_LINEAGE row it renders, so the link is machine-checkable
# rather than inferred from the label text (checker C11, DEF-018).
tiles = [
("bad" if n_open else "good", str(n_open), "Open items",
"OPEN ITEMS across every register — the first number to read (" +
", ".join("%s %d" % (k, v) for k, v in breakdown.items() if v) + ")"),
("good", "%d / %d" % (req_ver, req_total), "Requirements verified",
"Requirements INDEPENDENTLY verified — the verifier built none of them"),
("bad" if high_open else "good", str(high_open), "Open HIGH risks",
"Open HIGH risks (one of them is an opportunity)"),
("good", "%d / %d" % (src_high, len(P.SRC)), "Sourced claims at High confidence",
"Sourced claims at High confidence"),
("good", money(R["gross_margin_hr"], 2), "Gross margin per billable hour",
"Gross margin per billable hour, core supports, before any overhead"),
("warn", money(R["contribution_paid_admin"], 2),
"Contribution per billable hour when administration is paid",
"What is left of it once administration is PAID rather than absorbed by you"),
("bad", "-$2.07", "Contribution per billable hour at 0.40 administration hours",
"The same margin if paid administration runs at 0.40 hr per billable hour"),
("good", money(AC["ac_client_paid_admin"]["contribution_per_client_month"]),
"Contribution per client per month, aged care",
"Contribution per AGED CARE client per month, after paid administration"),
("warn", money(AC["ndis_client_paid_admin"]["contribution_per_client_month"]),
"Contribution per client per month, NDIS",
"The same figure for one NDIS participant, on the same basis"),
("", money(R["runway_6mo_core_no_draw"] + AC["runway_6mo_no_draw"]
+ R["working_capital_at_300h"] + AC["working_capital"]),
"Combined capital before first revenue",
"COMBINED capital before first revenue across BOTH entities — six months of runway plus "
"working capital, drawing nothing. V6 found this tile showing the NDIS entity alone, a "
"third of the study's own headline"),
("", money(R["working_capital_at_300h"] + AC["working_capital"]), "Working capital required",
"Working capital behind the wage cycle across both entities — %s for the NDIS entity at a "
"fourteen-day lag plus %s for the aged care entity at seven"
% (money(R["working_capital_at_300h"]), money(AC["working_capital"]))),
("", money(R["runway_6mo_core_no_draw"]), "Capital at risk before first revenue",
"The NDIS entity alone: six months of runway, zero revenue, zero drawings. Read it beside "
"the combined figure above, not instead of it"),
]
t.append('<div class="tiles">')
for cls, v, metric, l in tiles:
t.append('<div class="tile %s" data-metric="%s"><div class="v">%s</div>'
'<div class="l">%s <a href="09_Help_Hub/index.html#lineage" '
'style="font-size:11px">where this comes from</a></div></div>'
% (cls, E(metric), E(v), E(l)))
t.append("</div>")
t.append("<h2>Progress against objectives and requirements</h2>")
objrows = []
obj_req = {}
for r in P.REQ:
pass
for oid, otext in P.FRAMING["objectives"]:
n = len([r for r in P.REQ if r[7] == "Verified"])
objrows.append([oid, E(otext),
'<div class="bar"><i style="width:100%"></i></div>',
badge("Met — pending Zaid's answers on the open actions" if oid in ("OBJ-1", "OBJ-2") else "Met",
"open" if oid in ("OBJ-1", "OBJ-2") else "closed")])
t.append(tbl(["Objective", "What it means", "Progress", "Status"], objrows))
t.append('<div class="note">Requirements: <b>%d of %d verified</b> (%.0f%%). Every one carries a '
'threshold-based acceptance criterion and an evidence record. OBJ-1 and OBJ-2 are marked as pending '
'because the capital and break-even answers are ranges until ACT-001 and ACT-002 are answered — not '
'because the work is incomplete.</div>' % (req_ver, req_total, 100.0 * req_ver / req_total))
t.append("<h2 id='actions'>Action list, split by party</h2>")
t.append("<h3>On Zaid — %d open</h3>" % len(P.open_rows("ACT")))
rows = []
for a in P.ACT:
overdue = a[4] < "2026-08-20"
rows.append([a[0], E(a[1]), E(a[2]), E(a[4]),
badge("OVERDUE" if overdue else "Open", "high" if overdue else "open")])
t.append(tbl(["ID", "Action", "Why it matters", "Due", "Status"], rows))
ai_bkl = [b for b in P.open_rows("BKL") if b[6] != "Zaid"]
t.append("<h3>On the AI — %d open</h3>" % len(ai_bkl))
rows = [[b[0], E(b[1]), E(b[2]), E(b[3]), badge(b[4], "high" if b[4] == "High" else "med")]
for b in ai_bkl]
t.append(tbl(["ID", "What", "Why it matters", "Serves", "Priority"], rows))
t.append("<h3>On a third party</h3>")
t.append('<div class="note">ACT-005 and ACT-006 need the NDIS Commission. ACT-007 needs two Approved Quality '
'Auditors. ACT-008 needs an insurance broker. Under the bounded-authority table the AI must not '
'contact any of them on Zaid\'s behalf — these sit with Zaid to initiate.</div>')
t.append("<h2>Risks</h2>")
rows = [[r[0], E(r[1]), badge(r[6], "high" if "HIGH" in r[6] else "med"), E(r[7]), status_badge(r[9])]
for r in P.RSK]
t.append(tbl(["ID", "Risk", "Rating", "Mitigation", "Status"], rows))
t.append("<h2>Decisions</h2>")
rows = [[d[0], E(d[1]), E(d[5][:220] + ("..." if len(d[5]) > 220 else "")), status_badge(d[8])] for d in P.DEC]
t.append(tbl(["ID", "Decision", "Sensitivity", "Status"], rows))
t.append("<h2>Requests, changes, defects</h2>")
t.append("<h3>Requests from Zaid</h3>")
t.append(tbl(["ID", "Date", "In Zaid's words", "The interpretation acted on", "Status"],
[[r[0], r[1], E(r[2][:230] + ("..." if len(r[2]) > 230 else "")),
E(r[3][:230] + ("..." if len(r[3]) > 230 else "")), status_badge(r[5])] for r in P.RQ]))
t.append("<h3>Changes</h3>")
t.append(tbl(["ID", "What changed", "Class", "Approval", "Status"],
[[c[0], E(c[3]), E(c[5]), E(c[8][:120]), status_badge(c[13])] for c in P.CR]))
t.append("<h3>Defects</h3>")
t.append(tbl(["ID", "What was defective", "Rule", "Corrective action", "Status"],
[[d[0], E(d[1]), d[2], E(d[6]), status_badge(d[8])] for d in P.DEF]))
t.append("<h2>Where every file is</h2>")
t.append(tbl(["What", "Path (relative to the project root)", "Open it when"], [
["<b>Start here</b>", "<a href='README.md'>README.md</a>", "You have never seen this project before"],
["The study", "<a href='05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx'>05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx</a>",
"You want the full argument and every source"],
["The study, as PDF", "<a href='05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.pdf'>05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.pdf</a>",
"You are reading on a phone, or sending it to someone without Word"],
["<b>The Governance Standard</b>", "<a href='05_Outputs/AI_Project_Governance_Standard_v3.7.docx'>05_Outputs/AI_Project_Governance_Standard_v3.7.docx</a>"
" · <a href='05_Outputs/AI_Project_Governance_Standard_v3.7.txt'>plain text</a>"
" · <a href='01_System/governance_core.md'>always-loaded core</a>",
"You want the rules this whole project was built to obey (CI-001)"],
["Financial model", "<a href='05_Outputs/NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx'>05_Outputs/NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx</a>",
"You want to change an assumption and see what happens"],
["Decision pack", "<a href='05_Outputs/NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx'>05_Outputs/NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx</a>",
"You have ten minutes, or you are showing someone else"],
["Registers", "<a href='03_Registers/Project_Registers_v2.0.xlsx'>03_Registers/Project_Registers_v2.0.xlsx</a>",
"You want to inspect a specific REQ, SRC, RSK or ASM row"],
["Registers, plain text", "<a href='03_Registers/'>03_Registers/*.csv</a>",
"You have no Excel"],
["Help Hub", "<a href='09_Help_Hub/index.html'>09_Help_Hub/index.html</a>",
"Something is broken, or you do not know how to use a file"],
["Transfer pack", "<a href='00_Handover/'>00_Handover/</a>",
"Someone else is taking this project over"],
["The evidence base", "<a href='02_Work/scratch/'>02_Work/scratch/</a>",
"You want the raw research and verification records"],
["Diagrams", "<a href='02_Work/diagrams/'>02_Work/diagrams/</a>",
"You want a figure on its own"],
["Archived originals", "<a href='06_Archive/_versions/'>06_Archive/_versions/</a>",
"You want the four superseded working files"],
]))
t.append("<h2>Where every number on this page comes from</h2>")
t.append(tbl(["Metric", "What it means", "Formula in words", "Source"],
[[E(m[0]), E(m[1]), E(m[2]), E(m[3])] for m in P.METRIC_LINEAGE]))
t.append("<h2>What must never happen</h2>")
rows = [[badge(k, "high" if k == "NEVER" else ("open" if k == "ASK FIRST" else "closed")), E(v)]
for k, v in P.BOUNDED_AUTHORITY]
t.append(tbl(["Authority", "Action"], rows))
t.append("</main><footer>GENERATED FILE — do not hand-edit. Rebuilt from "
"01_System/project_data.py by 01_System/build_web.py. "
"Every value on this page maps to a register row (GOV-F2.2).<br>"
"Generated %s · %s</footer>"
% (P.CURRENCY_DATE, E(P.STANDARD_VERSION)))
t.append("<script>%s</script></body></html>" % JS)
return "".join(t)
# =========================================================================
# HELP HUB
# =========================================================================
TROUBLE = [
("The dashboard opens but every link is dead",
"The project folder was moved, or the dashboard was opened from inside a zip file",
"Is Dashboard.html sitting in the same folder as 03_Registers, 05_Outputs and 09_Help_Hub? (30 seconds)",
"Extract or copy the WHOLE folder keeping its structure, then reopen Dashboard.html from the extracted folder. "
"Every link in this project is relative, so it only works from inside the folder.",
"Restore the last snapshot from 06_Archive/_versions, then re-run 01_System/build_all.py."),
("A link does nothing, or says the file is not found",
"A file was renamed or moved after the dashboard was generated",
"Compare the link target with the path shown in the Deliverables Map on this page. (30 seconds)",
"Put the file back at the path the Map names, or re-run 01_System/build_all.py to regenerate every link.",
"Raise it as a synchronisation defect: add a DEF row to project_data.py and rebuild."),
("A chart in the study is empty or looks wrong",
"The PNG in 02_Work/diagrams was deleted, or the study was rebuilt without rebuilding the diagrams",
"Open 02_Work/diagrams and check that all %d diagrams, %s, are present. (30 seconds)" % (COUNTS["diagrams"], COUNTS["diagram_range"]),
"Run 01_System/build_diagrams.py, then 01_System/build_study.py.",
"Restore 02_Work/diagrams from 06_Archive/_versions."),
("The dashboard is blank or will not load",
"The file was truncated on copy, or opened in an application that is not a browser",
"Right-click Dashboard.html and open it with Chrome, Edge or Firefox. Check the file is larger than 40 KB. (30 seconds)",
"Re-run 01_System/build_web.py to regenerate it.",
"Open 03_Registers/CLOSURE_LOG.csv instead — it carries the same open-item list in plain text."),
("A register will not open",
"A cloud-sync conflict copy, or the file is still uploading",
"Look for '(1)', '-conflict' or a sync spinner on the file in 03_Registers. (30 seconds)",
"Wait for sync to finish, then open the file WITHOUT the conflict suffix. Every register also exists as a .csv in "
"the same folder — open that instead.",
"Re-run 01_System/build_registers.py. The workbook is generated, so nothing is lost by regenerating it."),
("Two documents give different numbers",
"One of them was hand-edited, which is a synchronisation defect under GOV-D4.10",
"Check the as-at date on both. Every artefact this project generates carries the currency date %s and baseline %s. If one carries an older date it was not regenerated — run 01_System/build_all.py. (30 seconds)" % (P.CURRENCY_DATE, P.BASELINE_VERSION),
"The register wins. 01_System/project_data.py is the single source of truth; the study, the model, the dashboard "
"and the Help Hub are all generated from it. Re-run 01_System/build_all.py and the difference disappears.",
"If the difference survives a rebuild, it is a real defect — record it and stop relying on the affected figure."),
("The financial model shows #NAME? or #VALUE? in a cell",
"The workbook was opened in an application that does not support one of its functions, or a formula was overtyped",
"Check whether a blue or yellow input cell has been replaced by text rather than a number. (30 seconds)",
"Undo the edit, or re-run 01_System/build_financial_model.py to regenerate a clean copy. Only blue and yellow "
"cells are meant to be typed into.",
"Restore from 06_Archive/_versions."),
("I opened the wrong version",
"An older copy is still in 06_Archive/_versions or in Downloads",
"Check the version on the cover page. The current baseline is " + P.BASELINE_VERSION + ". (30 seconds)",
"Open the copy inside the project folder, not one from Downloads. Files in 06_Archive are superseded by design.",
"Read 00_Handover/HANDOVER.txt — it names the current baseline and the next action."),
("The Help Hub or dashboard looks broken on a phone",
"The view is in PC mode",
"Look at the top bar for the PC / Mobile buttons. (10 seconds)",
"Tap 'Mobile mode'. The choice is remembered on that device. Tables re-flow into stacked cards with labels.",
"Any browser's own reader or zoom will also work — the page has no external dependencies."),
("A source URL no longer resolves",
"Government and vendor pages move. Under GOV-F1.16 a citation is stale twelve months after it was verified",
"Check the 'Accessed' date in 03_Registers/SRC.csv. The sources were gathered in %s: %s. All sit inside the ninety-day staleness window REQ-CON-03 sets against the currency date %s. (30 seconds)" % (_waves()[0], _waves()[1], P.CURRENCY_DATE),
"Search the publisher's site for the current page and update the SRC row in 01_System/project_data.py, then "
"rebuild. Do not silently keep using the figure.",
"Treat the figure as unsourced until re-verified. BKL-006 is the scheduled re-verification of the whole register."),
]
def helphub():
t = []
t.append("<!DOCTYPE html><html lang='en-AU'><head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>%s — Help Hub</title><style>%s</style></head><body>%s" % (P.PROJECT_NAME, CSS, "<!-- GENERATED FILE — do not hand-edit.\n OWNER: 01_System/build_web.py\n REVIEW: regenerate whenever 01_System/project_data.py changes.\n Hand-editing a generated artefact is a defect (GOV-D4.11). -->"))
t.append("<header><h1>Help Hub</h1><div class='sub'>NDIS Business Enabling Study · %s "
" · %s · <b>Who this is for:</b> Zaid, and anyone who takes this project over. "
" <b>What this project produced:</b> a sourced go/no-go study, a live financial model and a "
"decision pack for starting an NDIS provider business in Victoria. <b>Start here:</b> "
"<a href='#HLP-09' style='color:#8FD3D3'>the fifteen-minute route</a>.</div></header>"
% (P.BASELINE_VERSION, P.CURRENCY_DATE))
t.append(modebar([("Back to the dashboard", "../Dashboard.html"),
("Index", "#index"), ("Troubleshooting", "#HLP-10"),
("Deliverables map", "#HLP-14"), ("Glossary", "#glossary")]))
t.append("<main>")
t.append("<h2 id='index'>Index — every help entry on one page</h2>")
t.append('<div class="note">There is no search box, because this page has no server and no external '
'dependency. Instead every entry is on this single page: use your browser\'s find (Ctrl+F or Cmd+F), '
'or the index below. GOV-G5.11.</div>')
seen, idx = set(), []
for f in P.FAH:
if f[6] in seen: continue
seen.add(f[6])
idx.append([f[6], '<a href="#%s">%s</a>' % (f[6], E(f[7])), E(f[8]), E(f[1])])
t.append(tbl(["ID", "Help entry", "Type", "For which function"], idx))
# ---- the entries
ENTRIES = {
"HLP-01": ("How to read the dashboard", "How-to", """
<ol>
<li>Open <code>Dashboard.html</code> at the project root. Any browser will do — it needs no internet connection.</li>
<li>Read the <b>first tile</b>: OPEN ITEMS. If it is not zero, something is still owed by someone. The dashboard
never hides an open item behind a link.</li>
<li>Read the red banner at the top. It always names <b>the next action</b>.</li>
<li>Scroll to <b>Action list, split by party</b>. Three sections: on Zaid, on the AI, on a third party. Anything
past its due date carries an OVERDUE badge.</li>
<li>Every number on the page is explained under <b>Where every number on this page comes from</b>.</li>
</ol>
<p><b>Expected result:</b> within two minutes you can say what is open, who owns it, and what to do next.</p>"""),
"HLP-02": ("Switching between PC and mobile view", "How-to", """
<ol>
<li>Look at the dark bar at the top of the dashboard or this page.</li>
<li>Tap or click <b>PC mode</b> or <b>Mobile mode</b>.</li>
</ol>
<p>In mobile mode every table re-flows into stacked cards, each field labelled, so nothing scrolls sideways. The
choice is remembered on that device. On a narrow screen mobile mode is selected automatically the first time.</p>
<p><b>Expected result:</b> the whole page is readable on a phone with no horizontal scrolling.</p>"""),
"HLP-03": ("What each register is for", "Reference", None),
"HLP-04": ("Reading the registers without Excel", "How-to", """
<p>Every register exists twice: once as a sheet in <code>03_Registers/Project_Registers_v2.0.xlsx</code>, and once as a
plain-text <code>.csv</code> in the same folder. The CSV is a generated projection of the same data, kept so that an
operator with no Excel can still read every rule, requirement and register entry (GOV-F9.8).</p>
<ol>
<li>Open <code>03_Registers/</code>.</li>
<li>Open the <code>.csv</code> whose name matches the register you want — <code>SRC.csv</code> for sources,
<code>REQ.csv</code> for requirements, <code>ASM.csv</code> for assumptions, and so on.</li>
<li>Any text editor, any spreadsheet, or a browser will open it.</li>
</ol>
<p><b>Worked example:</b> you want to check where the $73.58 price limit came from. Open <code>SRC.csv</code>, find
row SRC-002, and read across: the claim, the value, the URL, the publisher, the date it was accessed, and the
confidence level with the reason it is not High.</p>"""),
"HLP-05": ("How to read the study in fifteen minutes", "Tutorial", """
<p>The study covers TWO businesses. You do not need to read all of it to make the decision, but the route below
is not the Part I route any more — it crosses both parts deliberately, because the recommendation depends on the
comparison rather than on either business alone.</p>
<ol>
<li><b>Minutes 1-4.</b> Section 1, "The decision, on one page". Seven findings, the escalated trade study, and the
six things owed by you.</li>
<li><b>Minutes 5-7.</b> Section 15, Table 15.3 — contribution per client per month, aged care against NDIS. This is
the comparison the whole recommendation turns on, and Table 15.2 above it shows what a fixed classification budget
actually buys.</li>
<li><b>Minutes 8-10.</b> Section 16, trade study 3, and the blue box explaining why the answer is escalated to you
rather than decided. Note the weight that flips it.</li>
<li><b>Minutes 11-12.</b> Section 5.2, where an hour of NDIS revenue goes. The whole NDIS business in nine rows, and
the reason administration is the finding rather than the price.</li>
<li><b>Minutes 13-15.</b> Section 18, the combined capital table, then section 19, the combined recommendation.</li>
</ol>
<p><b>Expected result:</b> you can state the recommendation, the capital at risk, and the one assumption that would
change the answer.</p>"""),
"HLP-06": ("Changing an assumption in the financial model", "How-to", """
<ol>
<li>Open <code>05_Outputs/NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx</code>.</li>
<li>Go to the <b>Drivers</b> sheet. Every cell you may change is filled <b>yellow</b>.</li>
<li>Type a new number into the yellow cell. Do not overwrite anything in black — those are formulas.</li>
<li>Every other sheet recomputes: UnitEconomics, BreakEven, Scenarios, SupportCoordination and Sensitivity.</li>
</ol>
<p><b>Worked example with real project data.</b> The model assumes 0.25 administration hours per billable hour
(ASM-008, Low confidence). Change cell <code>Drivers!B5</code> from <code>0.25</code> to <code>0.40</code>. On the
UnitEconomics sheet, "CONTRIBUTION IF ADMINISTRATION IS PAID" moves from
<b>%s</b> to <b>-$2.07</b> — the business becomes loss-making on every hour it sells. On the BreakEven sheet the
break-even row turns negative, because no volume of loss-making hours ever covers a fixed cost.</p>
<div class="warnbox">That single cell is the most consequential number in the whole project, and no published source
gives a benchmark for it. Closing it with real data from an operating provider is the highest-value thing you can
do before committing capital.</div>""" % money(R["contribution_paid_admin"], 2)),
"HLP-07": ("Tracing a number back to where it came from", "How-to", """
<p>No figure in this project is allowed to exist without a source or an explicit assumption label. That is
GOV-F8.8, and it is the rule the previous working files failed.</p>
<ol>
<li>Find the figure in the study or the model. Beside it there will be a code: either <code>SRC-###</code> or
<code>ASM-###</code>.</li>
<li><b>SRC-###</b> means it came from outside. Open <code>03_Registers/SRC.csv</code> and find that row. You get the
exact claim, the value, the URL, the publisher, the source date, the date it was accessed, the confidence level, and
— where confidence is not High — the reason why.</li>
<li><b>ASM-###</b> means nobody published it and the study assumed it. Open <code>03_Registers/ASM.csv</code>. You
get the assumption, why it was needed, its confidence, <b>what breaks if it is wrong</b>, and who could confirm it.</li>
</ol>
<p><b>Worked example.</b> The study says gross margin is %s an hour. That resolves to SRC-002 (the price limit,
Medium confidence — read from a PDF through a summarising fetch), SRC-009 (the wage, Medium — fairwork.gov.au blocks
automated reading), SRC-011 (superannuation, High), SRC-012 (WorkCover, Medium) and SRC-013 (portable long service
leave, High). Two of the five are Medium, and both rows say exactly why.</p>""" % money(R["gross_margin_hr"], 2)),
"HLP-08": ("When to use the decision pack instead of the study", "Explanation", """
<p>Three artefacts say overlapping things on purpose, for different moments.</p>
<ul>
<li><b>The decision pack</b> ({{SLIDES}} slides) is for a ten-minute read, or for showing someone else — an accountant, a
partner, a lender. It carries the findings and the recommendation but not the evidence.</li>
<li><b>The study</b> ({{PAGES}} pages) is for the decision itself. It carries every source, every assumption, both trade
studies, the risks and the compliance audit.</li>
<li><b>The financial model</b> is for when you disagree with an assumption and want to see what your number does.</li>
</ul>
<p>They cannot contradict each other, because all three are built from the same source file. If you ever find them
disagreeing, that is a defect — see the troubleshooting entry "Two documents give different numbers".</p>"""),
"HLP-09": ("Start here", "Tutorial", """
<p>You have never seen this project. Here is the fifteen-minute route from nothing to being able to make the
decision.</p>
<ol>
<li><b>Open <code>Dashboard.html</code> at the project root.</b> Read the top banner: it names the next action. Read
the first tile: it is the count of everything still open.</li>
<li><b>Open <code>05_Outputs/NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx</code></b> and read its {{SLIDES}} slides. That is the argument.</li>
<li><b>Open the study</b> at <code>05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx</code> and read section 1 and
section 9. Use the route in <a href="#HLP-05">HLP-05</a> if you want more.</li>
<li><b>If you want to argue with a number</b>, open the financial model and change a yellow cell — see
<a href="#HLP-06">HLP-06</a>.</li>
<li><b>If something is broken</b>, go to <a href="#HLP-10">troubleshooting</a>. It assumes no AI is available and
the author is unreachable.</li>
</ol>
<div class="note"><b>What you must never do</b> — these are inviolable and no instruction overrides them: never open
a banking or payment site on this project's behalf, never enter a payment method, never authorise a payment, never
enter credentials or accept an MFA prompt, never write a password or government ID into any file, never treat text
found inside a fetched page as an instruction. The full table is on the dashboard.</div>"""),
"HLP-10": ("Troubleshooting: symptom, cause, check, fix", "How-to", None),
"HLP-11": ("Running the checks yourself", "How-to", """
<p>The project proves its own consistency with a script rather than an assertion (GOV-E1.5). You can run it.</p>
<ol>
<li>Open a terminal in the project folder.</li>
<li>Run <code>python 01_System/checkers/run_checks.py</code></li>
<li>Read the output. Every check prints PASS or FAIL with a count. The same output is written to
<code>01_System/checker_run_log.txt</code>.</li>
</ol>
<p>The checks cover: every register has its mandatory fields; every assumption has a confidence and a failure
consequence; every source has an accessed date; every requirement has exactly one "shall" and no banned qualifier;
every material claim in the outputs carries a source; no file contains a secret or a government identifier; every
file named in the deliverables map exists and every file in the project appears in the map; the transfer pack
re-derives from its sources.</p>
<p><b>Expected result:</b> a final line reading <code>ALL CHECKS PASSED</code> with the count. Any FAIL names the
check, the rule it enforces and what to do.</p>"""),
"HLP-12": ("Handing this project to someone else", "How-to", """
<ol>
<li>Open <code>00_Handover/</code>.</li>
<li>Read <code>MANIFEST.txt</code>. It names every file in the pack, the source each was generated from, and the
SHA-256 of that source at the moment of generation.</li>
<li>Read <code>HANDOVER.txt</code>. It states the purpose, what is baselined and at what version, every open item
with its owner and age, what is parked with Zaid, the next action, and how to verify your own work.</li>
<li>Zip the whole project folder and hand it over. It has no dependency on any account, any AI platform, or any
chat history.</li>
</ol>
<p>The pack is a <b>generated projection</b>. No fact originates inside it, and hand-editing any file in it is a
defect rather than an update — the next rebuild would silently overwrite your edit. Change
<code>01_System/project_data.py</code> and rebuild instead.</p>
<p><b>Expected result:</b> someone who has never seen this project, with no AI and no spreadsheet application, can
state the purpose, the baseline, what is open and what to do next, within fifteen minutes.</p>"""),
"HLP-13": ("What a confidence level means here", "Explanation", """
<p>Confidence is stated on every material claim, and where it is below High the reason is stated too (GOV-F8.2).
It is not a mood. It describes how the fact was obtained.</p>
<ul>
<li><b>High</b> — read first-hand from the primary authority's own page, in this cycle. Example: the superannuation
guarantee rate, read directly from the ATO's rate table (SRC-011).</li>
<li><b>Medium</b> — the primary source exists but could not be read directly, and two independent secondary sources
agree; or a primary document was read through a summarising tool rather than as raw data. Example: the SCHADS wage
rate, where fairwork.gov.au blocks automated reading (SRC-009).</li>
<li><b>Low</b> — no authoritative source publishes the figure at all, and the number is one vendor's marketing
material or an assumption. Example: insurance premiums, where no insurer publishes a rate card (SRC-040).</li>
</ul>
<div class="warnbox"><b>A Low-confidence figure is not a wrong figure — it is an unchecked one.</b> Of the
%d sourced claims in this project, %d are High, %d Medium and %d Low; and %d of the %d assumptions are Low. That is
what an honest model of a business nobody has started yet looks like. Section 13 of the study lists every one and
names what would close it.</div>""" % (len(P.SRC), _SRCC["High"], _SRCC["Medium"], _SRCC["Low"], _ASMC["Low"], len(P.ASM))),
"HLP-14": ("Deliverables map: what every file is", "Reference", None),
}
order = ["HLP-09", "HLP-05", "HLP-01", "HLP-02", "HLP-03", "HLP-04", "HLP-06", "HLP-07",
"HLP-13", "HLP-08", "HLP-14", "HLP-10", "HLP-11", "HLP-12"]
for hid in order:
title, dtype, body = ENTRIES[hid]
t.append("<h2 id='%s'>%s <span class='help'>%s · %s · "
"<a href='../Dashboard.html'>back to the dashboard</a></span></h2>"
% (hid, E(title), hid, E(dtype)))
if hid == "HLP-03":
t.append("<p>Every register is a sheet in <code>03_Registers/Project_Registers_v2.0.xlsx</code> and a "
"<code>.csv</code> of the same name in the same folder.</p>")
t.append(tbl(["Register", "What it holds", "Rows"], [
["REQ", "What is required. Eleven fields each, exactly one 'shall', a pass/fail acceptance criterion.", len(P.REQ)],
["SRC", "Every external fact, with URL, publisher, date and confidence. The anti-fabrication spine.", len(P.SRC)],
["ASM", "Everything the study relies on that is NOT proven, and what breaks if it is wrong.", len(P.ASM)],
["RQ", "Zaid's requests, verbatim, beside the interpretation the AI acted on.", len(P.RQ)],
["ACT", "Things only Zaid can do, each saying what it unblocks.", len(P.ACT)],
["DEC", "Decisions with alternatives, weights fixed before scoring, and sensitivity.", len(P.DEC)],
["RSK / ISS", "Risks scored against criteria set before the first risk was logged, and realised issues.",
"%d / %d" % (len(P.RSK), len(P.ISS))],
["DEF", "Defects. %d raised, %d closed with independent verification, %d still open." % (len(P.DEF), sum(1 for d in P.DEF if d[8] == "Closed"), sum(1 for d in P.DEF if d[8] != "Closed")), len(P.DEF)],
["IF", "Interfaces between components, both endpoints and both owners.", len(P.IF_REG)],
["CR", "Change records, eight-state lifecycle, class and deciding question.", len(P.CR)],
["BKL", "Identified but not yet done.", len(P.BKL)],
["WVR", "Waivers. Empty by design — none were raised and the AI cannot self-grant one.", len(P.WVR)],
["CI / STD / RUN", "Configuration items, external standards with edition, and the AI run record.",
"%d / %d / %d" % (len(P.CI), len(P.STD), len(P.RUN))],
["SITE", "Every domain reached and what it saw. Zero banking or payment domains.", len(P.SITE)],
["EVD", "Evidence set. The traceability chain terminates here.", len(P.EVD)],
["FAH", "Function to action to help entry, fourteen fixed columns.", len(P.FAH)],
["RTM", "GENERATED traceability matrix, purpose through to evidence.", len(P.REQ)],
["CLOSURE_LOG", "GENERATED. Everything that must reach a terminal state, in one list.", "all"],
["GLOSSARY", "Every abbreviation used anywhere in this project.", len(P.GLOSSARY)],
]))
elif hid == "HLP-10":
t.append('<div class="note">These entries assume no AI is available and the author is unreachable. '
'None of them tells you to ask anyone as a first step (GOV-G3.8).</div>')
t.append(tbl(["Symptom (what you see)", "Likely cause", "Check (30 seconds)", "Fix", "If that fails"],
[[E(a), E(b), E(c), E(d), E(e)] for a, b, c, d, e in TROUBLE]))
t.append("<h3>Recovering from a bad state</h3>")
t.append("""<ol>
<li><b>Find the last known-good version.</b> Every superseded file is in <code>06_Archive/_versions/</code> with a
date stamp. Nothing in this project is ever deleted (GOV-D1.7).</li>
<li><b>Restore it.</b> Copy the file back to the path named in the Deliverables Map below.</li>
<li><b>Confirm the restore worked.</b> Run <code>python 01_System/checkers/run_checks.py</code> and read the final
line. If it says ALL CHECKS PASSED, the project is internally consistent again.</li>
<li><b>Or rebuild everything.</b> Every deliverable except the archived originals is generated. Run
<code>python 01_System/build_all.py</code> and the whole set is regenerated from
<code>01_System/project_data.py</code>.</li>
</ol>""")
t.append("<h3 id='limits'>Known limitations and known issues</h3>")
t.append(tbl(["What this project cannot do", "Detail"], [
["It does not model weekend or public holiday work",
"No arithmetically consistent SCHADS casual penalty rate could be sourced. The extrapolated figures "
"were withdrawn rather than published (DEF-003). BKL-002 closes this."],
["It does not model SIL or SDA financially",
"No property parameters were supplied. SIL is excluded from the recommended path on other grounds "
"(DEC-005), but if you revisit it the model needs a new branch (BKL-005)."],
["It excludes payroll tax, income tax and GST",
"Payroll tax is excluded on the assumption the business stays below the Victorian threshold "
"(ASM-003), which could not be confirmed from a primary source. Income tax and GST are out of scope."],
["Four figures rest on secondary corroboration, not a first-party read",
"fairwork.gov.au, fwc.gov.au, sro.vic.gov.au and several NDIS Commission pages block automated "
"fetching (ISS-001). Opening them in a browser yourself is the fastest confidence upgrade available."],
["The Victorian market-size figure is fourteen months old",
"The most recent Victoria-specific participant count located is dated 30 June 2025 (SRC-044). "
"BKL-004 closes this."],
["It is not advice",
"This is decision support, not legal, financial or tax advice, and not a substitute for the NDIS "
"Commission."],
]))
elif hid == "HLP-14":
t.append("<p>Every file this project produced. You should not have to open a file to find out what it "
"is (GOV-G4.3).</p>")
t.append(tbl(["File", "What it is", "Who it is for", "What question it answers", "Depends on"], [
["README.md", "The single door in. Reading order, current state, next action, how to verify.",
"Anyone, including a human with no AI", "Where do I start?", "Nothing"],
["Dashboard.html", "Generated status projection with a PC/Mobile toggle.", "Zaid",
"What is open, who owns it, what is next?", "project_data.py"],
["05_Outputs/AI_Project_Governance_Standard_v3.7.docx", "CI-001. The Governance Standard this whole "
"project was built to obey. Zaid's document, not an output of this project — filed here because "
"GOV-F5.6 says the thing the project must load every session still lives in OUTPUTS.",
"Zaid, and any operator taking over", "What rules bind this project?", "Nothing"],
["05_Outputs/AI_Project_Governance_Standard_v3.7.txt", "GENERATED complete plain-text projection of "
"the Standard, so every rule is readable with no Office suite (GOV-F9.8).", "An operator with no Word",
"The same, without Word", "The .docx it projects"],
["01_System/governance_core.md", "GENERATED subset carrying the 115 rules GOV-A1.6 requires be loaded "
"at the start of every session.", "The AI operating the project, at session start",
"What must I load before doing anything?", "The .docx it projects"],
["05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.pdf", "A read-only render of the study. Same content, "
"opens anywhere, no Word needed.", "Zaid on a phone, or anyone he sends it to",
"Can I read this without Word?", "The .docx it was rendered from"],
["05_Outputs/NDIS_and_Aged_Care_Business_Enabling_Study_v2.0.docx", "The full study, {{PAGES}} pages, with every source.",
"Zaid, and anyone he shows it to", "Should I start this business, and how would I disagree?",
"project_data.py, model_params.py, the diagrams"],
["05_Outputs/NDIS_and_Aged_Care_Financial_Model_v2.0.xlsx", "Live workbook, %d formulas across %d sheets, zero hard-coded results." % (COUNTS["formulas"], COUNTS["sheets"]),
"Zaid", "What happens if I change an assumption?", "model_params.py"],
["05_Outputs/NDIS_and_Aged_Care_Decision_Pack_v2.0.pptx", "{{SLIDES}} slides.", "Zaid, an accountant, a partner",
"What is the argument in ten minutes?", "model_params.py, the diagrams"],
["03_Registers/Project_Registers_v2.0.xlsx", "27 registers as typed sheets.", "Zaid, an auditor",
"What exactly does row SRC-002 say?", "project_data.py"],
["03_Registers/*.csv", "Plain-text projection of every register.", "An operator with no Excel",
"The same, without a spreadsheet application", "project_data.py"],
["09_Help_Hub/index.html", "This page. Troubleshooting, glossary, deliverables map, metric lineage.",
"Anyone using the deliverables", "How do I use this, and what do I do when it breaks?", "project_data.py"],
["00_Handover/", "Generated transfer pack with a SHA-256 manifest.", "The next operator",
"How do I take this project over?", "project_data.py"],
["01_System/project_data.py", "THE SINGLE SOURCE OF TRUTH. Every register lives here.",
"Whoever maintains the project", "Where do I change a fact?", "Nothing"],
["01_System/model_params.py", "The financial model in code.", "Whoever maintains the project",
"How is the margin actually computed?", "project_data.py conventions"],
["01_System/build_*.py, build_deck.js", "The generators.", "Whoever maintains the project",
"How do I regenerate everything?", "project_data.py, model_params.py"],
["01_System/checkers/run_checks.py", "The checker suite.", "Anyone",
"Is this project internally consistent?", "Everything"],
["01_System/threat_model.md", "Threat model and defence review.", "Zaid",
"What could go wrong with the project itself?", "Nothing"],
["02_Work/scratch/", "The raw research and independent verification records.", "An auditor",
"What did the research actually find, before it was summarised?", "Nothing"],
["02_Work/diagrams/", "%s as PNG, %d in all." % (COUNTS["diagram_range"], COUNTS["diagrams"]), "Anyone reusing a figure", "Can I have that chart on its own?",
"model_params.py"],
["04_Inputs/legacy_unsourced/", "The four superseded working files.", "An auditor",
"What was believed before this study?", "Nothing"],
["06_Archive/_versions/", "Dated snapshots taken before anything was moved.", "Anyone recovering",
"What did this look like before?", "Nothing"],
["07_Improvements/", "Improvement proposals, not self-applied.", "Zaid", "What would I do differently?",
"Nothing"],
["08_Reusable_Knowledge/", "Portable lessons for the next project.", "A future project",
"What is worth carrying forward?", "Nothing"],
]))
t.append("<h3>Reading order, by who you are</h3>")
t.append(tbl(["If you are...", "Read in this order"], [
["Zaid, deciding", "Dashboard.html → the decision pack → study sections 1, 16 and 19 → the %d open actions" % len(P.open_rows("ACT"))],
["Zaid, arguing with a number", "Study section 5.2 → the financial model Drivers sheet → SRC.csv and ASM.csv"],
["An accountant or adviser", "The decision pack → study sections 5, 8 and 12 → the financial model"],
["An auditor", "README.md → study Appendix B → 03_Registers/ → 02_Work/scratch/ → the checker run log"],
["A new operator taking over", "README.md → 00_Handover/HANDOVER.txt → 00_Handover/MANIFEST.txt → this Help Hub"],
]))
else:
t.append(body)
t.append("<h2 id='lineage'>What every number on the dashboard means<span class='help'>"
"<a href='../Dashboard.html'>back to the dashboard</a></span></h2>")
t.append('<div class="note">GOV-G3.7 requires plain-language data lineage for every displayed number: what it '
'means, the formula in words, where the data comes from, and as at when.</div>')
t.append(tbl(["Metric", "What it means", "Formula in words", "Source data", "As at"],
[[E(m[0]), E(m[1]), E(m[2]), E(m[3]), E(m[4])] for m in P.METRIC_LINEAGE]))
t.append("<h2 id='glossary'>Glossary<span class='help'><a href='../Dashboard.html'>back to the dashboard</a>"
"</span></h2>")
t.append('<div class="note">GOV-G5.6. Every acronym, abbreviation and technical term used anywhere in this '
'project.</div>')
t.append(tbl(["Term", "What it means"], [[E(g[0]), E(g[1])] for g in P.GLOSSARY]))
t.append("<h2>Function, action and help coverage</h2>")
t.append('<div class="note">Every function has at least one help entry and every help entry points at a live '
'function. Missing entries: 0. Orphan entries: 0. Stale entries: 0. Checked by '
'<code>01_System/checkers/run_checks.py</code> every build (GOV-G6.4).</div>')
t.append(tbl(["Function", "Where it lives", "You do this", "Help entry", "Status"],
[[E(f[1]), "<code>%s</code>" % E(f[2]), E(f[4]),
'<a href="#%s">%s — %s</a>' % (f[6], f[6], E(f[7])), badge(f[13], "closed")] for f in P.FAH]))
t.append("</main><footer>GENERATED FILE — do not hand-edit. Rebuilt from 01_System/project_data.py by "
"01_System/build_web.py.<br>Help Hub version %s, matching the project baseline. "
"Last verified %s against %s. · %s</footer>"
% (P.BASELINE_VERSION, P.CURRENCY_DATE, P.BASELINE_VERSION, E(P.STANDARD_VERSION)))
t.append("<script>%s</script></body></html>" % JS)
return "".join(t)
if __name__ == "__main__":
d = os.path.join(ROOT, "Dashboard.html")
open(d, "w", encoding="utf-8").write(_subst(dashboard()))
print("wrote", d)
h = os.path.join(ROOT, "09_Help_Hub", "index.html")
os.makedirs(os.path.dirname(h), exist_ok=True)
open(h, "w", encoding="utf-8").write(_subst(helphub()))
print("wrote", h)