Record that 2d's worked examples no longer exist

The registry reset deleted Lim Seonho and character_afa7623b, so there is nothing
to merge or split until the rerun mints a new set. What was done instead is the
safety net for that rerun, since reconcile runs inside it: a merge retires the
losing row rather than deleting it, and records which assignments moved.

Half-closes caveats/audit-open.md#destructive-reconcile. The unmerge path and the
split stay unwritten on purpose, with the revisit trigger named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:27:13 +04:00
parent ca4661763c
commit d63a337f15
3 changed files with 99 additions and 9 deletions
+63
View File
@@ -396,3 +396,66 @@ worker down. Both workers were dead for two minutes before `/health` caught it.
Not run: the GPU cycle. `has_face` and the NONE mint are both unproven on real panels and now land in the
same run.
## 2026-08-12, fourth session, continued — identity 2c, extras versus cast
Asked: "fix extra-vs-cast?"
Traced the three places that read `vision["characters"]` raw before changing anything. That corrected an
inference made earlier in the session. `build_scene` already drops an unassigned detection from
`characters` and `present` (`worker_scene.py:63`), so extras never reached the cast list at all.
The leak was their ACTIONS. `actions` was built from every detection, and that list is what the script
prompt renders and what the correctness verifier uses as evidence. So a background extra's "standing at the
window" arrived as a fact about the panel with no character attached, and the verifier confirmed it,
because the action really was in the blob. That is a second mechanism behind the invented-narration
complaints in item 3, independent of the model inventing anything.
Gated two consumers on `has_face is False`, matching the enrollment gate's semantics exactly:
`worker_scene`'s `actions`/`action`, and `service._beat`, which picks the director's "who" from the first
three detections and falls back to an action when a detection has no name.
Left `service._present_characters` ungated on purpose. It builds the dialogue stage's candidate speakers
and the set-of-mark boxes. An extra picked as speaker already resolves to unknown, not to a wrong name, so
the failure is contained. And the gate's cost lands hardest there, since a character drawn from behind has
no face box and gating would delete a real speaker from the only list that can attribute their line.
`decisions/identity-bbox.md#extras-gate-consumers`.
Checks: `worker_scene self-check ok` with three cases (cast action survives, faceless dropped, missing key
survives), 115 passed on homesrv. Deployed: image rebuilt, `_beat` verified inside the container, scene
worker restarted, `./check_stale.sh` clean. Committed `ca46617` and `8b27aec`.
Still not run on a GPU. Three changes now ride the same cycle: `has_face`, the NONE mint, and this.
## 2026-08-12, fourth session, continued — identity 2d, merge and split
Asked: "2d? merge and split?"
Checked the registry before planning anything, and 2d as written is stale. The registry reset earlier today
deleted both worked examples. There is no `Lim Seonho` to merge into, `character_afa7623b` does not exist,
and the current registry is 8 rows with one named character (`Seonho`). Nothing to merge or split until the
rerun mints a new set.
So the useful work was the safety net for that rerun, since `reconcile` runs inside it. The caveat's cost
line was the reason: one bad merge was unrecoverable without rebaselining the whole manga, and the cycle
about to run includes a merge pass over embeddings nobody has seen yet.
`merge_characters` no longer deletes the loser. It sets `merged_into = keeper`, so the row keeps its
embedding, description and gender, and it stamps every repointed assignment with
`method = merged_from:<loser_id>` in `identity_assignment_sources`. That reuses a free-form column on a
table that already existed rather than adding a merge log. `source` is left alone, so a reviewer's `manual`
assignment keeps its veto in `assign_identity` after being repointed.
Roster readers filter `merged_into IS NULL`, including the name-dedup in `create_character`, which would
otherwise fold new crops back into a character reconcile had retired. Lookup by id does not filter, because
an assignment or a narration reference may still point at a merged id.
Two existing assertions asserted the old destructive behaviour (`test_db.py:235`, `test_merge_refs.py:37`)
and were rewritten, not deleted: the invariant changed on purpose.
Checks: 116 passed on homesrv, up from 115. Additive `ALTER TABLE` through the existing `init_db` migration
block, verified on the live database (`merged_into` present, 0 rows merged). Committed `00096cc`.
Deliberately not built: the unmerge path and the split. No wrong merge has been observed since the crops
were fixed, so the consumer of these records waits for one. The forward case is partly covered by 2b, since
a resolver NONE now mints instead of folding a stranger into the nearest match.
+21 -4
View File
@@ -45,10 +45,27 @@ One number sets the agenda:
deliberate NONE and a hallucinated index both unassigned every crop of the tracklet. A NONE now
mints an anonymous character from the crop, using the embedding `/identity/resolve` ships beside
it as `emb_uri`. Deployed: image rebuilt, `resolve_outcome` verified inside the container.
c. **Separate extra from cast.** Four of the six detections on panel 7 are background extras or
nothing at all, and all six reach identity as equal candidates.
d. Only then merge `Seonho` into `Lim Seonho` and split `character_afa7623b`, which still needs the
reversible-merge design (`caveats/audit-open.md#destructive-reconcile`), not a patch.
c. ~~Separate extra from cast.~~ **Done 2026-08-12, not yet run on a GPU**
(`decisions/identity-bbox.md#face-gates-enrollment`,
`decisions/identity-bbox.md#extras-gate-consumers`). `has_face` stops a faceless detection
enrolling, and two more consumers now skip it: `worker_scene`'s `actions`, which is the script
prompt's content and the verifier's evidence, and `service._beat`, the director's "who".
`_present_characters` stays ungated on purpose, reasoned out in the decision.
The remaining gap is that vision still emits extras into the blob, which is deliberate so the
audit can see what was gated.
d. **The worked examples are gone.** The registry reset deleted `Lim Seonho` and
`character_afa7623b`. The current registry is 8 rows, one named (`Seonho`), so there is nothing to
merge or split until the rerun mints a new set.
What was done instead is the safety net for that rerun, since `reconcile` runs inside it. A merge no
longer deletes the losing row: it sets `merged_into`, and stamps every repointed assignment with
`method = merged_from:<loser_id>`. A wrong merge now costs a hand-written SQL walk, not a full
rebaseline (`caveats/audit-open.md#destructive-reconcile`).
Deliberately not built: the unmerge path and the split. No wrong merge has been observed since the
crops were fixed, so the consumer of those records waits for one. The forward case is partly covered
by 2b, because a resolver NONE now mints rather than folding a stranger into the nearest match.
Splitting a character that is ALREADY over-merged still needs a re-embed pass over its detections.
**Cast profiles already exist. Do not rebuild them.** The user asked whether the main cast could get a
profile built from reference frames and reused. `characters` already carries `ref_image_uris` and
+15 -5
View File
@@ -5,12 +5,22 @@ fixed findings live in `decisions/audit-phase1.md`. Line numbers are from the au
## Reconcile deletes the losing character irreversibly {#destructive-reconcile}
Reconciliation deletes the losing character row (`db.py:506`). Clearing the reconcile stage does not
undo it, and name claims attached to the merged-away character are not repointed.
Reconciliation used to delete the losing character row. Clearing the reconcile stage did not undo it, and
nothing recorded which detections had been the loser's.
Costs: one bad merge is unrecoverable without rebuilding the identity stage for the whole manga.
Revisit when: identity work resumes, or a reviewer reports a wrong merge on a real chapter.
Workaround: none. Clear identity and rerun, which loses the good merges too.
**Half-closed 2026-08-12.** The loss is no longer unrecoverable. `merge_characters` marks the loser
`merged_into = keeper` instead of deleting it, so its embedding, description and gender survive, and every
repointed assignment is stamped `method = merged_from:<loser_id>` in `identity_assignment_sources`. Those
two records are enough to walk a merge backwards. Roster readers filter `merged_into IS NULL`. Lookup by
id does not, so an assignment still pointing at a merged id resolves.
What is still missing is the mechanism that consumes them: there is no unmerge, and no split. Undoing a
merge today means a manual SQL walk of the two records above.
Costs: a wrong merge needs hand-written SQL to undo, not a rebaseline.
Revisit when: a reviewer reports a wrong merge, or the review gates from [#136] get a place to hang
name/merge/split actions. No wrong merge has been seen since the crops were fixed, so building the unmerge
before either trigger would be speculative.
## Clearing a stage does not undo what it wrote {#dishonest-clearing}