From d15766555cf9eedc9cab9db8163b60b182e6b299 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:20:46 -0400 Subject: [PATCH 01/21] chore: notify n8n on workflow completion --- .github/workflows/n8n-notify.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/n8n-notify.yml diff --git a/.github/workflows/n8n-notify.yml b/.github/workflows/n8n-notify.yml new file mode 100644 index 0000000..bdd332e --- /dev/null +++ b/.github/workflows/n8n-notify.yml @@ -0,0 +1,31 @@ +name: Notify n8n + +on: + workflow_run: + workflows: + - CI + types: + - completed + +jobs: + notify-n8n: + runs-on: ubuntu-latest + + steps: + - name: Send workflow result to n8n + env: + N8N_WEBHOOK_URL: ${{ secrets.N8N_WEBHOOK_URL }} + N8N_WEBHOOK_TOKEN: ${{ secrets.N8N_WEBHOOK_TOKEN }} + run: | + curl -X POST "$N8N_WEBHOOK_URL" \ + -H "Authorization: Bearer $N8N_WEBHOOK_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "event": "github.workflow.completed", + "repo": "${{ github.repository }}", + "workflow": "${{ github.event.workflow_run.name }}", + "status": "${{ github.event.workflow_run.conclusion }}", + "branch": "${{ github.event.workflow_run.head_branch }}", + "sha": "${{ github.event.workflow_run.head_sha }}", + "url": "${{ github.event.workflow_run.html_url }}" + }' From 384e7727390e74025e99ddb831f6e407c672412c Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:02:44 -0400 Subject: [PATCH 02/21] test: centralize test runner scopes --- scripts/lib/test-runner-scope.mts | 68 +++++++++++++++++++++++++++++++ scripts/run-tests-next.mts | 19 ++++----- scripts/run-tests-node.mts | 19 ++++----- 3 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 scripts/lib/test-runner-scope.mts diff --git a/scripts/lib/test-runner-scope.mts b/scripts/lib/test-runner-scope.mts new file mode 100644 index 0000000..4fbd20b --- /dev/null +++ b/scripts/lib/test-runner-scope.mts @@ -0,0 +1,68 @@ +import * as path from 'node:path'; +import fg from 'fast-glob'; + +export const TEST_GLOBS = ['tests/**/*.test.{js,ts,tsx}'] as const; + +export const ROOT_GLOBS = [...TEST_GLOBS] as const; + +export const GLOBAL_EXCLUDED = [ + 'tests/fixtures/**', + '**/__mocks__/**', +] as const; + +export const RUNTIME_EXCLUDED = [ + ...GLOBAL_EXCLUDED, + 'tests/db/**', +] as const; + +export const EXCLUDED = [ + ...GLOBAL_EXCLUDED, + 'tests/node/**', + 'tests/next/**', + 'tests/db/**', +] as const; + +export const NODE_GLOBS = ['tests/node/**/*.test.{js,ts,tsx}'] as const; + +export const NEXT_GLOBS = ['tests/next/**/*.test.{js,ts,tsx}'] as const; + +export const DB_GLOBS = ['tests/db/**/*.test.{js,ts,tsx}'] as const; + +export const ROOT_ALLOWED_GLOBS = [ + 'tests/*.test.{js,ts,tsx}', + 'tests/accounting/**/*.test.{js,ts,tsx}', + 'tests/authority/**/*.test.{js,ts,tsx}', + 'tests/engine/**/*.test.{js,ts,tsx}', + 'tests/guardrails/**/*.test.{js,ts,tsx}', + 'tests/migrations/**/*.test.{js,ts,tsx}', + 'tests/type/**/*.test.{js,ts,tsx}', +] as const; + +function normalizeRelativePath(filePath: string): string { + return path.normalize(filePath).split(path.sep).join('/'); +} + +export async function resolveFiles( + include: readonly string[], + exclude: readonly string[], + cwd = process.cwd() +): Promise { + const files = await fg([...include], { + cwd, + ignore: [...exclude], + onlyFiles: true, + unique: true, + dot: false, + absolute: false, + }); + + return [...new Set(files.map(normalizeRelativePath))].sort(); +} + +export async function resolveUnexpectedRootFiles( + rootFiles: readonly string[], + cwd = process.cwd() +): Promise { + const allowed = new Set(await resolveFiles(ROOT_ALLOWED_GLOBS, EXCLUDED, cwd)); + return rootFiles.filter((file) => !allowed.has(file)); +} diff --git a/scripts/run-tests-next.mts b/scripts/run-tests-next.mts index 92b81c2..663c287 100644 --- a/scripts/run-tests-next.mts +++ b/scripts/run-tests-next.mts @@ -1,11 +1,15 @@ import * as path from 'node:path'; import * as process from 'node:process'; import { fileURLToPath } from 'node:url'; -import fg from 'fast-glob'; import { buildDeterministicEnv } from './lib/deterministic-env.mjs'; import { ensureTsEsm } from './lib/ensure-ts-esm.mjs'; import { fail } from './guardrails/lib/fail.mjs'; import { runTsEsm } from './lib/run-ts-esm.mjs'; +import { + GLOBAL_EXCLUDED, + NEXT_GLOBS, + resolveFiles, +} from './lib/test-runner-scope.mjs'; ensureTsEsm(); @@ -19,20 +23,14 @@ const baseEnv = buildDeterministicEnv(); const nodeEnv = baseEnv['NODE_ENV'] ?? 'test'; const runEnv: NodeJS.ProcessEnv = { ...baseEnv, NODE_ENV: nodeEnv }; -const testFiles = fg - .sync(['tests/next/**/*.test.{js,ts,tsx}'], { - cwd: repoRoot, - absolute: true, - ignore: ['**/__mocks__/**', 'tests/fixtures/**', 'tests/db/**'], - }) - .sort(); +const testFiles = await resolveFiles(NEXT_GLOBS, GLOBAL_EXCLUDED, repoRoot); if (testFiles.length === 0) { fail(PREFIX, 'No Next tests found under tests/next/**/*.test.{js,ts,tsx}', { fix: FIX }); } for (const file of testFiles) { - process.stdout.write(`RUN ${path.relative(repoRoot, file)}\n`); + process.stdout.write(`RUN ${file}\n`); const result = runTsEsm( file, [ @@ -55,13 +53,12 @@ for (const file of testFiles) { if (result.exitCode !== 0) { const details = [`exit=${result.exitCode}`]; - const relPath = path.relative(repoRoot, file); if (result.stdout.trim().length > 0) { details.push(`stdout=${result.stdout.trim()}`); } if (result.stderr.trim().length > 0) { details.push(`stderr=${result.stderr.trim()}`); } - fail(PREFIX, `FAILED ${relPath}`, { details, fix: 'Fix the failing test(s) and rerun.' }); + fail(PREFIX, `FAILED ${file}`, { details, fix: 'Fix the failing test(s) and rerun.' }); } } diff --git a/scripts/run-tests-node.mts b/scripts/run-tests-node.mts index 9dd0a78..29b1e8e 100644 --- a/scripts/run-tests-node.mts +++ b/scripts/run-tests-node.mts @@ -1,11 +1,15 @@ import * as path from 'node:path'; import * as process from 'node:process'; import { fileURLToPath } from 'node:url'; -import fg from 'fast-glob'; import { buildDeterministicEnv } from './lib/deterministic-env.mjs'; import { ensureTsEsm } from './lib/ensure-ts-esm.mjs'; import { fail } from './guardrails/lib/fail.mjs'; import { runTsEsm } from './lib/run-ts-esm.mjs'; +import { + GLOBAL_EXCLUDED, + NODE_GLOBS, + resolveFiles, +} from './lib/test-runner-scope.mjs'; ensureTsEsm(); @@ -19,20 +23,14 @@ const baseEnv = buildDeterministicEnv(); const nodeEnv = baseEnv['NODE_ENV'] ?? 'test'; const runEnv: NodeJS.ProcessEnv = { ...baseEnv, NODE_ENV: nodeEnv }; -const testFiles = fg - .sync(['tests/node/**/*.test.{js,ts,tsx}'], { - cwd: repoRoot, - absolute: true, - ignore: ['**/__mocks__/**', 'tests/fixtures/**', 'tests/db/**'], - }) - .sort(); +const testFiles = await resolveFiles(NODE_GLOBS, GLOBAL_EXCLUDED, repoRoot); if (testFiles.length === 0) { fail(PREFIX, 'No Node tests found under tests/node/**/*.test.{js,ts,tsx}', { fix: FIX }); } for (const file of testFiles) { - process.stdout.write(`RUN ${path.relative(repoRoot, file)}\n`); + process.stdout.write(`RUN ${file}\n`); const result = runTsEsm( file, [ @@ -55,13 +53,12 @@ for (const file of testFiles) { if (result.exitCode !== 0) { const details = [`exit=${result.exitCode}`]; - const relPath = path.relative(repoRoot, file); if (result.stdout.trim().length > 0) { details.push(`stdout=${result.stdout.trim()}`); } if (result.stderr.trim().length > 0) { details.push(`stderr=${result.stderr.trim()}`); } - fail(PREFIX, `FAILED ${relPath}`, { details, fix: 'Fix the failing test(s) and rerun.' }); + fail(PREFIX, `FAILED ${file}`, { details, fix: 'Fix the failing test(s) and rerun.' }); } } From 8cc759aa58b9e59c0679fe77f499ee3cc858a3ef Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:02:55 -0400 Subject: [PATCH 03/21] fix: partition full test runner lanes --- scripts/run-tests.mts | 96 +++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/scripts/run-tests.mts b/scripts/run-tests.mts index ad255da..fab2c0e 100644 --- a/scripts/run-tests.mts +++ b/scripts/run-tests.mts @@ -1,10 +1,18 @@ import * as path from 'node:path'; import * as process from 'node:process'; import { fileURLToPath } from 'node:url'; -import fg from 'fast-glob'; +import { buildDeterministicEnv } from './lib/deterministic-env.mjs'; import { ensureTsEsm } from './lib/ensure-ts-esm.mjs'; +import { asMessage } from './guardrails/lib/error.mjs'; import { fail } from './guardrails/lib/fail.mjs'; -import { runTool } from './guardrails/lib/run-tool.mjs'; +import { spawnTool } from './guardrails/lib/run-tool.mjs'; +import { runTsEsm } from './lib/run-ts-esm.mjs'; +import { + EXCLUDED, + ROOT_GLOBS, + resolveFiles, + resolveUnexpectedRootFiles, +} from './lib/test-runner-scope.mjs'; ensureTsEsm(); @@ -23,49 +31,75 @@ const tsNodeCompilerOptions = JSON.stringify({ jsx: 'react-jsx', }); -const env = process.env as NodeJS.ProcessEnv; -env['TS_NODE_PROJECT'] = path.join(repoRoot, 'tsconfig.eslint.json'); -env['TS_NODE_COMPILER_OPTIONS'] = tsNodeCompilerOptions; -const nodeEnv = env['NODE_ENV'] ?? 'test'; - -const testFiles = fg - .sync(['tests/**/*.test.{js,ts,tsx}'], { +async function runLane(scriptName: 'check:run-tests:node' | 'check:run-tests:next'): Promise { + let errorMessage: string | null = null; + const child = spawnTool('npm', ['run', scriptName], { cwd: repoRoot, - absolute: true, - ignore: ['**/__mocks__/**', 'tests/fixtures/**', 'tests/db/**'], - }) - .sort(); + env: process.env, + stdio: 'inherit', + }); + + const exitCode = await new Promise((resolve) => { + child.on('error', (error: unknown) => { + errorMessage = asMessage(error); + resolve(127); + }); + child.on('close', (code) => { + resolve(code ?? 1); + }); + }); + + if (exitCode !== 0) { + const details = [`exit=${exitCode}`]; + if (errorMessage !== null) { + details.push(`error=${errorMessage}`); + } + fail(PREFIX, `${scriptName} failed`, { + details, + fix: 'Fix the failing test lane and rerun.', + }); + } +} + +const baseEnv = buildDeterministicEnv(); +const nodeEnv = baseEnv['NODE_ENV'] ?? 'test'; +const runEnv: NodeJS.ProcessEnv = { + ...baseEnv, + NODE_ENV: nodeEnv, + TS_NODE_PROJECT: path.join(repoRoot, 'tsconfig.eslint.json'), + TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions, +}; + +const testFiles = await resolveFiles(ROOT_GLOBS, EXCLUDED, repoRoot); if (testFiles.length === 0) { fail(PREFIX, 'No tests found under tests/**/*.test.{js,ts,tsx}', { fix: FIX }); } +const unexpectedRootFiles = await resolveUnexpectedRootFiles(testFiles, repoRoot); +if (unexpectedRootFiles.length > 0) { + fail(PREFIX, 'Root runner owns tests outside the explicit root allowlist', { + details: unexpectedRootFiles, + fix: + 'Move the tests into tests/node or tests/next, or explicitly add the root-owned path to ROOT_ALLOWED_GLOBS.', + }); +} + process.stdout.write(`TS_NODE_COMPILER_OPTIONS=${tsNodeCompilerOptions}\n`); for (const file of testFiles) { - process.stdout.write(`RUN ${path.relative(repoRoot, file)}\n`); - const result = runTool( - 'npm', + process.stdout.write(`RUN ${file}\n`); + const result = runTsEsm( + file, [ - 'run', - 'ts:esm', - '--', '--import', './scripts/lib/loaders/config.loader.mjs', '--import', './scripts/lib/loaders/prisma-mock.register.mjs', '-r', 'tsconfig-paths/register', - file, ], - { - cwd: repoRoot, - env: { - ...process.env, - NODE_ENV: nodeEnv, - TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions, - }, - } + runEnv ); if (result.stdout.length > 0) { @@ -77,13 +111,15 @@ for (const file of testFiles) { if (result.exitCode !== 0) { const details = [`exit=${result.exitCode}`]; - const relPath = path.relative(repoRoot, file); if (result.stdout.trim().length > 0) { details.push(`stdout=${result.stdout.trim()}`); } if (result.stderr.trim().length > 0) { details.push(`stderr=${result.stderr.trim()}`); } - fail(PREFIX, `FAILED ${relPath}`, { details, fix: 'Fix the failing test(s) and rerun.' }); + fail(PREFIX, `FAILED ${file}`, { details, fix: 'Fix the failing test(s) and rerun.' }); } } + +await runLane('check:run-tests:node'); +await runLane('check:run-tests:next'); From 5e24a913a1a0620a54dccfe95900defb395e0603 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:03:00 -0400 Subject: [PATCH 04/21] test: enforce test runner ownership --- .../guardrails/test-runner-ownership.test.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/node/guardrails/test-runner-ownership.test.ts diff --git a/tests/node/guardrails/test-runner-ownership.test.ts b/tests/node/guardrails/test-runner-ownership.test.ts new file mode 100644 index 0000000..f0001c8 --- /dev/null +++ b/tests/node/guardrails/test-runner-ownership.test.ts @@ -0,0 +1,94 @@ +import * as assert from 'node:assert/strict'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + DB_GLOBS, + EXCLUDED, + GLOBAL_EXCLUDED, + NEXT_GLOBS, + NODE_GLOBS, + ROOT_GLOBS, + RUNTIME_EXCLUDED, + TEST_GLOBS, + resolveFiles, + resolveUnexpectedRootFiles, +} from '../../../scripts/lib/test-runner-scope.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const repoRoot = path.resolve(path.dirname(__filename), '..', '..', '..'); + +function intersection(left: Set, right: Set): string[] { + return [...left].filter((file) => right.has(file)).sort(); +} + +function difference(left: Set, right: Set): string[] { + return [...left].filter((file) => !right.has(file)).sort(); +} + +function union(...sets: Set[]): Set { + const out = new Set(); + for (const set of sets) { + for (const file of set) { + out.add(file); + } + } + return out; +} + +function assertEmpty(label: string, files: string[]): void { + assert.deepEqual(files, [], `${label}:\n${files.join('\n')}`); +} + +function assertSetEqual(label: string, actual: Set, expected: Set): void { + const extra = difference(actual, expected); + const missing = difference(expected, actual); + assert.deepEqual( + { extra, missing }, + { extra: [], missing: [] }, + `${label}\nextra:\n${extra.join('\n')}\nmissing:\n${missing.join('\n')}` + ); +} + +const root = new Set(await resolveFiles(ROOT_GLOBS, EXCLUDED, repoRoot)); +const node = new Set(await resolveFiles(NODE_GLOBS, GLOBAL_EXCLUDED, repoRoot)); +const next = new Set(await resolveFiles(NEXT_GLOBS, GLOBAL_EXCLUDED, repoRoot)); +const db = new Set(await resolveFiles(DB_GLOBS, GLOBAL_EXCLUDED, repoRoot)); +const runtime = new Set(await resolveFiles(TEST_GLOBS, RUNTIME_EXCLUDED, repoRoot)); +const owned = new Set(await resolveFiles(TEST_GLOBS, GLOBAL_EXCLUDED, repoRoot)); + +assertEmpty('root and node lanes overlap', intersection(root, node)); +assertEmpty('root and next lanes overlap', intersection(root, next)); +assertEmpty('node and next lanes overlap', intersection(node, next)); + +const seen = new Map(); +for (const file of [...root, ...node, ...next]) { + seen.set(file, (seen.get(file) ?? 0) + 1); +} +assertEmpty( + 'runtime test files have duplicate ownership', + [...seen.entries()] + .filter(([, count]) => count > 1) + .map(([file, count]) => `${file} (${count})`) +); + +assertSetEqual( + 'runtime tests must equal root + node + next', + union(root, node, next), + runtime +); +assertSetEqual( + 'all owned tests must equal root + node + next + db', + union(root, node, next, db), + owned +); + +assertEmpty('db tests must not be root-owned', intersection(db, root)); +assertEmpty('db tests must not be node-owned', intersection(db, node)); +assertEmpty('db tests must not be next-owned', intersection(db, next)); + +assertEmpty( + 'root-owned tests must stay within the explicit legacy root allowlist', + await resolveUnexpectedRootFiles([...root], repoRoot) +); + +process.stdout.write('test-runner-ownership: ok\n'); From d7914646d67e2ce52edd2481b662ec499dcfbe18 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:25:39 -0400 Subject: [PATCH 05/21] ci(workflow): remove direct runtime test steps from ci.yml --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81697c7..dc16caa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,11 +43,5 @@ jobs: - name: Guardrails run: npm run check:guardrails - - name: Node runtime tests - run: npm run check:tests:node - - - name: Next runtime tests - run: npm run check:tests:next - - name: Verify CI truth run: npm run ci:verify From 3e6697f120b7dbda5fd8b31b3543b7eb69790e49 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:25:56 -0400 Subject: [PATCH 06/21] scripts(package): add new check scripts for static and runtime verification --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 52701b7..19435bd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "build:strict": "npm run check:guardrails && next build --webpack", "start": "next start", "ci:verify": "npm run check && npm run test && npm run build", + "check:static": "npm run check:guardrails && npm run lint:scripts && npm run typecheck:scripts && npm run lint && npm run typecheck", + "check:runtime": "npm test", + "check:fast": "npm run check:guardrails && npm run typecheck:scripts && npm test", "check:clean": "npm run ts:esm -- scripts/execution/run.mts check:clean", "check:db-ready": "npm run ts:esm -- scripts/execution/run-db.mts check:db-ready", "check:dev-login": "npm run ts:esm -- scripts/execution/run.mts check:dev-login", From b217a3cbb47e92ed578cc7ec5a136e4411ccc725 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:26:10 -0400 Subject: [PATCH 07/21] guardrails(ci-coverage): enhance ci guardrail coverage with script chain assertions --- scripts/check-ci-guardrail-coverage.mts | 86 +++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/scripts/check-ci-guardrail-coverage.mts b/scripts/check-ci-guardrail-coverage.mts index 0015ca8..d586312 100644 --- a/scripts/check-ci-guardrail-coverage.mts +++ b/scripts/check-ci-guardrail-coverage.mts @@ -26,6 +26,20 @@ const CI_WORKFLOW = path.join(WORKFLOWS_DIR, 'ci.yml'); const CHECK_SCRIPT = 'check'; const CI_ENTRYPOINT = 'ci:verify'; const GUARDRAIL_ENTRYPOINT_NAME = GUARDRAIL_ENTRYPOINT; +const DIRECT_RUNTIME_SCRIPTS = new Set([ + 'check', + 'check:fast', + 'check:runtime', + 'check:node', + 'check:next', + 'check:tests', + 'check:tests:node', + 'check:tests:next', + 'check:run-tests', + 'check:run-tests:node', + 'check:run-tests:next', + 'test', +]); function readPackageScripts(): PackageScripts { const packagePath = path.join(ROOT, 'package.json'); @@ -175,6 +189,68 @@ function assertCheckCoverage(errors: Violation[]): void { message: `${GUARDRAIL_ENTRYPOINT} must not use --aggregate`, }); } + + assertScriptChain(errors, scripts, 'ci:verify', ['check', 'test', 'build']); + assertScriptChain(errors, scripts, 'check', ['check:guardrails', 'check:node', 'check:next']); + assertScriptChain(errors, scripts, 'check:node', ['check:run-tests:node']); + assertScriptChain(errors, scripts, 'check:next', ['check:run-tests:next']); + assertScriptChain(errors, scripts, 'test', ['check:run-tests']); + const runTestsCommand = scripts['check:run-tests']; + if ( + runTestsCommand === undefined || + runTestsCommand.includes('scripts/execution/run.mts') === false || + runTestsCommand.includes('check:run-tests') === false + ) { + errors.push({ + file: pkgPath, + line: 1, + col: 1, + message: 'npm test must reach check:run-tests through the execution registry', + }); + } +} + +function assertScriptChain( + errors: Violation[], + scripts: Record, + scriptName: string, + orderedCalls: string[] +): void { + const pkgPath = path.normalize(path.relative(ROOT, 'package.json')); + const command = scripts[scriptName]; + if (command === undefined || command.trim().length === 0) { + errors.push({ + file: pkgPath, + line: 1, + col: 1, + message: `npm script "${scriptName}" is missing`, + }); + return; + } + const calls = parseGuardrailCalls(command); + let lastIndex = -1; + for (const expected of orderedCalls) { + const index = calls.indexOf(expected); + if (index === -1) { + errors.push({ + file: pkgPath, + line: 1, + col: 1, + message: `${scriptName} must reach npm run ${expected}`, + }); + continue; + } + if (index <= lastIndex) { + errors.push({ + file: pkgPath, + line: 1, + col: 1, + message: `${scriptName} must run ${orderedCalls.join(' -> ')} in order`, + }); + return; + } + lastIndex = index; + } } function assertCiRunsCheck(errors: Violation[]): void { @@ -191,9 +267,7 @@ function assertCiRunsCheck(errors: Violation[]): void { const runsCheck = calls.includes(CHECK_SCRIPT); const runsEntry = calls.includes(GUARDRAIL_ENTRYPOINT_NAME); const runsCiVerify = calls.includes(CI_ENTRYPOINT); - const runsCombined = calls.includes('check:tests'); - const runsNode = calls.includes('check:tests:node'); - const runsNext = calls.includes('check:tests:next'); + const directRuntime = calls.filter((name) => DIRECT_RUNTIME_SCRIPTS.has(name)); if (!runsCiVerify) { errors.push({ @@ -208,15 +282,15 @@ function assertCiRunsCheck(errors: Violation[]): void { file: ciPath, line: 1, col: 1, - message: `CI must run ${GUARDRAIL_ENTRYPOINT_NAME} before runtime tests`, + message: `CI must run ${GUARDRAIL_ENTRYPOINT_NAME} before ${CI_ENTRYPOINT}`, }); } - if (!runsCombined && !(runsNode && runsNext)) { + if (directRuntime.length > 0) { errors.push({ file: ciPath, line: 1, col: 1, - message: 'CI must run check:tests (or both check:tests:node and check:tests:next)', + message: `CI must not run runtime tests directly outside ${CI_ENTRYPOINT}: ${directRuntime.join(', ')}`, }); } if (runsCheck) { From b21912ae5899317b88eeccb61cad57f64b458946 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:26:27 -0400 Subject: [PATCH 08/21] guardrails(ci-check): update ci must run check to enforce single runtime entrypoint --- scripts/check-ci-must-run-check.mts | 47 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/scripts/check-ci-must-run-check.mts b/scripts/check-ci-must-run-check.mts index e1c0d77..b92e20d 100644 --- a/scripts/check-ci-must-run-check.mts +++ b/scripts/check-ci-must-run-check.mts @@ -16,11 +16,23 @@ const WORKFLOWS_DIR = path.join(ROOT, '.github', 'workflows'); const CI_WORKFLOW = path.join(WORKFLOWS_DIR, 'ci.yml'); const FIX = - 'Run guardrails and runtime separation tests before a final `npm run ci:verify` in ci.yml.'; + 'Run `npm run check:guardrails` before one final `npm run ci:verify`; do not run runtime tests directly in ci.yml.'; const REQUIRED_GUARDRAILS = ['check:guardrails']; -const SEPARATE_RUNTIME_TESTS = ['check:tests:node', 'check:tests:next']; -const COMBINED_RUNTIME_TESTS = ['check:tests']; +const DIRECT_RUNTIME_SCRIPTS = new Set([ + 'check', + 'check:fast', + 'check:runtime', + 'check:node', + 'check:next', + 'check:tests', + 'check:tests:node', + 'check:tests:next', + 'check:run-tests', + 'check:run-tests:node', + 'check:run-tests:next', + 'test', +]); type RunStep = { commands: string[]; @@ -169,40 +181,29 @@ function main(): void { const steps = parseRunSteps(content.split(/\r?\n/)); const commands = collectCommands(steps); const calls = collectNpmRunCalls(commands); - const allowed = new Set([ - 'ci:verify', - ...REQUIRED_GUARDRAILS, - ...SEPARATE_RUNTIME_TESTS, - ...COMBINED_RUNTIME_TESTS, - ]); - const forbidden = calls.filter((name) => !allowed.has(name)); - if (forbidden.length > 0) { - fail(PREFIX, 'CI must run only guardrails, runtime tests, and ci:verify via npm run', { + const ciVerifyCalls = calls.filter((name) => name === 'ci:verify'); + if (ciVerifyCalls.length !== 1) { + fail(PREFIX, 'CI must run exactly one ci:verify entrypoint', { details: [ path.normalize(path.relative(ROOT, CI_WORKFLOW)) + - `:1:1: forbidden npm scripts: ${forbidden.join(', ')}`, + `:1:1: found ${ciVerifyCalls.length} ci:verify calls`, ], fix: FIX, }); } - const hasCombined = COMBINED_RUNTIME_TESTS.some((name) => calls.includes(name)); - const hasSeparate = SEPARATE_RUNTIME_TESTS.some((name) => calls.includes(name)); - if (hasCombined && hasSeparate) { - fail(PREFIX, 'CI must choose either combined or separate runtime tests', { + const directRuntime = calls.filter((name) => DIRECT_RUNTIME_SCRIPTS.has(name)); + if (directRuntime.length > 0) { + fail(PREFIX, 'CI must not run runtime tests directly outside ci:verify', { details: [ path.normalize(path.relative(ROOT, CI_WORKFLOW)) + - ':1:1: do not mix check:tests with check:tests:node/check:tests:next', + `:1:1: direct runtime scripts: ${directRuntime.join(', ')}`, ], fix: FIX, }); } - if (hasCombined) { - ensureOrdering(calls, [...REQUIRED_GUARDRAILS, ...COMBINED_RUNTIME_TESTS], 'guardrails and runtime tests'); - } else { - ensureOrdering(calls, [...REQUIRED_GUARDRAILS, ...SEPARATE_RUNTIME_TESTS], 'guardrails and runtime tests'); - } + ensureOrdering(calls, [...REQUIRED_GUARDRAILS, 'ci:verify'], 'guardrails before ci:verify'); process.stdout.write('ci-must-run-check: ok\n'); } From 5b67796f02b9faef8fa2b156b5f62cbbb287c114 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:26:43 -0400 Subject: [PATCH 09/21] tests(fixtures): update ci must run check test fixtures for new logic --- .../direct-runtime/.github/workflows/ci.yml | 17 +++++++++++++++++ .../.github/workflows/ci.yml | 0 .../ok/.github/workflows/ci.yml | 13 +++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 tests/fixtures/guardrails/ci-must-run-check/direct-runtime/.github/workflows/ci.yml rename tests/fixtures/guardrails/ci-must-run-check/{ => missing-ci-verify}/.github/workflows/ci.yml (100%) create mode 100644 tests/fixtures/guardrails/ci-must-run-check/ok/.github/workflows/ci.yml diff --git a/tests/fixtures/guardrails/ci-must-run-check/direct-runtime/.github/workflows/ci.yml b/tests/fixtures/guardrails/ci-must-run-check/direct-runtime/.github/workflows/ci.yml new file mode 100644 index 0000000..33a7260 --- /dev/null +++ b/tests/fixtures/guardrails/ci-must-run-check/direct-runtime/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: ci + +on: + push: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Check guardrails + run: npm run check:guardrails + - name: Node runtime tests + run: npm run check:tests:node + - name: Next runtime tests + run: npm run check:tests:next + - name: Verify CI truth + run: npm run ci:verify diff --git a/tests/fixtures/guardrails/ci-must-run-check/.github/workflows/ci.yml b/tests/fixtures/guardrails/ci-must-run-check/missing-ci-verify/.github/workflows/ci.yml similarity index 100% rename from tests/fixtures/guardrails/ci-must-run-check/.github/workflows/ci.yml rename to tests/fixtures/guardrails/ci-must-run-check/missing-ci-verify/.github/workflows/ci.yml diff --git a/tests/fixtures/guardrails/ci-must-run-check/ok/.github/workflows/ci.yml b/tests/fixtures/guardrails/ci-must-run-check/ok/.github/workflows/ci.yml new file mode 100644 index 0000000..039e153 --- /dev/null +++ b/tests/fixtures/guardrails/ci-must-run-check/ok/.github/workflows/ci.yml @@ -0,0 +1,13 @@ +name: ci + +on: + push: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Check guardrails + run: npm run check:guardrails + - name: Verify CI truth + run: npm run ci:verify From f4f7c7c8f60d54f12bcdb9f5b0b6ebc5784dce20 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:27:01 -0400 Subject: [PATCH 10/21] tests(guardrails): update ci must run check tests for new assertions --- tests/guardrails/ci-must-run-check.test.ts | 41 ++++++++++++++----- .../node/guardrails/ci-must-run-check.test.ts | 41 ++++++++++++++----- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/tests/guardrails/ci-must-run-check.test.ts b/tests/guardrails/ci-must-run-check.test.ts index 62834bb..8def2b7 100644 --- a/tests/guardrails/ci-must-run-check.test.ts +++ b/tests/guardrails/ci-must-run-check.test.ts @@ -13,30 +13,49 @@ const fixtureRoot = path.join( 'ci-must-run-check' ); -const result = spawnSync( - 'npm', - ['run', 'ts:esm', '--', 'scripts/check-ci-must-run-check.mts'], - { +function runFixture(name: string): ReturnType { + return spawnSync('npm', ['run', 'ts:esm', '--', 'scripts/check-ci-must-run-check.mts'], { cwd: repoRoot, encoding: 'utf8', env: { ...process.env, - CHERRY_CI_MUST_RUN_CHECK_ROOT: fixtureRoot, + CHERRY_CI_MUST_RUN_CHECK_ROOT: path.join(fixtureRoot, name), }, - } + }); +} + +const okResult = runFixture('ok'); +assert.equal( + okResult.status, + 0, + `expected ok fixture to pass, got stderr=${okResult.stderr ?? ''}` ); -const stderr = result.stderr ?? ''; +const missingResult = runFixture('missing-ci-verify'); +const missingStderr = missingResult.stderr ?? ''; + +assert.notEqual( + missingResult.status, + 0, + `expected missing-ci-verify to fail, got status=${missingResult.status ?? 'null'}` +); +assert.equal( + missingStderr.includes('check:ci-must-run-check'), + true, + `expected check:ci-must-run-check output, got: ${missingStderr}` +); +const directRuntimeResult = runFixture('direct-runtime'); +const directRuntimeStderr = directRuntimeResult.stderr ?? ''; assert.notEqual( - result.status, + directRuntimeResult.status, 0, - `expected ci-must-run-check to fail, got status=${result.status ?? 'null'}` + `expected direct-runtime to fail, got status=${directRuntimeResult.status ?? 'null'}` ); assert.equal( - stderr.includes('check:ci-must-run-check'), + directRuntimeStderr.includes('direct runtime scripts'), true, - `expected check:ci-must-run-check output, got: ${stderr}` + `expected direct runtime failure, got: ${directRuntimeStderr}` ); process.stdout.write('ci-must-run-check: ok\n'); diff --git a/tests/node/guardrails/ci-must-run-check.test.ts b/tests/node/guardrails/ci-must-run-check.test.ts index 500e929..51c3b2a 100644 --- a/tests/node/guardrails/ci-must-run-check.test.ts +++ b/tests/node/guardrails/ci-must-run-check.test.ts @@ -13,30 +13,49 @@ const fixtureRoot = path.join( 'ci-must-run-check' ); -const result = spawnSync( - 'npm', - ['run', 'ts:esm', '--', 'scripts/check-ci-must-run-check.mts'], - { +function runFixture(name: string): ReturnType { + return spawnSync('npm', ['run', 'ts:esm', '--', 'scripts/check-ci-must-run-check.mts'], { cwd: repoRoot, encoding: 'utf8', env: { ...process.env, - CHERRY_CI_MUST_RUN_CHECK_ROOT: fixtureRoot, + CHERRY_CI_MUST_RUN_CHECK_ROOT: path.join(fixtureRoot, name), }, - } + }); +} + +const okResult = runFixture('ok'); +assert.equal( + okResult.status, + 0, + `expected ok fixture to pass, got stderr=${okResult.stderr ?? ''}` ); -const stderr = result.stderr ?? ''; +const missingResult = runFixture('missing-ci-verify'); +const missingStderr = missingResult.stderr ?? ''; + +assert.notEqual( + missingResult.status, + 0, + `expected missing-ci-verify to fail, got status=${missingResult.status ?? 'null'}` +); +assert.equal( + missingStderr.includes('check:ci-must-run-check'), + true, + `expected check:ci-must-run-check output, got: ${missingStderr}` +); +const directRuntimeResult = runFixture('direct-runtime'); +const directRuntimeStderr = directRuntimeResult.stderr ?? ''; assert.notEqual( - result.status, + directRuntimeResult.status, 0, - `expected ci-must-run-check to fail, got status=${result.status ?? 'null'}` + `expected direct-runtime to fail, got status=${directRuntimeResult.status ?? 'null'}` ); assert.equal( - stderr.includes('check:ci-must-run-check'), + directRuntimeStderr.includes('direct runtime scripts'), true, - `expected check:ci-must-run-check output, got: ${stderr}` + `expected direct runtime failure, got: ${directRuntimeStderr}` ); process.stdout.write('ci-must-run-check: ok\n'); From b22b2d5feb63c449947683ae7c05d4f912e0346e Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:27:20 -0400 Subject: [PATCH 11/21] docs(pr-template): update pull request template with new health gates --- .github/pull_request_template.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5442492..aec3d37 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ Status: Active -Last updated: 2026-01-02 +Last updated: 2026-04-28 # Pull Request Checklist @@ -13,10 +13,9 @@ Last updated: 2026-01-02 ## Testing - [ ] Not run (explain why) -- [ ] `npm run check` -- [ ] `npm test` -- [ ] `npm run build` -- [ ] `npm run ci:verify` +- [ ] Targeted proof for changed surface: +- [ ] `CHERRY_TMP_ROOT="$HOME/.cherry-tmp" CHERRY_VINE_SIGNATURE_MODE=enforce npm run verify:repo-closure` +- [ ] `npm run test:db` (only for DB/env changes) ## Engine Impact From 8dfa9402ce966b318243c1006060ca66059a5bc6 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:27:35 -0400 Subject: [PATCH 12/21] docs(agents): update agents guide with new pr checklist and proofs --- AGENTS.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9984fe6..9804f46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ Status: Active -Last updated: 2026-01-29 +Last updated: 2026-04-28 # Cherry Agents — Canonical Operating Guide @@ -93,14 +93,18 @@ Forbidden framings: “fronting card,” “proxy BIN,” “tap to pay with Che - `npx prisma format` - `npx prisma migrate dev --name ` - `npx prisma generate` - - Run `npm run check`, `npm test`, and `npm run build`. + - Run the issue's acceptance commands, or the narrowest proof that covers schema, runtime, and build impact. - For docs: add `Status` + `Last updated`, split Current vs Future, add Related docs. ## PR Checklist (what each command proves) - `npm run check` → guardrails + lint + typecheck are green. -- `npm test` → unit/guardrail tests green (Prisma mocked by loader). +- `npm test` → partitioned full runtime suite: root legacy tests, `tests/node`, then `tests/next` (Prisma mocked by loader). - `npm run build` → Next.js build passes. - `npm run ci:verify` → mirrors CI entrypoint. +- `npm run test:db` → DB/env tests only; not part of standard mocked runtime proof. +- `npm run check:fast` → local guardrails + script typecheck + partitioned runtime suite. +- Full repo proof → `CHERRY_TMP_ROOT="$HOME/.cherry-tmp" CHERRY_VINE_SIGNATURE_MODE=enforce npm run verify:repo-closure`. +- Agents must not run both `npm test` and `verify:repo-closure` unless explicitly required. - If schema changed: migrations apply and Prisma client is regenerated. ## Drift Policy From 6d5e4413d11b5d52d93c021f8a35468234585aaa Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:27:52 -0400 Subject: [PATCH 13/21] docs(readme): update health gates section with new verification commands --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7960d5d..d06c4e1 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,17 @@ npm run dev The repo runtime is Node 24.15.0. Use `.nvmrc` / `engines.node` as the source of truth, and keep PATH stable (e.g. `/usr/bin:/bin:/usr/local/bin`) so `rg`, `git`, and `node` resolve deterministically. -## Health Gates (must pass before pushing) +## Health Gates ```bash -npm run check -npm test -npm run build +CHERRY_TMP_ROOT="$HOME/.cherry-tmp" CHERRY_VINE_SIGNATURE_MODE=enforce npm run verify:repo-closure ``` +For day-to-day development, use the narrowest proof that covers the changed surface. +`npm test` is the partitioned full runtime runner: root legacy tests, `tests/node`, +then `tests/next`. Runtime ownership is enforced by +`tests/node/guardrails/test-runner-ownership.test.ts`. DB tests remain separate under +`npm run test:db`. + --- ## Key Commands and Scripts From 8bce30e5c39a816676e1047ab28e6116965363c0 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:28:10 -0400 Subject: [PATCH 14/21] docs(ci-guardrails): update ci and guardrails documentation for new ci structure --- docs/ci-and-guardrails.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/ci-and-guardrails.md b/docs/ci-and-guardrails.md index 00c4bc9..a1e1bf5 100644 --- a/docs/ci-and-guardrails.md +++ b/docs/ci-and-guardrails.md @@ -1,5 +1,5 @@ Status: Active -Last updated: 2026-04-26 +Last updated: 2026-04-28 # CI and guardrails @@ -9,7 +9,8 @@ Last updated: 2026-04-26 - Runs on every push to `main` and all PRs via `.github/workflows/ci.yml`. - Steps (fail-fast): 1) `npm ci` (postinstall runs `prisma generate`) - 2) `npm run ci:verify` (composite truth gate: check + build) + 2) `npm run check:guardrails` + 3) `npm run ci:verify` (composite truth gate: check + test + build) - Optional env lane (`.github/workflows/env-checks.yml`) provisions Postgres and runs: - `npx prisma generate` - `npx prisma migrate deploy` @@ -25,6 +26,8 @@ Last updated: 2026-04-26 - `check:guardrails` guarantees registry completeness, execution exclusivity, CI coverage, and ordering stability. - `check` is the aggregate of guardrails + node correctness + UI correctness; env checks live in `check:env`. - The last non-empty command in the CI job must be `npm run ci:verify`. +- There must be exactly one canonical runtime execution per CI run: `ci:verify` reaches `npm test`, and `npm test` runs root legacy tests, `tests/node`, then `tests/next`. +- CI must not directly run node/next runtime test steps outside `ci:verify`. ### Temp root requirement - `CHERRY_TMP_ROOT` is required for all guardrails and scripts that allocate temp. @@ -54,12 +57,13 @@ Last updated: 2026-04-26 - It does not mutate env in a way production would not. - It runs the Issue 8 proof slice, then `npm run lint`, `npm run check`, `npm run typecheck`, `npm test`, and `npm run build`. -> If CI ever runs individual guardrail scripts directly, the system is broken. +> If CI directly runs node/next runtime tests outside `ci:verify`, the system is broken. ### Ordering invariant - Guardrails execute before env-specific correctness and build. - `check:guardrails` runs core (env-free) guardrails; `check:env` runs env-dependent guardrails plus DB requirements. - Inside `check:node` and `check:next`, lint runs before typecheck and typecheck runs before tests. +- `npm test` is the partitioned full runtime runner, and ownership is enforced by `tests/node/guardrails/test-runner-ownership.test.ts`. - Build executes after `check` completes. ### Guardrails enforced @@ -71,7 +75,8 @@ Last updated: 2026-04-26 - Guardrail 5 (implicit config): `process.env` access is confined to `app/api/**` and `scripts/**`; load env into typed config via `initConfigFromEnv` and thread it explicitly. `check:config` must pass without allowlists. - Guardrail 6 (config immutability): server config is deep-frozen and locked after boundary load; `setServerConfig` rejects writes post-lock and loader registration fails once locked. `check:config-lock` must pass. - `check:check-contract` enforces the `ci:verify` contract and keeps `check` pure. -- `check:ci-must-run-check` enforces the single CI entrypoint (`ci:verify`). +- `check:ci-must-run-check` enforces fail-fast guardrails before the final CI entrypoint (`ci:verify`) and forbids direct runtime test execution in CI. +- `check:ci-guardrail-coverage` enforces the transitive proof chain from CI to `ci:verify`, `npm test`, and `check:run-tests`. - `check:guardrails-core` exits non-zero on any deviation; CI treats that as a hard failure. ### Guardrail scope invariant @@ -81,7 +86,13 @@ Last updated: 2026-04-26 ### How to run locally -Run the npm scripts: `check:aggregate` (guardrails only), `check` (aggregate + node + next), `test` (tests only), `build`, or the full gate `ci:verify`. +Use the narrowest proof that fully covers the changed surface: +- `npm run check:static` for guardrails, lint, and typecheck. +- `npm run check:runtime` or `npm test` for the partitioned runtime suite. +- `npm run check:fast` for local guardrails + script typecheck + runtime suite. +- `CHERRY_TMP_ROOT="$HOME/.cherry-tmp" CHERRY_VINE_SIGNATURE_MODE=enforce npm run verify:repo-closure` for canonical full proof. + +Agents must not blindly stack `npm run check`, `npm test`, `npm run build`, and `verify:repo-closure`; do not run both `npm test` and `verify:repo-closure` unless explicitly required. ### What CI green means (DB posture) - Standard CI (`ci:verify`) does not exercise a live database; tests run with Prisma mocked. From 544cf476db75caf59bee47f8ff496d4b1d6852db Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:28:26 -0400 Subject: [PATCH 15/21] docs(guardrails): update guardrails doc with new ci and runtime rules --- docs/guardrails.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/guardrails.md b/docs/guardrails.md index 15f461d..81a01d5 100644 --- a/docs/guardrails.md +++ b/docs/guardrails.md @@ -1,11 +1,12 @@ Status: Active -Last updated: 2026-01-31 +Last updated: 2026-04-28 # Guardrails ## Current behavior - Guardrail and execution script registration is mandatory; registries are the only authority. -- CI runs `npm run ci:verify` as the sole truth gate; `check` remains pure (guardrails + lint + typecheck), and env checks live in `check:env`. +- CI runs fail-fast `check:guardrails` before final `npm run ci:verify`; `ci:verify` is the sole runtime truth gate, and env checks live in `check:env`. +- `npm test` is the partitioned full runtime runner: root legacy tests, `tests/node`, then `tests/next`; ownership is enforced by `tests/node/guardrails/test-runner-ownership.test.ts`. - Script conventions (no raw JSON.parse, no any, .mts only under scripts) live in `docs/script-standards.md`. - Guardrail checks now enforce JSON.parse bans in scripts and npm arg forwarding (`check:script-json-parse`, `check:npm-arg-forwarding`). - Script runtime boundaries are enforced; scripts may not import app/components/lib-client runtime modules (`check:script-runtime-boundary`). @@ -465,7 +466,8 @@ Any duplication is a hard CI failure. - CI must include a step that runs `npm run ci:verify`. - The last non-empty command in the CI job must be `npm run ci:verify`. -- CI must not invoke other npm scripts directly; `ci:verify` is the only entrypoint. +- CI may run `npm run check:guardrails` before `ci:verify` for fail-fast coverage. +- CI must not invoke direct runtime scripts (`npm test`, `check`, `check:node`, `check:next`, `check:tests:*`, or `check:run-tests:*`) outside `ci:verify`. - Guardrail checks: `check:ci-must-run-check`, `check:ci-guardrail-coverage`. ### Guardrail 23 — Execution Registry Completeness @@ -534,6 +536,7 @@ Any duplication is a hard CI failure. - `ci:verify` must run `check`, `test`, and `build` in order. - `check` must remain pure (no env-dependent scripts). +- `test` must reach `check:run-tests`, the partitioned full runtime runner. - `test` and `build` must not invoke guardrails; use `test:strict` and `build:strict` when needed. - Guardrail: `check:check-contract`. From c8a0e628aaaecd03b65d75e1641482f5fa3bdec3 Mon Sep 17 00:00:00 2001 From: Mous <156965421+div0rce@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:28:43 -0400 Subject: [PATCH 16/21] docs(script-standards): update script standards for new ci entrypoints --- docs/script-standards.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/script-standards.md b/docs/script-standards.md index 062b9cf..1d15d4f 100644 --- a/docs/script-standards.md +++ b/docs/script-standards.md @@ -1,5 +1,5 @@ Status: Active -Last updated: 2026-01-02 +Last updated: 2026-04-28 # Script Standards @@ -7,7 +7,8 @@ Last updated: 2026-01-02 - Scripts are ESM by extension; `.mts` only lives under `scripts/`, runtime code stays `.ts`. - Guardrail entrypoints are registered in `scripts/guardrails/registry.mts` and must be reachable from `npm run check`. - Execution entrypoints are registered in `scripts/execution/registry.mts` and run via `npm run ts:esm -- scripts/execution/run.mts `. -- CI must run `npm run ci:verify` and it must be the final non-empty command in the job. +- CI may run `npm run check:guardrails` for fail-fast coverage, then must run one final `npm run ci:verify`. +- Direct CI runtime test scripts are forbidden; `ci:verify` reaches `npm test`, and `npm test` reaches `check:run-tests`. - JSON inputs must be parsed via `scripts/guardrails/lib/read-json.mts`; raw `JSON.parse` is forbidden outside that helper. - NPM script args must be forwarded with `--` (use `npm run