Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
test(tooling): ratchet the vi.mock factories that must inherit the real export surface by os-sam · Pull Request #6894 · objectstack-ai/objectui · GitHub
Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(tooling): ratchet the vi.mock factories that must inherit the real export surface by os-sam · Pull Request #6894 · objectstack-ai/objectui · GitHub
Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

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

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' test(tooling): ratchet the vi.mock factories that must inherit the real export surface by os-sam · Pull Request #6894 · objectstack-ai/objectui · GitHub
Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(tooling): ratchet the vi.mock factories that must inherit the real export surface by os-sam · Pull Request #6894 · objectstack-ai/objectui · GitHub
Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(tooling): ratchet the vi.mock factories that must inherit the real export surface by os-sam · Pull Request #6894 · objectstack-ai/objectui · GitHub
Skip to content

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

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

test(tooling): ratchet the vi.mock factories that must inherit the real export surface - #6894

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard
Aug 30, 2026
Merged

test(tooling): ratchet the vi.mock factories that must inherit the real export surface#6894
os-sam merged 3 commits into
mainfrom
claude/issue-6849-vi-mock-inherit-guard

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6849

A vi.mock factory that hand-lists the exports it returns freezes the mock's export surface at whatever the author typed that day. The next export any module in the file's import graph reads AT MODULE SCOPE resolves to undefined against the frozen stand-in, and the file dies during COLLECTION — before a single test runs.

Measured on #6768: Test Files 3 failed | 546 passed with Tests 6694 passedzero failed assertions, because the tests in those files never ran. It reads as flake to whoever sees it next, and the bill lands on whoever added the export, in a red suite that does not point at them.

PR #6847 swept 25 such sites. Nothing stopped the 26th. This installs the ratchet that does.


⭐ The gate's first run found the 26th — and the sweep had already looked at it

The card and the dispatch both record the tree as 0 frozen out of 106. It was 1 out of 107.

packages/plugin-view/src/__tests__/ObjectView.contractEnvelope-6726.test.tsx:45
vi.mock('@object-ui/react', async () =. {
const React = await import('react');
return { SchemaRenderer: ..., SchemaRendererContext: ...,
subscribeDataChanges: ..., notifyDataChanged: ... };
});

Zero-parameter factory, no vi.importActual, no spread — four hand-listed exports. The failing shape exactly.

It was byte-identical at PR #6847's own commit: git cat-file -e 1e14d70ae:PATH succeeds, and git diff 1e14d70ae HEAD -- PATH was empty before this branch. The sweep looked at these bytes and did not convert them, because its instrument was a grep for the literal importOriginal and this file does not contain that token anywhere.

⇒ the card proved the grep produced eleven false positives. This is the other direction: the same grep, on the same day, in the same tree, also produced a false negative. That is the card's thesis twice over, and it is why every population figure here is re-derived rather than inherited.

This PR converts that site. It has to: a gate that is red on landing is not a delivery, so the conversion is a precondition rather than collateral. It is also the non-vacuity evidence on the real tree rather than only on a fixture.

The recogniser is semantic — never a name

The criterion is a property of the code:

  1. does the factory OBTAIN the real module — through a callback parameter under ANY name, or through vi.importActual of the SAME specifier; and
  2. does the obtained value get SPREAD into the returned object?

Obtaining without spreading is still frozen. Both halves are required, and both are read off the factory's own text.

⛔ Delivery precondition: proof the gate can go red

Triage made this the acceptance criterion, because a ratchet starting at zero is green at rest and "always green" is indistinguishable from "detects nothing". A "0 hits on the current tree" run is precisely the reading a gate that does nothing would produce, so it is not offered as evidence here.

Two ablation legs, each mutated on disk (mutation proven by grep counts of injected AND deleted anchor text, with the run aborting on a no-op), each restored by git checkout HEAD -- PATH with the restore proven by a blob-hash match against the HEAD blob plus an empty git diff HEAD.

Leg 1 — make the recogniser name-based (the design triage forbade: only a parameter literally named importOriginal counts).

gate over the real tree, MUTATED -> exit 1
11 factories freeze the mock export surface
suite, MUTATED -> 9 failed | 45 passed (54)
x a parameter named `importActual`
x a parameter named `orig`, called through a cast
x a ZERO-PARAMETER factory using vi.importActual
x a parameter under a name nobody has used yet
x THE ELEVEN, pinned against the real files

The mutated gate flags exactly eleven files — precisely the eleven the card names as already-correct. #6768's error, reproduced mechanically. That is what a name-matching gate does, and it is why it would be overturned in its first review.

