refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

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

refactor(test-support): one shared home for the Zod wrapper-key list - #7022

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module
Aug 31, 2026
Merged

refactor(test-support): one shared home for the Zod wrapper-key list#7022
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-6923-shared-gate-data-module

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#6923

Implements the 2026-08-31 ruling — option B narrowed to data only. The five numbered constraints are each answered below; constraint 4 is the one that carries the weight, and it changed the shape of the test twice.

What the census actually found

The dispatch flagged its own census as incomplete and told me not to inherit it. Re-derived on origin/main, the literal list ['in','out','innerType','schema','left','right'] had exactly five copies:

sitelanguage
scripts/check-action-forward-parity.mjs.mjs gate
scripts/check-designer-field-key-parity.mjs.mjs gate
packages/core/src/actions/__tests__/actionKeys.pin.test.tsTS
packages/app-shell/.../inspectors/flow-node-config.spec-reconciliation.test.tsTS
packages/app-shell/.../previews/flow-canvas-seeds.spec-parse.test.tsxTS

The PM's zero-hit probe for single-quoted 'innerType' is falsified — three of those sites spell it that way. The other innerType hits in the tree are single-spelling walks (while (cur?._def?.innerType)), not this list, and are deliberately left alone: they are a different reader class.

After this change the literal list exists in one place; a repo-wide grep for the pattern returns zero other hits.

The shape

  • packages/test-support/src/zod-wrapper-keys.json — the data. Zero build step.
  • packages/test-support/src/zod-wrapper-keys.ts — the reasoning, and the typed ZOD_WRAPPER_KEYS the TypeScript side reads. index.ts re-exports it (constraint 3).
  • exports gains "./zod-wrapper-keys": "./src/zod-wrapper-keys.json" (constraint 2). private: true unchanged.

Two decisions the ruling left open, and why they went this way

JSON, not .mjs. Both were allowed. .mjs was rejected on a measurement: index.ts re-exporting from a .mjs is TS7016 in every consumer's program (the root config sets allowJs: false), so it would have cost either allowJs in each of the nine dependent packages or a hand-written .d.mts — the "second source of truth, free to drift silently" that tsconfig.scripts.json's own header already argues against. resolveJsonModule is already on repo-wide, so JSON types itself with no declaration file and no build. Verified: --listFiles shows zod-wrapper-keys.ts inside both @object-ui/core's and @object-ui/app-shell's type-check programs, with no allowJs anywhere.

⚠️ The ruling's parenthetical was not true yet, and this PR makes it true. Constraint 2 says the subpath works because "pnpm workspace 链接使 node scripts/check-*.mjs 可直接解析". Measured: @object-ui/test-support was a devDependency of nine packages but not of the workspace root, so there was no node_modules/@object-ui at the root and the bare specifier resolved from nowhere in scripts/. Adding it to root devDependencies is what creates the link. That is the mechanism the ruling names, so it is in scope; flagging it because it is a manifest change the card did not anticipate. check:phantom-deps documents this exact root-declaration allowance for tooling imports and stays green. packages/core also had to declare the package (its sibling consumers already did).

createRequire, not import … with { type: "json" }. Load-bearing, and measured: these gate modules are imported both by node (the gate run) and by Vite's SSR transform (their own pin tests). Under the latter the attributed JSON import yields no default export and the walk dies with __vite_ssr_import_N__.default is not iterable. createRequire is the idiom loadSpecSchemas in the same file already uses.

Constraint 4 — the counter-example, and the two things it caught

⭐ A test that only proves "both sides import the same list" passes just as well when the list is empty. scripts/__tests__/zod-wrapper-keys.shared.test.ts drives one fixture per key through each gate's real entry point (specShapeKeys; and schemaAcceptSet's own importSpec seam, since that gate keeps its walk private and the ruling says to leave it there), plus a negative: an unlisted wrapper spelling must raise ExtractionError, never return a clean empty verdict.

Why fixtures and not the installed spec — this is the finding that decided the design. Measured against @objectstack/spec@17.2.0, walking with the list emptied:

