test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(config): state the unit project's real isolate: false invariant, and enforce it - #7309

Merged
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise
Sep 2, 2026
Merged

test(config): state the unit project's real isolate: false invariant, and enforce it#7309
yinlianghui merged 6 commits into
mainfrom
claude/issue-7134-unit-isolate-premise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#7134

The unit project's isolate: false was justified by a premise that is false: "node-env pure logic with no ComponentRegistry or DOM state to leak across files". This PR replaces that justification with the constraint that actually has to hold, and ships the gate that enforces it.

Both populations, re-derived on this branch

Derived from vitest.config.mts itself — the project's own include list minus its own domTsTests — so the population is the project, not a hand-copied guess. It reconciles exactly with what Vitest collects: 811 files, against Test Files 811 passed (811) from the full run below.

WRITERS — measured by EXECUTION, not by grep. Fresh module graph (vi.resetModules()), import @object-ui/core, snapshot ComponentRegistry.getAllTypes(), import the project's import specifiers, diff. Over ec0a7b846 the population's 551 distinct resolved modules register 505 keys into the shared singleton, across 28 namespaces (ui 133, bare 215, field 47, view 14, record 13, element 10, ...).

Grep cannot answer this half, and that is why the gate executes. The live field path registers from dataregisterAllFields() walks a map — so field:multiselect exists at runtime and appears in noregister('field:multiselect') call site anywhere in the repo. A static reader would report "nothing registers it" and the gate would be green for the empty reason.

The one thing execution cannot see is a registration made in a test BODY (it happens when the test runs, not when it is imported). Those are read off the TypeScript AST: 1 file, packages/runner/src/plugin-integration.test.ts (test-kanban-manual, test-bar-chart-manual). The AST matters here — a raw-text scan reported 12 such files, 11 of them registrations written inside fixture template literals, which are source to a regex and not registrations.

READERS — files asserting a key ABSENT. Also off the AST: an expect(...) whose argument reads the singleton, followed by a matcher that asserts the subject is not there (toBeUndefined / toBeFalsy / toBeNull, and .not.toBeDefined / .not.toBeTruthy), with template keys resolved through local string consts.

ReaderSiteKey
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:81toBeUndefinedfield:capability-multiselect
packages/fields/src/__tests__/capability-multiselect-retired.test.ts:82toBeUndefinedcapability-multiselect
packages/app-shell/src/views/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.ts:222toBeFalsyUNRESOLVED — ComponentRegistry.get(type), a key set derived at runtime from PALETTE_EXCLUSIONS

2 readers, 2 statically resolved keys, 1 unresolvable site. The unresolvable one is reported and PINNED rather than dropped: a new one fails the gate, so a place it goes blind is a decision instead of a silent shrink.

The seat's grep found 7 candidate files. Five are not readers of this singleton: app-generator.test.ts mentions it only in a comment, component-deprecation-declaration.test.ts and report-bare-key-ownership.test.ts assert over a LOCAL Registry instance, plugin-editor/index.test.ts's absence matchers are about readOnly defaults, and timeline-bare-key-ownership.test.ts asserts a meta FLAG (getMeta(...)?.skipFallback), not key absence.

Latent, not live — confirmed. Neither field:capability-multiselect nor capability-multiselect is among the 505 registered keys, and neither is written by the one in-body writer. The absence assertion is also non-vacuous under the shared graph: the live path registers 47field:* keys, including field:multiselect and the field:owner tombstone, with the retired one absent.

One correction to the card

The card reads the hazard as a silent green ("if some other file registers that key first, this one goes green while proving nothing"). Measured, toBeUndefined() goes red when the key is registered — the ablation below shows exactly that. The defect is not a direction, it is ORDER DEPENDENCE: which of the two files the worker ran first decides the outcome, and neither outcome is information about the code under test. The invariant, and the fix, are unchanged.

The gate: chosen shape, and the one rejected

