From 7158f1b4d3ebc7fac8d8204b393dd093c183a882 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:00:11 +0000 Subject: [PATCH 1/5] test(devx): gate the module-top load of a dist-resolved workspace dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends `check:test-source-alias` with the clocked-window rule (#10126): a test file that loads one of its package's unaliased specifiers through a dynamic `import()` / `require()` inside a function body — an `it()` body, a hook, or any nested function — with no module-scope load of the same specifier is a finding. vitest clocks hooks and test bodies only; collection is clocked against nothing, so a module-top side-effect import moves a cold transform OUT of every budget rather than into a bigger one. #10115 / PR #10120 paid for that reading with 30 ejected merge-queue builds in one night. Baseline sweep: 31 violations across 21 files in 9 packages, all fixed here the PR #10120 way (module-top `import ''`), none quarantined or registry-exempted. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01URCaKuNTuK3BKJvwqM74QU --- AGENTS.md | 9 + examples/app-crm/test/smoke.test.ts | 7 + examples/embed-objectql/test/embed.test.ts | 7 + .../cli/src/commands/dev-default-db.test.ts | 15 +- .../doctor-ledger-dir-authority.test.ts | 7 + ...doctor-ledger-posture-independence.test.ts | 7 + .../doctor-ledger-read-failure.test.ts | 7 + ...form-migrations-arming.integration.test.ts | 8 + .../serve-marketplace-offline-install.test.ts | 7 + ...branch-policy.cross-package-parity.test.ts | 7 + .../src/engine-write-not-found-gate.test.ts | 8 + .../src/validation/rule-validator.test.ts | 7 + .../test/storage-growth.dogfood.test.ts | 7 + packages/rest/src/public-form-routes.test.ts | 7 + .../src/datasource-autoconnect.test.ts | 9 + .../runtime/src/datasource-visibility.test.ts | 9 + .../src/default-datasource-plugin.test.ts | 9 + .../src/federated-boot-binding.test.ts | 9 + packages/runtime/src/http-dispatcher.test.ts | 7 + .../src/standalone-stack.mysql.test.ts | 7 + packages/runtime/src/standalone-stack.test.ts | 7 + .../__tests__/datasource-admin-plugin.test.ts | 7 + scripts/check-test-source-alias.mjs | 456 +++++++++++++++++- 23 files changed, 623 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 058ee5a201..e02ae2e77c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,15 @@ judged on the **shallowest** point a path reaches, not where it ends — a liter climbs past the package root and then descends into a sibling ends at a positive depth while addressing another package entirely. +### Clocked windows measure behaviour, never loading + +**Clocked windows measure behaviour, never loading — a test that boots a real plugin +chain pays its first load at module top.** vitest clocks hooks (`hookTimeout`) and test +bodies (`testTimeout`) and nothing else; collection is clocked against nothing, so a +module-top `import ''` moves a cold transform OUT of every budget rather than +into a bigger one. `pnpm check:test-source-alias` gates this for the workspace deps a +package still resolves through `dist/`. + ### Running the dev server | Scenario | Command | Notes | diff --git a/examples/app-crm/test/smoke.test.ts b/examples/app-crm/test/smoke.test.ts index 8e59310bec..69a80119b6 100644 --- a/examples/app-crm/test/smoke.test.ts +++ b/examples/app-crm/test/smoke.test.ts @@ -4,6 +4,13 @@ import { describe, it, expect } from 'vitest'; import stack from '../objectstack.config.js'; import { PipelineDashboard } from '../src/dashboards/pipeline.dashboard.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/spec/ui'; + describe('app-crm minimal metadata bundle', () => { it('exposes the expected manifest', () => { // `manifest` is optional on the stack bundle type; this example always diff --git a/examples/embed-objectql/test/embed.test.ts b/examples/embed-objectql/test/embed.test.ts index 2b9e117c70..676071c217 100644 --- a/examples/embed-objectql/test/embed.test.ts +++ b/examples/embed-objectql/test/embed.test.ts @@ -3,6 +3,13 @@ import { describe, it, expect } from 'vitest'; import { runEmbeddedEngine } from '../src/index.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/objectql/core'; + describe('embed @objectstack/objectql/core (ADR-0076)', () => { it('runs the engine standalone and round-trips CRUD via the lean entry', async () => { const active = await runEmbeddedEngine(); diff --git a/packages/cli/src/commands/dev-default-db.test.ts b/packages/cli/src/commands/dev-default-db.test.ts index a9c50c6362..ce56b38955 100644 --- a/packages/cli/src/commands/dev-default-db.test.ts +++ b/packages/cli/src/commands/dev-default-db.test.ts @@ -12,16 +12,21 @@ * cross-command agreement is pinned by `unified-db-resolution.pin.test.ts`. */ -import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'path'; import { resolveDevDatabase } from './dev.js'; -// The dev seam lazy-imports @objectstack/runtime (oclif startup weight, #5726); -// the FIRST import cold-loads that graph and can exceed vitest's 5s default on -// a cold worker — warm it once, like standalone-stack.test.ts's BOOT_TIMEOUT. -beforeAll(async () => { await import('@objectstack/runtime'); }, 60_000); +// [#10126] The dev seam lazy-imports @objectstack/runtime (oclif startup weight, +// #5726), and the FIRST import cold-loads that whole graph. This file used to warm +// it in a `beforeAll` with a 60s budget; a hook is still a CLOCKED window, and any +// budget the cost is moved INTO can be exhausted by a heavier shard — which is how +// #10115 / PR #10120 ejected a file from the merge queue four times in one night. +// A module-top side-effect import pays the transform during COLLECTION instead, and +// vitest clocks hooks and test bodies only. See `scripts/check-test-source-alias.mjs` +// (the clocked-window rule), which now gates this shape. +import '@objectstack/runtime'; let cwd: string; beforeEach(() => { cwd = mkdtempSync(path.join(tmpdir(), 'os-dev-db-')); }); diff --git a/packages/cli/src/commands/doctor-ledger-dir-authority.test.ts b/packages/cli/src/commands/doctor-ledger-dir-authority.test.ts index 5acad42170..6de710a110 100644 --- a/packages/cli/src/commands/doctor-ledger-dir-authority.test.ts +++ b/packages/cli/src/commands/doctor-ledger-dir-authority.test.ts @@ -55,6 +55,13 @@ import Doctor, { installedPackageLedgerSkippedEntriesCheck, } from './doctor.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/cloud-connection'; + const HERE = path.dirname(fileURLToPath(import.meta.url)); /** `packages/cli` — the oclif root the real command is loaded against below. */ const CLI_ROOT = path.resolve(HERE, '..', '..'); diff --git a/packages/cli/src/commands/doctor-ledger-posture-independence.test.ts b/packages/cli/src/commands/doctor-ledger-posture-independence.test.ts index 0993ce9c0f..ad443e57eb 100644 --- a/packages/cli/src/commands/doctor-ledger-posture-independence.test.ts +++ b/packages/cli/src/commands/doctor-ledger-posture-independence.test.ts @@ -69,6 +69,13 @@ import { fileURLToPath } from 'node:url'; import Doctor from './doctor.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/cloud-connection'; + const HERE = path.dirname(fileURLToPath(import.meta.url)); /** `packages/cli` — the oclif root the real command is loaded against below. */ const CLI_ROOT = path.resolve(HERE, '..', '..'); diff --git a/packages/cli/src/commands/doctor-ledger-read-failure.test.ts b/packages/cli/src/commands/doctor-ledger-read-failure.test.ts index 5a6451e9ca..c86ed00b3a 100644 --- a/packages/cli/src/commands/doctor-ledger-read-failure.test.ts +++ b/packages/cli/src/commands/doctor-ledger-read-failure.test.ts @@ -93,6 +93,13 @@ import Doctor, { installedPackageLedgerReaderFailureCheck, } from './doctor.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/cloud-connection'; + const HERE = path.dirname(fileURLToPath(import.meta.url)); /** `packages/cli` — the oclif root the real command is loaded against below. */ const CLI_ROOT = path.resolve(HERE, '..', '..'); diff --git a/packages/cli/src/utils/platform-migrations-arming.integration.test.ts b/packages/cli/src/utils/platform-migrations-arming.integration.test.ts index 41e6bad173..84ec68b083 100644 --- a/packages/cli/src/utils/platform-migrations-arming.integration.test.ts +++ b/packages/cli/src/utils/platform-migrations-arming.integration.test.ts @@ -47,6 +47,14 @@ import { SqlDriver } from '@objectstack/driver-sql'; import { GLOBAL_TENANT, SEQUENCES_TABLE, ORGANIZATION_TABLE } from '@objectstack/metadata-protocol'; import { bootSchemaStack } from './schema-migrate.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/objectql'; +import '@objectstack/runtime'; + const ORG_ID = 'org_x'; /** The `__global__` counter the seed loader ran ahead to before the org existed. */ const SEEDED_LAST_VALUE = 38; diff --git a/packages/cli/test/serve-marketplace-offline-install.test.ts b/packages/cli/test/serve-marketplace-offline-install.test.ts index 28105f953f..55d1053a8b 100644 --- a/packages/cli/test/serve-marketplace-offline-install.test.ts +++ b/packages/cli/test/serve-marketplace-offline-install.test.ts @@ -31,6 +31,13 @@ import { describe, expect, it } from 'vitest'; import Serve from '../src/commands/serve.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/cloud-connection'; + /** Minimal stand-in for a loaded plugin: what the resolver actually reads. */ function plugin(name: string, ctorName: string): { name: string } { const Ctor = { [ctorName]: class { name: string; constructor(n: string) { this.name = n; } } }[ctorName]!; diff --git a/packages/metadata-protocol/src/union-branch-policy.cross-package-parity.test.ts b/packages/metadata-protocol/src/union-branch-policy.cross-package-parity.test.ts index fce756039f..2cf1f86201 100644 --- a/packages/metadata-protocol/src/union-branch-policy.cross-package-parity.test.ts +++ b/packages/metadata-protocol/src/union-branch-policy.cross-package-parity.test.ts @@ -106,6 +106,13 @@ import { zodIssuesToFields } from '@objectstack/spec/api'; // calls: `saveMetaItem`'s 422 (#5364) and `computeMetadataDiagnostics` (#5598). import { zodIssuesToMetadataIssues } from './protocol.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/spec/shared'; + // --------------------------------------------------------------------------- // Normalisation: the projection all three walks can be read as producing. // --------------------------------------------------------------------------- diff --git a/packages/objectql/src/engine-write-not-found-gate.test.ts b/packages/objectql/src/engine-write-not-found-gate.test.ts index 9685bbe705..a22e50c705 100644 --- a/packages/objectql/src/engine-write-not-found-gate.test.ts +++ b/packages/objectql/src/engine-write-not-found-gate.test.ts @@ -73,6 +73,14 @@ import { ObjectQL } from './engine.js'; import { bindHooksToEngine } from './hook-binder.js'; import type { Hook, ServiceObject } from '@objectstack/spec/data'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/core'; +import '@objectstack/metadata-protocol'; + const silentLogger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }; /** diff --git a/packages/objectql/src/validation/rule-validator.test.ts b/packages/objectql/src/validation/rule-validator.test.ts index c4830969b7..feb79797e4 100644 --- a/packages/objectql/src/validation/rule-validator.test.ts +++ b/packages/objectql/src/validation/rule-validator.test.ts @@ -18,6 +18,13 @@ import { } from './rule-validator.js'; import { ValidationError } from './record-validator.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/spec'; + // B2 — field-level conditional rules (CEL over `record`). const invoiceFields = { fields: { diff --git a/packages/qa/dogfood/test/storage-growth.dogfood.test.ts b/packages/qa/dogfood/test/storage-growth.dogfood.test.ts index d66f9c93f5..54cf97ca3b 100644 --- a/packages/qa/dogfood/test/storage-growth.dogfood.test.ts +++ b/packages/qa/dogfood/test/storage-growth.dogfood.test.ts @@ -26,6 +26,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; import showcaseStack from '@objectstack/example-showcase'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/driver-sql'; + const DAY_MS = 86_400_000; interface EngineLike { diff --git a/packages/rest/src/public-form-routes.test.ts b/packages/rest/src/public-form-routes.test.ts index e34db98f4a..17b864bef9 100644 --- a/packages/rest/src/public-form-routes.test.ts +++ b/packages/rest/src/public-form-routes.test.ts @@ -16,6 +16,13 @@ import { describe, it, expect, vi } from 'vitest'; import { RestServer } from './rest-server'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/spec/ui'; + function mockServer() { return { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), diff --git a/packages/runtime/src/datasource-autoconnect.test.ts b/packages/runtime/src/datasource-autoconnect.test.ts index 105133d615..7d3c60b804 100644 --- a/packages/runtime/src/datasource-autoconnect.test.ts +++ b/packages/runtime/src/datasource-autoconnect.test.ts @@ -28,6 +28,15 @@ import { DriverPlugin } from './driver-plugin.js'; import { AppPlugin } from './app-plugin.js'; import type { DatasourceConnectPolicy } from '@objectstack/service-datasource'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/driver-sql'; +import '@objectstack/objectql'; +import '@objectstack/service-datasource'; + const BOOT_TIMEOUT = 60_000; /** The host's default driver — sqlite `:memory:`, constructed the canonical way. */ diff --git a/packages/runtime/src/datasource-visibility.test.ts b/packages/runtime/src/datasource-visibility.test.ts index 24db33a550..6ebb1f3a68 100644 --- a/packages/runtime/src/datasource-visibility.test.ts +++ b/packages/runtime/src/datasource-visibility.test.ts @@ -26,6 +26,15 @@ import { Runtime } from './runtime.js'; import { DriverPlugin } from './driver-plugin.js'; import { AppPlugin } from './app-plugin.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/driver-sqlite-wasm'; +import '@objectstack/objectql'; +import '@objectstack/service-datasource'; + // A minimal compiled artifact carrying ONE code-defined external datasource // (origin stamped by `defineDatasource` at compile time) plus a single object. const ARTIFACT = { diff --git a/packages/runtime/src/default-datasource-plugin.test.ts b/packages/runtime/src/default-datasource-plugin.test.ts index 1cbf1ae37a..8beb074961 100644 --- a/packages/runtime/src/default-datasource-plugin.test.ts +++ b/packages/runtime/src/default-datasource-plugin.test.ts @@ -12,6 +12,15 @@ import { Runtime } from './runtime.js'; import { DefaultDatasourcePlugin } from './default-datasource-plugin.js'; import { AppPlugin } from './app-plugin.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/driver-sqlite-wasm'; +import '@objectstack/objectql'; +import '@objectstack/service-datasource'; + const BOOT_TIMEOUT = 60_000; const ENV = 'OS_ALLOW_DRIVER_CONNECT_FAILURE'; diff --git a/packages/runtime/src/federated-boot-binding.test.ts b/packages/runtime/src/federated-boot-binding.test.ts index 26f72b614b..1fda629e52 100644 --- a/packages/runtime/src/federated-boot-binding.test.ts +++ b/packages/runtime/src/federated-boot-binding.test.ts @@ -43,6 +43,15 @@ import { Runtime } from './runtime.js'; import { DriverPlugin } from './driver-plugin.js'; import { AppPlugin } from './app-plugin.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/driver-sql'; +import '@objectstack/objectql'; +import '@objectstack/service-datasource'; + const BOOT_TIMEOUT = 60_000; async function makeDefaultDriver() { diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index 4114e913c0..ba80466fdb 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -6,6 +6,13 @@ import { ApiErrorSchema } from '@objectstack/spec/api'; import type { ConnectorDescriptor } from '@objectstack/spec/integration'; import type { IAuthService, IAutomationService } from '@objectstack/spec/contracts'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/metadata-protocol'; + /** * [#4127] Mock-shape guard: every key must be a method the contract DECLARES. * diff --git a/packages/runtime/src/standalone-stack.mysql.test.ts b/packages/runtime/src/standalone-stack.mysql.test.ts index 67955988d7..268e814674 100644 --- a/packages/runtime/src/standalone-stack.mysql.test.ts +++ b/packages/runtime/src/standalone-stack.mysql.test.ts @@ -32,6 +32,13 @@ import { StandaloneDatabaseDriverSchema, } from './standalone-stack.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/service-datasource'; + /** Env keys these tests write; restored after every case. */ const ENV_KEYS = [ 'OS_DATABASE_URL', diff --git a/packages/runtime/src/standalone-stack.test.ts b/packages/runtime/src/standalone-stack.test.ts index bfb94385fc..1c7f4ad93c 100644 --- a/packages/runtime/src/standalone-stack.test.ts +++ b/packages/runtime/src/standalone-stack.test.ts @@ -43,6 +43,13 @@ import { createDefaultHostConfig, resolveDefaultArtifactPath } from './default-h // config that resolution reads correctly. import { appDefaultPermissionSetName, appSecurityPluginOptions } from '@objectstack/plugin-security'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/service-datasource'; + // A minimal `objectstack build` artifact carrying an app-declared default // profile with a hierarchy read scope, an add-on permission set, app roles, // plus the metadata the path already surfaced (objects/requires/manifest). diff --git a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts index bbf53186b3..8d4fab9c7e 100644 --- a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts +++ b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts @@ -8,6 +8,13 @@ import { type DatasourceAdminServicePluginOptions, } from '../datasource-admin-plugin.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/spec/kernel'; + /** * Minimal PluginContext + in-memory metadata service. Boots the plugin and * returns the registered `datasource-admin` service so we can exercise the diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 8a6cb3a916..1506cba453 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -173,6 +173,66 @@ // it silently to `dist/` — the vacuous-green direction this whole file // exists to refuse. // +// ── The clocked-window rule: pay the first load at module top (#10126) ────── +// +// The registry above names the deps a package's tests still resolve through +// `dist/`. Loading one of those is not free — it is a cold vite transform of +// that dependency's whole module graph — and WHERE a test pays it decides +// whether the suite is measuring behaviour or measuring a compiler. +// +// `dev-plugin-security-enforcement-warning.test.ts` paid the +// `@objectstack/plugin-security` transform inside a clocked window: first in +// whichever `it()` ran first (3.1-3.6s idle on 4 vCPU, ~70% of the 5000ms +// `testTimeout`), then — after a fix that only widened the window — inside a +// `beforeAll` (`hookTimeout` 10000ms). On a merge-queue shard, which runs the +// FULL suite where PR-side CI runs only the affected subset, the same load hit +// 20.26s. Cost: `Test timed out in 5000ms` / `Hook timed out in 10000ms` on 30 +// queue builds in 24h, `main` frozen 2h+, three innocent PRs ejected, and the +// first fix PR ejected twice by the very test it was fixing (#10115, PR #10120, +// #10112). +// +// Every budget the cost is moved INTO can be exhausted by a heavier shard. The +// fix is to move it OUT: a module-top `import ''` is paid during +// COLLECTION, and collection is clocked against nothing. Verified in PR #10120 +// against the installed runner rather than recalled — `@vitest/runner@4.1.10` +// wraps exactly hooks and test bodies in `withTimeout(...)`, while +// `collectTests()` awaits `runner.importFile(filepath, 'collect')` bare and +// merely RECORDS `file.collectDuration` for reporters; and vitest offers exactly +// three timeout knobs (`testTimeout`, `hookTimeout`, `teardownTimeout`), none of +// which covers module loading. +// +// So: a test file that loads one of ITS package's unaliased specifiers through a +// dynamic `import()` / `require()` **inside a function body** — an `it()`/`test()` +// body, a `beforeAll`/`beforeEach`/`afterAll` hook, or any nested function — +// with no module-scope load of the same specifier is a finding, reported against +// the test file. The convention is stated for authors in AGENTS.md § Build & Test +// ("clocked windows measure behaviour, never loading"). +// +// Boundaries, stated so they are not rediscovered as bugs: +// +// - **Compliance is a MODULE-SCOPE load, which is wider than "a static +// import".** A static `import`/`export … from` is the spelling to write and +// the one the remedy prints. But a dynamic `import()` at module scope (top- +// level await) is paid during collection too, by the same measurement above, +// so flagging it would be inventing a failure. Both are accepted; a +// type-only clause is accepted as NEITHER, since it never resolves. +// - `typeof import('x')` is a TYPE QUERY, erased before anything loads. Never +// a finding, and never a compliance token either. +// - The population is the specifiers THIS package really resolves through +// `dist/` — the same measurement `KNOWN_UNALIASED_TEST_IMPORTS` mirrors, at +// specifier granularity. A dep already aliased to source still costs a +// transform, and paying it in a hook is still a bad idea; it is simply not +// what this gate measures, and widening the population is a different card. +// - "Inside a function body" is decided by a brace scanner over the source +// with comments and string/template/regex CONTENT masked out (the same +// `scanSource` projection the alias reader uses). It reads `=> {`, and a `)` +// that is not preceded by `if`/`for`/`while`/`switch`/`catch`/`with`/`await`, +// as opening a function body, walking back over a return-type annotation +// (`): Promise< T > {`) to find it. A brace it cannot classify reads as NOT a +// function — the silent direction, chosen because the loud one would fail +// tests over a parse this file cannot afford to get right (see `asPath` for +// why evaluating instead of reading was rejected). +// // ── The registry, and why it is shaped like this ──────────────────────────── // // `KNOWN_UNALIASED_TEST_IMPORTS` is the measured state of the repo on the day @@ -209,7 +269,7 @@ // node scripts/check-test-source-alias.mjs --self-test import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; -import { stripComments } from './js-comment-mask.mjs'; +import { stripComments, scanSource, blank } from './js-comment-mask.mjs'; import { join, resolve, relative, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { tmpdir } from 'node:os'; @@ -591,6 +651,204 @@ function isTypeOnlyClause(clause) { return names.length > 0 && names.every((n) => /^type\s/.test(n)); } +// ── module-scope vs clocked-window load position (#10126) ─────────────────── + +/** + * Keywords whose `(…)` is a control header rather than a parameter list, so the + * `{` after it opens a BLOCK and not a function body. `await` is here for + * `for await (const x of y) {`, whose `(` is preceded by that word. + */ +const CONTROL_HEADS = new Set(['if', 'for', 'while', 'switch', 'catch', 'with', 'await']); + +/** `typeof import('x')` — a type query, erased before anything resolves. */ +const TYPE_QUERY_BEFORE = /\btypeof\s*$/; + +/** + * Two same-length projections of one source, so a byte offset means the same + * thing in both: `commentsOnly` keeps every string intact (the import regex has + * to read the specifier), `codeOnly` masks literal CONTENT as well (the brace + * scanner must not count a `{` inside a string or a template). + */ +function maskedProjections(source) { + const { comment, literal } = scanSource(source); + const both = new Uint8Array(source.length); + for (let i = 0; i < source.length; i++) both[i] = comment[i] || literal[i] ? 1 : 0; + return { commentsOnly: blank(source, comment), codeOnly: blank(source, both) }; +} + +/** The `<` matching a closing `>`, for a return-type annotation. -1 if none. */ +function matchingAngle(code, closeIndex) { + let depth = 0; + for (let i = closeIndex; i >= 0; i--) { + const c = code[i]; + if (c === '>') depth++; + else if (c === '<') { + depth--; + if (depth === 0) return i; + } else if (c === ';' || c === '{' || c === '}' || c === '\n') return -1; + } + return -1; +} + +/** + * Does the `{` at `braceIndex` open a FUNCTION BODY (as opposed to a block, an + * object literal, a class/interface body, or a `case` label's block)? + * + * Answered from the text before it, in the masked projection: `=> {` is an arrow + * body; a `)` whose head word is not a control keyword is a parameter list; and + * a return-type annotation may sit between the two (`): void {`, + * `): Promise< T > {`, `): { ok: true } {`), so the walk steps back over one. + * + * `openOf` carries the `(`/`[`/`{` matching each closer, collected by the same + * forward pass that calls this — matching backwards would have to re-answer + * "is this quote inside a string" that the projection already answered. + */ +function opensFunctionBody(code, braceIndex, openOf) { + let j = braceIndex - 1; + const skipSpace = () => { + while (j >= 0 && /\s/.test(code[j])) j--; + }; + skipSpace(); + for (let guard = 0; guard < 8 && j >= 0; guard++) { + const c = code[j]; + if (c === '>' && j > 0 && code[j - 1] === '=') return true; // `=> {` + if (c === ')') break; + if (c === '}' || c === ']') { + const open = openOf.get(j); + if (open == null) return false; + j = open - 1; + skipSpace(); + continue; + } + if (c === '>') { + const open = matchingAngle(code, j); + if (open < 0) return false; + j = open - 1; + skipSpace(); + continue; + } + if (/[A-Za-z0-9_$]/.test(c)) { + while (j >= 0 && /[A-Za-z0-9_$.]/.test(code[j])) j--; + skipSpace(); + // A return-type annotation is introduced by `:`. Anything else before the + // word means this brace is not a function body — `else {`, `try {`, a + // class body, an object literal. + if (j >= 0 && code[j] === ':') { + j--; + skipSpace(); + continue; + } + return false; + } + return false; + } + if (j < 0 || code[j] !== ')') return false; + const open = openOf.get(j); + if (open == null) return false; + let k = open - 1; + while (k >= 0 && /\s/.test(code[k])) k--; + const end = k; + while (k >= 0 && /[A-Za-z0-9_$]/.test(code[k])) k--; + return !CONTROL_HEADS.has(code.slice(k + 1, end + 1)); +} + +/** `[start, end]` of every function body in the masked source, outermost first. */ +function functionBodyRanges(code) { + const ranges = []; + const stack = []; + const openOf = new Map(); + const opens = { '(': [], '[': [], '{': [] }; + for (let i = 0; i < code.length; i++) { + const c = code[i]; + if (c === '(' || c === '[' || c === '{') { + opens[c].push(i); + if (c === '{') stack.push({ start: i, isFunction: opensFunctionBody(code, i, openOf) }); + continue; + } + if (c === ')' || c === ']' || c === '}') { + const opener = c === ')' ? '(' : c === ']' ? '[' : '{'; + const at = opens[opener].pop(); + if (at != null) openOf.set(i, at); + if (c === '}') { + const frame = stack.pop(); + if (frame && frame.isFunction) ranges.push([frame.start, i]); + } + } + } + return ranges; +} + +/** + * Where this file pays each of its module loads. + * + * `moduleScope` is every specifier loaded during COLLECTION — a static import or + * a module-scope dynamic one; `clocked` is every dynamic load that sits inside a + * function body, with the line and the spelling the diagnostic quotes. Type-only + * clauses and `typeof import(…)` queries appear in neither: they never resolve. + */ +function moduleLoadSites(source) { + const { commentsOnly, codeOnly } = maskedProjections(source); + const ranges = functionBodyRanges(codeOnly); + const inFunction = (index) => ranges.some(([start, end]) => index > start && index < end); + const moduleScope = new Set(); + const clocked = []; + IMPORT_PATTERNS.lastIndex = 0; + let match; + while ((match = IMPORT_PATTERNS.exec(commentsOnly))) { + const clause = match[1]; + const spec = match[2] ?? match[3] ?? match[4] ?? match[5]; + if (!spec) continue; + if (clause != null && isTypeOnlyClause(clause)) continue; + const dynamic = match[3] != null || match[4] != null; + if (!dynamic) { + moduleScope.add(spec); + continue; + } + // The pattern's leading `(?:^|[\s;{(=,])` eats one delimiter unless the match + // begins the file, so the keyword starts one character in. + const head = match.index + (/^[a-z]/.test(match[0]) ? 0 : 1); + if (TYPE_QUERY_BEFORE.test(codeOnly.slice(Math.max(0, head - 16), head))) continue; + if (!inFunction(head)) { + moduleScope.add(spec); + continue; + } + clocked.push({ spec, form: match[4] != null ? 'require' : 'import', line: lineOf(source, head) }); + } + return { moduleScope, clocked }; +} + +function lineOf(source, index) { + let line = 1; + for (let i = 0; i < index && i < source.length; i++) if (source[i] === '\n') line++; + return line; +} + +/** + * The clocked-window findings for one test file: a dynamic load of a specifier + * this package resolves through `dist/`, inside a function body, with no + * module-scope load of the same specifier anywhere in the file. + */ +function clockedWindowFindings(file, offendingSpecs) { + let source; + try { + source = readFileSync(file, 'utf8'); + } catch { + return []; + } + if (!/\b(?:import|require)\s*\(/.test(source)) return []; + const { moduleScope, clocked } = moduleLoadSites(source); + const seen = new Set(); + const findings = []; + for (const site of clocked) { + if (!offendingSpecs.has(site.spec)) continue; + if (moduleScope.has(site.spec)) continue; + if (seen.has(site.spec)) continue; + seen.add(site.spec); + findings.push(site); + } + return findings; +} + /** * `path -> specifiers`, for the whole process. Crossing the package boundary * means the SAME dependency source is walked once per consuming config — nine @@ -705,7 +963,7 @@ function testReachableWorkspaceImports(pkg, workspaceNames, crossInto) { } } - return { testCount: tests.length, imports, crossPackage }; + return { testCount: tests.length, testFiles: tests, imports, crossPackage }; } // ── vitest config alias reading ───────────────────────────────────────────── @@ -1248,10 +1506,25 @@ function scan(root) { throughAFile.push({ spec, result: resolved.result, via }); } + // The clocked-window rule (#10126), over the specifiers just measured as + // resolving through `dist/`. Deliberately downstream of that measurement: + // the population is not a second list to keep in step, it IS the list the + // registry mirrors, read at specifier granularity. + const offendingSpecs = new Set([...unaliasedSpecs.values()].flat().map((row) => row.spec)); + const clockedLoads = []; + if (offendingSpecs.size > 0) { + for (const file of reachable.testFiles.sort()) { + for (const finding of clockedWindowFindings(file, offendingSpecs)) { + clockedLoads.push({ ...finding, file: relative(root, file) }); + } + } + } + packages.push({ name: pkg.name, rel: pkg.rel, testCount: reachable.testCount, + clockedLoads, configPath: configPath ? relative(root, configPath) : null, unreadable, unaliased, @@ -1379,6 +1652,22 @@ function check(root, registry) { ' This gate must be able to see every alias. Write them as literal entries in this file.', ); } + for (const load of pkg.clockedLoads ?? []) { + failures.push( + `${load.file}:${load.line}: \`${load.form}('${load.spec}')\` is paid inside a function body — a CLOCKED window.\n` + + ` This file has no module-scope load of \`${load.spec}\`, and this package resolves that specifier\n` + + ' through `dist/`, so the first call transforms that dependency\'s whole module graph while a\n' + + " `testTimeout` or `hookTimeout` is running. Measured on the incident this rule comes from: 3.1-3.6s\n" + + ' idle on 4 vCPU, 20.26s on a starved core — past every clock, including a hypothetical 30s.\n' + + ' Add a module-top side-effect import so the transform is paid during COLLECTION, which vitest\n' + + ' clocks against nothing (it clocks hooks and test bodies only):\n' + + ` import '${load.spec}';\n` + + ' Keep the dynamic call where it is — this only decides WHERE the first load is paid. Widening the\n' + + ' timeout instead relocates the cliff to the next heavier shard; the merge queue runs the full\n' + + ' suite where PR-side CI runs only the affected subset, so that shard is heavier than anything the\n' + + ' PR checks measure.', + ); + } for (const trap of pkg.throughAFile) { failures.push( `${pkg.rel}: alias resolves \`${trap.spec}\` to \`${trap.result}\` — a path THROUGH a file (ENOTDIR at run time).\n` + @@ -1824,6 +2113,107 @@ function buildFixtureTree() { '] } };\n', }); + // ── (20-24) THE CLOCKED-WINDOW RULE (#10126) ────────────────────────────── + // + // `@fx/core` resolves to `dist/`, and none of these five aliases it, so every + // one of them is in the ledger. What separates them is WHERE the first load is + // paid: collection is unclocked, a hook or a test body is not. + + // (20) violating, in the incident's own two shapes: a hook and a test body, + // with no module-scope load anywhere in the file. `import.meta` style, an + // arrow hook, a `function` test body and a nested helper are all here so the + // brace scanner is exercised on more than one spelling of "a function". + fixture(root, 'packages/clocked-load', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-load'), + 'src/thing.test.ts': + "import { describe, it, beforeAll } from 'vitest';\n" + + 'beforeAll(async () => {\n' + + " await import('@fx/core');\n" + + '});\n' + + "describe('x', () => {\n" + + " it('y', async function (): Promise {\n" + + ' const load = async () => {\n' + + " await import('@fx/core/logger');\n" + + ' };\n' + + ' await load();\n' + + ' });\n' + + '});\n', + }); + + // (21) THE BOTH-PRESENT LEG. The same dynamic call inside the same hook — but + // the module top already loaded the specifier, so the transform is paid during + // collection and the dynamic call is a registry lookup. Compliant, and the + // real remediation shape (PR #10120), so a reader that reports it has undone + // the fix rather than found a defect. + fixture(root, 'packages/clocked-load-paid', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-load-paid'), + 'src/thing.test.ts': + "import { it } from 'vitest';\n" + + "import '@fx/core';\n" + + "it('y', async () => {\n" + + " const mod = await import('@fx/core');\n" + + ' return mod;\n' + + '});\n', + }); + + // (22) THE DECOY. A dynamic import in a test body of a specifier that is NOT + // in this package's measured set — `@fx/source-only` resolves to source, so it + // is not a stale-able artifact and is not in the ledger. Flagging it would + // silently re-scope the rule from the registry's population to "every import + // anywhere", which is a different card. + fixture(root, 'packages/source-only', { + 'package.json': JSON.stringify({ name: '@fx/source-only', exports: { '.': './src/index.ts' } }, null, 2), + 'src/index.ts': 'export const so = 1;\n', + }); + fixture(root, 'packages/clocked-decoy', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-decoy'), + 'src/thing.test.ts': + "import { it } from 'vitest';\n" + + "import '@fx/core';\n" + + "it('y', async () => {\n" + + " const a = await import('@fx/source-only');\n" + + " const b = await import('./local.js');\n" + + " const c = require('node:path');\n" + + ' return [a, b, c];\n' + + '});\n', + 'src/local.ts': 'export const local = 1;\n', + }); + + // (23) MODULE SCOPE IS NOT ONLY A STATIC IMPORT. A top-level `await import()` + // — and one inside a top-level `if`/`try` block, which is still module scope — + // is paid during collection by the same measurement. Reporting these would be + // inventing a failure, and the brace scanner is what tells them from a hook. + fixture(root, 'packages/clocked-top-level-await', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-top-level-await'), + 'src/thing.test.ts': + "import { it } from 'vitest';\n" + + "await import('@fx/core');\n" + + 'if (process.env.X) {\n' + + " await import('@fx/core/logger');\n" + + '}\n' + + "it('y', async () => {\n" + + " const mod = await import('@fx/core');\n" + + " const sub = await import('@fx/core/logger');\n" + + ' return [mod, sub];\n' + + '});\n', + }); + + // (24) A TYPE QUERY IS NOT A LOAD. `typeof import('x')` is erased before + // anything resolves, so it is neither a finding nor a compliance token — the + // shape `sys-metadata-repository.history-counters.test.ts` really writes. + // The dynamic call in the body is therefore still reported: the type query + // must not have quietly satisfied the rule. + fixture(root, 'packages/clocked-type-query', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-type-query'), + 'src/thing.test.ts': + "import { it } from 'vitest';\n" + + "type Core = typeof import('@fx/core');\n" + + "it('y', async () => {\n" + + " const mod: Core = await import('@fx/core');\n" + + ' return mod;\n' + + '});\n', + }); + return root; } @@ -2080,6 +2470,68 @@ function selfTest() { 'a reached subpath lost rule 5\'s finding — the dedupe dropped the stronger of the two', ); + // ── the clocked-window rule (#10126) ────────────────────────────────── + // + // Observed failing and observed silent, one leg per boundary the header + // states. The `has`/`!has` helpers cannot serve the silent legs here: every + // one of these fixtures ALSO carries an unregistered ledger entry, so a bare + // package-name search matches for a reason that has nothing to do with this + // rule. Each silent assertion is therefore scoped to the rule's own sentence. + const clockedIn = (needle) => + cross.failures.filter((f) => f.includes(needle) && f.includes('a CLOCKED window')); + + // The hook shape — `beforeAll(async () => { await import(…) })`, the second + // of the two spellings that ejected PR #10120 from the merge queue. + expect( + clockedIn('packages/clocked-load/src/thing.test.ts:3').some((f) => f.includes("import('@fx/core')")), + 'a dynamic import inside a `beforeAll` hook was not reported — the exact shape of the #10115 incident', + ); + // …and the test-body shape, reached through a nested arrow inside an + // `async function (): Promise< T >` body, so the brace scanner is pinned on + // more than the one arrow spelling. + expect( + clockedIn('packages/clocked-load/src/thing.test.ts:8').some((f) => f.includes("import('@fx/core/logger')")), + 'a dynamic import nested two functions deep inside an `it()` body went unseen — the brace scanner reads only `=> {`', + ); + expect( + clockedIn('packages/clocked-load').length === 2, + 'the clocked-window rule did not report exactly the two loads this fixture pays in a clocked window', + ); + // The remedy is the one PR #10120 landed, printed as a line to paste. + expect( + clockedIn('packages/clocked-load').every((f) => f.includes('COLLECTION') && f.includes("import '@fx/")), + 'the clocked-window finding printed no module-top import to add, or did not say where the cost moves TO', + ); + + // BOTH PRESENT IS COMPLIANT: the module top already paid it, so the dynamic + // call is a registry lookup. A reader that reports this has undone the fix. + expect( + clockedIn('packages/clocked-load-paid').length === 0, + 'a file with the module-top import AND the dynamic call was reported anyway — that is the remediation, not the defect', + ); + + // THE DECOY: a dynamic import of a specifier outside the measured set + // (`@fx/source-only` resolves to source), plus a relative import and a node + // builtin. None is a stale-able artifact and none may be flagged. + expect( + clockedIn('packages/clocked-decoy').length === 0, + 'the clocked-window rule flagged a specifier outside the ledger population — it has re-scoped itself to every import anywhere', + ); + + // Module scope is wider than "a static import": a top-level `await import()`, + // and one inside a top-level `if` block, are both paid during collection. + expect( + clockedIn('packages/clocked-top-level-await').length === 0, + 'a module-scope dynamic import was not accepted as paying the load — the rule invents a failure for top-level await', + ); + + // …and a TYPE QUERY is neither. `typeof import('x')` is erased before + // anything resolves, so it cannot satisfy the rule on the file's behalf. + expect( + clockedIn('packages/clocked-type-query/src/thing.test.ts:4').length === 1, + "`typeof import('x')` was read as a real module-scope load, and silenced a finding it never paid for", + ); + // The population the green line prints has to BE a number. `check()` returning // it under a different name still passes every failure assertion above and // reports `undefined published subpath(s)` — a census nobody can read. From 6e9dc1999c362218c5176dbed3e450cfbe53911b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:02:05 +0000 Subject: [PATCH 2/5] test(devx): count the clocked-window fixture's findings on its full path `packages/clocked-load` is also a prefix of `packages/clocked-load-paid`, so the count assertion read the compliant fixture's findings as the violating one's. Found by ablating the compliance leg: the count moved to 3 for a reason that was in the assertion rather than in the gate. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01URCaKuNTuK3BKJvwqM74QU --- scripts/check-test-source-alias.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 1506cba453..ff49d5a2e8 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -2493,13 +2493,19 @@ function selfTest() { clockedIn('packages/clocked-load/src/thing.test.ts:8').some((f) => f.includes("import('@fx/core/logger')")), 'a dynamic import nested two functions deep inside an `it()` body went unseen — the brace scanner reads only `=> {`', ); + // Counted on the FULL path: `packages/clocked-load` is also a prefix of + // `packages/clocked-load-paid`, and a loose needle here reads that fixture's + // findings as this one's — measured, when an ablation of the compliance leg + // moved this count to 3 for a reason that was in the assertion, not the gate. expect( - clockedIn('packages/clocked-load').length === 2, + clockedIn('packages/clocked-load/src/thing.test.ts').length === 2, 'the clocked-window rule did not report exactly the two loads this fixture pays in a clocked window', ); // The remedy is the one PR #10120 landed, printed as a line to paste. expect( - clockedIn('packages/clocked-load').every((f) => f.includes('COLLECTION') && f.includes("import '@fx/")), + clockedIn('packages/clocked-load/src/thing.test.ts').every( + (f) => f.includes('COLLECTION') && f.includes("import '@fx/"), + ), 'the clocked-window finding printed no module-top import to add, or did not say where the cost moves TO', ); From 94eb16cff902eb157a679794177806917117809e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:04:49 +0000 Subject: [PATCH 3/5] =?UTF-8?q?fix(devx):=20read=20a=20`}):=20Promise<=20{?= =?UTF-8?q?=20=E2=80=A6=20}=20>=20{`=20signature=20as=20a=20function=20bod?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The classifier's backward walk bailed on the braces, the semicolon and the newlines a multi-line signature puts between the parameter list and the body, so a module-level helper called from test bodies read as "not a function" and its file was exempted SILENTLY. Surfaced by ablating the classifier, not by the sweep: `serve-marketplace-offline-runtime-config.test.ts` sat green through it. Balanced groups are now jumped through the forward pass's `openOf` map, `=>` inside a function type is not counted as a generic closer, and the shape is pinned as its own self-test fixture. One more real violation follows, fixed the same way. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01URCaKuNTuK3BKJvwqM74QU --- ...marketplace-offline-runtime-config.test.ts | 7 ++ scripts/check-test-source-alias.mjs | 75 +++++++++++++++++-- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/cli/test/serve-marketplace-offline-runtime-config.test.ts b/packages/cli/test/serve-marketplace-offline-runtime-config.test.ts index 4261979fb8..cf2fdb2a13 100644 --- a/packages/cli/test/serve-marketplace-offline-runtime-config.test.ts +++ b/packages/cli/test/serve-marketplace-offline-runtime-config.test.ts @@ -61,6 +61,13 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import Serve from '../src/commands/serve.js'; +// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE +// LOAD. Each is reached below through a dynamic `import()` inside an `it()` body or a +// hook -- both of which vitest clocks, while collection is clocked against nothing. See +// `scripts/check-test-source-alias.mjs` (the clocked-window rule) and #10115 / PR #10120, +// where the same shape cost 30 ejected merge-queue builds in one night. +import '@objectstack/cloud-connection'; + interface RouteRecord { method: string; path: string } interface HonoShapedApp { diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index ff49d5a2e8..8f616ab5a4 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -676,16 +676,41 @@ function maskedProjections(source) { return { commentsOnly: blank(source, comment), codeOnly: blank(source, both) }; } -/** The `<` matching a closing `>`, for a return-type annotation. -1 if none. */ -function matchingAngle(code, closeIndex) { +/** + * The `<` matching a closing `>`, for a return-type annotation. -1 if none. + * + * Balanced groups inside the argument list are jumped through `openOf` rather + * than scanned: `Promise< { app: A; wiring: B } >` carries both braces and + * semicolons, and a scanner that bailed on either read a real function signature + * as "not a function". Measured — that spelling is what + * `serve-marketplace-offline-runtime-config.test.ts` writes, and it was silently + * exempting the file until an ablation of this classifier surfaced it. Newlines + * are not a boundary either: these signatures routinely span lines. + */ +function matchingAngle(code, closeIndex, openOf) { let depth = 0; for (let i = closeIndex; i >= 0; i--) { const c = code[i]; - if (c === '>') depth++; - else if (c === '<') { + if (c === '>') { + if (i > 0 && code[i - 1] === '=') { + i--; // `=>` inside a function TYPE — an arrow, not a generic closer + continue; + } + depth++; + continue; + } + if (c === '<') { depth--; if (depth === 0) return i; - } else if (c === ';' || c === '{' || c === '}' || c === '\n') return -1; + continue; + } + if (c === '}' || c === ']' || c === ')') { + const open = openOf.get(i); + if (open == null) return -1; + i = open; + continue; + } + if (c === ';' || c === '{' || c === '[' || c === '(') return -1; } return -1; } @@ -721,12 +746,20 @@ function opensFunctionBody(code, braceIndex, openOf) { continue; } if (c === '>') { - const open = matchingAngle(code, j); + const open = matchingAngle(code, j, openOf); if (open < 0) return false; j = open - 1; skipSpace(); continue; } + if (c === ':') { + // The `:` introducing a return-type annotation, reached after stepping + // back over an object or array type (`): { ok: true } {`). A `case`/label + // colon falls out below, on the token in front of it. + j--; + skipSpace(); + continue; + } if (/[A-Za-z0-9_$]/.test(c)) { while (j >= 0 && /[A-Za-z0-9_$.]/.test(code[j])) j--; skipSpace(); @@ -2214,6 +2247,27 @@ function buildFixtureTree() { '});\n', }); + // (25) THE SIGNATURE THAT DEFEATED THE CLASSIFIER ONCE. A module-level helper + // called from test bodies, whose parameter list spans lines and whose return + // type is `Promise< { … } >` — braces, a semicolon and newlines all between + // the parameter list and the body `{`. A backward walk that bailed on any of + // the three read this as "not a function body" and exempted the file + // SILENTLY, which is how `serve-marketplace-offline-runtime-config.test.ts` + // sat green through the sweep this rule shipped with. + fixture(root, 'packages/clocked-typed-signature', { + 'package.json': ARTIFACT_MANIFEST('@fx/clocked-typed-signature'), + 'src/thing.test.ts': + "import { it } from 'vitest';\n" + + 'async function boot(options: {\n' + + ' dir: string;\n' + + ' extra?: readonly unknown[];\n' + + '}): Promise<{ mod: object; dir: string }> {\n' + + " const mod = await import('@fx/core');\n" + + ' return { mod, dir: options.dir };\n' + + '}\n' + + "it('y', async () => boot({ dir: '.' }));\n", + }); + return root; } @@ -2538,6 +2592,15 @@ function selfTest() { "`typeof import('x')` was read as a real module-scope load, and silenced a finding it never paid for", ); + // A module-level helper called from a test body is still a clocked window, + // and its signature is where this classifier is hardest. Pinned by shape + // because the corpus taught it: the miss was silent, and a silent exemption + // is indistinguishable from compliance in every report the gate prints. + expect( + clockedIn('packages/clocked-typed-signature/src/thing.test.ts:6').length === 1, + 'a helper whose multi-line signature ends `}): Promise< { … } > {` was read as not a function body — a SILENT exemption', + ); + // The population the green line prints has to BE a number. `check()` returning // it under a different name still passes every failure assertion above and // reports `undefined published subpath(s)` — a census nobody can read. From c293c06f1bfae23c7a81aae41692a34b2aa281db Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:50:54 +0000 Subject: [PATCH 4/5] docs(devx): state the clocked-window convention where an author meets it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #10126's layer 4 asked for the sentence in AGENTS.md's testing conventions. That file sits exactly on its shrink-only line ceiling (958, headroom 0, `scripts/pm/check-skill-line-ratchet.mjs`); the sentence costs three lines and its own section has two lines of lossless rewrap headroom. Raising a ceiling takes a maintainer ruling, and truncating a maintainer-specified sentence to fit one is not this gate's call — so the doc placement stays open and is reported. Meanwhile the sentence is stated verbatim in the gate header AND in the failure text, pinned by its own self-test assertion so it cannot decay into a comment. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01URCaKuNTuK3BKJvwqM74QU --- AGENTS.md | 9 --------- scripts/check-test-source-alias.mjs | 26 ++++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e02ae2e77c..058ee5a201 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,15 +118,6 @@ judged on the **shallowest** point a path reaches, not where it ends — a liter climbs past the package root and then descends into a sibling ends at a positive depth while addressing another package entirely. -### Clocked windows measure behaviour, never loading - -**Clocked windows measure behaviour, never loading — a test that boots a real plugin -chain pays its first load at module top.** vitest clocks hooks (`hookTimeout`) and test -bodies (`testTimeout`) and nothing else; collection is clocked against nothing, so a -module-top `import ''` moves a cold transform OUT of every budget rather than -into a bigger one. `pnpm check:test-source-alias` gates this for the workspace deps a -package still resolves through `dist/`. - ### Running the dev server | Scenario | Command | Notes | diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 8f616ab5a4..fc6f1e6e91 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -205,8 +205,18 @@ // dynamic `import()` / `require()` **inside a function body** — an `it()`/`test()` // body, a `beforeAll`/`beforeEach`/`afterAll` hook, or any nested function — // with no module-scope load of the same specifier is a finding, reported against -// the test file. The convention is stated for authors in AGENTS.md § Build & Test -// ("clocked windows measure behaviour, never loading"). +// the test file. +// +// The convention, in one sentence: **clocked windows measure behaviour, never +// loading — a test that boots a real plugin chain pays its first load at module +// top.** It is stated here rather than in AGENTS.md § Build & Test, which is +// where #10126 asked for it: that file sits exactly on its shrink-only line +// ceiling (`scripts/pm/check-skill-line-ratchet.mjs`, 958, headroom 0), the +// sentence costs three lines, and its own section has two lines of lossless +// rewrap headroom. Raising a ceiling takes a maintainer ruling, and truncating +// the sentence to fit one is not this gate's call to make — so the doc placement +// is still open, and every author who trips this rule reads the sentence in the +// failure text either way. // // Boundaries, stated so they are not rediscovered as bugs: // @@ -1692,6 +1702,8 @@ function check(root, registry) { ' through `dist/`, so the first call transforms that dependency\'s whole module graph while a\n' + " `testTimeout` or `hookTimeout` is running. Measured on the incident this rule comes from: 3.1-3.6s\n" + ' idle on 4 vCPU, 20.26s on a starved core — past every clock, including a hypothetical 30s.\n' + + ' THE CONVENTION: clocked windows measure behaviour, never loading — a test that boots a real\n' + + ' plugin chain pays its first load at module top.\n' + ' Add a module-top side-effect import so the transform is paid during COLLECTION, which vitest\n' + ' clocks against nothing (it clocks hooks and test bodies only):\n' + ` import '${load.spec}';\n` + @@ -2562,6 +2574,16 @@ function selfTest() { ), 'the clocked-window finding printed no module-top import to add, or did not say where the cost moves TO', ); + // The convention sentence itself, carried in the text the author reads. + // #10126 asked for it in a doc; the doc that should hold it is on a + // headroom-0 line ceiling, so until that is ruled on, THIS is where an + // author meets it — which makes it a pin, not a comment. + expect( + clockedIn('packages/clocked-load/src/thing.test.ts').every((f) => + f.includes('clocked windows measure behaviour, never loading'), + ), + 'the clocked-window finding no longer states the convention it enforces — the sentence is back to being comment-only', + ); // BOTH PRESENT IS COMPLIANT: the module top already paid it, so the dynamic // call is a registry lookup. A reader that reports this has undone the fix. From 21a12d2707eaf9d14a517afbab75293355e9cf13 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 08:08:15 +0000 Subject: [PATCH 5/5] docs(devx): land the clocked-window convention in AGENTS.md (ceiling 958 -> 961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer 4 of #10126, unblocked by a maintainer ruling. AGENTS.md sat exactly on its shrink-only line ceiling with the mandated sentence costing three lines and its section carrying two lines of lossless rewrap headroom, so it could not be paid for in place. Ruling 2026-08-20, verbatim and untranslated: 「A — 抬上限到 961 (Recommended)」 The raise is exactly +3 and nothing more; headroom is 0 again by construction, so the next author needing a line is back to compressing. The sentence lands beside the sibling test-gate conventions in § Build & Test, unedited, and the gate header's note about why it was NOT there is corrected. The ratchet's self-test pins coverage and shape, never a ceiling's numeric value, so it needed no extension; re-run green at 19 cases. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01URCaKuNTuK3BKJvwqM74QU --- AGENTS.md | 3 +++ scripts/check-test-source-alias.mjs | 18 ++++++++++-------- scripts/pm/check-skill-line-ratchet.mjs | 11 ++++++++++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 058ee5a201..e91af9b31b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,9 @@ judged on the **shallowest** point a path reaches, not where it ends — a liter climbs past the package root and then descends into a sibling ends at a positive depth while addressing another package entirely. +Clocked windows measure behaviour, never loading — a test that boots a real plugin chain +pays its first load at module top; `pnpm check:test-source-alias` gates it. + ### Running the dev server | Scenario | Command | Notes | diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index fc6f1e6e91..2fd610d7de 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -209,14 +209,16 @@ // // The convention, in one sentence: **clocked windows measure behaviour, never // loading — a test that boots a real plugin chain pays its first load at module -// top.** It is stated here rather than in AGENTS.md § Build & Test, which is -// where #10126 asked for it: that file sits exactly on its shrink-only line -// ceiling (`scripts/pm/check-skill-line-ratchet.mjs`, 958, headroom 0), the -// sentence costs three lines, and its own section has two lines of lossless -// rewrap headroom. Raising a ceiling takes a maintainer ruling, and truncating -// the sentence to fit one is not this gate's call to make — so the doc placement -// is still open, and every author who trips this rule reads the sentence in the -// failure text either way. +// top.** It is stated for authors in AGENTS.md § Build & Test, beside the sibling +// test-gate conventions, and repeated in this gate's own failure text — where it +// is pinned by a self-test assertion, so it cannot decay back into a comment that +// no author tripping the rule ever reads. +// +// Getting it into that file took the one raise `scripts/pm/check-skill-line- +// ratchet.mjs` has granted: AGENTS.md sat exactly on its shrink-only ceiling of +// 958 with the sentence costing three lines and its section carrying two lines of +// lossless rewrap headroom, so it could not be paid for in place. Maintainer +// ruling 2026-08-20, verbatim and untranslated: 「A — 抬上限到 961 (Recommended)」. // // Boundaries, stated so they are not rediscovered as bugs: // diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index fd3a4f7de3..551c3abdbb 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -111,7 +111,16 @@ export const CEILINGS = new Map([ // file in the repo and had no ceiling — the hole the oversized 39-line // read-layer clause (compacted by #9715) entered through. Set at its line // count on `origin/main` after #9715 landed (headroom 0, same convention). - ['AGENTS.md', 958], + // + // 958 → 961 (#10126): the ONE raise this map has taken, by the header's own + // escape hatch. The queue-incident remediation adds a three-line convention to + // § Build & Test — clocked windows measure behaviour, never loading — and the + // section it joins had two lines of lossless rewrap headroom against a + // three-line cost, so it could not be paid for in place. Maintainer ruling + // 2026-08-20, verbatim and untranslated: 「A — 抬上限到 961 (Recommended)」 + // (issue #10126, comment 5353111732). Headroom is 0 again by construction, and + // the next author needing a line is back to compressing. + ['AGENTS.md', 961], ]); /**