Skip to content

Bug-hunt round 28: review id-lookup SafeText mismatch, file-constant reuse, Mode B tense - #45

Merged
REPPL merged 6 commits into
mainfrom
bughunt-28
Aug 5, 2026
Merged

Bug-hunt round 28: review id-lookup SafeText mismatch, file-constant reuse, Mode B tense#45
REPPL merged 6 commits into
mainfrom
bughunt-28

Conversation

@REPPL

@REPPL REPPL commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Round 28 of the automated bug-hunt loop (state: #24).

Confirmed findings (fixed)

1. internal/review/review.go:112-119 (findByID), :509-511 (contains) — substantive
Both compared a finding's ID with raw ==, while a finding's id is only ever displayed through session.SafeText (internal/report/report.go, internal/review/review.go's printFinding). analyze.Load/analyze.ParseRecords never re-validates ^F-\d{3}$ on load — only analyze -ingest does — so a hand-edited or exchanged findings.jsonl can carry a raw id with an invisible character (e.g. a zero-width space) that displays as a clean id like F-001 everywhere but was unreachable by that same id: testimony review -finding F-001 -verdict confirmed failed with finding 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.EffectiveStatus keys 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" in report.md even though a verdict was successfully appended to findings.jsonl.

Fix: findByID/contains now compare ids in their session.SafeText rendered form (matching the load-time uniqueness check analyze.ParseRecords already applies — its own comment states id-keyed consumers assume this). Verdict recording now stores the resolved finding's actual raw id for Finding, and the resolved duplicate target's raw id for Of, 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 under analyze.EffectiveStatus; verified failing before the fix (finding F-001 not found) and passing after.

2. internal/cli/cli.go:98,126 — nitpick
The merge/report success messages and report's write path named timeline.jsonl/report.md as independent string literals instead of session.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 — nitpick
Described Mode B's keyframe extraction ("keyframes extracted from the video supply the referents") in unqualified present tense, as if shipped, while 04-analysis.md:46 heads that section "Keyframes (planned)" and defers frame extraction to a later intent, and 01-phases.md:9 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 the Mode B mention in line with that existing convention.

Considered and rejected (adversarially refuted, or precedent duplicates)

  • report.go's orDash fallback branch is unreachable given eventLine's current call order — true, but a deliberate locally-redundant guard against a future caller-invariant change, matching the same rationale review.go's checkTargets states for its own belt-and-braces SafeText calls.
  • demo's -addr ":" (empty port) bypassing CheckAddr's numeric range guard — not silent (the real bound ephemeral port is reported back), and an empty port is net.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 under sessions/" line read as unverifiable — sessions/ is gitignored by design; committing a real captured session would violate the repo's own privacy rule.
  • Excluded before verification, as precedent duplicates of findings already discussed in earlier rounds: AGENTS.md's dangling 03-configuration.md link inside the abcd-managed fence; AGENTS.md claiming CI runs plain go 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/report on examples/sample-session) all pass.

.abcd/work/DECISIONS.md gains the round's entry.

REPPL added 6 commits August 5, 2026 13:35
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
@REPPL
REPPL merged commit 43559f8 into main Aug 5, 2026
6 checks passed
@REPPL
REPPL deleted the bughunt-28 branch August 5, 2026 23:23
@REPPL REPPL mentioned this pull request Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant