From 1e7f3be87a317e72dcecc03d533cf3a000a17eed Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 04:30:54 +0800 Subject: [PATCH 1/7] fix(review): detect secrets split across adjacent added lines (#2454) Port review-enrichment cross-line literal join into the unconditional secret_leak gate so credentials split across consecutive + lines cannot evade per-line regex matching. Preserves hunk/context boundaries and patch-less synthetic diffs from #2821. Co-authored-by: Cursor --- src/review/safety.ts | 21 ++------ src/review/secrets-scan.ts | 86 +++++++++++++++++++++++++++++++++ test/unit/safety-wiring.test.ts | 13 +++++ test/unit/secrets-scan.test.ts | 69 +++++++++++++++++++++++++- 4 files changed, 172 insertions(+), 17 deletions(-) diff --git a/src/review/safety.ts b/src/review/safety.ts index 9faf479b4a..995eb252e8 100644 --- a/src/review/safety.ts +++ b/src/review/safety.ts @@ -7,7 +7,7 @@ import type { AdvisoryFinding } from "../types"; import { neutralizePromptInjection, safeReviewTitle } from "./prompt-injection"; -import { scanForSecrets } from "./secrets-scan"; +import { scanPrDiffForSecretKinds } from "./secrets-scan"; // Concrete credential formats only — NOT the weak heuristics (`seed_or_mnemonic` / `bittensor_key`) that // false-positive on legitimate config/workflow content. A `coldkey:` / `hotkey =` line or the word @@ -98,21 +98,10 @@ export function secretLeakFinding(diff: string): AdvisoryFinding | null { // secret-shaped string (e.g. deleting/defanging a test fixture, or rotating a credential out). Added/renamed // file paths are also committed PR state, but buildSecretScanDiff carries them only in `### path (status)` // headers, so keep those metadata lines while still dropping modified/removed headers and `+++` patch headers. - const added = diff - .split("\n") - .filter( - (line) => - (line.startsWith("+") && !line.startsWith("+++")) || - /^### .+ \((?:added|renamed)\) /.test(line), - ) - .join("\n"); - // Only CONCRETE credential formats hard-block. The raw scanner also returns the weak `seed_or_mnemonic` / - // `bittensor_key` heuristics, which false-positive on `coldkey:` / `hotkey =` / "mnemonic" lines in - // legitimate config/workflow files (RC6); those are filtered out here so they never produce a `secret_leak` - // blocker. A real token (github_token, aws_access_key, …) still blocks regardless of which file it is in. - const kinds = scanForSecrets(added).kinds.filter((kind) => - HARD_SECRET_KINDS.has(kind), - ); + // scanPrDiffForSecretKinds walks the diff line-by-line (with a bounded cross-line literal join on consecutive + // added lines, #2454) instead of joining all `+` lines into one blob — that join would miss a credential + // split across adjacent assignments and would also ignore hunk/context boundaries the gate must respect. + const kinds = scanPrDiffForSecretKinds(diff).filter((kind) => HARD_SECRET_KINDS.has(kind)); if (kinds.length === 0) return null; return { code: "secret_leak", diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index 5603481805..be96053061 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -90,3 +90,89 @@ export function scanForSecrets(text: string): SecretScanResult { if (hasGenericSecretAssignment(text)) kinds.push("generic_secret_assignment"); return { found: kinds.length > 0, kinds }; } + +/** Extract quoted string-literal inner text from one source line (for cross-line join below). */ +function extractStringLiteralContents(line: string): string[] { + const literals: string[] = []; + const re = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g; + let match: RegExpExecArray | null; + while ((match = re.exec(line)) !== null) literals.push(match[0].slice(1, -1)); + return literals; +} + +function formatSecretKindsFromText(text: string): string[] { + const kinds = SECRET_PATTERNS.filter((pattern) => pattern.re.test(text)).map((pattern) => pattern.name); + if (hasGenericSecretAssignment(text)) kinds.push("generic_secret_assignment"); + return kinds; +} + +/** Scan a PR diff for secret kinds introduced on added lines (and added/renamed file headers). Per-line regex + * first; then a bounded cross-line join of consecutive added lines' adjacent string literals (#2454) so a + * credential split across `const a = "AKIA…"; const b = "REST";` still trips the unconditional gate. Context, + * removed, and hunk boundaries reset the join window — same semantics as review-enrichment's scanPatch. */ +export function scanPrDiffForSecretKinds(diff: string): string[] { + const found = new Set(); + const addedLines: string[] = []; + let inFileSection = false; + let inHunk = false; + let previousLiterals: string[] = []; + + for (const line of diff.split("\n")) { + if (line === "") { + inFileSection = false; + inHunk = false; + previousLiterals = []; + continue; + } + if (/^### .+ \(.+\) /.test(line)) { + inFileSection = true; + inHunk = false; + previousLiterals = []; + if (/^### .+ \((?:added|renamed)\) /.test(line)) { + for (const kind of formatSecretKindsFromText(line)) found.add(kind); + } + continue; + } + if (/^@@ /.test(line)) { + inHunk = true; + previousLiterals = []; + continue; + } + if (line.startsWith("+") && !line.startsWith("+++")) { + if (!inFileSection) continue; + const content = line.slice(1); + addedLines.push(content); + let matched = false; + for (const kind of formatSecretKindsFromText(content)) { + found.add(kind); + matched = true; + } + const currentLiterals = extractStringLiteralContents(content); + const lastPrevious = previousLiterals.at(-1); + const firstCurrent = currentLiterals[0]; + if (!matched && lastPrevious !== undefined && firstCurrent !== undefined) { + const joined = lastPrevious + firstCurrent; + for (const pattern of SECRET_PATTERNS) { + if (pattern.re.test(joined)) { + found.add(pattern.name); + break; + } + } + } + previousLiterals = currentLiterals; + continue; + } + if (line.startsWith("-")) { + previousLiterals = []; + continue; + } + if (inHunk && line.startsWith(" ")) { + previousLiterals = []; + } + } + + if (!found.has("generic_secret_assignment") && hasGenericSecretAssignment(addedLines.join("\n"))) { + found.add("generic_secret_assignment"); + } + return [...found]; +} diff --git a/test/unit/safety-wiring.test.ts b/test/unit/safety-wiring.test.ts index 750fb39d6c..7728c16ec4 100644 --- a/test/unit/safety-wiring.test.ts +++ b/test/unit/safety-wiring.test.ts @@ -435,4 +435,17 @@ describe("secretLeakFinding scans only ADDED lines", () => { const diff = `### fixtures/${fakeToken}.txt (removed) +0/-1\n@@\n-const unrelated = 1;`; expect(secretLeakFinding(diff)).toBeNull(); }); + + it("flags a credential split across consecutive added lines (#2454)", () => { + const awsKeyFragmentA = "AKIA" + "IOSFODNN7"; + const awsKeyFragmentB = "EXAMPLE"; + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,0 +1,2 @@", + `+const part1 = "${awsKeyFragmentA}";`, + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(secretLeakFinding(diff)?.code).toBe("secret_leak"); + expect(secretLeakFinding(diff)?.title).toContain("aws_access_key"); + }); }); diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index 04f30230a6..bd9504eb23 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { scanForSecrets } from "../../src/review/secrets-scan"; +import { scanForSecrets, scanPrDiffForSecretKinds } from "../../src/review/secrets-scan"; describe("scanForSecrets — deterministic secret-pattern scanner", () => { it("returns no findings for empty / benign text", () => { @@ -107,3 +107,70 @@ describe("scanForSecrets — deterministic secret-pattern scanner", () => { expect(scanForSecrets('token = "short12345"').kinds).not.toContain("generic_secret_assignment"); }); }); + +describe("scanPrDiffForSecretKinds — cross-line split credentials (#2454)", () => { + const awsKeyFragmentA = "AKIA" + "IOSFODNN7"; + const awsKeyFragmentB = "EXAMPLE"; + + it("catches an AWS key split across two adjacent added lines via string literals", () => { + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,0 +1,2 @@", + `+const part1 = "${awsKeyFragmentA}";`, + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toContain("aws_access_key"); + }); + + it("does not join literals across a context line inside the hunk", () => { + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,1 +1,3 @@", + `+const part1 = "${awsKeyFragmentA}";`, + ' const unrelated = "context line";', + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).not.toContain("aws_access_key"); + }); + + it("does not join literals across a hunk boundary", () => { + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,0 +1,1 @@", + `+const part1 = "${awsKeyFragmentA}";`, + "@@ -10,0 +11,1 @@", + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).not.toContain("aws_access_key"); + }); + + it("does not join unrelated short literals into a false positive", () => { + const diff = [ + "### src/app.ts (modified) +2/-0", + "@@ -1,0 +1,2 @@", + '+const a = "hello";', + '+const b = "world";', + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toEqual([]); + }); + + it("still flags a single-line secret on an added line", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const diff = `### src/config.ts (modified) +1/-0\n@@\n+const token = "${fakeToken}";`; + expect(scanPrDiffForSecretKinds(diff)).toContain("github_token"); + }); + + it("does not flag secrets on removed or context lines", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const removed = `### src/config.ts (modified) +0/-1\n@@\n-const token = "${fakeToken}";`; + const context = `### src/config.ts (modified) +1/-0\n@@\n const token = "${fakeToken}";\n+const unrelated = 1;`; + expect(scanPrDiffForSecretKinds(removed)).toEqual([]); + expect(scanPrDiffForSecretKinds(context)).toEqual([]); + }); + + it("scans patch-less synthetic + lines without a hunk header", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const diff = `### secrets.env (modified) +1/-0\n+const token = "${fakeToken}";`; + expect(scanPrDiffForSecretKinds(diff)).toContain("github_token"); + }); +}); From 4e3a2c8039978900aee41e22c5395e0e9478f51c Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 04:43:20 +0800 Subject: [PATCH 2/7] test(review): cover scanPrDiffForSecretKinds branches for #2824 Exercise empty-line section breaks, orphan + lines, removed-line join reset, added/renamed header paths, generic cross-line assignment, and the already-matched skip path so Codecov patch coverage meets 99%. Co-authored-by: Cursor --- test/unit/secrets-scan.test.ts | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index bd9504eb23..0a8da7b08d 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -173,4 +173,70 @@ describe("scanPrDiffForSecretKinds — cross-line split credentials (#2454)", () const diff = `### secrets.env (modified) +1/-0\n+const token = "${fakeToken}";`; expect(scanPrDiffForSecretKinds(diff)).toContain("github_token"); }); + + it("flags secrets embedded in added/renamed file path headers", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const added = `### fixtures/${fakeToken}.txt (added) +1/-0\n+clean content`; + const renamed = `### fixtures/${fakeToken}.txt (renamed) +0/-0\n+clean content`; + expect(scanPrDiffForSecretKinds(added)).toContain("github_token"); + expect(scanPrDiffForSecretKinds(renamed)).toContain("github_token"); + }); + + it("ignores orphan + lines that appear before any file header", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + expect(scanPrDiffForSecretKinds(`+const token = "${fakeToken}";`)).toEqual([]); + }); + + it("resets cross-line join state across blank lines between file sections", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const diff = [ + "### src/a.ts (modified) +1/-0", + `+const part1 = "${awsKeyFragmentA}";`, + "", + "### src/b.ts (modified) +1/-0", + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).not.toContain("aws_access_key"); + expect( + scanPrDiffForSecretKinds( + [ + "### src/b.ts (modified) +1/-0", + `+const token = "${fakeToken}";`, + ].join("\n"), + ), + ).toContain("github_token"); + }); + + it("resets cross-line join state when a removed line breaks the added-line run", () => { + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,0 +1,2 @@", + `+const part1 = "${awsKeyFragmentA}";`, + "-const removed = 1;", + `+const part2 = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).not.toContain("aws_access_key"); + }); + + it("recovers generic_secret_assignment when keyword and value span adjacent added lines", () => { + const fakeSecret = "sk_live_" + "aK9xQ2mZw7Ln4Rv8Pt3Bh6"; + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@", + "+client_secret =", + `+"${fakeSecret}"`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toContain("generic_secret_assignment"); + }); + + it("does not double-join when the first added line already matches on its own", () => { + const fakeAwsKey = awsKeyFragmentA + awsKeyFragmentB; + const diff = [ + "### src/config.ts (modified) +2/-0", + "@@", + `+const key = "${fakeAwsKey}";`, + `+const tail = "${awsKeyFragmentB}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toEqual(["aws_access_key"]); + }); }); From 9fd15dae60c3efe948cf22fe51148b75e24a6d76 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 06:16:31 +0800 Subject: [PATCH 3/7] fix(review): scan in-hunk +++ content in secret diff gate (#2824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only skip real unified-diff file headers before the first hunk; added lines whose content begins with ++ (rendered +++…) must still trip the gate. Co-authored-by: Cursor --- src/review/secrets-scan.ts | 11 +++++++++-- test/unit/secrets-scan.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index be96053061..7515d27c70 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -106,6 +106,11 @@ function formatSecretKindsFromText(text: string): string[] { return kinds; } +/** True for unified-diff file headers (`+++ b/path`, `--- a/path`), not added content like `+++const`. */ +function isUnifiedDiffFileHeaderLine(line: string): boolean { + return /^(?:\+\+\+|---) (?:[ab]\/|\/dev/null)/.test(line); +} + /** Scan a PR diff for secret kinds introduced on added lines (and added/renamed file headers). Per-line regex * first; then a bounded cross-line join of consecutive added lines' adjacent string literals (#2454) so a * credential split across `const a = "AKIA…"; const b = "REST";` still trips the unconditional gate. Context, @@ -133,13 +138,15 @@ export function scanPrDiffForSecretKinds(diff: string): string[] { } continue; } - if (/^@@ /.test(line)) { + if (line.startsWith("@@")) { inHunk = true; previousLiterals = []; continue; } - if (line.startsWith("+") && !line.startsWith("+++")) { + if (line.startsWith("+")) { if (!inFileSection) continue; + // Skip pre-hunk file headers only; inside a hunk `+++…` is added content, not a header. + if (!inHunk && isUnifiedDiffFileHeaderLine(line)) continue; const content = line.slice(1); addedLines.push(content); let matched = false; diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index 0a8da7b08d..47cee1bab0 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -239,4 +239,35 @@ describe("scanPrDiffForSecretKinds — cross-line split credentials (#2454)", () ].join("\n"); expect(scanPrDiffForSecretKinds(diff)).toEqual(["aws_access_key"]); }); + + it("scans in-hunk added content that begins with ++ (rendered +++…)", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const diff = [ + "### src/config.ts (modified) +1/-0", + "@@ -1,0 +1,1 @@", + `+++const token = "${fakeToken}";`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toContain("github_token"); + }); + + it("skips unified-diff file headers before the first hunk", () => { + const fakeToken = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const diff = [ + "### src/config.ts (modified) +1/-0", + "+++ b/src/config.ts", + "--- a/src/config.ts", + "@@ -1,0 +1,1 @@", + "+const ok = 1;", + ].join("\n"); + expect(scanPrDiffForSecretKinds(diff)).toEqual([]); + expect( + scanPrDiffForSecretKinds( + [ + "### src/config.ts (modified) +1/-0", + "+++ b/src/config.ts", + `+const token = "${fakeToken}";`, + ].join("\n"), + ), + ).toContain("github_token"); + }); }); From c979ef30aa4022706ca2d5812f789a0d88512b5b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 06:25:12 +0800 Subject: [PATCH 4/7] fix(review): escape /dev/null in diff header regex (#2824) The unescaped slash in /dev/null terminated the regex literal and broke typecheck/validate-code on PR #2824. Co-authored-by: Cursor --- src/review/secrets-scan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index 7515d27c70..86e610730e 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -108,7 +108,7 @@ function formatSecretKindsFromText(text: string): string[] { /** True for unified-diff file headers (`+++ b/path`, `--- a/path`), not added content like `+++const`. */ function isUnifiedDiffFileHeaderLine(line: string): boolean { - return /^(?:\+\+\+|---) (?:[ab]\/|\/dev/null)/.test(line); + return /^(?:\+\+\+|---) (?:[ab]\/|\/dev\/null)/.test(line); } /** Scan a PR diff for secret kinds introduced on added lines (and added/renamed file headers). Per-line regex From a264ba5f3d4c2a26a98dab4e442a0419b2370018 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 07:22:57 +0800 Subject: [PATCH 5/7] fix(review): bound generic secret scan to consecutive added runs (#2824) Reset generic_secret_assignment runs at the same diff boundaries as the cross-line literal join instead of scanning the whole diff blob. Add regression tests for context, removed, hunk, and file-section splits. Co-authored-by: Cursor --- src/review/secrets-scan.ts | 35 ++++++++++++++++++---------- test/unit/secrets-scan.test.ts | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/review/secrets-scan.ts b/src/review/secrets-scan.ts index 86e610730e..6bc3d120e6 100644 --- a/src/review/secrets-scan.ts +++ b/src/review/secrets-scan.ts @@ -106,7 +106,8 @@ function formatSecretKindsFromText(text: string): string[] { return kinds; } -/** True for unified-diff file headers (`+++ b/path`, `--- a/path`), not added content like `+++const`. */ +/** True for unified-diff file headers (`+++ b/path`, `--- a/path`), not added content like `+++const`. + * Mirrors review-enrichment/src/analyzers/diff-lines.ts `isDiffFileHeaderLine`. */ function isUnifiedDiffFileHeaderLine(line: string): boolean { return /^(?:\+\+\+|---) (?:[ab]\/|\/dev\/null)/.test(line); } @@ -114,25 +115,37 @@ function isUnifiedDiffFileHeaderLine(line: string): boolean { /** Scan a PR diff for secret kinds introduced on added lines (and added/renamed file headers). Per-line regex * first; then a bounded cross-line join of consecutive added lines' adjacent string literals (#2454) so a * credential split across `const a = "AKIA…"; const b = "REST";` still trips the unconditional gate. Context, - * removed, and hunk boundaries reset the join window — same semantics as review-enrichment's scanPatch. */ + * removed, hunk, and file-section boundaries reset both the literal-join window and generic-assignment runs. */ export function scanPrDiffForSecretKinds(diff: string): string[] { const found = new Set(); - const addedLines: string[] = []; let inFileSection = false; let inHunk = false; let previousLiterals: string[] = []; + let addedRun: string[] = []; + + const resetJoinState = (): void => { + previousLiterals = []; + addedRun = []; + }; + + const noteGenericFromAddedRun = (): void => { + if (found.has("generic_secret_assignment") || addedRun.length === 0) return; + if (hasGenericSecretAssignment(addedRun.join("\n"))) { + found.add("generic_secret_assignment"); + } + }; for (const line of diff.split("\n")) { if (line === "") { inFileSection = false; inHunk = false; - previousLiterals = []; + resetJoinState(); continue; } if (/^### .+ \(.+\) /.test(line)) { inFileSection = true; inHunk = false; - previousLiterals = []; + resetJoinState(); if (/^### .+ \((?:added|renamed)\) /.test(line)) { for (const kind of formatSecretKindsFromText(line)) found.add(kind); } @@ -140,7 +153,7 @@ export function scanPrDiffForSecretKinds(diff: string): string[] { } if (line.startsWith("@@")) { inHunk = true; - previousLiterals = []; + resetJoinState(); continue; } if (line.startsWith("+")) { @@ -148,12 +161,13 @@ export function scanPrDiffForSecretKinds(diff: string): string[] { // Skip pre-hunk file headers only; inside a hunk `+++…` is added content, not a header. if (!inHunk && isUnifiedDiffFileHeaderLine(line)) continue; const content = line.slice(1); - addedLines.push(content); + addedRun.push(content); let matched = false; for (const kind of formatSecretKindsFromText(content)) { found.add(kind); matched = true; } + noteGenericFromAddedRun(); const currentLiterals = extractStringLiteralContents(content); const lastPrevious = previousLiterals.at(-1); const firstCurrent = currentLiterals[0]; @@ -170,16 +184,13 @@ export function scanPrDiffForSecretKinds(diff: string): string[] { continue; } if (line.startsWith("-")) { - previousLiterals = []; + resetJoinState(); continue; } if (inHunk && line.startsWith(" ")) { - previousLiterals = []; + resetJoinState(); } } - if (!found.has("generic_secret_assignment") && hasGenericSecretAssignment(addedLines.join("\n"))) { - found.add("generic_secret_assignment"); - } return [...found]; } diff --git a/test/unit/secrets-scan.test.ts b/test/unit/secrets-scan.test.ts index 47cee1bab0..3270034372 100644 --- a/test/unit/secrets-scan.test.ts +++ b/test/unit/secrets-scan.test.ts @@ -229,6 +229,48 @@ describe("scanPrDiffForSecretKinds — cross-line split credentials (#2454)", () expect(scanPrDiffForSecretKinds(diff)).toContain("generic_secret_assignment"); }); + it("does not join generic keyword and value across context, removed, hunk, or file boundaries", () => { + const fakeSecret = "sk_live_" + "aK9xQ2mZw7Ln4Rv8Pt3Bh6"; + + const contextSplit = [ + "### src/config.ts (modified) +2/-0", + "@@", + "+client_secret =", + " const unrelated = 1;", + `+"${fakeSecret}"`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(contextSplit)).not.toContain("generic_secret_assignment"); + + const removedSplit = [ + "### src/config.ts (modified) +2/-0", + "@@", + "+client_secret =", + "-const gone = 1;", + `+"${fakeSecret}"`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(removedSplit)).not.toContain("generic_secret_assignment"); + + const hunkSplit = [ + "### src/config.ts (modified) +2/-0", + "@@ -1,0 +1,1 @@", + "+client_secret =", + "@@ -10,0 +11,1 @@", + `+"${fakeSecret}"`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(hunkSplit)).not.toContain("generic_secret_assignment"); + + const fileSplit = [ + "### src/a.ts (modified) +1/-0", + "@@", + "+client_secret =", + "", + "### src/b.ts (modified) +1/-0", + "@@", + `+"${fakeSecret}"`, + ].join("\n"); + expect(scanPrDiffForSecretKinds(fileSplit)).not.toContain("generic_secret_assignment"); + }); + it("does not double-join when the first added line already matches on its own", () => { const fakeAwsKey = awsKeyFragmentA + awsKeyFragmentB; const diff = [ From 05c310119b1a6b15f0c477d2031aa96ddc9acc58 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 07:32:06 +0800 Subject: [PATCH 6/7] fix(engine): restore missing contributor-fit export block (#2824) Main dropped the export opener in packages/gittensory-engine/src/index.ts (#2787), breaking tsc for every backend PR including this one. Co-authored-by: Cursor --- packages/gittensory-engine/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 4adbe14d99..43b5f0cf32 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -49,6 +49,7 @@ export { computeLaneFit, type GoalModelInput, } from "./goal-model.js"; +export { classifyContributorFit, type ContributorFit, type ContributorFitCheck, From 909a3cb62d28c0c7c476787d454af19e51a84094 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sat, 4 Jul 2026 07:42:47 +0800 Subject: [PATCH 7/7] fix(engine): treat missing timestamps as stale age, not fresh (#2824) Main opportunity-freshness returned age 0 for null/invalid timestamps, which scored as fully fresh and broke four validate-code tests. Align with upstream fix/opportunity-freshness-clock-drift by using a 9999-day sentinel so unparseable timestamps clamp to the 0.05 stale floor. Co-authored-by: Cursor --- .../gittensory-engine/src/opportunity-freshness.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/gittensory-engine/src/opportunity-freshness.ts b/packages/gittensory-engine/src/opportunity-freshness.ts index 943ee78f49..d23ef0ff02 100644 --- a/packages/gittensory-engine/src/opportunity-freshness.ts +++ b/packages/gittensory-engine/src/opportunity-freshness.ts @@ -26,10 +26,16 @@ function pickTimestamp(issue: FreshnessIssue): string | null { return null; } +// No usable timestamp survived pickTimestamp's updatedAt->createdAt fallback -- an unknown age must never +// register as "just updated" (age 0, the freshest possible score). Floor it to a large sentinel so +// computeOpportunityFreshness's exponential decay clamps straight to the 0.05 floor, matching how a genuinely +// stale issue scores, not a fresh one. +const UNKNOWN_AGE_DAYS = 9999; + function issueAgeDays(value: string | null, nowMs: number): number { - if (!value) return 0; + if (!value) return UNKNOWN_AGE_DAYS; const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return 0; + if (!Number.isFinite(parsed)) return UNKNOWN_AGE_DAYS; return Math.floor((nowMs - parsed) / 86_400_000); }