Record two GPU cycles: the gate works, coverage is the open question

No worker code changed. This is the evidence from the 2026-08-12 16:39 and
17:38 runs, and where each finding now lives.

The fourth session's four identity changes all work on real panels. Panel 7's
two wrong bindings are gone. The lead going unassigned there is correct and
was measured, not assumed: face_detect finds one face on the whole panel at
conf 0.599, nothing else above 0.056 even at a 0.04 threshold, and the crop
shows him drawn from behind.

Two decisions, both closed: a roster name is a guess so it never reaches
detection, and merged_into is exactly one hop deep. Two caveats, both open:
detection can order a bbox backwards (1 in 117), and identity coverage has
fallen on every run since the gate landed (70 -> 61 -> 50).

Coverage is the thing to settle next, and not by reading the number.
identity_labels already holds 145 rows of ground truth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 21:51:36 +04:00
parent c51871348f
commit 18b49c43bd
7 changed files with 366 additions and 120 deletions
+2
View File
@@ -45,3 +45,5 @@ still live belongs in `caveats/`.
| [A detection with no detected face never enrolls or binds](identity-bbox.md#face-gates-enrollment) | closed |
| [A resolver NONE mints an anonymous character, it does not clear the crop](identity-bbox.md#none-mints-an-anonymous-character) | closed |
| [The extras gate runs at enrollment and at narration, not at the speaker prompt](identity-bbox.md#extras-gate-consumers) | closed |
| [A roster name is a guess, so it never reaches detection](identity-bbox.md#roster-does-not-name) | closed |
| [`merged_into` is exactly one hop deep](identity-bbox.md#merge-chains-flatten) | closed |
+38
View File
@@ -183,3 +183,41 @@ previous behaviour.
Forbids: adding a fourth consumer of `vision["characters"]` without deciding which side of this line it is
on. The blob keeps every detection on purpose, so the audit can still see what was gated.
## A roster name is a guess, so it never reaches detection {#roster-does-not-name}
`service.py` seeded `known_characters` with `_roster_char_hints` before every detect call, so the chapter
roster's names were in front of gemma before any panel had identified anyone. On the 2026-08-12 16:39 run
that put "Seonho (short brown hair, yellow plaid shirt)" on a different man wearing glasses, who then held
15 assignments under the lead's name. The real lead was minted separately from the p010 caption as
"Lim Seonho", and p020 held both as two people in one panel.
The seeding is removed. A name now reaches detection only from a registry row, and a registry row is named
from an in-panel caption or address through `name_claims`. The distinction is evidence: a roster name is a
claim about art nobody has looked at yet, and a registry row carries an embedding plus the panel that
named it.
The roster still feeds `roster_cast` in `run_stage_dialogue`. There the names are matched against speech,
not against faces, so a wrong guess costs an unresolved speaker rather than a wrong face binding.
Confirmed on the 17:38 run: "Lim Seonho" came back as one row with 25 assignments, so dropping the hint
did not split the lead across the panels before his caption. "Seonho" fell to 1 assignment.
Not covered: detection still reads a name off the panel and can attach it to the wrong body. p011 and p026
emit `name: "Seonho"` on `person_2` with no roster hint present. That is a separate mechanism and it is
what keeps the glasses man named at all.
## `merged_into` is exactly one hop deep {#merge-chains-flatten}
A merge chain means `merged_into` points at a row that is itself merged, so anything resolving one hop
lands on a retired character. Roster readers filter `merged_into IS NULL` and were never affected.
Two rules, one per direction, and both are needed:
- `merge_characters` resolves the keeper to its chain root before merging. Bounded 64-step walk, the cap
only exists so a cycle cannot hang reconcile.
- retiring a loser repoints every row whose `merged_into` was that loser.
The keeper walk alone is not enough, which the 17:38 run proved by producing
`477c1894 -> a92d9df4 -> 4fb94c15` with the walk deployed. At merge time that pair was fine. The chain
formed later, when a row that was already somebody's keeper was itself retired.