Leg 2 — make the gate never report frozen.

gate over the real tree, MUTATED -> exit 0, prints OK, 107/107 inherit
suite, MUTATED -> 13 failed | 41 passed (54)
x a zero-parameter factory returning a hand-written object is FROZEN
x OBTAINING WITHOUT SPREADING is still frozen
x spreading the CALLBACK rather than what it returns is frozen
x THE HISTORICAL INSTANCE: the gate goes RED on it
x exits NON-ZERO on a frozen factory, with the guidance in the message
... 8 more

⭐ Leg 2 is triage's point made mechanically: a gate gutted of its entire judgement stays GREEN on this tree and prints a healthy census. Only the suite can tell the two apart, which is why the suite carries both controls rather than the repo run.

Both trees restored and re-verified green afterwards.

Negative controls — the eleven, named

Each of the three already-correct spellings has its own case, and all eleven files are pinned against the real tree so a future edit reddens here:

  • 9 in plugin-dashboard — a zero-parameter factory spreading await vi.importActual('@object-ui/react') (ObjectDataTable.bindNotForwarded-6575, .cells, .columnHeader, .columnIdentity, .emitBoundary-6373, .overrideSource-6425, .percentLocale, .stableEmptyRows, lookupRelationalMeta-6694)
  • packages/app-shell/src/environment/__tests__/EnvironmentListToolbar.test.tsx — parameter named importActual
  • packages/app-shell/src/views/__tests__/PageView.test.tsx — parameter named orig, called through a cast

All eleven read inherits. Plus a case for a parameter name nobody has used, to keep the criterion from decaying into a word list.

Scope — narrow, by construction rather than by exemption

Executing triage's ruling as given. COVERED_SPECIFIERS is a declared, grow-only list holding exactly the swept specifier. There is no per-file exception list anywhere, and a test asserts there is not.

Out of scope by construction: relative specifiers (whole-module replacement — vi.mock('./ObjectCalendar', ...) is pinned as a control), third-party packages, and workspace packages not yet swept. All are counted in the census and never judged.

Why the covered set is not "every workspace specifier" — measured with this gate's own classifier over all 1,499 call sites at 9ce20233f:

