pipelines/apply_motif_roster.py
#!/usr/bin/env python3
"""Writer: THE CARDINAL MOTIF ROSTER RECONCILIATION on T0_Theme_Registry.
RULED by Josh 2026-07-29 (twenty-second sitting, ruling 4 -- "On 4 yes approve"), recorded at
docs/spine/DECISIONS_PENDING_JOSH.md. The work order itself is the APPROVED block inside
docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md **section 1.5** ("The cardinal roster, reconciled
against the live registry", the approval banner at line 186). NOTE THE CITATION DRIFT, flagged and
not silently fixed here: both the ruling record and the apply brief label that work order "1.9";
on disk today 1.9 is "The care line, stated as two structural rules". Section 1.9 IS load-bearing
for this apply (it supplies the region-as-threat care clause quoted into JOURNEY_WORLD's
hard_line_relevance), but the roster reconciliation lives at 1.5. Every citation written into the
registry by this script points at 1.5.
WHAT THIS DOES (deterministic, idempotent-by-hard-fail, fail-stop clean):
(1) MINTS TWO COLUMNS, appended at the END of the header (mandatory -- harness/registry_fidelity.py
lines 177-190 require the candidate header to EXTEND the frozen reference header as a prefix
match, so a mid-header insert fails the gate):
row_class -- cardinal | cue_variant | timbral_signature
parent_theme_id -- blank except on cue_variant rows
(2) THE CASSIUS COLLAPSE. CASSIUS_PHASE_1 survives as THE cardinal Cassius motif (theme_id
UNCHANGED -> zero FK churn: T0_Scene_Spec_Registry references all three ids 18 times,
T0_Voice_Registry once each, T0_Quest_Definition_Registry twice). It is retitled
"Cassius Velheim"; _2 and _3 become row_class=cue_variant with parent_theme_id=CASSIUS_PHASE_1.
EVERY hard_line_relevance cell is preserved BYTE-IDENTICAL (the HL_0109 guardrail, the
Ch-76-only timing, the HL_0035 naming clause). composer_in_loop_required stays True on all
three rows.
(3) MINTS TWO NEW CARDINAL ROWS, APPENDED AT THE END OF THE FILE: HOME_AND_LOSS and
JOURNEY_WORLD. "The freed slots" of the work order is a ROSTER-COUNT statement, not a
physical row position -- inserting mid-file would shift every later row index and blow up
the fidelity gate's positional cell comparison (registry_fidelity.py lines 215-219).
(4) RETITLES VIMANA_ACTIVATION to the wonder-identity framing; canonical_anchor UNCHANGED.
(5) Stamps recent_changes / last_updated / version on the CONTENT-TOUCHED rows only.
DELIBERATELY NOT DONE HERE (each flagged in PROPOSED_ROWS.md):
- docs/fidelity_baseline.json is NOT re-emitted. That is the director's landing step:
python harness/registry_fidelity.py --emit-baseline
It will pick up added_columns +2, added_rows 2, and the new expected_cell_diffs.
- docs/registry_extensions.json is NOT written unless --manifest is passed. The I2 rule of
harness/check_registry_extensions.py requires EVERY baseline added_column to be owned by
exactly one system, so the two new columns MUST gain a manifest entry or that gate goes red.
--manifest performs that one merge idempotently; the exact JSON block is also printed.
- No tier / motif_class / reveal_gate / deny_register_list column is minted. Those are the
Appendix S schema wave; this pass mints exactly the two columns the ruling names.
Usage:
python apply_motif_roster.py --src <live.csv> --dst <out.csv> [--manifest <registry_extensions.json>]
Exit 0 on a verified apply; non-zero (and nothing written) on any precondition or postcondition
failure.
"""
import argparse
import csv
import io
import json
import os
import sys
import tempfile
# ---------------------------------------------------------------------------
# THE ROW_CLASS TOKEN -- a declared divergence, isolated to one constant.
#
# The apply brief and the roster-of-twelve verification say row_class = "cardinal".
# Appendix S of the doctrine enumerates the column as
# cardinal_theme | family_motif | tag | tuned_bed | cue_variant | timbral_signature
# i.e. "cardinal_theme", not "cardinal". The other two tokens this pass can emit
# ("cue_variant", "timbral_signature") match Appendix S exactly; only the cardinal token
# diverges. The brief wins here because it is the ruling-bearing instruction and its own
# acceptance test counts row_class == "cardinal". If the director prefers the Appendix S
# spelling, flip this ONE constant and re-run -- nothing else changes.
CARDINAL = "cardinal_theme" # DIRECTOR RULING at review: Appendix S (doctrine line 2006) owns the enum
CUE_VARIANT = "cue_variant"
# Set True to also stamp recent_changes/last_updated/version on the eight rows that receive
# ONLY the new-column population (no content edit). Default False: populating a newly minted
# schema column is the column mint, not a row edit, and stamping all eight would add 24
# cell diffs carrying no information. One-line reversal if the director rules otherwise.
STAMP_SCHEMA_ONLY_ROWS = False
STAMP_DATE = "2026-07-29T00:00:00Z"
NEW_VERSION = "1.1.0"
EXPECTED_HEADER = [
"theme_id", "theme_name", "theme_type", "canonical_anchor", "mood_tags",
"instrumentation_substrate", "cultural_substrate", "era_substrate",
"recurrence_anchors", "hard_line_relevance", "composer_in_loop_required",
"generation_status", "generation_prompt_hash", "last_generated_timestamp",
"music_cue_id_ref", "cinematic_anchor_array", "notes", "extensions",
"populated_from", "recent_changes", "last_updated", "version", "tempo_bpm",
"bar_length", "per_stem_role", "ue_sound_asset_path",
]
NEW_COLUMNS = ["row_class", "parent_theme_id"]
EXPECTED_IDS = [
"PROTAGONIST_THEME", "HOUSE_OF_VELHEIM_THEME", "ARCHITECT_THEME",
"CASSIUS_PHASE_1", "CASSIUS_PHASE_2", "CASSIUS_PHASE_3", "GRAND_SAGE_REVEAL",
"ENDING_TRULY_GOOD", "ENDING_GOOD_ENOUGH", "ENDING_EVIL", "VIMANA_ACTIVATION",
"FAMILIAR_BOND",
]
# Cells asserted to hold these exact values BEFORE they are changed.
PRE = {
"CASSIUS_PHASE_1": {"theme_name": "Cassius Phase 1"},
"CASSIUS_PHASE_2": {"theme_name": "Cassius Phase 2"},
"CASSIUS_PHASE_3": {"theme_name": "Cassius Phase 3"},
"VIMANA_ACTIVATION": {"theme_name": "Vimana Activation"},
}
# Every row in the live file carries these; asserted on the content-touched rows.
PRE_COMMON = {"recent_changes": "[]", "last_updated": "2026-05-16T15:30:00Z",
"version": "1.0.0"}
TOUCHED = ["CASSIUS_PHASE_1", "CASSIUS_PHASE_2", "CASSIUS_PHASE_3", "VIMANA_ACTIVATION"]
# ---------------------------------------------------------------------------
# AUTHORED TEXT. Every newly authored byte is pure ASCII (asserted below); the preserved
# originals keep their own non-ASCII characters untouched, which is what the
# prefix/suffix construction below proves cell by cell.
NOTES_PREFIX = {
"CASSIUS_PHASE_1": (
"THE CARDINAL CASSIUS MOTIF -- one motif with three logged transformations, per the RULED "
"cardinal-roster reconciliation (docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5, "
"approved by Josh at the twenty-second sitting 2026-07-29; the ruling record labels that "
"work order section 1.9). P1a SOCIAL is this row and carries the head statement; P1b COMBAT "
"is CASSIUS_PHASE_2 and P1c RESOLUTION is CASSIUS_PHASE_3, both re-classed row_class = "
"cue_variant with parent_theme_id pointing here. The theme_id is unchanged so no foreign key "
"moves: T0_Scene_Spec_Registry, T0_Voice_Registry and T0_Quest_Definition_Registry keep every "
"existing reference to all three ids. THE ROSTER-OF-TWELVE QUERY IS row_class = cardinal. "
"Escalation across the three stages is carried by carrier migration and transformation rather "
"than by three separate identities (section 6.5), and composer_in_loop_required stays True on "
"all three rows because Cassius material is never generated (HL_0109). Tier A, motif_class "
"tune -- the tier and motif_class columns belong to the Appendix S schema wave and are not "
"minted by this pass. ORIGINAL NOTE, preserved unchanged: "),
"CASSIUS_PHASE_2": (
"CUE VARIANT of CASSIUS_PHASE_1 -- the COMBAT transformation of the one cardinal Cassius "
"motif, not a separate identity (the RULED cardinal-roster reconciliation, "
"docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5, approved 2026-07-29 twenty-second "
"sitting). row_class = cue_variant, parent_theme_id = CASSIUS_PHASE_1. The row survives in "
"place with its anchors, scene array and hard-line clauses unchanged so no foreign key moves, "
"and it sits OUTSIDE the roster of twelve (the roster query is row_class = cardinal). "
"composer_in_loop_required stays True -- Cassius material is never generated (HL_0109). "
"ORIGINAL NOTE, preserved unchanged: "),
"CASSIUS_PHASE_3": (
"CUE VARIANT of CASSIUS_PHASE_1 -- the RESOLUTION transformation of the one cardinal Cassius "
"motif, not a separate identity (the RULED cardinal-roster reconciliation, "
"docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5, approved 2026-07-29 twenty-second "
"sitting). row_class = cue_variant, parent_theme_id = CASSIUS_PHASE_1. The row survives in "
"place with its anchors, its three-ending scene array and its hard-line clauses unchanged so "
"no foreign key moves, and it sits OUTSIDE the roster of twelve (the roster query is "
"row_class = cardinal). composer_in_loop_required stays True -- Cassius material is never "
"generated (HL_0109). ORIGINAL NOTE, preserved unchanged: "),
}
NOTES_SUFFIX = {
"VIMANA_ACTIVATION": (
"; RETITLED 2026-07-29 by the RULED cardinal-roster reconciliation "
"(docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5): this row occupies the vril and "
"wonder slot, and the wonder identity is a HARMONIC-TRANSFORMATION identity (motif_class "
"harmonic) whose cardinal payoff surface is the Vimana activation. It therefore takes the "
"progression-recognition test of Appendix T.4 and never the hummability checklist, and that "
"test must be authored and dry-run before this theme starts. The canonical_anchor is "
"UNCHANGED and stays as canon has it. row_class = cardinal"),
}
RECENT = {
"CASSIUS_PHASE_1": (
"2026-07-29 THE CARDINAL MOTIF ROSTER RECONCILIATION, RULED by Josh (twenty-second sitting, "
"ruling 4): this row becomes THE cardinal Cassius motif; theme_name retitled to Cassius "
"Velheim; row_class cardinal; CASSIUS_PHASE_2 and CASSIUS_PHASE_3 re-classed as its "
"cue_variants; theme_id unchanged so no FK churn; hard_line_relevance preserved verbatim"),
"CASSIUS_PHASE_2": (
"2026-07-29 THE CARDINAL MOTIF ROSTER RECONCILIATION, RULED by Josh (twenty-second sitting, "
"ruling 4): re-classed row_class cue_variant with parent_theme_id CASSIUS_PHASE_1 -- the "
"COMBAT transformation of the one cardinal Cassius motif; anchors, scene array and "
"hard-line clauses unchanged"),
"CASSIUS_PHASE_3": (
"2026-07-29 THE CARDINAL MOTIF ROSTER RECONCILIATION, RULED by Josh (twenty-second sitting, "
"ruling 4): re-classed row_class cue_variant with parent_theme_id CASSIUS_PHASE_1 -- the "
"RESOLUTION transformation of the one cardinal Cassius motif; anchors, scene array and "
"hard-line clauses unchanged"),
"VIMANA_ACTIVATION": (
"2026-07-29 THE CARDINAL MOTIF ROSTER RECONCILIATION, RULED by Josh (twenty-second sitting, "
"ruling 4): theme_name retitled to the wonder-identity framing (a harmonic-transformation "
"identity whose cardinal payoff surface is the Vimana activation); canonical_anchor "
"unchanged; row_class cardinal"),
}
RECENT_NEW = (
"2026-07-29 MINTED by THE CARDINAL MOTIF ROSTER RECONCILIATION, RULED by Josh (twenty-second "
"sitting, ruling 4), into a Tier-A slot freed by the Cassius collapse; the cardinal roster pins "
"at exactly twelve")
NEW_NAME = {
"CASSIUS_PHASE_1": "Cassius Velheim",
"VIMANA_ACTIVATION": "Vril and Wonder: the Vimana Activation",
}
# --- the two new cardinal rows -------------------------------------------------------------
HOME_AND_LOSS = {
"theme_id": "HOME_AND_LOSS",
"theme_name": "Home and Loss",
"theme_type": "concept",
"canonical_anchor": (
"the fairy-realm home and the one thing its abundance cannot give back: Talvaeren, the "
"sovereign seat and the childhood home inside it (docs/spine/CH_PROLOGUE.md, "
"docs/spine/CH_01.md), the mother written by relation and never by name, and the Mother's "
"Amulet (EQ_0001) passed from her throat to the newborn at CHPRO_B07, taken off the neck on "
"the last morning at CH01_B13, crossing the seam with the child at CH01_B08 and given to the "
"healer's child at the Ch-13 departure at CH13_B13 (the ARC_0001 provenance chain)"),
"mood_tags": json.dumps(["intimate", "warm", "mournful", "contemplative"]),
"instrumentation_substrate": (
"orchestral at chamber scale with a single solo carrier voice; the fairy-realm register of "
"the Prologue and Ch 1 (the ceiling-abundance and childhood-abundance cues), full and "
"luminous and unhurried at the origin, reduced to near-silence at the mother's scene, and "
"returning to the abundance register intact and unironised at the exit"),
"cultural_substrate": "",
"era_substrate": "all eras across 77-chapter arc; origin statement at the Prologue and Ch 1",
"recurrence_anchors": json.dumps([
"the origin statements and the departure payoff on the v3.1 spine -- beats CHPRO_B07, "
"CHPRO_B16, CH01_B13, CH01_B08, CH02_B11 and CH13_B13; no SCENE_ rows exist for these "
"nodes (T0_Scene_Spec_Registry currently covers the endgame only), so the anchors are "
"named as beats rather than minted as a dangling scene FK"]),
"hard_line_relevance": (
"the Prologue and Ch 1 are hard-line nodes and show-don't-tell is absolute: NO prophetic or "
"foreshadow cue may sound anywhere in the origin (HL_0116 plus HL_0043 plus HL_0046) -- no "
"dread swell under the chase, no held note as the amulet comes off the neck, no minor turn "
"before the rift, and the exit returns to the abundance register unironised because the "
"plenitude is REAL and the score must never retroactively mourn it. The mother's death has "
"no author and no intent, so this motif shares NO material with HOUSE_OF_VELHEIM_THEME "
"before that theme's Ch-38 gate: a loss cue that reads as aimed at the royal house leaks the "
"antagonist pattern roughly thirty-eight chapters early. The mother is written by relation "
"and never by name (HL_0099) and the ward is uninscribed and unidentified before the Ch-8 "
"reveal (HL_0095), so no sung, chanted or spoken text may name either. SHARED-MATERIAL "
"GOVERNANCE per section 1.8: this motif is DECLARED INDEPENDENT of PROTAGONIST_THEME's head "
"cell, because PROTAGONIST_THEME carries a gated thematic kinship recorded in its own notes "
"field and any material shared with it would transmit that gated kinship into a Prologue "
"statement, which is the earliest possible statement in the game; if a later pass proposes "
"shared material anyway, it is governed by 1.8 and every pre-gate statement takes the gate-8 "
"reveal check with a fresh-context critic and its archived capture"),
"composer_in_loop_required": "True",
"generation_status": "pending",
"generation_prompt_hash": "",
"last_generated_timestamp": "",
"music_cue_id_ref": "",
"cinematic_anchor_array": "",
"notes": (
"Tier A, motif_class tune, row_class cardinal -- NEW row minted by the RULED cardinal-roster "
"reconciliation (docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5, approved by Josh "
"at the twenty-second sitting 2026-07-29) into a Tier-A slot freed by the Cassius collapse. "
"THE IDENTITY: the home that was whole and the loss that is inside it -- the register the "
"whole seventy-five-chapter climb aches back toward, which is why it must be established as "
"an unqualified good BEFORE it is ever a wound. It is a cohort-one theme (section 7.10), so "
"it is composed early alongside PROTAGONIST_THEME, HOUSE_OF_VELHEIM_THEME and FAMILIAR_BOND "
"and evaluated for combinability at cohort close. Its payoff surface is the Ch-13 departure, "
"where the amulet passes onward. The tier, motif_class, reveal_gate, closure_policy and "
"allow/deny register-list columns belong to the Appendix S schema wave and are not minted by "
"this pass; cultural_substrate is left BLANK rather than half-filled, because section 1.9 "
"makes the allow list meaningless without the deny list and its region-as-threat check, and "
"the deny column does not exist yet"),
"extensions": "{}",
"populated_from": json.dumps({
"theme_name": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.5"],
"canonical_anchor": ["docs/spine/CH_PROLOGUE.md", "docs/spine/CH_01.md",
"docs/spine/CH_13.md", "T0_Equipment_Registry @ EQ_0001",
"T0_Arc_Index @ ARC_0001",
"docs/proposals/AMULET_PROVENANCE_BRIEF.md"],
"instrumentation_substrate": ["docs/spine/CH_PROLOGUE.md # music_mood",
"docs/spine/CH_01.md # music_mood"],
"hard_line_relevance": ["T0_Hard_Lines @ HL_0116", "T0_Hard_Lines @ HL_0099",
"T0_Hard_Lines @ HL_0095", "T0_Hard_Lines @ HL_0043",
"T0_Hard_Lines @ HL_0046",
"docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.8"],
"composer_in_loop_required": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.4"]}),
"recent_changes": json.dumps([RECENT_NEW]),
"last_updated": STAMP_DATE,
"version": "1.0.0",
"tempo_bpm": "", "bar_length": "", "per_stem_role": "", "ue_sound_asset_path": "",
"row_class": CARDINAL, "parent_theme_id": "",
}
JOURNEY_WORLD = {
"theme_id": "JOURNEY_WORLD",
"theme_name": "Journey and World",
"theme_type": "concept",
"canonical_anchor": (
"the overworld and travel identity of the seventy-seven-chapter journey: the 78 inter-node "
"travel seams of the 79-node spine (Prologue through Epilogue), typed T0 CONTINUOUS / T1 "
"THRESHOLD / T2 CONVEYANCE / T3 RITUAL ELISION under the RULED tiered-traversal doctrine -- "
"playable navigable crossings compressed in DISTANCE and never in accuracy (the West Texas "
"principle), with the important crossings built as real staged or open dungeons "
"(docs/proposals/systems/SEAM_TYPING_SPEC.md sections 0.1-0.2)"),
"mood_tags": json.dumps(["heroic", "contemplative", "hopeful", "warm"]),
"instrumentation_substrate": (
"an orchestral home voice with a single solo carrier, re-scored into each region's own "
"register as the journey crosses it: period-appropriate instrumentation per region and per "
"era, wordless voice only (hums, chants, harmonies) and only where the register card allows "
"it, and ambient texture treated as part of the score. The carrier migrates and the tune "
"does not change -- interchangeable rather than additive layering, so the melody is present "
"at every intensity and no state is left without it"),
"cultural_substrate": "",
"era_substrate": "all eras across 77-chapter arc",
"recurrence_anchors": json.dumps([
"every inter-node travel seam across the 79-node spine (78 seams, typed T0-T3; zero typed "
"on disk today per SEAM_TYPING_SPEC section 0.3) plus the per-region overworld traversal "
"of each chapter"]),
"hard_line_relevance": (
"the per-region re-scoring is REFERENCE-COMPOSITION in each tradition's own terms -- "
"instrumentation, mode or scale, ensemble shape and era descriptors -- and is never sampling "
"of consecrated performance and never a fine-tune on one (the sourcing law). CVD 17.1 "
"collective protection expressed in music binds this theme harder than any other, because "
"this is the motif that visits every region: A REGION'S REGISTER IS NEVER THE VILLAINY "
"SIGNAL, the register itself may not be systematically darkened, inverted or menace-coded, "
"and this theme may never become the recurring cue for threat in any region it enters -- the "
"darkening operations belong to HOUSE_OF_VELHEIM_THEME and travel with it. Care means weight "
"and authenticity: care-tier inflation is a defect, and a generic world-music wash under "
"this theme is a care failure rather than a taste note. Per section 1.9 the allow and deny "
"register lists are required fields with an explicit region-as-threat check, and a motif "
"with an empty deny list has not been reviewed"),
"composer_in_loop_required": "True",
"generation_status": "pending",
"generation_prompt_hash": "",
"last_generated_timestamp": "",
"music_cue_id_ref": "",
"cinematic_anchor_array": "",
"notes": (
"Tier A, motif_class tune, row_class cardinal -- NEW row minted by the RULED cardinal-roster "
"reconciliation (docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md section 1.5, approved by Josh "
"at the twenty-second sitting 2026-07-29) into a Tier-A slot freed by the Cassius collapse. "
"THE DEFINING TRANSFORMATION SURFACE IS THE PER-CULTURE REGISTER MIGRATION: the same tune "
"re-scored into an entirely different civilisational voice and still recognisably the same "
"tune (the Nerevar Rising to Dragonborn precedent, section 1.7), which is the mechanism that "
"makes a 79-node world tour possible and is bounded by the care line of section 1.9. Its "
"transformation budget is spent on orchestration migration, register substitution and "
"reharmonisation under a fixed melody rather than on new material (section 6.5), and its "
"head-motif stinger is composed to each region's tonic and its relative only. It is a "
"cohort-one theme (section 7.10). The tier, motif_class, reveal_gate, closure_policy and "
"allow/deny register-list columns belong to the Appendix S schema wave and are not minted by "
"this pass; cultural_substrate is left BLANK rather than half-filled, because section 1.9 "
"makes the allow list meaningless without the deny list and its region-as-threat check, and "
"the deny column does not exist yet"),
"extensions": "{}",
"populated_from": json.dumps({
"theme_name": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.5"],
"canonical_anchor": ["docs/proposals/systems/SEAM_TYPING_SPEC.md 0.1-0.2",
"docs/spine/DECISIONS_PENDING_JOSH.md # RULED 2026-07-27 FORK C"],
"instrumentation_substrate": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 0",
"docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 6.5"],
"recurrence_anchors": ["docs/proposals/systems/SEAM_TYPING_SPEC.md 0.3"],
"hard_line_relevance": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.9", "CVD 17.1"],
"composer_in_loop_required": ["docs/proposals/MUSIC_COMPOSITION_DOCTRINE.md 1.4"]}),
"recent_changes": json.dumps([RECENT_NEW]),
"last_updated": STAMP_DATE,
"version": "1.0.0",
"tempo_bpm": "", "bar_length": "", "per_stem_role": "", "ue_sound_asset_path": "",
"row_class": CARDINAL, "parent_theme_id": "",
}
NEW_ROWS = [HOME_AND_LOSS, JOURNEY_WORLD]
MANIFEST_SYSTEM = "motif-roster"
MANIFEST_KEY = "T0_Theme_Registry/T0_Theme_Registry"
class Stop(Exception):
pass
def need(cond, msg):
if not cond:
raise Stop(msg)
def read_csv(path):
raw = open(path, "rb").read()
need(not raw.startswith(b"\xef\xbb\xbf"), "source carries a UTF-8 BOM; this writer expects none")
text = raw.decode("utf-8")
crlf = raw.count(b"\r\n")
lf = raw.count(b"\n")
need(crlf == lf, f"mixed line endings: {crlf} CRLF of {lf} LF -- refusing to normalise silently")
term = "\r\n" if crlf else "\n"
rows = list(csv.reader(io.StringIO(text, newline="")))
# Prove the reader/writer round-trip is byte-exact on THIS file before trusting it to
# preserve every untouched cell.
buf = io.StringIO(newline="")
csv.writer(buf, quoting=csv.QUOTE_MINIMAL, lineterminator=term).writerows(rows)
need(buf.getvalue().encode("utf-8") == raw,
"csv round-trip is NOT byte-exact on this file; a cell-level rewrite would reformat "
"untouched cells")
return raw, rows, term
def write_csv(path, rows, term):
buf = io.StringIO(newline="")
csv.writer(buf, quoting=csv.QUOTE_MINIMAL, lineterminator=term).writerows(rows)
data = buf.getvalue().encode("utf-8")
d = os.path.dirname(os.path.abspath(path)) or "."
os.makedirs(d, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
try:
with os.fdopen(fd, "wb") as fh:
fh.write(data)
os.replace(tmp, path)
except BaseException:
if os.path.exists(tmp):
os.unlink(tmp)
raise
return data
def merge_manifest(path, columns):
man = json.load(open(path, encoding="utf-8"))
ext = man.setdefault("extensions", {})
key = ext.setdefault(MANIFEST_KEY, {})
owned = {c for cols in key.values() for c in cols}
dup = owned & set(columns)
need(not dup, f"manifest already owns {sorted(dup)} under another system (I2 forbids two owners)")
cur = key.setdefault(MANIFEST_SYSTEM, [])
for c in columns:
if c not in cur:
cur.append(c)
text = json.dumps(man, indent=1, ensure_ascii=False) + "\n"
d = os.path.dirname(os.path.abspath(path)) or "."
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as fh:
fh.write(text)
os.replace(tmp, path)
except BaseException:
if os.path.exists(tmp):
os.unlink(tmp)
raise
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--src", required=True)
ap.add_argument("--dst", required=True)
ap.add_argument("--manifest", default=None,
help="optional: merge the two new columns into docs/registry_extensions.json "
"under the 'motif-roster' system (check_registry_extensions I2)")
a = ap.parse_args()
raw, rows, term = read_csv(a.src)
# ---- PRECONDITIONS ---------------------------------------------------------------
hdr = rows[0]
for c in NEW_COLUMNS:
need(c not in hdr, f"column '{c}' already present -- ALREADY APPLIED, refusing to re-apply")
need(len(rows) == 13, f"expected header + 12 rows, found {len(rows)} lines")
need(hdr == EXPECTED_HEADER, "header does not match the expected 26-column live schema")
for i, r in enumerate(rows):
need(len(r) == 26, f"line {i} width {len(r)}, expected 26 (ragged source)")
ids = [r[0] for r in rows[1:]]
need(ids == EXPECTED_IDS, f"theme_id roster mismatch: {ids}")
col = {n: i for i, n in enumerate(hdr)}
by_id = {r[0]: r for r in rows[1:]}
for tid, cells in PRE.items():
for cname, want in cells.items():
got = by_id[tid][col[cname]]
need(got == want, f"{tid}.{cname} expected {want!r}, found {got!r}")
for tid in TOUCHED:
for cname, want in PRE_COMMON.items():
got = by_id[tid][col[cname]]
need(got == want, f"{tid}.{cname} expected {want!r}, found {got!r}")
need(all(r[col["composer_in_loop_required"]] == "True" for r in rows[1:]),
"not every live row is composer_in_loop_required=True")
for d in NEW_ROWS:
need(d["theme_id"] not in by_id, f"{d['theme_id']} already exists")
need(set(d) == set(EXPECTED_HEADER) | set(NEW_COLUMNS),
f"new row {d['theme_id']} key set does not match the post-mint header")
original_notes = {t: by_id[t][col["notes"]] for t in TOUCHED}
src_snapshot = [list(r) for r in rows]
# ---- TRANSFORM -------------------------------------------------------------------
census = [] # (theme_id, column, before, after)
out = [hdr + list(NEW_COLUMNS)]
for r in rows[1:]:
tid = r[0]
n = list(r) + ["", ""]
rc, pc = len(EXPECTED_HEADER), len(EXPECTED_HEADER) + 1
if tid in ("CASSIUS_PHASE_2", "CASSIUS_PHASE_3"):
n[rc], n[pc] = CUE_VARIANT, "CASSIUS_PHASE_1"
else:
n[rc], n[pc] = CARDINAL, ""
census.append((tid, "row_class", "", n[rc]))
if n[pc]:
census.append((tid, "parent_theme_id", "", n[pc]))
if tid in NOTES_PREFIX:
new = NOTES_PREFIX[tid] + original_notes[tid]
need(new.endswith(original_notes[tid]), "notes prefix construction lost the original")
census.append((tid, "notes", r[col["notes"]], new))
n[col["notes"]] = new
if tid in NOTES_SUFFIX:
new = original_notes[tid] + NOTES_SUFFIX[tid]
need(new.startswith(original_notes[tid]), "notes suffix construction lost the original")
census.append((tid, "notes", r[col["notes"]], new))
n[col["notes"]] = new
if tid in NEW_NAME:
census.append((tid, "theme_name", r[col["theme_name"]], NEW_NAME[tid]))
n[col["theme_name"]] = NEW_NAME[tid]
stamp = tid in TOUCHED or STAMP_SCHEMA_ONLY_ROWS
if stamp:
rc_json = json.dumps([RECENT[tid]]) if tid in RECENT else json.dumps(
["2026-07-29 row_class populated by the cardinal motif roster reconciliation"])
for cname, val in (("recent_changes", rc_json), ("last_updated", STAMP_DATE),
("version", NEW_VERSION)):
census.append((tid, cname, r[col[cname]], val))
n[col[cname]] = val
out.append(n)
full_hdr = EXPECTED_HEADER + NEW_COLUMNS
for d in NEW_ROWS:
out.append([d[c] for c in full_hdr])
census.append((d["theme_id"], "<NEW ROW>", "", "28 cells authored"))
data = write_csv(a.dst, out, term)
# ---- POSTCONDITIONS (re-read from disk; never trust the in-memory object) ---------
raw2 = open(a.dst, "rb").read()
need(raw2 == data, "written bytes differ from what was composed")
need(not raw2.startswith(b"\xef\xbb\xbf"), "output gained a BOM")
crlf2, lf2 = raw2.count(b"\r\n"), raw2.count(b"\n")
need(crlf2 == lf2 if term == "\r\n" else crlf2 == 0,
f"output line endings drifted ({crlf2} CRLF / {lf2} LF)")
need(raw2.endswith(term.encode()), "output lost its trailing line terminator")
back = list(csv.reader(io.StringIO(raw2.decode("utf-8"), newline="")))
need(len(back) == 15, f"expected header + 14 rows, found {len(back)} lines")
need(back[0] == full_hdr, "output header is not the 28-column schema")
widths = {i: len(r) for i, r in enumerate(back) if len(r) != 28}
need(not widths, f"RAGGED ROWS in output: {widths}")
oids = [r[0] for r in back[1:]]
need(len(set(oids)) == 14, "duplicate theme_id in output")
need(oids[:12] == EXPECTED_IDS, "the twelve live rows moved or were reordered")
need(oids[12:] == [d["theme_id"] for d in NEW_ROWS], "new rows are not appended last")
ocol = {n: i for i, n in enumerate(back[0])}
classes = [r[ocol["row_class"]] for r in back[1:]]
need(classes.count(CARDINAL) == 12, f"cardinal count is {classes.count(CARDINAL)}, expected 12")
need(classes.count(CUE_VARIANT) == 2, f"cue_variant count is {classes.count(CUE_VARIANT)}")
need(set(classes) == {CARDINAL, CUE_VARIANT}, f"unexpected row_class tokens: {set(classes)}")
need(all(r[ocol["row_class"]] != "" for r in back[1:]), "a row has an empty row_class")
for r in back[1:]:
p = r[ocol["parent_theme_id"]]
if r[ocol["row_class"]] == CUE_VARIANT:
need(p in set(oids), f"{r[0]} parent_theme_id {p!r} does not resolve")
need(p == "CASSIUS_PHASE_1", f"{r[0]} parent is {p!r}")
else:
need(p == "", f"{r[0]} is {CARDINAL} but carries parent_theme_id {p!r}")
need(all(r[ocol["composer_in_loop_required"]] == "True" for r in back[1:]),
"composer_in_loop_required lost its True on some row")
for r in back[1:13]:
need(r[ocol["hard_line_relevance"]] ==
src_snapshot[oids.index(r[0]) + 1][col["hard_line_relevance"]],
f"{r[0]} hard_line_relevance changed -- these cells are care-load-bearing")
# every unchanged reference-zone cell must be BYTE-IDENTICAL to the source
intended = {(t, c) for t, c, _b, _a in census}
drift = []
for i, srow in enumerate(src_snapshot[1:], start=1):
orow = back[i]
for j, cname in enumerate(EXPECTED_HEADER):
if orow[j] != srow[j] and (srow[0], cname) not in intended:
drift.append((srow[0], cname))
need(not drift, f"UNINTENDED cell drift: {drift}")
# newly authored bytes are pure ASCII (the mojibake guard); preserved originals may not be
authored = "".join(
[NOTES_PREFIX[t] for t in NOTES_PREFIX] + [NOTES_SUFFIX[t] for t in NOTES_SUFFIX] +
list(NEW_NAME.values()) + list(RECENT.values()) + [RECENT_NEW] +
[str(v) for d in NEW_ROWS for v in d.values()])
bad = sorted({c for c in authored if ord(c) > 127})
need(not bad, f"authored text carries non-ASCII characters {bad} -- mojibake risk")
# reveal-leak guard: no NEW occurrence of the gated tokens
for token in ("Grand Sage", "GRAND_SAGE_REVEAL", "Ch 77", "Ch 76-77"):
b = raw.decode("utf-8").count(token)
a2 = raw2.decode("utf-8").count(token)
need(a2 == b, f"reveal-leak token {token!r} count moved {b} -> {a2}")
if a.manifest:
merge_manifest(a.manifest, NEW_COLUMNS)
print(f"OK {a.dst}")
print(f" rows {len(back) - 1} x cols {len(back[0])} | cardinal {classes.count(CARDINAL)}"
f" cue_variant {classes.count(CUE_VARIANT)} | line ending "
f"{'CRLF' if term == chr(13) + chr(10) else 'LF'}, no BOM")
print(f" intended diffs: {len(census)}")
for t, c, b, aft in census:
s = "" if c == "<NEW ROW>" else f" {b[:38]!r} -> {aft[:48]!r}"
print(f" {t:<24} {c}{s}")
print("\n REQUIRED AT LANDING (not done here):")
print(" python harness/registry_fidelity.py --emit-baseline")
print(" docs/registry_extensions.json needs, under extensions."
f"{MANIFEST_KEY!r}: {{\"{MANIFEST_SYSTEM}\": {json.dumps(NEW_COLUMNS)}}}"
+ (" [WRITTEN by --manifest]" if a.manifest else ""))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Stop as e:
sys.stderr.write(f"[motif_roster] FAIL-STOP: {e}\n")
sys.exit(2)