From 6f79c83e80508f90d5d37f271ee72b4f2028063f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 2 Sep 2026 22:05:56 +0200 Subject: [PATCH 1/4] test(coverage): rename-only hunks owe no changed-line coverage Pass --find-renames=90% to the changed-line diff so rename detection no longer depends on the host diff.renames setting: a 100%-similarity move contributes no changed lines and an edited move contributes only the hunks that differ from its source. Threshold unchanged. --- scripts/coverage-changed/model.test.ts | 71 ++++++++++++++++++++++++++ scripts/coverage-changed/run.test.ts | 11 ++++ scripts/coverage-changed/run.ts | 20 ++++++-- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/scripts/coverage-changed/model.test.ts b/scripts/coverage-changed/model.test.ts index 217372333a..058e81d8e2 100644 --- a/scripts/coverage-changed/model.test.ts +++ b/scripts/coverage-changed/model.test.ts @@ -239,3 +239,74 @@ test('changed line suppressed by an ignore directive is tallied, not gated', () test('threshold constant is the single source of truth', () => { assert.equal(CHANGED_LINE_COVERAGE_THRESHOLD, 70); }); + +// Shape of `git diff --unified=0 --find-renames=90%` for a move PR: a pure +// move carries no hunks, an edited move carries only the hunks that differ from +// its source, and an ordinary edit is unaffected. +const MOVE_PR_DIFF = [ + 'diff --git a/src/mod.ts b/src/mod.ts', + 'index 57ee600..9fe64ef 100644', + '--- a/src/mod.ts', + '+++ b/src/mod.ts', + '@@ -2 +2,2 @@ export const m1 = 1;', + '-export const m2 = 2;', + '+export const m2 = 22;', + '+export const m3 = 3;', + 'diff --git a/src/edited.ts b/src/moved-edited.ts', + 'similarity index 90%', + 'rename from src/edited.ts', + 'rename to src/moved-edited.ts', + 'index a3d7378..fe85380 100644', + '--- a/src/edited.ts', + '+++ b/src/moved-edited.ts', + '@@ -3 +3 @@ export const b2 = 2;', + '-export const b3 = 3;', + '+export const b3 = 33;', + '@@ -7 +7 @@ export const b6 = 6;', + '-export const b7 = 7;', + '+export const b7 = 77;', + '@@ -9 +9 @@ export const b8 = 8;', + '-export const b9 = 9;', + '+export const b9 = 99;', + 'diff --git a/src/pure.ts b/src/moved-pure.ts', + 'similarity index 100%', + 'rename from src/pure.ts', + 'rename to src/moved-pure.ts', +].join('\n'); + +function uncoveredRecord(file: string, lineCount: number): string { + const da = Array.from({ length: lineCount }, (_, i) => `DA:${i + 1},0`); + return [`SF:${file}`, ...da, 'end_of_record'].join('\n'); +} + +test('rename-only hunks owe no changed-line coverage; edited moves owe their edits', () => { + const diffs = parseUnifiedDiff(MOVE_PR_DIFF); + assert.deepEqual( + diffs.map((d) => [d.path, d.added]), + [ + ['src/mod.ts', [2, 3]], + ['src/moved-edited.ts', [3, 7, 9]], + ['src/moved-pure.ts', []], + ], + ); + const result = computeChangedCoverage({ + diffs, + coverage: parseLcov( + [ + uncoveredRecord('src/mod.ts', 3), + uncoveredRecord('src/moved-edited.ts', 30), + uncoveredRecord('src/moved-pure.ts', 30), + ].join('\n'), + ), + fileLines: () => null, + }); + // 2 ordinary edits + 3 edited move lines; the 30-line pure move owes nothing. + assert.equal(result.totalLines, 5); + assert.deepEqual( + result.offenders.map((f) => [f.path, f.uncoveredLines]), + [ + ['src/mod.ts', [2, 3]], + ['src/moved-edited.ts', [3, 7, 9]], + ], + ); +}); diff --git a/scripts/coverage-changed/run.test.ts b/scripts/coverage-changed/run.test.ts index b16972d4bf..4c2af3bb09 100644 --- a/scripts/coverage-changed/run.test.ts +++ b/scripts/coverage-changed/run.test.ts @@ -147,3 +147,14 @@ test('errors when the lcov report is missing rather than silently passing', () = assert.equal(code, 1); assert.match(out, /no lcov report/); }); + +test('a pure move owes nothing regardless of the host diff.renames setting', () => { + git('config', 'diff.renames', 'false'); + git('mv', 'src/base.ts', 'src/moved.ts'); + git('commit', '-q', '-m', 'move'); + writeLcov('SF:src/moved.ts\nDA:1,0\nend_of_record\n'); + const { code, out } = capture(() => run(['--base', 'main'], repo)); + assert.equal(code, 0); + assert.match(out, /Changed-line coverage gate: PASS/); + assert.match(out, /0\/0 \(n\/a\)/); +}); diff --git a/scripts/coverage-changed/run.ts b/scripts/coverage-changed/run.ts index 8d7644b07c..1a265fe11d 100644 --- a/scripts/coverage-changed/run.ts +++ b/scripts/coverage-changed/run.ts @@ -3,7 +3,9 @@ // // Reuses the lcov report that `pnpm test:coverage` already produced (never runs // coverage a second time), joins it with `git diff --unified=0 ...HEAD`, -// and fails when changed-line coverage is below the threshold. A PR carrying +// and fails when changed-line coverage is below the threshold. Renames are +// detected at RENAME_SIMILARITY regardless of the host's `diff.renames`, so a +// moved file owes only the hunks that differ from its source. A PR carrying // the `coverage-waiver` label skips the failure but still prints every number. // The same markdown report goes to stdout and to the GitHub job summary; it // includes changed-branch coverage and the count of changed executable lines @@ -23,6 +25,7 @@ import { const USAGE = 'Usage: pnpm check:coverage-changed [--base ]\n'; const LCOV_PATH = 'coverage/lcov.info'; +const RENAME_SIMILARITY = '90%'; const GIT_DIFF_MAX_BUFFER_BYTES = 16 * 1024 * 1024; function fmtPct(pct: number | null): string { @@ -72,10 +75,17 @@ export function run(argv: readonly string[], cwd?: string): number { return 1; } - const diff = runCmdSync('git', ['diff', '--unified=0', '--no-color', `${base}...HEAD`], { - cwd: root, - maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES, - }).stdout; + const diff = runCmdSync( + 'git', + [ + 'diff', + '--unified=0', + '--no-color', + `--find-renames=${RENAME_SIMILARITY}`, + `${base}...HEAD`, + ], + { cwd: root, maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES }, + ).stdout; const result = computeChangedCoverage({ diffs: parseUnifiedDiff(diff), coverage: parseLcov(fs.readFileSync(lcovPath, 'utf8'), root), From a372acfdb9365177697faa9985a18bf7bee4fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 2 Sep 2026 22:05:57 +0200 Subject: [PATCH 2/4] docs(agents): pure moves carry their tests unchanged Drops the stale src/daemon/handlers/session.ts over-budget bullet (242 lines on main) to stay under the AGENTS.md byte budget. --- AGENTS.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4bb4dacbac..5e20b49e3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,10 +91,9 @@ cross-language rules change through golden tables under `contracts/fixtures/`. - Implementation files target at most 300 lines. Extract before adding behavior past 500 lines; files past 1,000 lines are architecture debt unless generated or fixture data. - Tests mirror source topology one-to-one. Split a source module and its test together; do not add to - the legacy `interaction.test.ts` or platform `index.test.ts` aggregations. + the legacy `interaction.test.ts` or platform `index.test.ts` aggregations. Pure moves carry their + tests unchanged; rename-only hunks owe no new coverage. - Shared fixtures are named exports in a sibling fixture module, not repeated inline literals. -- `src/daemon/handlers/session.ts` is already over budget; extract the relevant platform-specific - concept before adding behavior. ## Toolchain and worktree traps From 4526f80967059fc332284f6089d2a84579f149b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 3 Sep 2026 08:09:47 +0200 Subject: [PATCH 3/4] style: format coverage-changed run.ts --- scripts/coverage-changed/run.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/scripts/coverage-changed/run.ts b/scripts/coverage-changed/run.ts index 1a265fe11d..6a4bfd965f 100644 --- a/scripts/coverage-changed/run.ts +++ b/scripts/coverage-changed/run.ts @@ -77,13 +77,7 @@ export function run(argv: readonly string[], cwd?: string): number { const diff = runCmdSync( 'git', - [ - 'diff', - '--unified=0', - '--no-color', - `--find-renames=${RENAME_SIMILARITY}`, - `${base}...HEAD`, - ], + ['diff', '--unified=0', '--no-color', `--find-renames=${RENAME_SIMILARITY}`, `${base}...HEAD`], { cwd: root, maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES }, ).stdout; const result = computeChangedCoverage({ From cc6c52151c890a04fbb1523b58f11fa9053c14f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 3 Sep 2026 14:43:59 +0200 Subject: [PATCH 4/4] docs(agents): restore the session.ts over-budget rule --- AGENTS.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5e20b49e3e..3b77dcb130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,8 @@ task procedures only when needed: | Writing issues or PRDs, and triage labels | `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md` | | Web backend setup or diagnostics | `docs/agents/web-backend.md` | -Versioned CLI help is the source of truth for command behavior. Start workflow planning with -`agent-device help workflow`, then use the relevant topic help. +Versioned CLI help is the source of truth for command behavior. Start with `agent-device help +workflow`, then the relevant topic help. ## Incident-derived principles @@ -94,11 +94,13 @@ cross-language rules change through golden tables under `contracts/fixtures/`. the legacy `interaction.test.ts` or platform `index.test.ts` aggregations. Pure moves carry their tests unchanged; rename-only hunks owe no new coverage. - Shared fixtures are named exports in a sibling fixture module, not repeated inline literals. +- `src/daemon/handlers/session.ts` is already over budget; extract the relevant platform-specific + concept before adding behavior. ## Toolchain and worktree traps -- Use `pnpm`; never add `package-lock.json`. OXC owns lint and format. Run `pnpm format` for the - repository, not a path-scoped formatter invocation. +- Use `pnpm`; never add `package-lock.json`. OXC owns lint and format. Run `pnpm format` + repository-wide, not path-scoped. - A fresh worktree requires `pnpm install --frozen-lockfile && pnpm build`. Until then package and optional-peer resolution may point at another checkout and produce false failures. - Source-checkout daemon state is worktree-scoped, but devices are not. Use `pnpm daemon:state-dir` @@ -154,5 +156,5 @@ contents. Keep a sentence in this file only when no gate, lint rule, versioned h decision-site comment can own it. `CONTEXT.md` is glossary-only: no implementation paths, architecture decisions, migration state, or workflows. -Behavior changes update their owning help/metadata and user docs when relevant. In the final summary, +Behavior changes update their owning help/metadata and user docs. In the final summary, state whether docs or skills changed and why.