#!/usr/bin/env python3 """Render the static ledger from graph.json. No client fetch, no JS dependency.""" import json, html, os, sys HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) G = json.load(open(os.path.join(HERE, "graph.json"))) S = G["sources"] NODE = {n["id"]: n for n in G["nodes"]} def e(x): return html.escape(str(x), quote=True) def money(v, unit="M"): return f"${v:,}" + (unit if unit else "") def cites(src, blocked=False): if isinstance(src, str): src = [src] out = [] for s in src: if s not in S: continue d = S[s] out.append(f'{e(d["publisher"])}') if blocked: out.append('join blocked') return '
THE GRAPH One bill · walked end to end · {c['nodes']} nodes · {c['edges']} edges
The argument on the front page asked for a permanent, queryable accountability graph. This is that graph, built to the full vocabulary and populated for a single bill: {e(bill["short_title"])}. Every node and every edge carries a citation. Nothing here is asserted without a link, and the places where Canada does not publish a joinable record are marked as gaps rather than filled in.
{c['distinct_acts']}
Acts amended or created
{c['nodes']}/{c['edges']}
Nodes / edges, all sourced
{c['populated_node_types']}/{len(G['schema']['node_types'])}
Node types populated
{len(G['blocked_joins'])}
Joins Canada will not make
{e(bill["long_title"])} — sponsored by the {e(bill["sponsor_title"])}, {e(bill["sponsor"])} ({e(bill["sponsor_riding"])}). First reading {e(bill["first_reading"])}, royal assent {e(bill["royal_assent"])}, enacted as {e(bill["statute"])}.
The chain the front page specified, traversed on real records. Each stage is a node in the graph; the type is the schema type it occupies.
Each of these needs two or more published sources placed side by side. Each is factual, each is linked, and none of them appeared as a headline.
On the record
{e(l["text"])}
{cites(l["src"])}Placed against
{e(r["text"])}
{cites(r["src"])}The schema is the full vocabulary from the specification. Populating it for one bill shows exactly which parts of Canada’s political economy are recordable today and which are not. The empty cells are the finding, not an omission.
{e(t.replace("_"," "))}
{v}
Edge types populated: {c['populated_edge_types']} of {len(G['schema']['edge_types'])} Empty = no joinable Canadian record for this bill
{e(t)}
{v}
This desk holds no secrets. Every dataset below is public. What is missing is the key that would let one be read against another.
{e(b["consequence"])}
{cites(b["src"])}Sector targets and projected disbursements in millions of dollars, from the Parliamentary Budget Officer’s 10 July 2025 update. Two of these targets were raised after the fact.
| Sector | Target | By 2027-28 | By 2029-30 | Achieved by 2027-28 | Gap 2027-28 | Target changed |
|---|---|---|---|---|---|---|
| {e(t["sector"])} | {t["target"]:,} | {t["by_2027_28"]:,} | {t["by_2029_30"]:,} | =100 else "negatived" if frac<50 else "amended"}">{e(pct)} | {t["gap_2027_28"]:,} | {e(chg)} |
{e(sectors["note"])}
{cites("pbo2025")}Derived mechanically from the table of provisions of {e(sub["statute"])}. This is the dependency graph the front page said someone has to construct. It took a script and one public page.
| Part | Div | Division subject | Act | Relation | From s. |
|---|---|---|---|---|---|
| {a["part"]} | {a["division"] or "—"} | {e(a["division_title"] or "—")} | {e(a["act"])} | {e(rel)} | {e(a["first_section"] or "—")} |
Two time-allocation motions carried; twelve report-stage amendments negatived. Retrieved as structured data from LEGISinfo, not transcribed.
| No. | Date | Subject | Yeas | Nays | Result |
|---|---|---|---|---|---|
| {d["n"]} | {e(d["date"])} | {e(d["subject"])} | {d["yeas"]} | {d["nays"]} | {e(d["result"])} |
| Committee | Meeting | Date |
|---|---|---|
| {e(m["committee"])} | {e(m["number"])} | ' f'{e(m["date"])} |
Primary records unless marked otherwise. Three were read by machine rather than by hand, which is the point of the exercise.
{e(d["kind"])} · {e(k)}
{e(d["title"])}{e(d["publisher"])}
{f'machine-read into the graph
' if d.get("machine_read") else ""} {f'{e(d["note"])}
' if d.get("note") else ""}The bill record — stages, sponsor, seventeen recorded divisions, ten committee meetings — was read as structured data from LEGISinfo. The {c['distinct_acts']}-Act amendment graph was derived by parsing the table of provisions of {e(sub["statute"])} on the Justice Laws site. The sector delivery table was extracted from the Parliamentary Budget Officer’s July 2025 PDF. Those three joins are reproducible: re-run the scripts and you get this page.
The influence, ownership and revolving-door nodes were assembled by hand from named public
records — a designation order, two ministerial releases, a Prime Minister’s Office
backgrounder, two press releases from the parties to the loan, an Auditor General report and a
committee report. Every one is linked. Two sources are marked secondary: they are
news reporting used for a date and a job title, not for a dollar figure.
It is not an allegation. Nothing here asserts that any person broke a rule. Every step in the Sabia chain is a published appointment or a published transaction; the finding is that no Canadian institution joins them, so no citizen can see the sequence without doing what this page did. It is also not complete: {len(G['schema']['node_types']) - c['populated_node_types']} node types and {len(G['schema']['edge_types']) - c['populated_edge_types']} edge types are empty, because the records that would fill them either are not published, are not keyed to anything, or are gated against retrieval.
This is one bill. The schema is the whole vocabulary, so the next bill adds rows rather than a rebuild. That is the difference between an article and an accounting system.
The graph is served as it was built: graph.json — {c['nodes']} nodes, {c['edges']} edges, {len(S)} sources, every record carrying its citation. The three machine joins are published with it, so the claim above is checkable rather than asserted: extract_statutes.py parses the table of provisions into the amendment graph, build_graph.py assembles nodes and edges and refuses any record without a citation, and gen_site.py renders this page from the result. Take it and extend it.