schemareal listemptiedwrapper keys load-bearing?
ui.ActionSchema45 keysnullyes
automation.FlowNodeSchema11 keysnullyes
data.FieldSchema71 keys71 keysno
data.ObjectSchema42 keys42 keysno

FieldSchema/ObjectSchema expose .shape at depth 0 and never need a wrapper hop. So a counter-test anchored on the installed schemas would be vacuous for the designer gate today, and could go vacuous for the others the next time upstream unwraps something — silently, which is the whole complaint. Fixtures cannot rot that way.

The ablation, and the defect it found in the counter-test itself

Emptying zod-wrapper-keys.json to [] (mutation confirmed on disk: innerType count 0, [] count 1, blob 2aac729cfe51488c), on the committed implementation:

check:action-forward-parity EXIT=1 ❌ could not resolve `ActionSchema`'s shape
check:designer-field-key-parity EXIT=0 ← green, exactly as the table predicts
vitest (8 files) EXIT=1 5 files failed, 12 tests failed

Restore verified by observation, not exit code: git hash-object back to 2aac729c (= the HEAD blob) and git diff HEAD empty.

The first ablation run found a real defect in this PR's own test. The per-key checks were written as it.each(onDisk) — which generates its cases from the list under test, so with the list emptied it generated none. The suite reported fewer tests and stayed green on precisely the part carrying the discrimination: this card's defect, reproduced inside its own counter-example. Fixed in c83904d5b — one looping test per gate, with the non-vacuity floor asserted in the same test, so no-data still fails. Re-ablated: 12 failing tests instead of 10, and both every entry in the list is load-bearing tests now among them, including the designer one — the leg the real designer gate cannot supply today.

Constraint 5 — the boundary is respected

Only data moved. Each caller keeps its own walk, and they are legitimately not identical: the designer gate reads node._def ?? node.def ?? node._zod?.def, the action gate reads s._def ?? s.def. Unifying those is sharing a function across the language boundary, which the ruling explicitly does not open a door for. Both gate diffs are two lines: an import and a loop head.

Verification

Union run after the final commit, at c83904d5b, tree clean.

pnpm exec vitest run scripts/ packages/test-support/ \
packages/core/src/actions/__tests__/actionKeys.pin.test.ts \
packages/app-shell/.../flow-node-config.spec-reconciliation.test.ts \
packages/app-shell/.../flow-canvas-seeds.spec-parse.test.tsx
→ Test Files 98 passed (98) · Tests 2663 passed | 1 skipped (2664)

Gates, exit code captured before any pipe: check-action-forward-parity, check-designer-field-key-parity, check-phantom-dependencies, check-pre-install-import-graph, check-control-bytes, check-changeset-presence — all EXIT=0. Also green: check-readme-exports (on a built tree), check-governed-queue-guard --test over all 16 changed paths (NOT GOVERNED), check-self-import, check-side-effects-array, check-vi-mock-specifiers, check-vi-mock-inherit, check-changeset-{fixed,no-major,overwrite}, check-lint-coverage, check-type-check-coverage.

Type-check: type-check:scripts, and @object-ui/{test-support,core,app-shell} — all EXIT=0 (the last two only after building their dependency closures; unbuilt they fail on missing packages/types/dist, which is an unbuilt-tree condition, not this diff). --listFiles confirms all five edited/added test files are genuinely inside those programs rather than excluded.

Lint — a declared narrowing, with its evidence.eslint --no-inline-config --format json over all 9 changed lintable files: 9 linted, 0 ignored, 0 errors, 0 warnings. The narrowing is measurable rather than a gap because eslint.config.js configures no type-aware linting (no projectService, no parserOptions.project), so a file's verdict depends only on its own text plus the flat config — this diff cannot move the verdict on any file it did not touch. The repo-wide sweep remains CI's run.

Changeset: empty frontmatter — test files, CI gate scripts and a private: true package only; nothing releases. check-changeset-presence confirms the exemption is complete.

Two reds hit locally, neither from this diff, both already tracked

