mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
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
This commit is contained in:
1 parent
bd46f3c1ee
commit
ecf5372627
8 files changed
+67
-31
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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.`);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
/(?<![A-Za-z0-9_])(?:rea_restart_required|restartRequired|restart_required)(?![A-Za-z0-9_])|effect:\s*"(?:immediate|next_process)"|SettingChangeEffect/gu;
|
||||
// Docs and verification receipts keep the field names they recorded.
|
||||
// Source, tests, skills, and scripts cannot reintroduce those names.
|
||||
const scanRoots = ["src", "tests", ".agents", "scripts"].map((dir) =>
|
||||
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;
|
||||
|
||||
@@ -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<string, { inputs: readonly string[] }> };
|
||||
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([]);
|
||||
});
|
||||
@@ -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/**",
|
||||
|
||||
Reference in new issue
Block a user