#!/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 '
' + "".join(out) + "
" if out else "" P = [] w = P.append sub = G["subject"]; c = G["counts"] bill = NODE["bill:c-44-42-1"] # ------------------------------------------------------------------ head w(f''' The Ledger — Bill C-44 walked end to end | Follow the Money
FTMFOLLOW
THE MONEY
THE LEDGER · BILL C-44 (42-1) · {e(sub["statute"])} The argument

THE GRAPH One bill · walked end to end · {c['nodes']} nodes · {c['edges']} edges

The Ledger

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

''') # ------------------------------------------------------------- the ten rows w(f'''
The bill ledger

Ten rows. One link per claim.

{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"])}.

''') for i, r in enumerate(G["ledger"], 1): blocked = r.get("blocked", False) w(f'''
{e(r["row"])}Row {i:02d} · {e(r["date"])}
{e(r["value"])}
{cites(r["src"], blocked)}
''') w('
') # ------------------------------------------------------------------- the walk w('''
The walk

Law → outcome. Thirteen stages, no breaks.

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.

''') for i, (stage, nid, val, src) in enumerate(G["stages"], 1): n = NODE.get(nid, {}) ty = n.get("type", "—").replace("_", " ") url = S[src]["url"] if src in S else "#sources" w(f''' {i:02d} {e(stage)} {e(val)} {e(ty)} · {e(S[src]["publisher"] if src in S else "")} ''') w('
') # ------------------------------------------------------------------ findings w('''
What the joins produce

Six statements no single record makes

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.

''') for f in G["findings"]: l, r = f["left"], f["right"] w(f'''

{e(f["claim"])}

On the record

{e(l["text"])}

{cites(l["src"])}

Placed against

{e(r["text"])}

{cites(r["src"])}
{e(f["join"])}
{f'
Note. {e(f["note"])}
' if f.get("note") else ""}
''') w('
') # ------------------------------------------------------------------ coverage w(f'''
Schema coverage

{c['populated_node_types']} of {len(G['schema']['node_types'])} node types carry records

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.

''') for t in G["schema"]["node_types"]: v = c["by_node_type"][t] w(f'

{e(t.replace("_"," "))}

{v}

') w('
') w(f'''

Edge types populated: {c['populated_edge_types']} of {len(G['schema']['edge_types'])} Empty = no joinable Canadian record for this bill

''') for t in G["schema"]["edge_types"]: v = c["by_edge_type"][t] w(f'

{e(t)}

{v}

') w('
') # ------------------------------------------------------------------- blocked w('''
Decision desk

The joins Canada does not publish

This desk holds no secrets. Every dataset below is public. What is missing is the key that would let one be read against another.

''') for b in G["blocked_joins"]: w(f'''

{e(b["join"])}

The question
{e(b["what"])}
Published?
{e(b["published"])}
Why it will not join
{e(b["why_blocked"])}

{e(b["consequence"])}

{cites(b["src"])}
''') w('
') # ------------------------------------------------------------------- delivery sectors = NODE["program:cib-sectors"] w(f'''
Promise against outcome

What was promised, what will arrive

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.

''') for t in sectors["targets"]: pct = t["pct_2027_28"] try: frac = int(str(pct).rstrip("%")) except ValueError: frac = 100 cls = "met" if frac >= 100 else ("short" if frac < 50 else "") chg = (f'from {money(t["original_target"])} to {money(t["target"])}' if t.get("original_target") else "—") tot = ' style="font-weight:600;background:#f2f2ee"' if t["sector"] == "Total" else "" w(f'''''') w(f'''
Projected disbursements and gaps with targets · millions of dollars · source: PBO Table 3
SectorTargetBy 2027-28 By 2029-30Achieved by 2027-28Gap 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")}
''') # ---------------------------------------------------------------- the diff w(f'''
The omnibus diff

{c['distinct_acts']} Acts, one bill, four Parts, twenty-one Divisions

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.

Every Act touched by C-44 {len(G["acts"])} references
''') for a in G["acts"]: rel = a["relation"] w(f'''''') w(f'
Part 4 divisions carry their own subject matter; a single Act can be touched by several divisions
PartDivDivision subject ActRelationFrom s.
{a["part"]}{a["division"] or "—"} {e(a["division_title"] or "—")}{e(a["act"])} {e(rel)} {e(a["first_section"] or "—")}
{cites("sc2017c20")}
') # ---------------------------------------------------------------- divisions w(f'''
What Parliament actually did

Seventeen recorded divisions

Two time-allocation motions carried; twelve report-stage amendments negatived. Retrieved as structured data from LEGISinfo, not transcribed.

Every recorded division on C-44 {len(G["divisions"])} votes
''') for d in G["divisions"]: res = "agreed" if d["result"] == "Agreed To" else "negatived" w(f'''''') w(f'''
No.DateSubjectYeas NaysResult
{d["n"]}{e(d["date"])} {e(d["subject"])}{d["yeas"]}{d["nays"]} {e(d["result"])}
{cites("legisinfo")}
Committee consideration {len(G["committee_meetings"])} meetings
''') for m in G["committee_meetings"]: w(f'' f'') w(f'
CommitteeMeetingDate
{e(m["committee"])}{e(m["number"])}{e(m["date"])}
{cites("legisinfo")}
') # ------------------------------------------------------------------ sources w('''
Evidence

Every source, once

Primary records unless marked otherwise. Three were read by machine rather than by hand, which is the point of the exercise.

''') for k, d in S.items(): w(f'''

{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 ""}
''') w('
') # ------------------------------------------------------------------- method w(f'''
Method and limits

How this was built, and what it is not

What a machine did

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.

What a person did

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.

What this is not

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.

Depth one

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 data and the code

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.

''') out = os.path.join(HERE, "site", "index.html") open(out, "w").write("\n".join(P)) print(f"wrote {out} — {os.path.getsize(out):,} bytes")