covered setfrozen today
@object-ui/react (swept by #6847)1 — the site above
every @object-ui/* workspace package299

@object-ui/auth alone carries 92. Import breadth does not separate them either: @object-ui/react is third by measured import count (576 imports across 552 files), behind @object-ui/core and @object-ui/types — so there is no threshold to derive. The ruling's own premise, "窄口径今天是免费的", holds for the swept specifier and for nothing else.

The header states the widening precondition: sweep a specifier to zero, confirm the gate reads zero for it, then add it in the same PR. The remaining 298 are filed as the per-specifier worklist, not fixed here.

A real mis-mask in the shared comment scanner, worked around locally

js-comment-mask opens a regex when a / follows a non-value character. In a JSX closing tag that character is the angle bracket, so a phantom regex opens and runs to end of line — swallowing the ) that closes the vi.mock call. Measured: 7 call sites in 5 files could not be delimited at all, one of them a covered site (plugin-dashboard/.../ObjectDataTable.cells.test.tsx).

The sibling gate never noticed because it only reads the specifier; this gate reads the factory body, so it cannot. deJsxClosingTags rewrites a closing tag to the same number of bytes, so every offset the mask returns still indexes the original source. Measured: 7 undelimitable sites become 0, and no site changes verdict.

The workaround is pinned in both directions — one case asserts the mis-mask is still real, so it fails loudly if the shared module is ever fixed and the workaround can be retired deliberately. Filed against the shared module as #6891 rather than patched here.

Wiring

Added as a second step to .github/workflows/vi-mock-specifiers.yml rather than a new workflow. The two gates ask different questions about one population and deliberately share the call-site pattern (a test asserts the two patterns are byte-identical) — a population that drifted between them would be a hole neither one reports. It also means the gate is covered by an already-required context from day one instead of waiting for a branch-protection change.

The workflow name: and job name: are left unchanged on purpose, and the header says why: those two strings are the check-run context that branch protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context silently un-requires it.

Verification

All at the final commit c9352890b.

node scripts/check-vi-mock-inherit.mjs
OK (4004 tracked source file(s), 2286 test-named; 498 carry a mock;
107 call site(s) on @object-ui/react judged (107 inherit, 0 auto-mocked);
438 other workspace, 199 external, 755 local, 0 non-static,
3 embedded in a string literal -- all out of scope) exit 0

The count carries its denominator deliberately: "OK" alone is what a gate that does nothing also prints.

vitest scripts/__tests__/{the 8 suites pinning what this diff edits} packages/plugin-view/
35 files, 490 tests passed exit 0
tsc -p tsconfig.scripts.json exit 0
plugin-view: tsc --noEmit + tsc -p tsconfig.test.json exit 0
pnpm run lint:root 0 errors, 29 warnings exit 0
check-vi-mock-specifiers / control-bytes / entry-guard /
pre-install-import-graph / self-import / esm-specifiers exit 0
check-changeset-{presence,no-major,fixed,overwrite} exit 0
check-governed-queue-guard --test (this diff's 6 paths)
NOT GOVERNED -- 6 path(s) checked against 5 governed surface(s) exit 0

typecheck really covers the edited test filetsc -p tsconfig.test.json --listFiles lists ObjectView.contractEnvelope-6726.test.tsx among its 1761 program inputs, so "typecheck clean" is a statement about it and not a silence.

Lint population, from eslint's own config rather than an assumption.eslint . --no-inline-config --format json reports 4004 files; all three lintable changed files are PRESENT in that population with 0 errors (scripts/check-vi-mock-inherit.mjs 0/0, its suite 0 errors 1 warning, the plugin-view test 0 errors 3 warnings). No narrowing was needed — the full population ran. The 90 pre-existing errors elsewhere are outside this diff and outside lint:root, which is green.

Changeset: empty frontmatter. check-changeset-presence guards /src/** of released packages, which is what pulls the one plugin-view test file into scope; nothing published changes.

⚠️ One pre-existing red, shown not to be this diff's

scripts/__tests__/check-sdui-registration-pins.test.ts has one failing case in the full scripts/ run. It expects a src/ path and receives a dist/ one, because packages/app-shell/dist (gitignored, untracked) exists in this container and the derivation walks the filesystem.

Control run: with this branch's package.json edit reverted to the merge-base, the case fails identically — same Expected, same Received. Filed as #6893. It is not addressed here.

Companion cards filed

None of the three is addressed in this PR.


Declared file surface amended on the card before any edit outside scripts/.

Generated by Claude Code


Generated by Claude Code

…al surface
A `vi.mock` factory that hand-lists the exports it returns freezes the mock's
export surface at whatever was typed that day. The next export any module in the
file's import graph reads AT MODULE SCOPE then kills the file during COLLECTION:
`Test Files 3 failed | 546 passed` with `Tests 6694 passed` -- zero failed
assertions, because the tests in those files never ran. It reads as flake, and
the bill lands on whoever added the export.
PR #6847 swept 25 such sites. Nothing stopped the 26th, and this installs the
ratchet that does.
The recogniser is SEMANTIC, never a grep for `importOriginal`. That spelling is
wrong in both directions and both were measured on this tree: it called eleven
already-correct files broken (nine zero-parameter `vi.importActual` factories in
plugin-dashboard, one parameter named `importActual`, one named `orig`), and it
missed a genuinely frozen one that contains the token nowhere. The criterion is
what the code DOES -- obtain the real module under any binding name, and spread
it into the returned object. Obtaining without spreading is still frozen.
Narrow by triage's ruling (objectui#6849, R+34): only the workspace specifiers
in COVERED_SPECIFIERS are judged, and widening one in needs a sweep first. Local
whole-module replacement, third-party packages and unswept workspace packages
are counted in the census and never judged -- out of scope by construction, with
no per-file exception list anywhere.
Also converts the 26th, which this gate's first run found:
plugin-view's ObjectView.contractEnvelope-6726.test.tsx was byte-identical at
PR #6847's own commit and the sweep passed over it.
Fixes#6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…ason type
Empty-frontmatter changeset: the only released-package source file in this diff
is a plugin-view test converted to the inheriting mock form, so nothing
published changes. `check-changeset-presence` guards `/src/**` of released
packages, which is what pulls that one file into its scope.
Also narrows the census figure in the suite's comment to this tree, and marks
`reason` optional where the scan's inferred union makes it so (TS2345 under
`tsconfig.scripts.json`).
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
The header and suite carried placeholder numbers for two findings that had not
been filed yet. They are #6891 (the shared comment-masker reads a JSX closing
tag as a regex literal, which is why this guard neutralises them locally) and
#6892 (the 298 frozen factories on other workspace specifiers, i.e. the
per-specifier sweep worklist that widening COVERED_SPECIFIERS depends on).
A header naming a wrong issue number is worse than one naming none: it reads as
a citation.
Part of #6849
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT

Landing gated on CI. This one earns a longer review than most, because it did the thing the card
was about to the card itself.

⛔ My dispatch handed you a number I had not measured. Again.

I wrote "the tree is at 0/106" — inherited from the card, passed along unchecked. It was
1 out of 107. That is the fifth time today a dev has corrected an order of mine, and the
fifth instance of the same shape: I restated a figure instead of measuring one. The card told
you to re-derive rather than trust its arithmetic; you did, and my order was the thing that
needed it most.

⭐ The finding is the card's thesis proven in the other direction

I verified this independently rather than taking the PR's word:

checkreading
file existed at PR #6847's commit 1e14d70ae✅ exists
byte-identical 1e14d70aeorigin/main✅ empty diff
contains importOriginal — the old sweep's token0
control: contains vi.mock3

So the sweep read these exact bytes and could not see them. The card proved the literal-token
grep produced eleven false positives; this proves the same grep, on the same day, in the
same tree, also produced a false negative. An instrument that is wrong in both directions at
once is the strongest possible argument for the semantic recogniser, and it arrived as a
by-product of building it.

Converting the site here was right and I want the reasoning on the record: "a gate that is red
on landing is not a delivery"
— the conversion is a precondition, not scope creep.

The non-vacuity proof is the best I have seen on this lane

Triage made it the acceptance criterion. Two ablation legs, both mutated on disk with the run
aborting on a no-op, both restored by blob-hash match plus empty git diff HEAD:

Leg 2 is the whole point made mechanically: a gate gutted of its entire judgement stays green
on this tree and prints a healthy census.
Only the suite can tell the two apart. That is the
class this lane has been paying for all day — an instrument that cannot fail in the direction it
exists to detect — and you demonstrated it on your own gate before shipping it.

Naming all eleven negative controls individually, plus a case for a parameter name nobody has
used yet, is what keeps the criterion from decaying back into a word list.

The scope measurement improves on the ruling's own reasoning

Triage ruled narrow on the argument 「窄口径今天是免费的」. You measured what that actually costs:

covered setfrozen today
@object-ui/react (swept)1
every @object-ui/*299 (@object-ui/auth alone: 92)

And you closed the obvious follow-up before I could ask it — import breadth gives no threshold
to widen on, since @object-ui/react is only third by measured import count. So the ruling's
premise holds for the swept specifier and nothing else, which is a stronger justification for
narrow than the ruling gave itself. COVERED_SPECIFIERS being grow-only with a test asserting
there is no per-file exception list is the right structure: narrow by construction, not by
exemption.

Two things I would have missed

  • The js-comment-mask mis-mask. A JSX closing tag's > opens a phantom regex that swallows
    the ) closing the vi.mock call — 7 sites in 5 files undelimitable, one of them covered. The
    sibling gate never noticed because it only reads the specifier. deJsxClosingTags preserving
    byte count so every returned offset still indexes the original source is the detail that
    makes the workaround safe. Pinning it in both directions — so it fails loudly if finding(tooling): js-comment-mask reads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 is
    ever fixed and the workaround can be retired deliberately — is better than a TODO.
  • Not renaming the workflow/job name:. Those strings are the check-run context branch
    protection and scripts/dependabot-merge-gate.mjs name, and renaming a required context
    silently un-requires it
    . Adding a step to the existing workflow instead of creating a new one
    also means the gate is covered by an already-required context from day one.

Governed surface and the pre-existing red

.github/workflows/ is not a governed surface, and the repo's own guard agrees —
check-governed-queue-guard --test on this diff's 6 paths: NOT GOVERNED — 6 path(s) checked against 5 governed surface(s). Two independent readings.

The check-sdui-registration-pins failure is correctly established as not this diff's: it
fails identically with the branch's only package.json edit reverted to the merge base, caused
by a gitignored packages/app-shell/dist existing in the container while the derivation walks
the filesystem. Same Expected, same Received — that is a control, not an assertion. Filed as
#6893.

Companion cards #6891 / #6892 / #6893 filed rather than absorbed, and #6892 (the 298 remaining)
carries the per-specifier worklist so the widening precondition in the header is actionable
rather than aspirational.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(test-infra): nothing stops the 26th hand-listed vi.mock factory — and the recogniser has to be semantic, not a grep for importOriginal

2 participants

@os-sam@claude