Bug-hunt round 28: review id-lookup SafeText mismatch, file-constant reuse, Mode B tense - #45
Merged
Conversation
findByID and contains compared a finding's raw ID with ==, while every
display path (report, review's printFinding) renders it through
session.SafeText. A hand-edited or exchanged findings.jsonl carrying an
invisible character in an id (analyze.Load never re-validates
^F-\d{3}$; only ingest does) therefore displays as a clean id like
"F-001" but is unreachable by that same id via -finding, and by an
interactive duplicate-of target naming it.
Recording a verdict now also stores the finding's actual raw id (not
the operator's clean flag/typed value) so analyze.EffectiveStatus,
keyed on the raw id, attaches the verdict instead of silently dropping
it from the report.
Assisted-by: Claude:claude-sonnet-5
cli.go named timeline.jsonl/report.md as string literals in the merge and report success messages instead of session.TimelineFile/ReportFile, the constants already used elsewhere for the same filenames (report.go, tests). Same values today, but two independent spellings of one well-known name is exactly what the constants exist to prevent. Assisted-by: Claude:claude-sonnet-5
01-purpose.md described Mode B's keyframe extraction in unqualified
present tense, as if shipped, while 04-analysis.md heads that section
"Keyframes (planned)" and defers frame extraction to a later intent,
and 01-phases.md marks Phase 4 (Mode B) not started. The same page
already qualifies its other unshipped item ("codebase mapping as a
future goal") one paragraph earlier; this brings Mode B's keyframe
mention in line with that convention.
Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
checkTargets' "cannot be a duplicate of itself" guard still compared of == id as raw bytes, while findByID/contains (and the verdict resolution in single()/applyChoice) now establish finding identity under session.SafeText. For a finding whose raw id carries an invisible character (a hand-edited or exchanged findings.jsonl), a duplicate-of target matching that finding's own rendered id slipped past the raw comparison and resolved back to the same finding's raw id, recording it as a duplicate of itself. checkTargets now compares of and id in their SafeText rendered form, matching every other identity check on this path. Covered by a new interactive-path regression test (TestInteractiveDuplicateRefusesRenderedSelfMatch); verified failing before the fix (records finding-duplicate-of-itself) and passing after. Assisted-by: Claude:claude-sonnet-5
Two issues raised by this round's docs-accuracy adversarial review: - CHANGELOG.md had no entry for review's SafeText id-matching fix (this round's substantive finding), the same omission round 27 went back and repaired for round 26 — added alongside the sibling analyze.ParseRecords entry it parallels, and folded in the self-duplicate gap closed in the previous commit. - 01-purpose.md's Mode B keyframe-extraction mention was requalified as "a planned fallback", a phrase that exists nowhere else in the repo and is inaccurate in Mode B specifically: keyframes stand in for the missing event stream there (per the architecture note and itd-4), not a fallback from a cheaper primary path the way 04-analysis.md frames them for Mode A. Reworded to "a future goal", matching the same phrase the page's own opening paragraph already uses for its other unshipped item (codebase mapping). DECISIONS.md's round 28 entry is corrected to match: it previously claimed the "future goal" convention sat "one paragraph earlier" (it is in the page's opening paragraph, separated by a heading and the whole Mode A bullet) and that the edit adopted it (it had coined "planned fallback" instead). Assisted-by: Claude:claude-sonnet-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 28 of the automated bug-hunt loop (state: #24).
Confirmed findings (fixed)
1.
internal/review/review.go:112-119(findByID),:509-511(contains) — substantiveBoth compared a finding's
IDwith raw==, while a finding's id is only ever displayed throughsession.SafeText(internal/report/report.go,internal/review/review.go'sprintFinding).analyze.Load/analyze.ParseRecordsnever re-validates^F-\d{3}$on load — onlyanalyze -ingestdoes — so a hand-edited or exchangedfindings.jsonlcan carry a raw id with an invisible character (e.g. a zero-width space) that displays as a clean id likeF-001everywhere but was unreachable by that same id:testimony review -finding F-001 -verdict confirmedfailed withfinding F-001 not found, and the same mismatch blocked marking any finding a duplicate-of it.Deeper still: even resolving the lookup,
single()(non-interactive) and the interactive duplicate-of path recorded the verdict under the operator's clean flag/typed value rather than the finding's actual raw id.analyze.EffectiveStatuskeys its map on each finding's raw id, so a verdict recorded under the clean form would silently fail to attach — the finding would keep showing "unverified" inreport.mdeven though a verdict was successfully appended tofindings.jsonl.Fix:
findByID/containsnow compare ids in theirsession.SafeTextrendered form (matching the load-time uniqueness checkanalyze.ParseRecordsalready applies — its own comment states id-keyed consumers assume this). Verdict recording now stores the resolved finding's actual raw id forFinding, and the resolved duplicate target's raw id forOf, in both the non-interactive and interactive paths. Covered by a new regression test (TestNonInteractiveConfirmMatchesRenderedID) that writes a finding with a raw zero-width-space id and asserts the verdict attaches underanalyze.EffectiveStatus; verified failing before the fix (finding F-001 not found) and passing after.2.
internal/cli/cli.go:98,126— nitpickThe
merge/reportsuccess messages andreport's write path namedtimeline.jsonl/report.mdas independent string literals instead ofsession.TimelineFile/session.ReportFile, the constants already used for the same filenames elsewhere (internal/timeline/timeline.go, tests). Same values today, but two independent spellings of one well-known name is exactly what the constants exist to prevent. No behavioural change, so no new test.3.
.abcd/development/brief/01-product/01-purpose.md:26-28— nitpickDescribed Mode B's keyframe extraction ("keyframes extracted from the video supply the referents") in unqualified present tense, as if shipped, while
04-analysis.md:46heads that section "Keyframes (planned)" and defers frame extraction to a later intent, and01-phases.md:9marks Phase 4 (Mode B) "Not started". The same page already qualifies its other unshipped item ("codebase mapping as a future goal") one paragraph earlier — this brings the Mode B mention in line with that existing convention.Considered and rejected (adversarially refuted, or precedent duplicates)
report.go'sorDashfallback branch is unreachable giveneventLine's current call order — true, but a deliberate locally-redundant guard against a future caller-invariant change, matching the same rationalereview.go'scheckTargetsstates for its own belt-and-bracesSafeTextcalls.demo's-addr ":"(empty port) bypassingCheckAddr's numeric range guard — not silent (the real bound ephemeral port is reported back), and an empty port isnet.Listen's own deferred-to-runtime case, the same as a named service port; this guard's scope was already settled by an earlier round's identical precedent.itd-2-analysis-findings.md's AC2 wording ("the finding's status becomes...") read as contradicting the append-only invariant — AC2's own second clause names the append-only mechanism; "status" at intent altitude is the effective status the pipeline derives and displays, not the stored field the invariant docs constrain.02-verification.md's "kept undersessions/" line read as unverifiable —sessions/is gitignored by design; committing a real captured session would violate the repo's own privacy rule.AGENTS.md's dangling03-configuration.mdlink inside the abcd-managed fence;AGENTS.mdclaiming CI runs plaingo test ./...; persona role-label wording drift across intent drafts; the intents-README "always they/them" persona-quote rule read against the personas page's gendered narrative pronouns.Verification
go build,gofmt -l .,go vet ./...,go test ./...,go test -race ./..., and the pipeline smoke (merge/reportonexamples/sample-session) all pass..abcd/work/DECISIONS.mdgains the round's entry.