From eb9263538d40f01e7518129cf9e4b23d97502d22 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 10 Oct 2026 16:55:42 +0000 Subject: [PATCH] fix(gate-diff): RH-15? only on CI/config floors after calibration Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- docs/designs/gate-diff-calibration.md | 7 ++++--- lib/gate-diff/tags.ts | 12 +++++++++--- test/gate-diff.test.ts | 2 ++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/designs/gate-diff-calibration.md b/docs/designs/gate-diff-calibration.md index 9b7624cd9..4d57039f3 100644 --- a/docs/designs/gate-diff-calibration.md +++ b/docs/designs/gate-diff-calibration.md @@ -38,9 +38,10 @@ No artifact was written: calibration never touches the state root. { timeout: 30_000 })` in new or moved test code; this repository requires a timeout on every synchronous spawn (`test/spawnsync-timeout-tripwire.test.ts`), so nearly every test PR carries dozens. As specified ("unpaired values are - tagged RH-15? and read") the tag is noise here. Candidates for the human - decision the design reserves: demote `RH-15?` to inventory, or keep it only - for CI/config files and test-framework timeout options. The detector was + tagged RH-15? and read") the tag is noise here. **Decision (integrator, per + the design's demotion rule):** `RH-15?` now applies only to CI and + runner/lint-config files; in test files unpaired values are inventory and + only paired RH-15 is tagged. The table below predates that change. The detector was tightened to key-value assignments (`key: N`, `key = N`, `key(N)`, `--key N`, and a gate key opening a nested structure) and to numeric literals with `_` separators; prose strings and `setTimeout` calls no longer fire. diff --git a/lib/gate-diff/tags.ts b/lib/gate-diff/tags.ts index 00b7548f4..f39e7c7e2 100644 --- a/lib/gate-diff/tags.ts +++ b/lib/gate-diff/tags.ts @@ -162,9 +162,10 @@ function valueLines(lines: string[]): ValueLine[] { /** * RH-15 inside one hunk: removed/added pairs with the same gate key and the * same numeric shape whose numbers differ (direction shown in notes), an - * exact-count assertion relaxed to a bound, and RH-15? for unpaired values. + * exact-count assertion relaxed to a bound, and RH-15? for unpaired values + * when `opts.unpaired` is not false (callers disable it for test files). */ -export function detectRelaxation(h: Hunk): { tags: string[]; notes: string[] } { +export function detectRelaxation(h: Hunk, opts: { unpaired?: boolean } = { unpaired: true }): { tags: string[]; notes: string[] } { const dels = valueLines(removed(h)); const adds = valueLines(added(h)); const tags = new Set(); @@ -188,6 +189,7 @@ export function detectRelaxation(h: Hunk): { tags: string[]; notes: string[] } { tags.add('RH-15'); notes.push(`RH-15: exact-count assertion relaxed to a bound (${subject.trim()})`); } + if (opts.unpaired === false) return { tags: [...tags], notes }; for (const v of [...dels, ...adds]) { if (v.paired) continue; tags.add('RH-15?'); @@ -245,8 +247,12 @@ export function tagHunk(h: Hunk, ctx: TagContext): TagResult { if (nonTest && anyMatch(adds, ENV_SNIFF_IDIOMS)) tags.add('RH-12'); if (anyMatch(adds, SUPPRESSION_IDIOMS)) tags.add('RH-13'); if (anyMatch(adds, BYPASS_IDIOMS)) tags.add('RH-14'); + // Calibration over gstack's last 50 PRs (docs/designs/gate-diff-calibration.md): + // unpaired values in test code are mandated `spawnSync` timeouts on 41 of + // 50 PRs, so RH-15? applies only to CI and runner/lint config; test files + // keep paired RH-15 and stay inventory otherwise. if (ctx.floor === 'test' || ctx.floor === 'ci' || ctx.floor === 'config') { - const r = detectRelaxation(h); + const r = detectRelaxation(h, { unpaired: ctx.floor !== 'test' }); for (const t of r.tags) tags.add(t); notes.push(...r.notes); } diff --git a/test/gate-diff.test.ts b/test/gate-diff.test.ts index 49e40b382..35c37bff4 100644 --- a/test/gate-diff.test.ts +++ b/test/gate-diff.test.ts @@ -111,6 +111,8 @@ describe('gate-diff tags: one planted case per id', () => { test('RH-15?: an unpaired gate value (new file or reordered lines) is read, not inferred', () => { const d = detectRelaxation({ header: '@@', lines: [{ kind: 'add', text: 'export default { testTimeout: 60000 };' }] }); expect(d.tags).toEqual(['RH-15?']); + const inTest = detectRelaxation({ header: '@@', lines: [{ kind: 'add', text: " spawnSync('git', ['status'], { timeout: 30_000 });" }] }, { unpaired: false }); + expect(inTest.tags).toEqual([]); expect(gateKey('const minScore = 0.4;')).toEqual({ key: 'minScore', relaxes: 'down', value: 0.4 }); expect(gateKey("console.log('Test Coverage Audit: 5 new code paths');")).toBeNull(); expect(gateKey('setTimeout(done, 0);')).toBeNull();