Skip to content

Commit 92ca4cd

Browse files
test(spec): three src/ test-budget rows trimmed, two at a one-program floor left listed (#23819)
Part of #23180 Clause-②: no The five `packages/spec/src/` rows of the test-budget census, measured first. Three rows are trimmed in the test, with every `expect` line unchanged. Two rows are left unchanged: each one is already at its floor of one TypeScript program, which is neither of ruling record `6181973363`'s two classes, so they get no declaration (see the last section). This PR counts no row off the card: the next refresh list decides each one, per the card's pin. ## Readings, per file CI readings are the vitest module line (seconds) in the `Test Core (1/6)` shard of the hourly runs that executed `@objectstack/spec` (no `cache hit` for it in the log): `38833767749` (01:11, head `4fe8b3def0`), `38838260718` (02:01, head `ec44cf60bf`) and `38848429828` (04:01, head `9b799e6fb5`). Run `38843417737` (03:01) replayed spec from the cache and is not used. The job-log blob host is unreachable from this container, so only each log's last 5000 lines were readable; that is why 04:01 has no reading for `view` and `app`. Local readings are the sum of the file's case durations (ms), from vitest's JSON reporter. Each one is a five-file run with `--maxWorkers=2` under `os-verify-lock.sh` (shared-box ms), interleaved between a base worktree at `f846cc2020` and this branch. Three runs on each side. | file (`src/`) | census | CI 01:11 / 02:01 / 04:01 | local before (ms) | local after (ms) | disposition | |:--|--:|:--|:--|:--|:--| | `contracts/scoped-context.test.ts` | 91.1 | 14.7 / 14.0 / 16.7 | 2296 / 2050 / 2251 | file unchanged | unchanged: at its floor, no class | | `integration/connector-author-shape.test.ts` | 49.5 | 16.8 / 15.9 / 18.1 | 2723 / 2323 / 2305 | file unchanged | unchanged: at its floor, no class | | `kernel/metadata-type-registry-allow-org-override-absence.test.ts` | 11.4 | 8.8 / 11.1 / 10.3 | 1308 / 1244 / 1285 (alone) | 817 / 805 / 805 (alone) | trimmed | | `ui/view.test.ts` | 10.9 | 9.1 / 10.7 / not readable | 3574 / 3226 / 3193 | 223 / 194 / 187 | trimmed | | `ui/app.test.ts` | 10.8 | 10.2 / 10.8 / not readable | 1772 / 1723 / 1716 | 1244 / 1257 / 1172 | trimmed | The allow-org row was re-measured alone, three runs on each side, after its second commit; the five-file runs before that commit read 1570 / 1359 / 1311 against 1235 / 1118 / 1102. If each file keeps its own CI-to-local ratio (slowest CI reading over the slowest local before-reading), the after-readings project to about 7.0 s (allow-org), 0.7 s (view) and 7.7 s (app) in the battery. That is NOT MEASURED on CI; this PR's own `Test Core (1/6)` run is the first post-trim reading. The census premise still holds for all five: each row read over 10 s in at least one of the three executed runs. ## Trimmed: what each one waited on - **`ui/view.test.ts`**: two cases took 1.1 to 1.6 s each, and all of it was `await import('../shared/index')`, `'../ui/index'` and `'../api/index'` inside the case, which loads three published entries. The three loads are now static imports at module top, which the census line does not count (AGENTS.md: "Clocked windows measure behaviour, never loading"). The four cases read the same module namespaces as before, so identity (`toBe`) assertions compare the same objects. Honest accounting: the load cost is moved, not removed. The five-file run's import phase rose from 2.4 to 2.9 s to 5.4 to 5.9 s, and its total wall went from 7.7 to 7.9 s to 7.2 to 7.7 s. - **`ui/app.test.ts`**: the compiler-API case loaded `typescript` (about 300 ms) and `node:path` / `node:url` with `await import()` inside the case. They are now module-top imports. The rest of the case is one `ts.createProgram` over `app.zod.ts` (about 330 ms) and resolving `NavigationArea`'s members (about 640 ms). Neither the compiler options nor the lib set changes the latter, so that stays as is. - **`kernel/metadata-type-registry-allow-org-override-absence.test.ts`**: the walk reads 8,792 files (about 290 ms, the declared radius) and parsed every file mentioning either name. Two costs were removable: 1. A file holding both names was parsed twice. The two largest files in the radius are among them: `spec/src/migrations/registry.ts` (2.6 MB) and `metadata-protocol/src/protocol.ts` (1.8 MB). Each file is now parsed once, and both names are read off that one tree (`codeReferencesByName`; the matcher's predicate is unchanged, and `codeReferences` is the one-name view of it). 2. The new-name readers are asserted only by `arrayContaining` of three named files, so a reader anywhere else cannot change that verdict. Only those three files are now asked the new name (`NEW_READERS`, the same list the assertion uses). The old name is still read off every file in the radius that mentions it. Parsed bytes went from 14.6 MB to about 6 MB. ## Ablations: the changed allow-org paths still go red Run through `scripts/ablation-replace.mjs` in wrap mode on `packages/metadata-protocol/src/sys-metadata-repository.ts`, which holds both names and is one of the three `NEW_READERS`. The expected direction was red every time, and red was observed every time. Each restore was proven by the tool: blob `5630b9de7b42` equals HEAD, and `git diff HEAD` is empty. No `dist/` is on the path, because the test reads source text. - **A1**, `.filter((e) => e.allowEnvironmentOverlay)` changed to `.filter((e) => e.allowEnvironmentOverlay && !e.allowOrgOverride)` (blob to `b96bfd450123`): 1 of 2 failed, on the offenders assertion, naming `packages/metadata-protocol/src/sys-metadata-repository.ts:406:52`. This is the old-name leg on a file parsed once for both names. - **A2**, the same anchor changed to `.filter((e) => e.overlayGone)` (blob to `6132dc4d627f`): 1 of 2 failed, on `expect([...newReaders]).toEqual(expect.arrayContaining(NEW_READERS))`. This is the new-name leg. The `view` and `app` changes move import statements and change no logic, so they have no ablation. ## Verification - The five files pass at ``2b6764e25d``: 586 tests across the five. - `pnpm --filter @objectstack/spec typecheck`: VERDICT command-exit 0. It includes `check:test-typecheck`, which compiles `src/**/*.test.ts` through `tsconfig.test.json` and held its debt ledger. - `eslint --no-inline-config` on the three changed files: exit 0. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `2b6764e25d` derived 76 commands from the 3 changed paths (merge base `f846cc202`). All 76 ran here, each exit captured before any pipe. 74 exited 0. Two of them (`check-plugin-teardown-shape --self-test` and `lint`'s `check:doc-formula-expressions`) first exited 3 (PREREQUISITE NOT MET: a shallow clone, and an unbuilt `@objectstack/formula`); they exited 0 after `git fetch --unshallow` and a build of `@objectstack/lint`'s dependency closure. `--ran` reports "76 derived famil(ies) accounted for — 74 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". The two are `check:dual-build-cjs-loads` and `check:lean-entry-closure`, which need every package's `dist/` built. Neither reads a test file. CI runs both. - No publish: `npm pack --dry-run` of `@objectstack/spec` 18.0.0-next.2 lists 2094 files, 0 of them `*.test.ts`. The positive controls `dist/index.js` and `src/ui/view.zod.ts` are present. So `skip-changeset`. - Local test scope: only the five named files ran here. The diff is test files only, and no other test imports them. CI's `Test Core` runs both spec projects. ## Not trimmed: `scoped-context` and `connector-author-shape` are at their floor PR #23547 already folded each file's probes into one `ts.createProgram`. Timing that program per probe (an instrumented local copy, not committed) shows where the time goes. The first probe whose diagnostics are asked pays the whole type-graph resolution: 740 ms (`jsdoc-usage`) and 860 ms (`doc-l3-connector-0`). Every other probe takes 0 to 7 ms, except the second connector example at 117 ms. Program construction parses 586 and 614 files (the spec entry barrels a hook or connector author imports, `lib.dom.d.ts` and `@types/node`) in about 1.45 s each under two workers. Changing `lib` or `types` did not change the file count, because the spec source itself pulls those in. So each file's floor is one compile of the spec's type graph. That is neither "spawns the real producer once per single-cause fixture" nor "whole-package import", so these files carry no `// test-budget:` line, and they stay listed. Whether a third class is wanted is a question for the seat (the report's `open_questions`); it is not decided here. ## Acceptance notes - The `view` trim moves load time from the clocked window to the import phase, as the repository's own rule for clocked windows says it should. It does not make the file cheaper end to end. The census line counts only the clocked window. - The vitest module line excludes import time, so the census and these readings measure the clocked window only. This is the same caveat PR #23752 records. --- _Generated by [Claude Code](https://claude.ai/code/session_01VTJ1Sey3QK5wZbayQXfFfH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f606ba2 commit 92ca4cd

3 files changed

Lines changed: 53 additions & 36 deletions

File tree

‎packages/spec/src/kernel/metadata-type-registry-allow-org-override-absence.test.ts‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ describe('tree-scoped absence: no TypeScript source in the declared radius reads
4747
const IN_RADIUS = (rel: string) =>
4848
(rel.startsWith('packages/') && rel.endsWith('.ts'))
4949
|| /^examples\/[^/]+\/src\/.+\.ts$/.test(rel);
50+
/**
51+
* Hand-renamed readers the walk must see as code references under the NEW
52+
* name (anti-vacuity, asserted below). Only these files are asked the new
53+
* name: the assertion is containment of exactly this list, so no other file
54+
* can change its verdict, and parsing every other file that mentions the new
55+
* name was about a third of the walk's parsing.
56+
*/
57+
const NEW_READERS = [
58+
'packages/metadata-protocol/src/protocol.ts',
59+
'packages/metadata-protocol/src/sys-metadata-repository.ts',
60+
'packages/spec/src/kernel/metadata-plugin.zod.ts',
61+
];
5062
/** Build, SCM and cache state — not authored sources. */
5163
const SKIPPED_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', '.cache', '.objectstack', 'coverage', '.next', '.source']);
5264

@@ -58,28 +70,36 @@ describe('tree-scoped absence: no TypeScript source in the declared radius reads
5870
&& ts.isIdentifier(node.parent.initializer.expression)
5971
&& node.parent.initializer.expression.text === 'retiredKey';
6072

61-
/** Every CODE reference to `name` in `text`, as `line:col` — identifiers and quoted keys, never comments or prose strings. */
62-
const codeReferences = (fileName: string, text: string, name: string): string[] => {
73+
/**
74+
* Every CODE reference to each of `names` in `text`, as `line:col` per name —
75+
* identifiers and quoted keys, never comments or prose strings. One parse
76+
* serves every name: the walk below can ask two names of the same file, and
77+
* the files holding both are the largest in the radius (megabytes each).
78+
*/
79+
const codeReferencesByName = (fileName: string, text: string, names: readonly string[]): Map<string, string[]> => {
6380
const sf = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
64-
const found: string[] = [];
65-
const at = (node: ts.Node) => {
81+
const found = new Map(names.map((name) => [name, [] as string[]]));
82+
const at = (name: string, node: ts.Node) => {
6683
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
67-
found.push(`${line + 1}:${character + 1}`);
84+
found.get(name)!.push(`${line + 1}:${character + 1}`);
6885
};
6986
const visit = (node: ts.Node) => {
70-
if (ts.isIdentifier(node) && node.text === name && !isTombstoneDeclaration(node)) at(node);
71-
else if (ts.isStringLiteral(node) && node.text === name && node.parent) {
87+
if (ts.isIdentifier(node) && found.has(node.text) && !isTombstoneDeclaration(node)) at(node.text, node);
88+
else if (ts.isStringLiteral(node) && found.has(node.text) && node.parent) {
7289
const p = node.parent;
7390
// A quoted key or an element access is a reference; any other string is prose.
7491
if ((ts.isPropertyAssignment(p) && p.name === node)
7592
|| (ts.isElementAccessExpression(p) && p.argumentExpression === node)
76-
|| (ts.isPropertySignature(p) && p.name === node)) at(node);
93+
|| (ts.isPropertySignature(p) && p.name === node)) at(node.text, node);
7794
}
7895
ts.forEachChild(node, visit);
7996
};
8097
visit(sf);
8198
return found;
8299
};
100+
/** Every CODE reference to `name` in `text`, as `line:col`. */
101+
const codeReferences = (fileName: string, text: string, name: string): string[] =>
102+
codeReferencesByName(fileName, text, [name]).get(name)!;
83103

84104
it('the matcher sees a code reference and ignores a mention (anti-vacuity)', () => {
85105
const refs = (src: string) => codeReferences('probe.ts', src, OLD);
@@ -122,9 +142,11 @@ describe('tree-scoped absence: no TypeScript source in the declared radius reads
122142
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') continue;
123143
throw err;
124144
}
125-
if (text.includes(NEW) && codeReferences(rel, text, NEW).length > 0) newReaders.add(rel);
126-
if (!text.includes(OLD)) continue;
127-
for (const where of codeReferences(rel, text, OLD)) {
145+
const names = [OLD, ...(NEW_READERS.includes(rel) ? [NEW] : [])].filter((name) => text.includes(name));
146+
if (names.length === 0) continue;
147+
const refs = codeReferencesByName(rel, text, names);
148+
if ((refs.get(NEW)?.length ?? 0) > 0) newReaders.add(rel);
149+
for (const where of refs.get(OLD) ?? []) {
128150
(rel === REFUSAL_PIN ? refusalPinReferences : offenders).push(`${rel}:${where}`);
129151
}
130152
}
@@ -140,11 +162,7 @@ describe('tree-scoped absence: no TypeScript source in the declared radius reads
140162
// Live positive control: the refusal pin's deliberate `allowOrgOverride: true`
141163
// and `: false` are real code references, and the walk found them.
142164
expect(refusalPinReferences.length).toBeGreaterThanOrEqual(2);
143-
expect([...newReaders]).toEqual(expect.arrayContaining([
144-
'packages/metadata-protocol/src/protocol.ts',
145-
'packages/metadata-protocol/src/sys-metadata-repository.ts',
146-
'packages/spec/src/kernel/metadata-plugin.zod.ts',
147-
]));
165+
expect([...newReaders]).toEqual(expect.arrayContaining(NEW_READERS));
148166
expect(
149167
offenders,
150168
'a code reference to `allowOrgOverride` reads `undefined` against the tombstone and closes every type — read `allowEnvironmentOverlay`',

‎packages/spec/src/ui/app.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { describe, it, expect } from 'vitest';
2+
// Loaded here, at module top, not inside the compiler-API case below: a
3+
// clocked window measures behaviour, never loading, and `typescript` alone is
4+
// about 300 ms of module load.
5+
import ts from 'typescript';
6+
import { dirname, resolve } from 'node:path';
7+
import { fileURLToPath } from 'node:url';
28
import {
39
AppSchema,
410
AppBrandingSchema,
@@ -1587,11 +1593,7 @@ describe('unknown keys are rejected, not stripped', () => {
15871593
// compiler API and reads its members — the same symbol-identity measurement
15881594
// `check:dual-source-exports` makes, over `src/`, so it runs in `pnpm test`
15891595
// without a build.
1590-
it('the retired gates are gone from the TYPE, not only from the parse (compiler API)', async () => {
1591-
const ts = (await import('typescript')).default;
1592-
const { resolve, dirname } = await import('node:path');
1593-
const { fileURLToPath } = await import('node:url');
1594-
1596+
it('the retired gates are gone from the TYPE, not only from the parse (compiler API)', () => {
15951597
const entry = resolve(dirname(fileURLToPath(import.meta.url)), 'app.zod.ts');
15961598
const program = ts.createProgram([entry], {
15971599
module: ts.ModuleKind.ESNext,

‎packages/spec/src/ui/view.test.ts‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ import {
5454
originOf,
5555
} from '../../scripts/lib/export-origins-testkit';
5656

57+
// The three published entries the dual-source pins below compare. Loaded
58+
// here, at module top, and not by an `await import()` inside a case: a clocked
59+
// window measures behaviour, never loading, and these three loads were most
60+
// of this file's test time.
61+
import * as sharedEntry from '../shared/index';
62+
import * as uiEntry from '../ui/index';
63+
import * as apiEntry from '../api/index';
64+
5765
// [#16577] The door that carries the `type: '<layout>'` axis these view
5866
// schemas deliberately leave open — imported so the two-door split is asserted
5967
// from the side that can see both.
@@ -4050,20 +4058,14 @@ describe('HttpMethodSubsetSchema/HttpRequestSchema backward compat', () => {
40504058
// re-exports `./shared`'s const — so the only thing that ever split was the type
40514059
// alias, which is exactly the part runtime cannot see. Hence two layers.
40524060
describe('HttpRequest is single-source across ./shared and ./ui', () => {
4053-
it('both entry points expose the very same schema declaration at runtime', async () => {
4054-
const sharedEntry = await import('../shared/index');
4055-
const uiEntry = await import('../ui/index');
4056-
4061+
it('both entry points expose the very same schema declaration at runtime', () => {
40574062
// Identity, not shape: `lazySchema` returns one Proxy per declaration site,
40584063
// so two declarations could never be `toBe`-equal however alike they look.
40594064
// A re-introduced local `HttpRequestSchema` in view.zod.ts fails here.
40604065
expect(uiEntry.HttpRequestSchema).toBe(sharedEntry.HttpRequestSchema);
40614066
});
40624067

4063-
it('the shared declaration validates identically on both entries', async () => {
4064-
const sharedEntry = await import('../shared/index');
4065-
const uiEntry = await import('../ui/index');
4066-
4068+
it('the shared declaration validates identically on both entries', () => {
40674069
for (const [entry, schema] of [
40684070
['./shared', sharedEntry.HttpRequestSchema],
40694071
['./ui', uiEntry.HttpRequestSchema],
@@ -4154,10 +4156,7 @@ describe('`HttpMethod` is not exported from ./ui', () => {
41544156
expect(originFileOf('./shared', 'HttpMethod')).toBe('src/shared/http.zod.ts');
41554157
});
41564158

4157-
it('keeps the two value ranges distinct: 7 for `HttpMethod`, 5 for `HttpMethodSubsetSchema`', async () => {
4158-
const sharedEntry = await import('../shared/index');
4159-
const apiEntry = await import('../api/index');
4160-
4159+
it('keeps the two value ranges distinct: 7 for `HttpMethod`, 5 for `HttpMethodSubsetSchema`', () => {
41614160
// `./api` re-exports the const, so this is one object seen twice.
41624161
expect(apiEntry.HttpMethod).toBe(sharedEntry.HttpMethod);
41634162

@@ -4173,9 +4172,7 @@ describe('`HttpMethod` is not exported from ./ui', () => {
41734172
expect(sharedEntry.HttpMethodSubsetSchema.options).not.toContain('HEAD');
41744173
});
41754174

4176-
it('rejects `HEAD` at the parse layer — the runtime the ./ui type must not out-promise', async () => {
4177-
const uiEntry = await import('../ui/index');
4178-
4175+
it('rejects `HEAD` at the parse layer — the runtime the ./ui type must not out-promise', () => {
41794176
expect(() => uiEntry.HttpMethodSubsetSchema.parse('HEAD')).toThrow();
41804177
expect(() => uiEntry.HttpMethodSubsetSchema.parse('OPTIONS')).toThrow();
41814178
expect(() => uiEntry.HttpRequestSchema.parse({ url: '/api/data', method: 'HEAD' })).toThrow();

0 commit comments

Comments
 (0)