⛔ No new issue filed: dedup ran against the 294 open issues via the repo-scoped REST list plus a local grep, with a control term that had to hit and did. #6699 is untouched.


Generated by Claude Code

The literal wrapper-key list `['in','out','innerType','schema','left','right']`
had five copies: three TypeScript test files and two `.mjs` CI gate scripts.
The copies spanned a language boundary, so the objectui#5872 class-(1) pattern
was unavailable across it -- `@object-ui/test-support`'s `exports["."]` is
TypeScript source, and a bare `node scripts/check-*.mjs` has no build artefact
to reach.
Per the 2026-08-31 ruling on objectui#6923, the DATA gets a build-free home and
the walks stay with their callers:
- `packages/test-support/src/zod-wrapper-keys.json` holds the list;
- `packages/test-support/src/zod-wrapper-keys.ts` holds the reasoning and types
it for TypeScript consumers; `index.ts` re-exports `ZOD_WRAPPER_KEYS`;
- a new `exports` subpath, `./zod-wrapper-keys`, points straight at the JSON so
a bare-node gate can resolve it. The workspace root declares the package so
the bare specifier resolves from `scripts/`.
The gates read it via `createRequire` rather than an attributed JSON import:
this module is loaded both by `node` and by Vite's SSR transform (its own pin
tests), and under the latter the attributed import yields no default export --
measured, as "__vite_ssr_import_N__.default is not iterable".
Constraint 4 of the ruling -- empty the list and the gate must go RED -- is
paid by `scripts/__tests__/zod-wrapper-keys.shared.test.ts`, which drives one
fixture per key through both gates' real entry points and asserts an unlisted
spelling raises `ExtractionError`. It uses fixtures rather than the installed
spec on purpose: measured against @objectstack/spec@17.2.0, `ui.ActionSchema`
and `automation.FlowNodeSchema` need a wrapper hop but `data.FieldSchema` and
`data.ObjectSchema` expose `.shape` at depth 0, so a spec-anchored counter-test
would be vacuous for the designer gate today.
Sharing a FUNCTION across the language boundary is explicitly outside that
ruling and is not attempted here; the two walks stay local and stay different.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
Running this file's own ablation caught it: `it.each(onDisk)` generates its
cases FROM the list under test, so with the list emptied it generated NONE.
The suite reported fewer tests and stayed green on exactly the part that
carries the discrimination -- this card's own defect, reproduced inside its
counter-example.
Replaced with one looping test per gate, each asserting the non-vacuity floor
IN THE SAME TEST. No data, no cases, but the floor still fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDDqh6YnkXqsnVTCa7wHk
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3180.2 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BsvtHAQI.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.68KB7.36KB
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.08KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
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)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review August 31, 2026 13:30
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d3bf4faAug 31, 2026
32 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-6923-shared-gate-data-module branch August 31, 2026 13:47
os-sam pushed a commit that referenced this pull request Aug 31, 2026
Append-collision with PR #7022 (`refactor(test-support): one shared home for
the Zod wrapper-key list`), which landed on main after this branch was cut and
touches the same package barrel and README.
Both sides kept, in two files:
- `packages/test-support/src/index.ts` -- exports BOTH #7022's
`ZOD_WRAPPER_KEYS` (with its docblock) and this branch's
`enumOptions, shapeEnumOptions`.
- `packages/test-support/README.md` -- this branch's rewritten
`spec-enum-options.ts` entry (two exports, one walk) AND #7022's two
`zod-wrapper-keys` entries, in main's ordering. One sentence of this branch's
text was corrected rather than kept verbatim: it said the wrapper-key walk is
"NOT confined here yet and still hand-copied", which #7022 made false. The
merged sentence carries #7022's fact (the wrapper-key list is now shared as
DATA) and this branch's remaining half (array-element unwrapping is still
hand-copied).
`pnpm-lock.yaml` auto-merged; `pnpm install` left it unchanged, so it needed no
regeneration and none was hand-written.
No behaviour from either side was dropped or altered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configurationdependenciesdocumentationImprovements or additions to documentationpackage: coretests

Projects

None yet

2 participants

@zhuangjianguo@claude