diff --git a/.claude/skills/correctness/scripts/checklist.mjs b/.claude/skills/correctness/scripts/checklist.mjs index 54b6974f..48581d9f 100644 --- a/.claude/skills/correctness/scripts/checklist.mjs +++ b/.claude/skills/correctness/scripts/checklist.mjs @@ -62,9 +62,7 @@ function extractLens(argv) { // Group-scoped path. Sorted so the SAME group always resolves to the same file regardless of the // order the caller listed its lenses — otherwise `a,b` and `b,a` would be two different runs. const lensPath = (lens) => - lens && lens.length - ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` - : CHECKLIST_PATH; + lens?.length ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` : CHECKLIST_PATH; const log = console.log; diff --git a/.cursor/skills/correctness/scripts/checklist.mjs b/.cursor/skills/correctness/scripts/checklist.mjs index 54b6974f..48581d9f 100644 --- a/.cursor/skills/correctness/scripts/checklist.mjs +++ b/.cursor/skills/correctness/scripts/checklist.mjs @@ -62,9 +62,7 @@ function extractLens(argv) { // Group-scoped path. Sorted so the SAME group always resolves to the same file regardless of the // order the caller listed its lenses — otherwise `a,b` and `b,a` would be two different runs. const lensPath = (lens) => - lens && lens.length - ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` - : CHECKLIST_PATH; + lens?.length ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` : CHECKLIST_PATH; const log = console.log; diff --git a/.devkit/skills-manifest.json b/.devkit/skills-manifest.json index 0c84cd49..5393d402 100644 --- a/.devkit/skills-manifest.json +++ b/.devkit/skills-manifest.json @@ -1,6 +1,6 @@ { - "devkitRef": "v0.49.1", - "generatedAt": "2026-08-07T09:41:05.307Z", + "devkitRef": "v0.50.0", + "generatedAt": "2026-08-07T11:47:22.452Z", "targets": [ "claude", "cursor" @@ -17,7 +17,7 @@ "commit-guard/SKILL.md": "9a5310f972f70d625db1c4fd2c149c899eff081ee2ff11fc372d7450dbffb5ce", "commit-guard/scripts/checklist.mjs": "dc57c1f1407a0fe7503ebe72e30e9167b29bdf1154ab44e68de82b6d558770e1", "correctness/SKILL.md": "16fca786d4b2fa85c2588aee13d14f4cf6a71a30014cbcc24993f526f67770c7", - "correctness/scripts/checklist.mjs": "4ea70cf9257dbae589b8648b396fc3c12c5b306026ec455c80c1c438023c6e58", + "correctness/scripts/checklist.mjs": "adb9777dc7b3fdeb5e4dc31bc5e231c2f947b0bca235b0d04b54f81c3180c427", "decisions/SKILL.md": "9bdad17d600fed8c0bc5be1bb6f04822ed03327635629b266e2b5e07cb92b498", "dup-detection/SKILL.md": "74a4964795bbb27ce2bda0e13b91da00f04a01a989bcbf6149d10ff9c3688a4c", "feature-critique/SKILL.md": "0a595267e6dab2fad6e571b3861da4964ccff934d50ac9efaf137a23d3a8efef", diff --git a/cli/__tests__/husky-block.test.mts b/cli/__tests__/husky-block.test.mts index 3b677808..28ba21d3 100644 --- a/cli/__tests__/husky-block.test.mts +++ b/cli/__tests__/husky-block.test.mts @@ -415,6 +415,7 @@ describe('buildOverlayHook — fallow gate (overlay)', () => { }); it('emits the fallow gate scoped by DEVKIT_SHIP_BASE_SHA', () => { + // biome-ignore lint/suspicious/noTemplateCurlyInString: shell ${VAR:-default}, not a JS template expect(hook).toContain('[ -n "${DEVKIT_SHIP_BASE_SHA:-}" ]'); expect(hook).toContain('FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA"'); expect(hook).toContain('fallow audit $FALLOW_BASE_ARGS || exit 1'); @@ -437,6 +438,7 @@ describe('buildOverlayHook — fallow gate (overlay)', () => { const stub = join(binDir, 'fallow'); writeFileSync( stub, + // biome-ignore lint/suspicious/noTemplateCurlyInString: shell ${VAR:-default}, not a JS template '#!/bin/sh\necho "FALLOW_ARGS:$*"\necho "GIT_DIR:${GIT_DIR:-unset} GIT_INDEX_FILE:${GIT_INDEX_FILE:-unset}"\n', ); chmodSync(stub, 0o755); diff --git a/cli/__tests__/self-host.test.mts b/cli/__tests__/self-host.test.mts index b6d6bc57..3f51e7dc 100644 --- a/cli/__tests__/self-host.test.mts +++ b/cli/__tests__/self-host.test.mts @@ -115,6 +115,20 @@ describe('buildSelfHostHook', () => { expect(hook).not.toContain('@norvalbv/devkit'); }); + // The `--extra` above is only as hard as the script it names, and biome exits 0 when every + // diagnostic is warn-severity. A bare `biome check .` therefore PRINTS its findings into the gate + // log — indistinguishable from a real failure to the reader — and passes the commit anyway (a + // v0.50.0 ship shipped with six of them). `--error-on-warnings` is what makes the exit code match + // what the log shows. devkit's own root config turns `noConsole` (the one deliberately-advisory + // rule in biome/base.jsonc) off for the whole authored surface, so nothing advisory is caught here. + it('backs the lint extra with a biome invocation that exits non-zero on WARNINGS too', () => { + const pkg: { scripts?: Record } = JSON.parse( + readFileSync(join(ROOT, 'package.json'), 'utf8'), + ); + expect(SELF_HOST_EXTRAS).toContainEqual({ label: 'lint', cmd: 'bun run lint' }); + expect(pkg.scripts?.lint).toContain('--error-on-warnings'); + }); + it('preserves the advisory fallow-audit gate INSIDE the block (never blocks, survives re-run)', () => { const hook = buildSelfHostHook(HOOK_SEL, '', ROOT); expect(hook).toContain( @@ -144,6 +158,7 @@ describe('buildSelfHostHook', () => { // which execs a binary and cannot see shell functions. const binDir = mkdtempSync(join(tmpdir(), 'fallow-stub-')); const stub = join(binDir, 'fallow'); + // biome-ignore lint/suspicious/noTemplateCurlyInString: shell ${VAR:-default}, not a JS template writeFileSync(stub, '#!/bin/sh\necho "FALLOW_ARGS:$*"\necho "GIT_DIR:${GIT_DIR:-unset}"\n'); chmodSync(stub, 0o755); const script = `${DK_NO_GIT_ENV_HELPER}\n${fragment}`; diff --git a/cli/lib/husky/self-host.mts b/cli/lib/husky/self-host.mts index d001132c..e737bd00 100644 --- a/cli/lib/husky/self-host.mts +++ b/cli/lib/husky/self-host.mts @@ -32,8 +32,10 @@ import { } from './husky-block.mts'; // devkit's own structure-lint command (package.json `lint:structure` = `eslint cli gate-engine`) -// and its hard biome-lint gate (`lint` = `biome check .`). The hard-lint is folded into the -// deterministic orchestrator via `--extra` (any non-zero blocks); both run via real devDeps +// and its hard biome-lint gate (`lint` = `biome check --error-on-warnings .` — biome exits 0 when +// every diagnostic is warn-severity, so without that flag the gate PRINTS its findings into the log +// and passes the commit anyway). The hard-lint is folded into the deterministic orchestrator via +// `--extra` (any non-zero blocks); both run via real devDeps // (eslint/biome), so toSelfHost leaves them untouched. Together with the advisory fallow fragment // below, the self-host hook preserves every gate the pre-self-host hand hook ran AND adds review + dup/clone. export const SELF_HOST_STRUCTURE_CMD = 'bun run lint:structure'; diff --git a/dist/cli/lib/husky/self-host.mjs b/dist/cli/lib/husky/self-host.mjs index 36be5fdf..54a06d21 100644 --- a/dist/cli/lib/husky/self-host.mjs +++ b/dist/cli/lib/husky/self-host.mjs @@ -25,8 +25,10 @@ import { readJson } from "../fs-helpers.mjs"; import { markEnd } from "./husky.mjs"; import { buildFullHook, buildGuardBlock, extractGuardBlock, replaceGuardBlock, } from "./husky-block.mjs"; // devkit's own structure-lint command (package.json `lint:structure` = `eslint cli gate-engine`) -// and its hard biome-lint gate (`lint` = `biome check .`). The hard-lint is folded into the -// deterministic orchestrator via `--extra` (any non-zero blocks); both run via real devDeps +// and its hard biome-lint gate (`lint` = `biome check --error-on-warnings .` — biome exits 0 when +// every diagnostic is warn-severity, so without that flag the gate PRINTS its findings into the log +// and passes the commit anyway). The hard-lint is folded into the deterministic orchestrator via +// `--extra` (any non-zero blocks); both run via real devDeps // (eslint/biome), so toSelfHost leaves them untouched. Together with the advisory fallow fragment // below, the self-host hook preserves every gate the pre-self-host hand hook ran AND adds review + dup/clone. export const SELF_HOST_STRUCTURE_CMD = 'bun run lint:structure'; diff --git a/dist/package.json b/dist/package.json index ae2f24c4..bec0b909 100644 --- a/dist/package.json +++ b/dist/package.json @@ -58,7 +58,7 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check .", + "lint": "biome check --error-on-warnings .", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", "benchmarks:render": "bun gate-engine/eval/cli.mts render", diff --git a/dist/skills/correctness/scripts/checklist.mjs b/dist/skills/correctness/scripts/checklist.mjs index 54b6974f..48581d9f 100644 --- a/dist/skills/correctness/scripts/checklist.mjs +++ b/dist/skills/correctness/scripts/checklist.mjs @@ -62,9 +62,7 @@ function extractLens(argv) { // Group-scoped path. Sorted so the SAME group always resolves to the same file regardless of the // order the caller listed its lenses — otherwise `a,b` and `b,a` would be two different runs. const lensPath = (lens) => - lens && lens.length - ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` - : CHECKLIST_PATH; + lens?.length ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` : CHECKLIST_PATH; const log = console.log; diff --git a/docs/decisions/gate-opt-out-is-visible-and-detectable.md b/docs/decisions/gate-opt-out-is-visible-and-detectable.md index 5874612d..85d38d5a 100644 --- a/docs/decisions/gate-opt-out-is-visible-and-detectable.md +++ b/docs/decisions/gate-opt-out-is-visible-and-detectable.md @@ -20,3 +20,4 @@ created: 2026-08-05 **Scope:** gate-engine/deterministic/run.mts,cli/lib/doctor/guard-config-checks.mts,gate-engine/config.mts **Category:** commit-gates **Source:** collab · sc-search-index-drift +- 2026-08-07 — devkit's own hard biome gate now runs `biome check --error-on-warnings .`. This is the REVERSE of the failure mode the Target addresses: not a gate that silently opts out, but one that PRINTS its findings into the gate log and exits 0 anyway. biome's exit code counts only error-severity diagnostics, so six warn-severity findings (noTemplateCurlyInString x3, noNonNullAssertion x2, useOptionalChain) rendered in the v0.50.0 ship log at the visual weight of a block and shipped. The deterministic runner's trichotomy was working correctly — `--extra "lint=bun run lint"` is only ever as hard as the script it names, and the tool under it was not. Scoped to devkit's OWN lint script: the consumer emitter (cli/lib/install/package-json.mts) and the overlay biome gate (cli/lib/husky/husky-block.mts) keep a bare `biome check`, because biome/base.jsonc holds noConsole at "warn" DELIBERATELY for consumers while devkit's root config turns it off across its whole authored surface — the flag therefore costs devkit nothing and would cost a consumer every console.log. Regression-tested in cli/__tests__/self-host.test.mts against the repo's real package.json, since the strictness lives in a script string that no hook-text assertion can reach. diff --git a/gate-engine/coverage/__tests__/produce.test.mts b/gate-engine/coverage/__tests__/produce.test.mts index a0a355cc..bc109205 100644 --- a/gate-engine/coverage/__tests__/produce.test.mts +++ b/gate-engine/coverage/__tests__/produce.test.mts @@ -120,7 +120,11 @@ describe('publishCoverage', () => { mkdirSync(join(root, COVERAGE_DIR), { recursive: true }); writeFileSync(join(root, COVERAGE_FILE), '{"sibling.ts":{}}'); const dir = runDirWith(root, 'runA'); - const aDifferentFileThanTheOneThereNow = snapshotArtifact(root)! - 5_000; + // The artifact was just written, so it HAS an mtime — asserted rather than assumed, because a + // null here would silently read as mtime 0 and make the -5s below meaningless. + const mtimeNow = snapshotArtifact(root); + if (mtimeNow === null) throw new Error('the artifact written above must have an mtime'); + const aDifferentFileThanTheOneThereNow = mtimeNow - 5_000; expect(publishCoverage(dir, root, aDifferentFileThanTheOneThereNow)).toBe(false); expect(JSON.parse(readFileSync(join(root, COVERAGE_FILE), 'utf8'))).toEqual({ diff --git a/gate-engine/decisions/__tests__/recall-scoring.test.mts b/gate-engine/decisions/__tests__/recall-scoring.test.mts index a79e64e1..bf0bbae5 100644 --- a/gate-engine/decisions/__tests__/recall-scoring.test.mts +++ b/gate-engine/decisions/__tests__/recall-scoring.test.mts @@ -118,18 +118,21 @@ describe('scoreCase — MULTI', () => { }); describe('scoreCase — CURRENT_STATE', () => { + // Hoisted (not inlined into `cs()`) so a case can vary ONE field off the same baseline without + // reaching back through the optional `currentState` of a freshly built case. + const CURRENT_STATE: NonNullable = { + axis: 'hooks', + liveId: 'target:2026-06-17', + staleId: 'target:2026-06-08', + mustSurface: ['nothing executable to bridge'], + mustNotAssert: ['hooks FOLLOW the user'], + }; const cs = (): RecallCase => ({ id: 'c1', q: 'q', type: 'CURRENT_STATE', gold: ['hooks'], - currentState: { - axis: 'hooks', - liveId: 'target:2026-06-17', - staleId: 'target:2026-06-08', - mustSurface: ['nothing executable to bridge'], - mustNotAssert: ['hooks FOLLOW the user'], - }, + currentState: { ...CURRENT_STATE }, }); it('CSA needs all three: right axis, LIVE block, live content without an unqualified stale claim', () => { @@ -252,7 +255,7 @@ describe('scoreCase — CURRENT_STATE', () => { it('an EMPTY mustSurface cannot silently disable SFER (vacuous [].every)', () => { // `[].every()` is true, which would claim the live content surfaced and switch staleness off. // An empty list is evidence of nothing, so it must read as NOT surfaced. - const empty = { ...cs(), currentState: { ...cs().currentState!, mustSurface: [] } }; + const empty = { ...cs(), currentState: { ...CURRENT_STATE, mustSurface: [] } }; const s = scoreCase( empty, env( diff --git a/package.json b/package.json index ae2f24c4..bec0b909 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "test:run": "vitest run", "test:e2e": "vitest run -c vitest.e2e.config.mjs", "playground": "bun scripts/playground.mts", - "lint": "biome check .", + "lint": "biome check --error-on-warnings .", "lint:structure": "eslint cli gate-engine", "benchmarks:check": "bun gate-engine/eval/cli.mts check", "benchmarks:render": "bun gate-engine/eval/cli.mts render", diff --git a/skills/correctness/scripts/checklist.mjs b/skills/correctness/scripts/checklist.mjs index 54b6974f..48581d9f 100644 --- a/skills/correctness/scripts/checklist.mjs +++ b/skills/correctness/scripts/checklist.mjs @@ -62,9 +62,7 @@ function extractLens(argv) { // Group-scoped path. Sorted so the SAME group always resolves to the same file regardless of the // order the caller listed its lenses — otherwise `a,b` and `b,a` would be two different runs. const lensPath = (lens) => - lens && lens.length - ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` - : CHECKLIST_PATH; + lens?.length ? `.claude/.correctness-review-${[...lens].sort().join('+')}.json` : CHECKLIST_PATH; const log = console.log;