The controller (oldbox) sent a data-model alignment proposal and asked for six
additive changes on this node, the validation protocol, and an honest report.
Six of the seven changes are done; the seventh is declared missing rather than faked.
Stage 0 - reachability verified exactly as documented: health 200, heartbeat 401
(the correct answer - auth is refusing), missing Host 400. Nothing else was sent.
source_unit_key (controller 5.1, the blocking item)
umbraaudio/source_units.py. sha256("uk1" | chapter_source_text_sha256 |
occurrence | text_sha256). Nothing positional in the key: "occurrence"
disambiguates repeated text in document order.
- 18,181/18,181 units keyed across all four projects, 0 collisions
- chapter 1 renumbered +100 -> 0 keys changed (the load-bearing proof)
- backfill 10,015 rows in 0.11s, second run stamps 0 (idempotent)
- alignment phase 3: 200/200 probe units resolved with used_index: false
- tolerates the older projects that have no chapters.chapter_index
Recorded at generation time in chapter_builder, BEFORE the rows are read and
BEFORE the marker is written, so every path that renders audio also produces
identity and the clip can name its source unit.
5.7 recorded: utterance_index is KEPT as a display ordinal and made
non-load-bearing. spec 21 §10.
Blob hashes and effect chain (5.2)
Markers now carry the full sha256 of the delivered bytes and the effects that
produced them. Verified the existing order is already correct - write_marker
runs AFTER apply_effects - and closed the remaining hole: a changed trait list
changed the hash with no recorded cause. sha256 verification is opt-in, since
hashing every file on every validation pass would re-read gigabytes.
Vocabularies published (5.4) - umbraaudio/definitions.py, 19 sets, 126 values,
each value carrying whether it is 'declared' (spec/controller) or 'extracted'
(read out of code), so a diff shows a difference of meaning, not of wording.
Alignment report tool (6.6) - scripts/alignment_report.py --phase all. Emits the
controller's envelope with the per-phase evidence shapes, and obeys the two
rules they set: no evidence means blocked, never pass; a field we cannot produce
is null PLUS a finding, never omitted. Phase 2 is deliberately 'fail' - the
three-way store disagreement is real, and a pass there would be a check that
passed by doing nothing.
Fingerprint discipline (5.5) - the drift detector reported its OWN fingerprint
change when source_unit_key was added rather than hiding it. Node profile
updated with the new fingerprints and an honest note that they moved.
Node report extended - per-project source-unit-key coverage, per-voice-asset
sha256 + object_key, definition_set_version, controller stage reached.
Six gaps found in the proposal (spec 21 §11), including the one they invited plus
five more. The sharpest: "hash at generation" ignores that audio is
post-processed afterwards, and our key is anchored to chapters.source_text - a
second copy of the manuscript - so any change to chapter storage silently
invalidates every key in that chapter.
5.6 deliberately NOT faked: personality has no versioned artifact here, only
unversioned LLM prose in speakers.json. Reported as phase 6b finding B-03
instead, because wrapping unversioned inputs in a version field would produce
artifacts claiming a lineage they do not have.
Answered the controller's two open questions (infra/controller-handoff-2026-10-03.md):
forward-only link for a revealed recurring character, and yes - a new edition may
contradict a frozen state, since this node's correction path IS a regeneration.
Full suite 484 passed / 8 failed - the same 8 pre-existing and unrelated failures.
ruff clean on every file touched. The parallel session's chapter_builder hunk
(helper_url) was filtered out of the commit and left intact in the working tree.
10 KiB
Controller handoff — UmbraAudio node umbraaudio-m4, 2026-10-03
To: the Umbra control plane on oldbox (192.168.123.14:8442)
From: node umbraaudio-m4 (Mac mini M4), product umbraaudio
Subject: alignment proposal §5 and §6 — what is done, what is proven, what is missing
Status: Stage 0 verified. Stage 1 ready, blocked on one credential.
1. Stage 0 — reachability: PASS, all three as documented
GET /api/health (Host: umbratrans.bdmurphy.me) -> 200
POST /api/node/heartbeat (same Host, no credential) -> 401
GET /api/health (no Host header) -> 400
200on health.401 node_credential_requiredon the node surface — the correct answer, and evidence that authentication is working rather than being bypassed.- The
400withoutHostreproduces exactly as your document predicted. Noted so nobody later reads it as the deployment being down.
No other request was made. Nothing was sent beyond those three probes.
2. §5 changes — 6 of 7 complete, 1 deliberately deferred
| # | change | status | proof |
|---|---|---|---|
| 5.1 | source_unit_key |
done | see §3 |
| 5.2 | SHA-256 per blob at generation | done | marker now carries the full sha256 of the delivered bytes |
| 5.3 | Speaker model → chain mapping | done | alignment phase 2 evidence: per project, speaker_names → characters → speakers, plus every unresolved name listed |
| 5.4 | Publish vocabularies as JSON | done | definitions/umbraaudio-defs.json — 19 sets, 126 values, each with provenance |
| 5.5 | Keep schema fingerprints current | done | the drift detector reported its own fingerprint change rather than hiding it; profile updated |
| 5.6 | Personality version + provenance | deferred, honestly | no versioned personality artifact exists. What exists is unversioned LLM prose in speakers.json. Reported as phase 6b finding B-03, not glossed. |
| 5.7 | utterance_index decision |
recorded | kept as a display ordinal, made non-load-bearing. specs/21 §10 |
Why 5.6 is deferred rather than faked. Personality is the thing your §1.3 gets right — one per character, language-neutral, versioned. The node has an LLM character-discovery pass that writes exactly the wrong shape: prose fields, no version, no model record, no prompt version. Wrapping it in a version field without versioning the inputs would produce artifacts that claim a lineage they do not have. Declaring it missing is cheaper than a plausible lie.
3. §5.1 in detail — the part everything else depends on
source_unit_key = sha256("uk1" | chapter_source_text_sha256 | occurrence | text_sha256)
occurrencedisambiguates repeated text within a chapter, in document order. Nothing in the key is a position.- Filenames unchanged.
utterance_indexuntouched.is_currentunaffected. - 18,181 / 18,181 units keyed across all four projects. 0 collisions.
- Backfill of 10,015 rows takes 0.11 s; a second run changes 0 rows (idempotent).
- Renumbering proven safe: chapter 1 shifted
+100on everyutterance_index, then re-stamped — 0 keys changed. - Old schema shapes handled:
chapter_indexis absent on the two older projects, so the code falls back rather than assuming it.
Your §6.6.1 probe set, already selected
We choose it, you send us those exact units. Per your rule — sort source_unit_key ASC
(byte order), every 17th from index 0, cap 200 — on Last Dwarf:
- total keys: 10,015 · stride: 17 · selected: 200
selection_sha256:851257dfafebefea8ed2e4c8616ba03f7f4e0487bba6c849eff6f7618f1e5928
Our own pre-resolution of all 200: used_index: false for every single one. That is
the answer to your phase 3 question, before you have sent us anything.
4. §6 validation — per-phase verdict
| phase | verdict | why |
|---|---|---|
| 1 vocabulary | pass | 19 sets, 126 values, provenance included |
| 2 cardinality | fail | evidence present; the three-way store disagreement is real. A pass would be a check that passed by doing nothing |
| 3 identity | pass | 200/200 resolved without the index |
| 4 lineage | blocked | needs the artifact you pick |
| 5 timeline | blocked | no time-scoped attribute model here yet |
| 6 voice matrix | blocked | voices are not captured as structured matrix values |
| 6b persistence | blocked | persistence is not captured at all (finding B-01) |
| 6c editions | blocked | no edition concept (finding E-01) |
| 7 round trip | blocked | needs the ingest API |
Regenerate with scripts/alignment_report.py --phase all. Phases needing your side are
emitted as blocked with the reason, never omitted.
Phase 2, honestly
| project | speaker_names | characters | speakers (have lines) | characters w/o lines | speakers.json | voice_profiles | ref WAVs |
|---|---|---|---|---|---|---|---|
| Last Dwarf | 62 | 62 | 61 | 32 | 93 | 68 | 71 |
| DealHunter | 55 | 52 | 51 | 10 | 61 | 64 | 97 |
| Treasure Island | 34 | 34 | 33 | 7 | 40 | 41 | 44 |
| Vocals | 0 | 0 | 0 | 2 | 2 | 2 | 2 |
The disagreement is real and is not resolved by this node. Note it moved: Last Dwarf
speakers.json went 70 → 93 on 2026-09-25 when 23 genuinely missing speakers were recovered
mechanically.
The column your §1.2 asks for is the fourth one. 32 of Last Dwarf's 93 characters never
have lines — they are characters, not speakers, and the schema must hold them as such.
That distinction is now measured rather than assumed. DealHunter shows the other shape: 55
raw names collapse to 52 characters, so alias resolution is already merging three of
them.
5. Six gaps found — the fifth you asked for, and five more
Full detail in specs/21 §11. Ranked:
| ID | Sev | Gap |
|---|---|---|
| G-A | major | "hash at generation" — but audio is post-processed after generation. A generation-time hash identifies bytes that never shipped. Your order is already correct (write_marker runs after effects) — verified, and I closed the remaining hole by recording effects: [...] with the hash. |
| G-B | major | The key is anchored to chapters.source_text, a second copy of the manuscript. Any change to chapter storage silently invalidates every key in that chapter. Recommend treating the chapter-text hash as the source revision identity. |
| G-C | major | Your key spec contains source_revision_id; we have no revision concept, so we substitute a chapter-text hash. You must store our key verbatim, not recompute it — you cannot know our occurrence rule. |
| G-D | minor | Markers did not name their source unit. They do now. |
| G-E | major | Voice assets are keyed by name-derived slugs; the same slug can mean different voices in different projects. Report now emits each ref's sha256 + object_key. voice_assets must key on bytes and carry project scope. |
| G-F | minor | Keys are project-agnostic by content. This answers your §8.3 question, and it is a tenancy question as much as a design one. If isolation is required, salt at ingest (sha256(tenant ‖ key)) — doing it here would make keys non-portable for no security gain. |
6. Your §8.2 — the two open questions, answered
Q1 — a book_local character is revealed to recur. Merge, or forward-only link?
Forward-only link. Concur with D16, and this node has direct evidence for it: the
designation work on 2026-09-25 found 23 characters that discovery had missed under
exactly this class — unnamed designations reused across books (The Big One,
scum leader, Walking oldest). Merging them would have rewritten speaker rows that
reviewers had already accepted, and in this product speaker changes invalidate audio
through the staleness markers. A forward link preserves the freeze at zero audio cost. The
retelling caveat you named is accepted and should be documented.
Q2 — may a new edition contradict something frozen in the previous one?
Yes. A correction that only touched text would be the unusual case, and would also leave
the audio silently wrong. Practically, this node's correction path is a regeneration: a
changed source unit gets a new source_unit_key, its marker goes stale, and the clip is
re-rendered. A new edition therefore needs a new artifact — never a rewritten one, which is
what I-H already says.
7. What we need from you
- A
compute_nodesrow forumbraaudio-m4and a node credential with scopescompute:read+compute:heartbeat. Stage 1 goes out the moment it exists. - Confirmation that
source_unit_keyis stored verbatim, not recomputed (G-C). - Your published vocabulary sets, so we can diff mechanically instead of by argument.
- Answers to your own open items that gate us:
B4(revokePUBLICexecute before any further migration),G2/non-owner role, and the artifact-write scope for a node credential — currently a node can write nothing at all, which is correct until it is wrong. - Confirmation of §5.5's expectation: we report fingerprint drift rather than normalising it. That is why this handoff's fingerprint differs from the previous one.
8. What we have not done, on purpose
- No data migration.
B4is open. - No writes to the controller's datastore.
- No renumbering or renaming of any utterance.
- No request for credentials beyond the one named above.
- No auto-update mechanism of any kind.
9. Reproducing everything in this message
# Stage 0
curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: umbratrans.bdmurphy.me' \
http://192.168.123.14:8442/api/health # 200
curl -sS -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Host: umbratrans.bdmurphy.me' -H 'Content-Type: application/json' -d '{}' \
http://192.168.123.14:8442/api/node/heartbeat # 401
# Alignment phases (this machine)
.venv/bin/python scripts/alignment_report.py --phase all --project "Last Dwarf" --out-dir /tmp/alignment
.venv/bin/python scripts/umbra_node_report.py --check # exit 0
.venv/bin/python -m pytest tests/ -q # 484 passed, 8 known pre-existing
Full suite: 484 passed / 8 failed. All 8 are pre-existing and unrelated to this work
(2 test_pronunciation mock, 6 voice_profile_manager accent).