From ecf5372627e5ee81259fdae129031e2ae9f4a45c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 21:38:59 +0800 Subject: [PATCH] ci(guards): run the architecture guards in the pre-push check (#10948) * ci(guards): run the architecture guards in the pre-push check * docs(limits): name lint as the numeric-limit check's owner * ci(lint): cache the numeric-limit check by every file its restart-field scan reads --- docs/design/rea-limit-inventory.md | 9 ++++--- docs/development/testing.md | 2 +- package.json | 2 +- scripts/verify/architecture-guards.mjs | 21 --------------- scripts/verify/rea-numeric-limits.d.mts | 6 +++++ scripts/verify/rea-numeric-limits.mjs | 30 +++++++++++++++++++--- tests/evaluation/numericLimitCheck.test.ts | 24 +++++++++++++++++ turbo.json | 4 +++ 8 files changed, 67 insertions(+), 31 deletions(-) diff --git a/docs/design/rea-limit-inventory.md b/docs/design/rea-limit-inventory.md index 45f9290b0d..f7635abb59 100644 --- a/docs/design/rea-limit-inventory.md +++ b/docs/design/rea-limit-inventory.md @@ -236,10 +236,11 @@ control-reply budget stays separate from ## Static check -`npm run lint` runs `scripts/verify/rea-numeric-limits.mjs` after -dependencies are installed. `scripts/verify/architecture-guards.mjs` runs the -same check when `typescript6` is already installed; the pre-install CI step -cannot. The check scans `src` excluding tests, +`npm run lint` owns this check: it runs +`scripts/verify/rea-numeric-limits.mjs` after dependencies are installed, so +`npm run check:fast`, the pre-push hook and CI's static checks all include it. +`scripts/verify/architecture-guards.mjs` does not run it. The check scans `src` +excluding tests, keeps names that are numeric limits, and requires each name to be an `environment` on an analysis setting or a registry entry with a reason. The registry accepts only `restart-only`. A limit checked on each call has to diff --git a/docs/development/testing.md b/docs/development/testing.md index ecd98c13f0..3a8c6ade65 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -42,7 +42,7 @@ that invoke PowerShell themselves remain subject to their own policy checks. | `npm run test:python` | LLDB and mitmproxy producer contracts plus analysis-efficiency checks; no real debugger or proxy required | | `npm run test:watch` | Dirty source tests in watch mode, without build | | `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests | -| `npm run check:fast` | Cached typecheck and lint | +| `npm run check:fast` | Cached typecheck and lint, then the architecture guards and their counted baselines | | `npm run check:changed` | Cached typecheck/lint and branch-related source feedback | | `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks | | `npm run docs:check` | Generated-document validation and fresh onboarding example consistency checks | diff --git a/package.json b/package.json index 668849b9ea..35c6aa244c 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "precheck": "npm run deps:check", "check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint format:check knip metadata:check && npm run verify:architecture-guards && npm run knip:production && npm run lint:typed", "precheck:fast": "npm run deps:check", - "check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only", + "check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only && npm run verify:architecture-guards", "precheck:changed": "npm run deps:check", "check:changed": "npm run check:fast && npm run test:changed", "precheck:test": "npm run deps:check", diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index 27c3ea27da..1dde594197 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -1,5 +1,3 @@ -import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; import { readFile, readdir, writeFile } from "node:fs/promises"; import { join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; @@ -692,23 +690,4 @@ if (failures.length > 0) { console.error(`Architecture guards failed:\n\n${failures.join("\n\n")}\n`); process.exit(1); } -// CI runs this file before `npm ci`. The numeric-limit parser needs -// typescript6, so that check waits until dependencies exist. `npm run lint` -// runs it after install. -const typescript6 = new URL( - "../../node_modules/typescript6/package.json", - import.meta.url, -); -if (!existsSync(typescript6)) { - console.log( - "Numeric REA_ limit check waits until dependencies are installed.", - ); -} else { - const numericLimits = spawnSync( - process.execPath, - [fileURLToPath(new URL("./rea-numeric-limits.mjs", import.meta.url))], - { stdio: "inherit" }, - ); - if (numericLimits.status !== 0) process.exit(numericLimits.status ?? 1); -} console.log(`Architecture guards passed across ${files.length} source files.`); diff --git a/scripts/verify/rea-numeric-limits.d.mts b/scripts/verify/rea-numeric-limits.d.mts index 9b3762519c..84b9e974bf 100644 --- a/scripts/verify/rea-numeric-limits.d.mts +++ b/scripts/verify/rea-numeric-limits.d.mts @@ -1,6 +1,12 @@ /** Relative path of the registry. Its text is a declaration, not a producer. */ export const NUMERIC_LIMIT_DECLARATION: "src/domain/numericLimitRegistry.ts"; +/** Roots scanned for the removed restart-field names. */ +export const FORBIDDEN_FIELD_SCAN_ROOTS: readonly string[]; + +/** File extensions, without the dot, scanned under those roots. */ +export const FORBIDDEN_FIELD_SCAN_EXTENSIONS: readonly string[]; + /** True for a numeric `REA_` limit, including the documented endpoint-candidate count. */ export function isNumericLimit(name: string): boolean; diff --git a/scripts/verify/rea-numeric-limits.mjs b/scripts/verify/rea-numeric-limits.mjs index 53f981ae6f..5b12505f21 100644 --- a/scripts/verify/rea-numeric-limits.mjs +++ b/scripts/verify/rea-numeric-limits.mjs @@ -36,6 +36,29 @@ const listFiles = async (dir) => { /** Registry text is a declaration, not a producer that uses the limit. */ export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts"; +/** + * Where the removed restart-field names cannot return: these roots, files with + * these extensions. The cached `lint` task's inputs in turbo.json must cover + * every such file, or a cached pass would hide a reintroduced name. + */ +export const FORBIDDEN_FIELD_SCAN_ROOTS = [ + "src", + "tests", + ".agents", + "scripts", +]; +export const FORBIDDEN_FIELD_SCAN_EXTENSIONS = [ + "ts", + "tsx", + "mjs", + "js", + "md", + "json", + "py", + "yml", + "yaml", +]; + /** * These limits are moving onto runtime analysis settings on their own PRs. * A producer or a registry entry cannot satisfy that requirement. @@ -1037,9 +1060,8 @@ if (direct) { /(? - join(root, dir), - ); + const scanRoots = FORBIDDEN_FIELD_SCAN_ROOTS.map((dir) => join(root, dir)); + const scanned = new Set(FORBIDDEN_FIELD_SCAN_EXTENSIONS); const skipDir = new Set([ "node_modules", "dist", @@ -1054,7 +1076,7 @@ if (direct) { if (skipDir.has(entry.name)) continue; const path = join(dir, entry.name); if (entry.isDirectory()) output.push(...(await listText(path))); - else if (/\.(?:ts|tsx|mjs|js|md|json|py|yml|yaml)$/u.test(entry.name)) + else if (scanned.has(entry.name.slice(entry.name.lastIndexOf(".") + 1))) output.push(path); } return output; diff --git a/tests/evaluation/numericLimitCheck.test.ts b/tests/evaluation/numericLimitCheck.test.ts index 3d08c61145..af472acd98 100644 --- a/tests/evaluation/numericLimitCheck.test.ts +++ b/tests/evaluation/numericLimitCheck.test.ts @@ -1,5 +1,9 @@ +import { readFile } from "node:fs/promises"; +import { matchesGlob } from "node:path"; import { expect, it } from "vitest"; import { + FORBIDDEN_FIELD_SCAN_EXTENSIONS, + FORBIDDEN_FIELD_SCAN_ROOTS, NUMERIC_LIMIT_DECLARATION, discoverNumericLimitUses, isNumericLimit, @@ -592,3 +596,23 @@ it("requires the tracked endpoint-candidate limit to be an analysis setting", () }), ).toMatch(/numericLimitRegistry/); }); + +it("lets no cached lint pass hide a file the restart-field scan reads", async () => { + // `npm run lint` owns this check and turbo caches it by these inputs; a + // scanned file outside them would let a cached pass skip a new violation. + const turbo = JSON.parse( + await readFile(new URL("../../turbo.json", import.meta.url), "utf8"), + ) as { tasks: Record }; + const globs = turbo.tasks.lint!.inputs; + const cached = (path: string) => + globs.some((glob) => !glob.startsWith("!") && matchesGlob(path, glob)) && + !globs.some( + (glob) => glob.startsWith("!") && matchesGlob(path, glob.slice(1)), + ); + const samples = FORBIDDEN_FIELD_SCAN_ROOTS.flatMap((root) => + FORBIDDEN_FIELD_SCAN_EXTENSIONS.map( + (extension) => `${root}/nested/file.${extension}`, + ), + ); + expect(samples.filter((path) => !cached(path))).toEqual([]); +}); diff --git a/turbo.json b/turbo.json index ca4d80f4e1..8c68b4fe12 100644 --- a/turbo.json +++ b/turbo.json @@ -86,6 +86,10 @@ "docs/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}", ".github/actions/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}", "website/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}", + "src/**/*.{json,md,py,yaml,yml}", + "tests/**/*.{json,md,py,yaml,yml}", + "scripts/**/*.{json,md,py,yaml,yml}", + ".agents/**/*.{js,json,md,mjs,py,ts,tsx,yaml,yml}", "!docs/.vitepress/cache/**", "!docs/.vitepress/dist/**", "!website/_site/**",