From 897c50e1a4515b31e8172e6d5263e151c126fd6c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 13:14:59 +0100 Subject: [PATCH] perf: speed up type check gate --- AGENTS.md | 7 ++-- docs/ci.md | 5 +-- package.json | 9 +++-- scripts/check-timed.mjs | 57 +++++++++++++++++++++++++++++++ tsconfig.core.projects.json | 4 +++ tsconfig.extensions.projects.json | 7 ++++ tsconfig.projects.json | 7 ++++ 7 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 scripts/check-timed.mjs create mode 100644 tsconfig.core.projects.json create mode 100644 tsconfig.extensions.projects.json create mode 100644 tsconfig.projects.json diff --git a/AGENTS.md b/AGENTS.md index daef230c6fe..db9f06e84e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,7 +132,9 @@ - `pnpm tsgo:core:test`: core colocated tests. - `pnpm tsgo:extensions`: bundled extension production graph. - `pnpm tsgo:extensions:test`: bundled extension colocated tests. - - `pnpm tsgo:all`: every TypeScript graph above; this is what `pnpm check` runs. + - `pnpm tsgo:core:all`: core production + core test project references (`tsconfig.core.projects.json`). + - `pnpm tsgo:extensions:all`: extension production + extension test project references (`tsconfig.extensions.projects.json`). + - `pnpm tsgo:all`: every TypeScript graph above through the project-reference root (`tsconfig.projects.json`); this is what `pnpm check` runs. - `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`. Diagnostic-only profile slices (`core-test-agents`, `core-test-non-agents`) exist for investigating agent graph cost; do not treat them as normal user-facing checks. - Narrow aliases remain for local loops: `pnpm tsgo:test:src`, `pnpm tsgo:test:ui`, `pnpm tsgo:test:packages`. - Do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes for repo type checking. Use `tsgo` graphs. `tsc` is allowed only when emitting declaration/package-boundary compatibility artifacts that `tsgo` does not replace. @@ -147,6 +149,7 @@ - A landing gate is the broader bar before pushing `main`, usually `pnpm check`, `pnpm test`, and `pnpm build` when the touched surface can affect build output, packaging, lazy-loading/module boundaries, or published surfaces. - A CI gate is whatever the relevant workflow enforces for that lane (for example `check`, `check-additional`, `build-smoke`, or release validation). - Local dev gate: prefer `pnpm check` for the normal edit loop. It keeps the repo-architecture policy guards out of the default local loop. +- Timed local gate: use `pnpm check:timed` to see per-stage cost. Add `:architecture` only when investigating the CI architecture gate locally. - CI architecture gate: `check-additional` enforces architecture and boundary policy guards that are intentionally kept out of the default local loop. - Formatting gate: the pre-commit hook runs targeted formatting on staged source files before `pnpm check`. If you want a repo-wide formatting-only preflight locally, run `pnpm format:check` explicitly. - If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hook’s repo-wide `pnpm check`; targeted formatting/linting still runs, so use that only when you are deliberately covering the touched surface some other way. @@ -190,7 +193,7 @@ - New runtime control-flow code should not branch on `error: string` or `reason: string` when a closed code union would be reasonable. - Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only. - Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting. -- Circular dependencies: keep both `pnpm check:import-cycles` and `pnpm check:madge-import-cycles` green; do not reintroduce runtime import cycles or madge-detected import loops. +- Circular dependencies: `pnpm check` runs the fast runtime import-cycle guard. `pnpm check:architecture` (and CI `check-additional`) also runs the broader madge import-cycle guard; keep both green before landing architecture-sensitive changes. - Extension SDK self-import guardrail: inside an extension package, do not import that same extension via `openclaw/plugin-sdk/` from production files. Route internal imports through a local barrel such as `./api.ts` or `./runtime-api.ts`, and keep the `plugin-sdk/` path as the external contract only. - Extension package boundary guardrail: inside a bundled plugin package, do not use relative imports/exports that resolve outside that same package root. If shared code belongs in the plugin SDK, import `openclaw/plugin-sdk/` instead of reaching into `src/plugin-sdk/**` or other repo paths via `../`. - Extension API surface rule: `openclaw/plugin-sdk/` is the only public cross-package contract for extension-facing SDK code. If an extension needs a new seam, add a public subpath first; do not reach into `src/plugin-sdk/**` by relative path. diff --git a/docs/ci.md b/docs/ci.md index 514ec5ab7cd..373fafeeec9 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -57,9 +57,10 @@ On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull ## Local Equivalents ```bash -pnpm check # types + lint + format +pnpm check # fast local gate: project-reference tsgo + lint + fast guards +pnpm check:timed # same gate with per-stage timings pnpm build:strict-smoke -pnpm check:import-cycles +pnpm check:architecture pnpm test:gateway:watch-regression pnpm test # vitest tests pnpm test:channels diff --git a/package.json b/package.json index b7fc309f300..ba8ac88e593 100644 --- a/package.json +++ b/package.json @@ -1236,7 +1236,8 @@ "canon:check:json": "node scripts/canon.mjs check --json", "canon:enforce": "node scripts/canon.mjs enforce --json", "canvas:a2ui:bundle": "node scripts/bundle-a2ui.mjs", - "check": "pnpm check:no-conflict-markers && pnpm tool-display:check && pnpm check:host-env-policy:swift && pnpm tsgo:all && pnpm lint && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:import-cycles && pnpm check:madge-import-cycles", + "check": "pnpm check:no-conflict-markers && pnpm tool-display:check && pnpm check:host-env-policy:swift && pnpm tsgo:all && pnpm lint && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:import-cycles", + "check:architecture": "pnpm check:import-cycles && pnpm check:madge-import-cycles", "check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check", "check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links", @@ -1246,6 +1247,8 @@ "check:madge-import-cycles": "node --import tsx scripts/check-madge-import-cycles.ts", "check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs", "check:static-import-sccs": "pnpm check:madge-import-cycles", + "check:timed": "node scripts/check-timed.mjs", + "check:timed:architecture": "node scripts/check-timed.mjs --include-architecture", "codex-app-server:protocol:check": "node --import tsx scripts/check-codex-app-server-protocol.ts", "config:channels:check": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check", "config:channels:gen": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --write", @@ -1471,10 +1474,12 @@ "tool-display:write": "node --import tsx scripts/tool-display.ts --write", "ts-topology": "node --import tsx scripts/ts-topology.ts", "tsgo": "pnpm tsgo:core", - "tsgo:all": "pnpm tsgo:core && pnpm tsgo:core:test && pnpm tsgo:extensions && pnpm tsgo:extensions:test", + "tsgo:all": "node scripts/run-tsgo.mjs -b tsconfig.projects.json", "tsgo:core": "node scripts/run-tsgo.mjs -p tsconfig.core.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core.tsbuildinfo", + "tsgo:core:all": "node scripts/run-tsgo.mjs -b tsconfig.core.projects.json", "tsgo:core:test": "node scripts/run-tsgo.mjs -p tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo", "tsgo:extensions": "node scripts/run-tsgo.mjs -p tsconfig.extensions.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions.tsbuildinfo", + "tsgo:extensions:all": "node scripts/run-tsgo.mjs -b tsconfig.extensions.projects.json", "tsgo:extensions:test": "node scripts/run-tsgo.mjs -p tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo", "tsgo:prod": "pnpm tsgo:core && pnpm tsgo:extensions", "tsgo:profile": "node scripts/profile-tsgo.mjs", diff --git a/scripts/check-timed.mjs b/scripts/check-timed.mjs new file mode 100644 index 00000000000..43d1b5a14e3 --- /dev/null +++ b/scripts/check-timed.mjs @@ -0,0 +1,57 @@ +import { spawnSync } from "node:child_process"; +import { performance } from "node:perf_hooks"; + +const includeArchitecture = process.argv.includes("--include-architecture"); + +const stages = [ + { name: "conflict markers", args: ["check:no-conflict-markers"] }, + { name: "tool display", args: ["tool-display:check"] }, + { name: "host env policy", args: ["check:host-env-policy:swift"] }, + { name: "typecheck", args: ["tsgo:all"] }, + { name: "lint", args: ["lint"] }, + { name: "webhook body guard", args: ["lint:webhook:no-low-level-body-read"] }, + { name: "pairing store guard", args: ["lint:auth:no-pairing-store-group"] }, + { name: "pairing account guard", args: ["lint:auth:pairing-account-scope"] }, + { name: "runtime import cycles", args: ["check:import-cycles"] }, +]; + +if (includeArchitecture) { + stages.push({ name: "architecture import cycles", args: ["check:madge-import-cycles"] }); +} + +const timings = []; +let exitCode = 0; + +for (const { name, args } of stages) { + const startedAt = performance.now(); + console.error(`\n[check:timed] ${name}`); + const result = spawnSync("pnpm", args, { + stdio: "inherit", + shell: process.platform === "win32", + }); + const durationMs = performance.now() - startedAt; + timings.push({ name, durationMs, status: result.status ?? 1 }); + + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + exitCode = result.status ?? 1; + break; + } +} + +console.error("\n[check:timed] summary"); +for (const timing of timings) { + const status = timing.status === 0 ? "ok" : `failed:${timing.status}`; + console.error(`${formatMs(timing.durationMs).padStart(8)} ${status.padEnd(9)} ${timing.name}`); +} + +process.exitCode = exitCode; + +function formatMs(durationMs) { + if (durationMs < 1000) { + return `${Math.round(durationMs)}ms`; + } + return `${(durationMs / 1000).toFixed(2)}s`; +} diff --git a/tsconfig.core.projects.json b/tsconfig.core.projects.json new file mode 100644 index 00000000000..611edf76190 --- /dev/null +++ b/tsconfig.core.projects.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.core.json" }, { "path": "./tsconfig.core.test.json" }] +} diff --git a/tsconfig.extensions.projects.json b/tsconfig.extensions.projects.json new file mode 100644 index 00000000000..0cd86db0446 --- /dev/null +++ b/tsconfig.extensions.projects.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.extensions.json" }, + { "path": "./tsconfig.extensions.test.json" } + ] +} diff --git a/tsconfig.projects.json b/tsconfig.projects.json new file mode 100644 index 00000000000..3b5517c1943 --- /dev/null +++ b/tsconfig.projects.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.core.projects.json" }, + { "path": "./tsconfig.extensions.projects.json" } + ] +}