Context
linkedIssueSatisfactionCacheInputFingerprint in src/review/linked-issue-satisfaction-cache-input.ts (lines 20-30) builds the fingerprint payload used to cache/replay a linked-issue-satisfaction AI verdict:
export async function linkedIssueSatisfactionCacheInputFingerprint(input: LinkedIssueSatisfactionCacheInput): Promise<string> {
const payload = [
LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION,
input.byok ? "1" : "0",
input.provider ?? "",
input.model ?? "",
input.issueText ?? "",
input.prTitle ?? "",
input.prBody ?? "",
input.diff ?? "",
].join("|");
return `${LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION}:${await sha256Hex(payload)}`;
}
The payload is a bare "|"-joined string of free-form GitHub text fields (issueText, prTitle, prBody, diff). None of that text is escaped, so a | character occurring naturally inside one field can shift the field boundary and produce the exact same joined string for two genuinely different inputs — e.g. {issueText: "foo|bar", prTitle: "baz", ...} and {issueText: "foo", prTitle: "bar|baz", ...} (other fields held equal) serialize identically and hash to the same fingerprint/cache key.
Its own file comment says it "mirrors ai-slop-cache-input.ts's fingerprint discipline" — but that sibling module (src/review/ai-slop-cache-input.ts) actually builds its payload via JSON.stringify({...}), which structurally escapes delimiters and can't collide this way. src/review/ai-review-cache-input.ts follows the same JSON.stringify discipline. linked-issue-satisfaction-cache-input.ts is the one outlier still using a raw string join.
Effect: two structurally different (issue text, PR title/body, diff) combinations can collide on the same cache key, causing getCachedLinkedIssueSatisfaction/putCachedLinkedIssueSatisfaction (src/db/repositories) to serve a stale AI verdict written for a different prompt — silently wrong linked-issue-satisfaction gate results.
test/unit/linked-issue-satisfaction-cache.test.ts asserts that edits to individual fields change the fingerprint, but no case exercises a field value containing |, so this collision is untested.
Requirements
- Change
linkedIssueSatisfactionCacheInputFingerprint's payload construction from a raw "|"-joined string to a JSON.stringify-based structured payload, mirroring the pattern already used in src/review/ai-slop-cache-input.ts and src/review/ai-review-cache-input.ts.
- Bump
LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION to a new version string (e.g. linked-issue-satisfaction-input:v3) so existing cached rows keyed on the old, collision-prone fingerprint format are not accidentally treated as valid under the new scheme.
- Preserve the existing field semantics (
?? fallback to ""/null as appropriate) — only the serialization mechanism changes, not which fields are included or their null-handling meaning.
- Do not change the function's public signature, return type, or the surrounding cache read/write call sites in
src/db/repositories.
Deliverables
Test Coverage Requirements
Aim for 99%+ Codecov patch coverage (100% including the new branch/invariant) on the touched lines in src/review/linked-issue-satisfaction-cache-input.ts. This is a fix for a real cache-key collision, so the delimiter-collision regression test is required, not just incidental line coverage.
Expected Outcome
Two structurally different linked-issue-satisfaction prompt inputs can no longer produce the same cache fingerprint because of an unescaped | inside free-form GitHub text, matching the collision-resistant discipline already used by the sibling ai-slop-cache-input.ts and ai-review-cache-input.ts modules.
Links & Resources
src/review/linked-issue-satisfaction-cache-input.ts (linkedIssueSatisfactionCacheInputFingerprint, lines 20-30)
src/review/ai-slop-cache-input.ts (aiSlopCacheInputFingerprint — the JSON.stringify pattern to mirror)
src/review/ai-review-cache-input.ts (same pattern)
test/unit/linked-issue-satisfaction-cache.test.ts (existing test suite)
Context
linkedIssueSatisfactionCacheInputFingerprintinsrc/review/linked-issue-satisfaction-cache-input.ts(lines 20-30) builds the fingerprint payload used to cache/replay a linked-issue-satisfaction AI verdict:The payload is a bare
"|"-joined string of free-form GitHub text fields (issueText,prTitle,prBody,diff). None of that text is escaped, so a|character occurring naturally inside one field can shift the field boundary and produce the exact same joined string for two genuinely different inputs — e.g.{issueText: "foo|bar", prTitle: "baz", ...}and{issueText: "foo", prTitle: "bar|baz", ...}(other fields held equal) serialize identically and hash to the same fingerprint/cache key.Its own file comment says it "mirrors ai-slop-cache-input.ts's fingerprint discipline" — but that sibling module (
src/review/ai-slop-cache-input.ts) actually builds its payload viaJSON.stringify({...}), which structurally escapes delimiters and can't collide this way.src/review/ai-review-cache-input.tsfollows the sameJSON.stringifydiscipline.linked-issue-satisfaction-cache-input.tsis the one outlier still using a raw string join.Effect: two structurally different (issue text, PR title/body, diff) combinations can collide on the same cache key, causing
getCachedLinkedIssueSatisfaction/putCachedLinkedIssueSatisfaction(src/db/repositories) to serve a stale AI verdict written for a different prompt — silently wrong linked-issue-satisfaction gate results.test/unit/linked-issue-satisfaction-cache.test.tsasserts that edits to individual fields change the fingerprint, but no case exercises a field value containing|, so this collision is untested.Requirements
linkedIssueSatisfactionCacheInputFingerprint's payload construction from a raw"|"-joined string to aJSON.stringify-based structured payload, mirroring the pattern already used insrc/review/ai-slop-cache-input.tsandsrc/review/ai-review-cache-input.ts.LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSIONto a new version string (e.g.linked-issue-satisfaction-input:v3) so existing cached rows keyed on the old, collision-prone fingerprint format are not accidentally treated as valid under the new scheme.??fallback to""/nullas appropriate) — only the serialization mechanism changes, not which fields are included or their null-handling meaning.src/db/repositories.Deliverables
linkedIssueSatisfactionCacheInputFingerprintbuilds its payload viaJSON.stringifyinstead of a delimiter-joined string.LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSIONbumped to a new version string.test/unit/linked-issue-satisfaction-cache.test.tsproving two inputs that previously collided under the raw-join scheme (a|-containing field value shifted across two different field boundaries) now produce distinct fingerprints.Test Coverage Requirements
Aim for 99%+ Codecov patch coverage (100% including the new branch/invariant) on the touched lines in
src/review/linked-issue-satisfaction-cache-input.ts. This is a fix for a real cache-key collision, so the delimiter-collision regression test is required, not just incidental line coverage.Expected Outcome
Two structurally different linked-issue-satisfaction prompt inputs can no longer produce the same cache fingerprint because of an unescaped
|inside free-form GitHub text, matching the collision-resistant discipline already used by the siblingai-slop-cache-input.tsandai-review-cache-input.tsmodules.Links & Resources
src/review/linked-issue-satisfaction-cache-input.ts(linkedIssueSatisfactionCacheInputFingerprint, lines 20-30)src/review/ai-slop-cache-input.ts(aiSlopCacheInputFingerprint— theJSON.stringifypattern to mirror)src/review/ai-review-cache-input.ts(same pattern)test/unit/linked-issue-satisfaction-cache.test.ts(existing test suite)