Files
UmbraAudio/infra/controller-handoff-2026-10-03.md
B.D. Murphy 829a03b89f Action the controller alignment proposal: source_unit_key + phase reports
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.
2026-10-02 20:16:06 -07:00

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
  • 200 on health. 401 node_credential_required on the node surface — the correct answer, and evidence that authentication is working rather than being bypassed.
  • The 400 without Host reproduces 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)
  • occurrence disambiguates repeated text within a chapter, in document order. Nothing in the key is a position.
  • Filenames unchanged. utterance_index untouched. is_current unaffected.
  • 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 +100 on every utterance_index, then re-stamped — 0 keys changed.
  • Old schema shapes handled: chapter_index is 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

  1. A compute_nodes row for umbraaudio-m4 and a node credential with scopes compute:read + compute:heartbeat. Stage 1 goes out the moment it exists.
  2. Confirmation that source_unit_key is stored verbatim, not recomputed (G-C).
  3. Your published vocabulary sets, so we can diff mechanically instead of by argument.
  4. Answers to your own open items that gate us: B4 (revoke PUBLIC execute 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.
  5. 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. B4 is 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).