Chosen (A): a collision gate.scripts/__tests__/unit-registry-absence-collision.test.ts computes both populations on every run and fails when a key asserted absent by one file is registered by another, naming both files and the key. It satisfies the dispatch's criterion (i) literally: it goes red on a planted collision.

Rejected (B): an isolation-proof pattern plus a style gate (every absence assertion resets the module graph and re-imports only its own subject; a gate fails when one does not). Rejected for three measured reasons, not for taste:

  1. It does not go red on a planted collision — it is a gate about a PATTERN, so it cannot satisfy criterion (i). It answers "is this file written the approved way", not "is the invariant true".
  2. Its cost is not the one that matters. Static, so ~0 s — but it buys that by requiring a rewrite of the two readers, and one of them (exclusion-reason-truthfulness.test.ts) deliberately imports six renderer leaves instead of the package barrel because the barrel costs 6105 ms against 553 ms (its own measurement, finding(app-shell): exclusion-reason-truthfulness's import set excludes app-shell, so a false "no renderer" on a shell singleton passes green #7117). Making its dynamic-key absence loop hermetic means re-importing ten specifiers after each reset.
  3. A style gate is satisfiable vacuously: the pattern present but pointed at the wrong module still passes.

And (A) turned out to be nearly free, which is the measurement that decided it. Its cold standalone cost is ~43 s, but the modules it loads are overwhelmingly the ones the project it measures already loads into the same worker under isolate: false. Full-project wall clock: 173.52 s before, 182.16 s after — +8.6 s, about +5% (shared-box seconds; four sibling agents build in this container, so this is an upper-ish bound).

Moving registry-touching files out of the shared-graph project — the dispatch's fallback — was not needed and is not done: the gate is non-vacuous, so the 3.2x is kept on every file.

Why the gate does not pollute the project it measures

It runs inside the very project it is about, so its own imports would otherwise be the single largest registry write in it. vi.resetModules() before the measurement gives it a private module graph — a fresh @object-ui/core, therefore a fresh singleton, not the one its worker's other files hold — and this is asserted, not assumed (startedEmpty). A second reset afterwards drops that graph so files running later re-import their own. Evidence it works: the full project is green with the gate in it, all 811 files.

Attribution runs only on the red path, and only over the specifiers some other file imports — a file that registers what it then asserts absent is hermetic, not a collision, and reporting one would be a false red.

The corrected comment

vitest.config.mts, the unit project. The perf measurement is kept (it is still true); the false premise is replaced by the invariant, the order-dependence is named, and the justification points at where the constraint is enforced:

// Share a module graph per worker instead of re-executing it per
// file. Measured 3.2x faster (38s -> 12s for the project) with zero
// failures, holding green across repeated and shuffled runs.
//
// What that buys is paid for by an INVARIANT, not by a property of
// the files (objectui#7134). The premise written here used to be
// "node-env pure logic with no ComponentRegistry or DOM state to leak
// across files". It was false in both directions and had been for
// some time: this project holds files whose import closure REGISTERS
// into the `ComponentRegistry` singleton - measured over `ec0a7b846`,
// its 811 files import 551 distinct modules whose closures register
// 505 keys into it - and files that assert a key is ABSENT from it. A
// shared graph makes each visible to the other, so the constraint that
// actually has to hold is:
//
// a key one file asserts ABSENT from the ComponentRegistry must
// be registered by no other file in this project.
//
// Nothing about a breach of it fails safe. The outcome is ORDER
// dependent - whether the absence assertion runs before or after the
// writer in its worker decides it - so a collision surfaces as a
// failure in a file that did nothing wrong, in some shards and not
// others, and says nothing about the code under test.
//
// So it is ENFORCED rather than left written down here, because this
// comment is the only thing a future author consults before adding a
// registering import to this project, and it had already gone false
// without anyone noticing:
//
// scripts/__tests__/unit-registry-absence-collision.test.ts
//
// [...] It EXECUTES the closures in a fresh module graph to learn what
// they register, because the writers' keys cannot be read off the
// source [...]

isolate itself is untouched. The gate asserts that this comment still names it, so the justification and its enforcement cannot drift apart silently.

Ablation

Every mutation is proven ON DISK by marker count andgit hash-object before the run; every restore is proven by blob hash equal to the HEAD blob, marker count back to 0, and an empty git diff HEAD — never by an exit code. Each leg carries a trap ... EXIT INT TERM restoring by ABSOLUTE path.

#PlantedProof it landedGateWhat it named
A1'capability-multiselect' added to RETIRED_FIELD_TYPES in packages/core/src/utils/retired-field-types.ts — a DATA-driven registration, invisible to any static readermarker 0 to 1; blob bb80173b to e9d313deRED, exit 1 (1 failed / 14 passed)key "field:capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/app-shell/src/__tests__/spec-symbol-parity.test.ts (via packages/app-shell/src/views/ScreenView.tsx)
A2ComponentRegistry.register('capability-multiselect', ...) in the body of packages/runner/src/plugin-integration.test.tsmarker 0 to 1; blob 3273700b to b70843a5RED, exit 1 (1 failed / 14 passed)key "capability-multiselect", asserted absent by capability-multiselect-retired.test.ts, registered by packages/runner/src/plugin-integration.test.ts
A3non-vacuity control: collectFiles forced to return [] in scripts/unit-registry-collision.mjsmarker 0 to 1; blob 0dbb587b to ed0c4f98RED, exit 1 (2 failed / 13 passed)a population COLLAPSED - this run proves nothing: with all five census counts at 0

Restores: A1 bb80173b = HEAD blob; A2 3273700b = HEAD blob; A3 0dbb587b = HEAD blob, git diff HEAD0 bytes, git status --porcelain empty.

A1 is the leg that proves the EXECUTION half is load-bearing: the planted key is registered from a frozen data table, so a grep-based gate would have stayed green on it. A3 is the gate's own control — an empty population must FAIL, not pass — and the file also carries the pure-function form of it (checkFloors({}) reports NOT MEASURED, never zero).

The gate additionally carries fixture controls for its readers: a registration written inside a template literal is NOT counted as a registration, .not.toBeDefined() counts as absence while plain .toBeDefined() does not, a dynamic registration key is reported rather than dropped, and register(type, C, { namespace: n }) yields both n:type and the bare fallback (only n:type under skipFallback), matching Registry.register.

One honest property of the red message: attribution bisects for a specifier whose closure registers the key, so in A1 it named ScreenView.tsx rather than the more obvious @object-ui/fields. Both statements are true; the message names a file and a module an author can act on, which is what it is for.

Verification

Full unit project, from the repo root, both runs under the shared verify lock (os-verify-lock, VERDICT command-exit 0 for each):

The controlled pair — same tree, with and without this gate:

Summary lineDuration
before, eb33a8d4c (no gate)Test Files 810 passed (810) / Tests 12610 passed | 9 skipped (12619)173.52 s
after, e41a20cc8 (gate in)Test Files 811 passed (811) / Tests 12625 passed | 9 skipped (12634)182.16 s

The delta is +1 file and +15 tests, which is exactly this gate, and +8.6 s of wall clock.

And the final head, after merging origin/main at 9bf0abfec — not a controlled comparison, since the sibling work merged in brings its own tests:

Summary lineDuration
final, 6044abb9fTest Files 811 passed (811) / Tests 12652 passed | 9 skipped (12661)177.99 s

Gates, re-run on the final head 6044abb9f; every exit code captured by redirect-then-$?, never through a pipe:

GateVerdict
pnpm exec vitest run --project unit --maxWorkers=2 (under the lock, at 6044abb9f)VERDICT command-exit 0Test Files 811 passed (811)
pnpm exec vitest run --project unit ... unit-registry-absence-collision.test.tsexit 0 — Test Files 1 passed (1) / Tests 15 passed (15)
pnpm type-check:scriptsexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm check:entry-guardexit 0 — "59 scripts/ file(s) — no entry guard outside the baseline"
node scripts/check-changeset-presence.mjsexit 0 — "No source or published contract of a released package changed in this range, so no changeset is owed."
eslint --no-inline-config --format json on the changed filesexit 0 — 0 errors

type-check:scripts was red on the first attempt (TS2307: Cannot find module '@object-ui/core'tsconfig.scripts.json has no path mapping into the workspace packages) and is fixed in the gate rather than in the tsconfig: @object-ui/core is imported by computed specifier, like every other import in that file. Changing the scripts tsconfig to type one line would have put every scripts/ file on a different module resolution than the one CI type-checks them with.

Declared narrowing. The repo-wide pnpm lint is CI's run; the lint here is narrowed to the diff, and the narrowing is measured rather than asserted: (1) the population comes from ESLint's own configuration, which reports vitest.config.mts as "File ignored because no matching configuration was supplied" — it is outside the configured population, not skipped by me; (2) the count comes from --format json: 3 paths requested, 2 linted, 0 errors, 1 warning (that ignore notice); (3) eslint.config.js contains 0 occurrences of projectService / parserOptions / project: — type-aware linting is not enabled, so this diff cannot move the verdict on any untouched file.

Changeset: none owed, and the gate says so in its own words (quoted above). No package's published source or contract changed — the diff is one config comment plus two new files under scripts/. No skip-changeset label: in this repo that label is inert.

Serial constraint (#7291 / #7183)

origin/main is merged (9bf0abfec, no conflict; the final head is 6044abb9f). PR #7291 appends a dist project and two constants to the same file and had not landed within the dispatch's 20-minute poll budget (14 polls of git ls-remote origin refs/heads/main from 05:03Z to 05:22Z; main moved once, to #7294, which is docs-only). It is still an open DRAFT, mergeable_state: behind. Its hunks and this one are disjoint (its constants sit above defineConfig, its project appends to the end of projects; this change is inside the unit project block), and that was verified rather than assumed: git merge-tree --write-tree against its head 857c8afc5 produced a clean tree, exit 0, and this gate's config reader parses that merged config correctly (isolateFalse: true, the same four include globs, the same 18 domTsTests, and both changes present). No hunk of another PR was resolved by hand.

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b


Generated by Claude Code

…ant, and enforce it
`vitest.config.mts` justified the `unit` project's `isolate: false` with "node-env
pure logic with no ComponentRegistry or DOM state to leak across files". The
premise was false in both directions: the project holds files whose import
closure registers into the `ComponentRegistry` singleton AND files that assert a
key is ABSENT from it, and under a shared module graph each is visible to the
other.
Measured on eb33a8d: the project's 810 files import 600 distinct specifiers
whose closures register 502 keys into the singleton. No registered key collides
with an asserted-absent key today, so the defect is latent — but the outcome of
a collision is order-dependent, so it would arrive as a failure in a file that
did nothing wrong, in some shards and not others.
The comment now states the constraint that actually has to hold and names where
it is enforced. The enforcement is a new gate that derives both populations from
the config's own `include`/`domTsTests` on every run, EXECUTES the import
closures in a fresh module graph to learn what they register (the live field
path registers from data, so the writers' keys appear in no `register(...)` call
site anywhere), and fails naming both files and the key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…es import
A file that registers what it then asserts absent is hermetic, not a collision.
Bisecting the whole union let the reader's own closure answer for the key, which
would have reported a false red (and, in the ablation, hidden a true one behind
the reader's own import of the same module).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
… gate
`tsconfig.scripts.json` has no path mapping into the workspace packages, so a
static specifier failed `type-check:scripts` (TS2307). Every other import in
the file already goes through a computed id.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@yinlianghui
yinlianghui marked this pull request as ready for review September 2, 2026 05:43
@yinlianghui
yinlianghui added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit d717e8bSep 2, 2026
29 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-7134-unit-isolate-premise branch September 2, 2026 05:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude