<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
The addresser's report↔review-comment matcher mislabelled genuinely-addressed
findings as unreported (and skipped their fixed in <sha> reply) when the
reviewer's fan-out consolidated a finding flagged by N dimensions into one
inline comment with a comma-joined multi-severity / multi-dimension head, e.g.
High, Medium · error-handling-review, backend-review. findingLookupKeys
built its alias prefix as a verbatim, comma-joined, order-dependent string
(` [${severity} · ${dimensions}] ), so any drift in order or grouping
between the reviewer's comment head and the addresser's ADDRESS_REPORT head
routed the entry to bodyMatchDistance and, when the consolidated summary
diverged enough, to unreported. On PR #64 that surfaced as
3 fixed / 14 unreported with 0 fixed in <sha> replies posted even
though all 17 findings had actually been addressed.
This PR makes findingLookupKeys split comma-joined severity/dimensions into
their atomic tokens and emit the sev × dim cross-product of atomic
[sev_i · dim_j] aliases (× the existing citation forms — range, endpoint,
path-only). Both sides of the match already route through
findingLookupKeys, so any shared constituent atom wins the match regardless
of order or grouping.
## Why It's Needed
Reporting-fidelity, not analytics — the impact CSV re-derives finding counts
from the review independently and the unaddressedHighSeverity disposition
map degrades-and-labels via the AI-96 fidelity path, so this bug does not
corrupt historical metrics. But a 0/17 fixed in <sha> reply tally on a PR
where every finding was actually addressed destroys operator trust in the
tally and the reply provenance, and forces a manual per-comment audit to
confirm what the drone did. Restoring atomic-alias parity between the two
sides of the matcher makes the tally trustworthy again and gets every fixed
finding its fixed in <sha> reply thread back.
## Changes
Core fix (commit b1e94bb)
- src/addresser.ts
- Add splitCsvTokens(s) helper: comma-split, trim, drop empties.
- Rewrite findingLookupKeys(severity, dimensions, path, line?, startLine?)
to:
1. Enumerate citation forms once (single-line, range, endpoint aliases,
or path-only when line is absent — verbatim to pre-fix behaviour).
2. Split severity and dimensions into atomic tokens; fall back to the
raw string when a segment has no comma tokens (preserves the
historical single-token / empty-string alias byte-for-byte).
3. Emit the sev × dim cross-product of [sev_i · dim_j] <cite>
aliases for every citation form.
4. When either segment is genuinely multi-token, also emit one
canonical sorted-token combined-string alias (belt-and-braces).
Review-round tweaks (commits 2f030db, 0a515a6, d78ab32, 0e74bca)
- test(addresser): pin exact 3-way multi-dim partition — reviewer-flagged
Medium: the 3-way multi-dim test was under-asserting
(fixed.length >= 1 / unreported.length < expected.length). Tightened
to the exact 1 fixed / (N-1) unreported partition with commentId
identity, renamed to reflect the partial-fix framing, and mirrored the
disposition note into the findingLookupKeys docstring so operators
reading 1 fixed / 2 unreported on N-way consolidation interpret it as
by-design.
- docs(addresser): align splitCsvTokens example + reword fallback + note
alias precedence — three doc-only tweaks: splitCsvTokens example now
matches the empirical whitespace-tolerance test input; the raw-string
fallback comment reworded from "guards against fabricating" to the
accurate "preserves the single historical alias"; explicit note that
atomic-first, combined-last insertion order is intentional.
- fix(addresser): case-insensitive sort for combined-string alias —
reviewer-flagged case-sensitivity gap: [...tokens].sort() places ASCII
uppercase before lowercase, so ["High","medium"] and ["high","Medium"]
produced lexicographically distinct pre-normalise strings that only
converged post-hoc via normaliseFindingKey. Sort on the lowercased form
so the "one canonical combined alias regardless of casing" intent holds
literally; regression test pinning the case-fold invariant.
- test(addresser): pin asymmetric-empty single-side fallback edge —
reviewer-flagged: the whitespace-tolerance test only covered embedded
empties; the asymmetric total-empty single-side case
(severity="" + dimensions="a") slipped past. Not reachable via
production callers (extractExpectedFindings pairs "unknown" with
"" as a matched fallback), but pinned to preserve byte-for-byte
parity with pre-fix behaviour on this edge.
Contract surface affected
* findingLookupKeys(severity, dimensions, path, line?, startLine?): same
signature, but the return set now includes cross-product atomic aliases
and (for multi-token heads) a normalised combined-string alias.
- Consumers: buildReportedAliasIndex (2 call sites — verified: they
consume the returned array via for … of with no positional
assumptions) and crossCheckAccountability pass-1 candidate-key loop
(verified: same for … of shape, breaks on first hit).
- Pre-existing single-token behaviour byte-preserved (findingLookupKeys("Medium", "correctness-review", …) returns the same one-element array); no
other symbols touched.
## Breaking Changes
None. Single-severity / single-dimension heads keep the historical one-alias
output byte-for-byte (pinned by the boundary test); the range/line-drift
matcher (lineDistanceForCitation, endpoint aliases) is untouched;
parseFindingHeader is untouched. Only additive alias entries for
comma-joined heads, which is precisely the fix.
## Test plan
- [x] pnpm exec vitest run src/addresser.test.ts → 51 passed (up from
40 baseline; +11 new tests covering cross-product, order-independence,
boundary single-token, range × cross-product, 3-way multi-dim with
exact partition, whitespace-only tokens, case-insensitive combined-
alias sort, asymmetric-empty fallback edge, and 4 matcher-level
scenarios end-to-end).
- [x] pnpm test → 685 passed across 36 test files (full suite green).
- [x] pnpm typecheck → clean (tsc --noEmit).
- [ ] Operator-side verification: fire the addresser on a PR whose
reviewer inline-comment set contains a multi-severity / multi-dim
consolidated head, and confirm the fixed / unreported tally
matches the actual disposition and every fixed finding gets its
fixed in <sha> reply.
## Verification artifact
Under the fix, the alias set for a consolidated head expands as follows
(captured from a debug print of findingLookupKeys("High, Medium",
"error-handling-review, backend-review", "src/foo.ts", 50), matching the
findingLookupKeys — multi-severity/multi-dimension cross-product test):
["[high · error-handling-review] src/foo.ts:50",
"[high · backend-review] src/foo.ts:50",
"[medium · error-handling-review] src/foo.ts:50",
"[medium · backend-review] src/foo.ts:50",
"[high, medium · backend-review, error-handling-review] src/foo.ts:50"
]
Any constituent single-token head on the other side of the matcher
([Medium · backend-review] src/foo.ts:50 from a posted comment,
[High · error-handling-review] src/foo.ts:50 from a differently-grouped
ADDRESS_REPORT bullet) now shares at least one atomic alias with the
consolidated set → pass-1 verbatim match binds → fixed in <sha> reply
posts. The single-severity single-dimension boundary case still returns
exactly ["[medium · correctness-review] src/foo.ts:50"]` — pinned by
the boundary test.
Closes AI-127.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-a2bd091a-0cca-4447-aa84-6a3ca1c92106"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-a2bd091a-0cca-4447-aa84-6a3ca1c92106"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>