Repository navigation
fix(console): return packages/core to the framework chunk by grouping rule, and pin chunk membership - #9488
Conversation
… rule, and pin chunk membership
`data-adapter` declares `includeDependenciesRecursively: false`, so the group
holds exactly what its regex names. The rule that re-attributed the subgraph is
rolldown's `includeDependenciesRecursively` (default `true`): a group captures
its matched modules AND everything they import, and the same option's priority
doc states those are then removed from lower-priority groups whose regex does
match them. `packages/data-objectstack` imports `@object-ui/core` and this group
outranks `framework` (84 over 80), so the recursive half handed `framework`'s
declared members to a group with no ceiling and no baseline.
The transfer tracks TREE-SHAKING, which is why it moved with no edit here:
while `@object-ui/core` was imported by name only, the slice reachable through
`data-objectstack` was a handful of modules; objectui#9185 added an
`import('@object-ui/core')` of the BARREL, every export became live, and the
same walk reached all 92.
Measured, console build either side of the one-line repair:
data-adapter 78,110 -> 18,537 gzipped, holding only packages/data-objectstack
framework 45,278 -> 104,636 gzipped, holding core|react|types and nothing else
eager closure -461 bytes across the whole bundle
The bytes were always downloaded — `data-adapter` is in the eager closure — so
this is a re-attribution, not a payload change.
Second half: `scripts/check-eager-closure-budget.mjs` gains a MEMBERSHIP half.
`apps/console/vite.config.ts` emits `dist/chunk-membership.json` (which chunk
each workspace package's modules landed in, counted over every emitted chunk,
lazy ones included) and `evaluatePerChunkMembership` asserts each budgeted
group's declared packages landed wholly in their declared chunk. EXACT, not a
ratchet: one stray module is a finding, by name. Every pass-by-measuring-nothing
route is an error rather than a pass — absent artifact, unknown version, empty
attribution, a declared package absent from the bundle, a declaration naming a
chunk no ceiling governs.
No ceiling, baseline or exemption constant moves.
Refs: objectui#9345
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VGeMu3p6qEFWR6K6GGLaW
❌ Console Performance Budget
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. Which half objected:
📦 Bundle Size Report
Size Limits
|
HELD — correct, reviewed, and ⛔ not landable until another lane's one-liner lands
The option-B measurement is in (card objectui#9345, comment
⇒ this PR stays open, stays draft, and is ⛔ NOT enqueued. Card objectui#9345 moves ⛔ No approval from this seat, ⛔ no merge, ⛔ no ceiling moved, ⛔ no test skipped. Generated by Claude Code |
…e-chunk-membership
|
changeset-claim-re-read
|
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Per-chunk membership (declared packages) | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 146.9 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 16.69KB | 6.21KB |
| app-shell (runtime-config.js) | 20.68KB | 7.36KB |
| app-shell (types.js) | 0.01KB | 0.04KB |
| app-shell (urlParams.js) | 10.06KB | 3.86KB |
| auth (ActiveOrganizationStorage.js) | 25.05KB | 9.16KB |
| auth (AuthContext.js) | 0.31KB | 0.24KB |
| auth (AuthGuard.js) | 2.07KB | 1.00KB |
| auth (AuthProvider.js) | 40.18KB | 10.59KB |
| auth (AuthShell.js) | 3.49KB | 1.40KB |
| auth (ForgotPasswordForm.js) | 12.21KB | 3.45KB |
| auth (LoginForm.js) | 18.15KB | 5.39KB |
| auth (PreviewBanner.js) | 0.90KB | 0.50KB |
| auth (RegisterForm.js) | 6.65KB | 2.22KB |
| auth (SocialSignInButtons.js) | 9.61KB | 3.89KB |
| auth (UserMenu.js) | 3.41KB | 1.23KB |
| auth (auth-gate-events.js) | 1.29KB | 0.66KB |
| auth (authStyles.js) | 5.04KB | 1.72KB |
| auth (createAuthClient.js) | 40.21KB | 10.80KB |
| auth (createAuthenticatedFetch.js) | 8.46KB | 3.43KB |
| auth (index.js) | 3.19KB | 1.44KB |
| auth (invitation-status.js) | 1.22KB | 0.70KB |
| auth (org-roles.js) | 6.66KB | 2.78KB |
| auth (phone-identifier.js) | 1.11KB | 0.66KB |
| auth (types.js) | 0.59KB | 0.35KB |
| auth (useAuth.js) | 5.30KB | 1.02KB |
| auth (useWorkspaceAdminStatus.js) | 11.08KB | 4.58KB |
| collaboration (CommentThread.js) | 26.08KB | 7.56KB |
| collaboration (LiveCursors.js) | 3.17KB | 1.27KB |
| collaboration (PresenceAvatars.js) | 6.49KB | 2.64KB |
| collaboration (PresenceProvider.js) | 2.79KB | 1.13KB |
| collaboration (index.js) | 1.68KB | 0.73KB |
| collaboration (useCollaborationTranslation.js) | 6.05KB | 2.52KB |
| collaboration (useCommentSearch.js) | 1.98KB | 0.88KB |
| collaboration (useConflictResolution.js) | 7.75KB | 1.86KB |
| collaboration (useMentionNotifications.js) | 1.81KB | 0.68KB |
| collaboration (usePresence.js) | 6.33KB | 1.84KB |
| collaboration (useRealtimeSubscription.js) | 7.91KB | 2.01KB |
| components (index.js) | 545.92KB | 130.72KB |
| core (index.js) | 8.94KB | 3.59KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 221.99KB | 61.72KB |
| fields (index.js) | 251.67KB | 63.42KB |
| i18n (LocalizationContext.js) | 1.76KB | 0.96KB |
| i18n (builtinAggregateLabels.js) | 0.86KB | 0.49KB |
| i18n (currency.js) | 1.22KB | 0.64KB |
| i18n (fallbackInterpolation.js) | 6.25KB | 2.77KB |
| i18n (i18n.js) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.22KB | 2.26KB |
| i18n (pickLocalized.js) | 9.86KB | 3.95KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| i18n (useDisplayLocale.js) | 2.85KB | 1.45KB |
| i18n (useObjectLabel.js) | 34.34KB | 9.17KB |
| i18n (useSafeTranslation.js) | 5.60KB | 2.33KB |
| layout (index.js) | 38.90KB | 10.97KB |
| mobile (MobileProvider.js) | 0.92KB | 0.49KB |
| mobile (ResponsiveContainer.js) | 0.94KB | 0.38KB |
| mobile (breakpoints.js) | 1.51KB | 0.70KB |
| mobile (createOfflineDataSource.js) | 5.61KB | 1.75KB |
| mobile (index.js) | 1.99KB | 0.87KB |
| mobile (offlineQueue.js) | 3.91KB | 1.35KB |
| mobile (pwa.js) | 0.97KB | 0.49KB |
| mobile (serviceWorker.js) | 1.48KB | 0.62KB |
| mobile (serviceWorkerSource.js) | 3.41KB | 1.48KB |
| mobile (useBreakpoint.js) | 1.54KB | 0.65KB |
| mobile (useGesture.js) | 6.96KB | 1.98KB |
| mobile (useOfflineSync.js) | 1.99KB | 0.72KB |
| mobile (usePullToRefresh.js) | 2.53KB | 0.85KB |
| mobile (useResponsive.js) | 0.72KB | 0.42KB |
| mobile (useSpecGesture.js) | 5.52KB | 2.10KB |
| mobile (useTouchTarget.js) | 1.01KB | 0.54KB |
| permissions (MePermissionsProvider.js) | 13.52KB | 4.88KB |
| permissions (PermissionContext.js) | 0.31KB | 0.25KB |
| permissions (PermissionGuard.js) | 0.89KB | 0.45KB |
| permissions (PermissionProvider.js) | 6.24KB | 2.16KB |
| permissions (discardProofCache.js) | 1.04KB | 0.55KB |
| permissions (evaluator.js) | 8.39KB | 3.10KB |
| permissions (index.js) | 0.93KB | 0.41KB |
| permissions (store.js) | 0.91KB | 0.42KB |
| permissions (useFieldPermissions.js) | 1.28KB | 0.53KB |
| permissions (usePermissions.js) | 4.83KB | 2.27KB |
| plugin-ai (index.js) | 14.81KB | 3.63KB |
| plugin-calendar (index.js) | 50.26KB | 14.36KB |
| plugin-charts (index.js) | 71.73KB | 20.08KB |
| plugin-chatbot (index.js) | 198.20KB | 47.14KB |
| plugin-dashboard (index.js) | 132.96KB | 35.17KB |
| plugin-designer (index.js) | 215.94KB | 44.33KB |
| plugin-detail (index.js) | 255.18KB | 66.49KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 139.56KB | 35.40KB |
| plugin-gantt (index.js) | 167.62KB | 41.26KB |
| plugin-grid (index.js) | 213.44KB | 58.21KB |
| plugin-kanban (index.js) | 48.71KB | 15.17KB |
| plugin-list (index.js) | 113.53KB | 27.99KB |
| plugin-map (index.js) | 21.48KB | 6.99KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.41KB | 11.93KB |
| plugin-timeline (index.js) | 30.68KB | 8.95KB |
| plugin-tree (index.js) | 10.58KB | 3.72KB |
| plugin-view (index.js) | 85.18KB | 21.05KB |
| providers (DataSourceProvider.js) | 0.75KB | 0.39KB |
| providers (MetadataProvider.js) | 1.37KB | 0.59KB |
| providers (ThemeProvider.js) | 1.90KB | 0.85KB |
| providers (UploadProvider.js) | 11.66KB | 3.50KB |
| providers (index.js) | 0.45KB | 0.23KB |
| providers (types.js) | 0.01KB | 0.04KB |
| react-runtime (index.js) | 5.62KB | 2.34KB |
| react (LazyPluginLoader.js) | 4.47KB | 1.63KB |
| react (SchemaRenderer.js) | 109.04KB | 36.08KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| react (spec-input.js) | 0.20KB | 0.18KB |
| sdui-parser (body-dialect.js) | 4.38KB | 1.98KB |
| sdui-parser (codegen.js) | 6.58KB | 2.74KB |
| sdui-parser (dashboard-widget-options.js) | 3.08KB | 1.30KB |
| sdui-parser (index.js) | 5.74KB | 2.54KB |
| sdui-parser (input-type.js) | 2.84KB | 1.40KB |
| sdui-parser (kanban-quick-add.js) | 3.89KB | 1.87KB |
| sdui-parser (parse.js) | 25.28KB | 7.80KB |
| sdui-parser (provenance.js) | 3.66KB | 1.82KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| sdui-parser (validate.js) | 15.71KB | 5.30KB |
| types (ai.js) | 4.11KB | 2.06KB |
| types (api-types.js) | 0.20KB | 0.18KB |
| types (app.js) | 2.87KB | 1.00KB |
| types (base.js) | 0.20KB | 0.18KB |
| types (blocks.js) | 0.20KB | 0.18KB |
| types (complex.js) | 2.93KB | 1.49KB |
| types (crud.js) | 0.20KB | 0.18KB |
| types (dashboard-filter-alias.js) | 6.23KB | 2.74KB |
| types (data-display.js) | 3.75KB | 1.85KB |
| types (data-protocol.js) | 0.20KB | 0.19KB |
| types (data.js) | 0.20KB | 0.18KB |
| types (designer.js) | 1.85KB | 0.85KB |
| types (disclosure.js) | 0.20KB | 0.18KB |
| types (error-code.js) | 1.54KB | 0.88KB |
| types (expression.js) | 0.20KB | 0.18KB |
| types (feedback.js) | 0.20KB | 0.18KB |
| types (field-types.js) | 0.20KB | 0.18KB |
| types (form.js) | 0.20KB | 0.18KB |
| types (http-inflight.js) | 8.87KB | 3.73KB |
| types (http-retry.js) | 4.32KB | 2.02KB |
| types (icon-key-migration.js) | 4.26KB | 1.63KB |
| types (index.js) | 4.74KB | 2.25KB |
| types (layout.js) | 0.20KB | 0.18KB |
| types (managed-by.js) | 0.19KB | 0.18KB |
| types (mobile.js) | 5.00KB | 2.39KB |
| types (navigation.js) | 0.20KB | 0.18KB |
| types (objectql.js) | 0.20KB | 0.18KB |
| types (overlay.js) | 0.20KB | 0.18KB |
| types (permissions.js) | 0.20KB | 0.18KB |
| types (plugin-scope.js) | 0.20KB | 0.18KB |
| types (record-components.js) | 0.20KB | 0.19KB |
| types (record-semantics.js) | 1.28KB | 0.67KB |
| types (registry.js) | 0.20KB | 0.18KB |
| types (reports.js) | 0.20KB | 0.18KB |
| types (select-option.js) | 0.20KB | 0.19KB |
| types (spec-report.js) | 5.05KB | 1.93KB |
| types (spec-ui-namespace.js) | 0.20KB | 0.19KB |
| types (strict-authoring-face.js) | 14.04KB | 5.36KB |
| types (system-fields.js) | 3.33KB | 1.54KB |
| types (theme.js) | 6.28KB | 2.87KB |
| types (ui-action.js) | 8.11KB | 3.32KB |
| types (views.js) | 0.20KB | 0.18KB |
| types (widget.js) | 0.20KB | 0.18KB |
Size Limits
- ✅ Core packages should be < 50KB gzipped
- ✅ Component packages should be < 100KB gzipped
⚠️ Plugin packages should be < 150KB gzipped
|
os-dev-report {
"issue": 9488,
"status": "blocked",
"branch": "claude/issue-9345-core-chunk-membership",
"pr": "https://github.com/objectstack-ai/objectui/pull/9488",
"session": "session_018HrVaotisyhgmot9o2MLRq",
"premise_still_valid": false,
"summary": "MEASUREMENT ROUND on PR 9488, on a worktree of the branch merged with today's origin/main (0c2eb5eee, clean merge, pushed as 86d130c6d — merge commit only, no behaviour change). THE DISPATCH PREMISE IS FALSIFIED: `framework` is NOT over its ceiling any more. It measures 95,454 gzipped against the unchanged 100,000 ceiling — UNDER by 4,546 bytes, where the PR body measured 104,636 (over by 4,636). The figure moved DOWN by 9,182 bytes. `pnpm check:eager-closure` still fails, but on a DIFFERENT verdict and a different exit code: every byte ceiling passes and the membership half passes; the failing leg is the EXHAUSTED-headroom sensitivity check (headroom 4,546 = 0.0499x, under the 0.10x floor), so the gate exits 2 (an error about the GAUGE) and no longer exits 1 (a verdict about the bundle). The decision therefore changes shape: not 'raise the ceiling or get under it', but 'restore at least 9,114 bytes of headroom on the framework row'. The overage was never created by this change: across the pair the branch REMOVES 617 gzipped bytes and the whole eager closure falls 822 bytes, with the eager chunk count unchanged at 329 of 2309; `data-adapter` is itself eager on both legs, so its bytes were always downloaded. No constant, ceiling, baseline or exemption was touched, and nothing was implemented.",
"tests": "Two legs, same container, same command family, same gate constants (verified: MAX_EAGER_CLOSURE_GZIP_BYTES 3,179,000; REGRESSION 91,136; FLOOR 0.10; PER_CHUNK_GZIP_CEILINGS and PER_CHUNK_BASELINE byte-identical on both legs; EXHAUSTED_HEADROOM_ALLOWANCES empty). Per leg: `pnpm turbo run build --filter=./packages/* --concurrency=2` then `pnpm --filter @object-ui/console build` then `node scripts/check-eager-closure-budget.mjs`. Exit codes captured by redirect-then-capture, never across a pipe. LEG MAIN (0c2eb5eee): PKGBUILD 0, CONSOLEBUILD 0, GATE 0 — framework 69,560 (headroom 30,440 = 0.334x), data-adapter 45,317, eagerGzipBytes 3,132,117. LEG HEAD (86d130c6d): PKGBUILD 0, CONSOLEBUILD 0, GATE 2 — framework 95,454 (headroom 4,546 = 0.0499x), data-adapter 18,806, eagerGzipBytes 3,131,295, membership half PASS (5 packages, each wholly in its declared chunk). LEG HEADVIZ (same HEAD tree, visualizer on, CI unset): framework 95,454 and data-adapter 18,806 — byte-identical to LEG HEAD, the control that the non-CI plugin set does not move what the gate weighs. Cross-instrument control: `gzip -c` over the emitted chunks reproduces raw bytes to the byte (322,796 / 67,175 / 244,780 / 146,090) and gzip within 0.14%. Every zero carries a same-subject control in the same command: the string probe that finds `objectql.zod.ts`'s `.describe('ObjectQL object name')` in `framework` on BOTH legs is paired with a must-miss probe for `crud.zod.ts`'s `.describe('Action execution mode for chaining')`, absent from every emitted chunk on both legs. FINAL ACT, after the push, on 86d130c6d: `node scripts/check-eager-closure-budget.mjs` exit 2 (verdict lines quoted in the PR comment); `node scripts/check-changeset-presence.mjs` exit 0 ('no changeset is owed'). NOT MEASURED: a counterfactual build with any shrink candidate removed — this round is forbidden to implement one, so the shrink saving is given as a two-estimator bracket, not a figure. NOT MEASURED: which commits on origin/main moved the pair between ff1d5ea8d1 and 0c2eb5eee — that needs a bisect and is stated as a decomposition instead of a cause. One shared-lock acquisition returned VERDICT queue-timeout (exit 99, waited 540s, ticket kept) for an extra origin/main visualizer build; that build was dropped as not load-bearing and its absence is declared here rather than worked around.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
"api_writes": "1 — POST /repos/objectstack-ai/objectui/issues/9488/comments (the os-dev-report comment). Plus one non-REST write: `git push origin claude/issue-9345-core-chunk-membership`, d2ac39197..86d130c6d, the merge of origin/main alone, which the dispatch authorises when the merge is needed to measure. No POST /pulls (no new PR), no labels touched, no ready flip, no auto-merge, no PATCH of the PR body.",
"open_questions": [
{
"question": "The framework row has 4,546 bytes of headroom (0.0499x) and the gate's EXHAUSTED floor is 0.10x = 9,113.6 bytes, so the row must gain at least 4,568 gzipped bytes of headroom. SHRINK or RAISE?",
"options": [
"A — SHRINK. Target: framework at most 90,886 gzipped, i.e. shed at least 4,568 bytes. The chunk holds 162 modules, ALL of them matched by the framework group's own regex packages/(core|react|types) — the membership half prints pass, there is no stray. By pre-minification rendered bytes: packages/core 76 modules 249,999 (49.1%), packages/types 22 modules 154,924 (30.4%), packages/react 64 modules 104,735 (20.6%). The one candidate that is not payload: packages/types/src/zod/** — 9 modules, 140,541 rendered (27.6% of the chunk), led by objectql.zod.ts 46,737, data-display.zod.ts 34,613, complex.zod.ts 32,589. Their ONLY live consumer in the console bundle is packages/plugin-map/src/ObjectMap.tsx importing ObjectMapConfigSchema from @object-ui/types/zod, and plugin-map is NOT in the eager closure; the @object-ui/types main barrel deliberately does not re-export the zod validators, and the tree's two other importers (zod/index.zod.ts, strict-authoring-face.ts) emit zero bytes. COST: give that directory its own advancedChunks group; the new chunk is reached only from plugin-map, so it becomes LAZY and the bytes LEAVE the eager closure rather than moving to another eager line. The object-map view pays one extra fetch when it opens; every other page load stops paying. SAVING, bracketed by two estimators because the exact figure needs a counterfactual build this round may not make: 20,695 (per-module gzip share) to 26,322 (rendered share) gzipped — 4.5x to 5.8x what the floor needs, landing framework near 69.1K–74.8K, i.e. back around its 72,245 baseline. ⚠️ This condition is PRE-EXISTING on main (the probe finds objectql.zod.ts in framework on BOTH legs), so it is its own card and does not unblock PR 9488 on its own.",
"B — RAISE. The framework ceiling would have to become at least 104,568 for the row to clear the 0.10x floor at today's 95,454 (the historic framework ratio of 0.30x would put it at 122,795). Legal upper bounds: under 186,590 from the live blind-gauge rule, and under 163,381 from the unit test's ceiling-minus-baseline constraint against PER_CHUNK_BASELINE.framework = 72,245; a raise to 104,568 satisfies both without moving the baseline. COST, in the gate's own words rather than paraphrased — on what the per-chunk lines are for: 'The aggregate is one number over every chunk in the closure. Inside its headroom, a single chunk can grow by the whole allowance while every other chunk shrinks by the same amount, and the gate reports a green tick either way. ... The aggregate says whether the payload grew; these say WHERE.' On how wide headroom may get: 'The headroom must stay SMALLER than the regression the gate exists to catch. objectui#5266 was 89 KiB = 91,136 bytes, and this ceiling is built to carry half of that, so it would have failed on that change. Widening the headroom past ~89 KiB would leave the gate green through a repeat of its own motivating incident.' And the exhausted leg's own printed remedy and prohibition: 'The remedy is the bytes: take them out of the chunk this row names, or take a deliberate, authorised re-pin and say in the PR what the new headroom buys.' / '⛔ Never raise a ceiling because this leg noticed it is full — that is how a budget gets switched off rather than met.' CONCRETELY BLIND TO: 9,114 bytes of framework growth at 104,568, or 27,341 at 122,795, in the chunk that holds the whole engine. The aggregate keeps 47,705 bytes (0.52x) of its own headroom, so a framework-only regression between those two figures would still be caught in total but no longer ATTRIBUTED — which is the exact capability the per-chunk lines exist to add."
],
"recommendation": "A, the shrink, filed as its own card — with the ordering said out loud, because it is what makes A actionable. The gate's own text forbids B as a response to this leg ('Never raise a ceiling because this leg noticed it is full'), and A is the only option that reduces the payload instead of the gauge's resolution: the packages/types/src/zod/** tree is 27.6% of a chunk every page load fetches, and its sole live consumer is a chunk no page load fetches. It is also the same defect class this very PR repairs — a grouping rule deciding membership independently of reachability — one tier over, which is why it is worth a card rather than a constant edit. ⚠️ What A does NOT do is unblock PR 9488 today: until that card lands, this PR's Bundle Analysis job stays red (exit 2 is converted to a step failure by the workflow, with budget_status=error). So the decision is really two: rule A for the bytes, and then answer the second question below for this PR. The BRACKET, not the point estimate, is what I stand behind: both ends clear the requirement by more than 4x, so the ranking of A over B does not turn on which estimator is right."
},
{
"question": "Given that framework now PASSES its byte ceiling and only the sensitivity leg fails, may PR 9488 land with `Bundle Analysis` red on that one leg (exit 2, budget_status=error), or must the headroom be restored first?",
"options": [
"A — land it red and carry the debt on the shrink card. Defensible because the PR's own contribution is a net REMOVAL (pair -617, whole eager closure -822, chunk count unchanged) and because the red is about the gauge's resolution, not about this diff's bytes: on origin/main the same row reads 0.334x and green, so the row went thin because the bytes were returned to the line that weighs them — which is the repair working.",
"B — hold the PR until the shrink lands, so it never merges with a red required-adjacent check. Cost: the membership pin — the only thing in the tree that can see a whole package leaving a budgeted chunk — stays unlanded while it waits, which is the hole objectui#9345 was filed about."
],
"recommendation": "A, on the condition that the shrink card exists and is linked from this PR before it lands, so the debt is recorded rather than absorbed. ⛔ Not mine to rule, and ⛔ not taken: this round flipped nothing, touched no label, and left the PR in draft."
}
],
"out_of_scope_findings": [
"to file (3 classes, dedupe words: eager closure, types zod, plugin-map, advancedChunks group, framework chunk membership) — packages/types/src/zod/** (9 modules, 140,541 rendered bytes, 27.6% of the eager `framework` chunk) is written onto the EAGER budgeted line although its only live consumer in the console bundle is `packages/plugin-map/src/ObjectMap.tsx`, which sits in a LAZY chunk. Cause: the `framework` group's regex packages/(core|react|types) claims every `packages/types/` module wherever it is reached from, and the chunk is eager. Reproducible on origin/main as well as on this branch (probe: the `.describe('ObjectQL object name')` literal resolves to `framework` on both legs; must-miss control `.describe('Action execution mode for chaining')` from `crud.zod.ts` is absent from every emitted chunk on both). Same defect class as objectui#9345 one tier over, and it is the priced shrink candidate in open_questions.",
"noted, not filed: `packages/types/src/zod/index.zod.ts` and `packages/types/src/strict-authoring-face.ts` are retained in the module graph but emit zero bytes in this bundle. That is correct tree-shaking, not a defect; recorded only because it is what makes the zero in the finding above readable. Who would meet it: the author of the shrink card above, in the same files — that is the downstream reader.",
"noted, not filed: the PR body's measurement tables are anchored to `ff1d5ea8d1` and no longer reproduce on today's tree (framework 104,636 then, 95,454 now; the pair 123,173 then, 114,260 now). ⛔ Not filed and ⛔ not edited: the tables name the commit they were measured on, which is the anchored form this repo's own rule asks for, so they are old rather than stale. The downstream reader is whoever next re-pins this PR's body, and the new pair is in this comment for them.",
"noted, not filed: one shared-lock acquisition returned exit 99 (queue-timeout, 540s, ticket kept) while another agent held the box for 700+ seconds. Declared as a narrowing rather than a silent skip; the dropped build was an origin/main visualizer build whose only output would have been main-side composition, which the string probes answered instead. downstream reader: none — it is a container-scheduling observation, not a repo condition."
]
}Appendix — the readings, in the instruments' own wordsMeasurement round only. Nothing was implemented, no constant moved, no label touched, the PR stays draft. The one write to this branch is the merge commit 1. The premise moved:
|
| reading | PR body (on ff1d5ea8d1) |
today (86d130c6d) |
moved by |
|---|---|---|---|
framework gzipped |
104,636 | 95,454 | -9,182 |
| ceiling | 100,000 | 100,000 | 0 |
| verdict on that row | OVER by 4,636 | UNDER by 4,546 | |
| gate exit code | 1 | 2 |
2. Pre-existing? The pair, measured on both legs in one container
Same command family, same gate constants on both legs — MAX_EAGER_CLOSURE_GZIP_BYTES 3,179,000, REGRESSION_THIS_GATE_MUST_CATCH_BYTES 91,136, floor 0.10, PER_CHUNK_GZIP_CEILINGS and PER_CHUNK_BASELINE byte-identical, allowances empty. Only the measurement differs.
| chunk | origin/main 0c2eb5eee |
branch merged 86d130c6d |
delta |
|---|---|---|---|
framework |
69,560 | 95,454 | +25,894 |
data-adapter |
45,317 | 18,806 | -26,511 |
| the two together | 114,877 | 114,260 | -617 |
| whole eager closure | 3,132,117 | 3,131,295 | -822 |
| eager chunks / total | 329 / 2309 | 329 / 2309 | 0 |
data-adapter is itself in the eager list on both legs. ⇒ this change creates no bytes; it removes 822 of them and makes the rest visible. What it DOES create is the red: on origin/main the same row reads framework 67.9 KB / 97.7 KB (headroom 29.7 KB) = 0.334x and the gate exits 0.
✅ Console eager closure is 3058.7 KB gzipped across 329 of 2309 chunks (budget: 3104.5 KB, headroom: 45.8 KB).
✅ Per-chunk eager budgets (4 chunks weighed):
✅ vendor-objectstack 1207.3 KB / 1224.6 KB ceiling (headroom 17.3 KB)
✅ ui-components 261.1 KB / 282.2 KB ceiling (headroom 21.2 KB)
- the pair shrank on BOTH sides — PR-era before-pair 123,388 against today's
main114,877 (-8,511), PR-era after-pair 123,173 against today's branch 114,260 (-8,913). Real retained surface left the bundle onmain. - and the recursive capture shrank on
mainby itself:framework45,278 to 69,560 (+24,282) whiledata-adapter78,110 to 45,317 (-32,793), with no help from this branch. - module counts agree: the PR body reports
packages/coreat 92 modules inframework; the membership half reads 76 today.
3. Pricing the shrink — what is actually in the chunk
162 modules, 322,796 raw / 95,454 gzipped. Every one of them is there by the framework group's own regex packages/(core|react|types) — the membership half prints pass and names no stray. By pre-minification rendered bytes, read from the build's own visualizer:
| source | modules | rendered | share |
|---|---|---|---|
packages/core |
76 | 249,999 | 49.1% |
packages/types |
22 | 154,924 | 30.4% |
packages/react |
64 | 104,735 | 20.6% |
⭐ The candidate, and it is not payload: packages/types/src/zod/** — 9 modules, 140,541 rendered, 27.6% of the chunk.
| module | rendered | own gzip |
|---|---|---|
objectql.zod.ts |
46,737 | 11,977 |
data-display.zod.ts |
34,613 | 7,556 |
complex.zod.ts |
32,589 | 7,533 |
views.zod.ts |
12,353 | 3,121 |
base.zod.ts |
8,590 | 2,215 |
| the other four | 5,659 | 2,524 |
Its only live consumer in the console bundle is packages/plugin-map/src/ObjectMap.tsx, which imports ObjectMapConfigSchema from @object-ui/types/zod — and plugin-map is absent from the 329-chunk eager list. The @object-ui/types main barrel deliberately does not re-export the zod validators (its own comment: "The companion zod validators (…Schema) are deliberately NOT" on the main entry), and the tree's two other importers emit zero bytes.
Probe and its must-miss control, same command, both trees:
types/zod/objectql.zod.ts marker .describe('ObjectQL object name') main=[framework] head=[framework]
types/zod/crud.zod.ts marker .describe('Action execution mode for chaining') main=[ABSENT] head=[ABSENT]
⇒ 9 modules whose only live consumer is a lazy chunk are written onto the eager, budgeted line, because the group regex claims every packages/types/ module wherever it is reached from. Same defect class as objectui#9345, one tier over — and pre-existing on main, which is why it belongs on its own card and not in this PR.
Cost of moving it: its own advancedChunks group; the new chunk is reached only from plugin-map, so it becomes lazy and the bytes leave the eager closure instead of moving to another eager line. The object-map view pays one extra fetch when it opens; every other page load stops paying.
Saving, as a bracket rather than a figure — the exact number needs a counterfactual build this round may not make:
| estimator | saving | leaves framework at |
|---|---|---|
| per-module gzip share | 20,695 | 74,759 |
| rendered share | 26,322 | 69,132 |
The floor needs 4,568. Both ends clear it by 4.5x to 5.8x, and both land the chunk back around its PER_CHUNK_BASELINE.framework of 72,245.
4. Pricing the raise — what the machinery would stop seeing
The ceiling would have to become at least 104,568 for the row to clear the 0.10x floor at 95,454 (the 0.30x this row historically carried would put it at 122,795). Legal ceiling under 186,590 by the live blind-gauge rule, and under 163,381 by the unit test's ceiling - PER_CHUNK_BASELINE.framework constraint — 104,568 satisfies both without moving the baseline.
What it costs, quoted from scripts/check-eager-closure-budget.mjs's own header rather than paraphrased:
The aggregate is one number over every chunk in the closure. Inside its headroom, a single chunk can grow by the whole allowance while every other chunk shrinks by the same amount, and the gate reports a green tick either way. [...] The aggregate says whether the payload grew; these say WHERE.
The headroom must stay SMALLER than the regression the gate exists to catch. objectui#5266 was 89 KiB = 91,136 bytes, and this ceiling is built to carry half of that, so it would have failed on that change. Widening the headroom past ~89 KiB would leave the gate green through a repeat of its own motivating incident.
And the exhausted leg's own printed remedy and prohibition, from this very run:
The remedy is the bytes: take them out of the chunk this row names, or take a deliberate, authorised re-pin and say in the PR what the new headroom buys.
⛔ Never raise a ceiling because this leg noticed it is full — that is how a budget gets switched off rather than met.
⇒ concretely blind to 9,114 bytes of framework growth at 104,568, or 27,341 at 122,795, in the chunk that holds the whole engine. The aggregate keeps 47,705 bytes (0.52x) of its own, so a framework-only regression between those figures would still be caught in total but no longer attributed — which is the one capability the per-chunk lines exist to add.
Measured by the domain:ui#2 execution seat, Claude Code session session_018HrVaotisyhgmot9o2MLRq — written here as prose and as a code span so the reference survives a body rewrite. ⛔ Nothing in this round flipped a state, moved a constant, or touched a label.
Generated by Claude Code
Generated by Claude Code
|
Measurement round ACCEPTED — and it FALSIFIED the premise this PR has been parked on. The premise is gone:
|
PR body (anchored at ff1d5ea8d1) |
today, on this branch merged with origin/main |
|
|---|---|---|
framework gzipped |
104,636 — over by 4,636 | 95,454 — under by 4,546 |
| the ceiling | 100,000 | 100,000, unchanged |
⇒ the row moved down 9,182 bytes because main moved, ⛔ not because anything here was re-pinned. ⭐ The body's tables are old, ⛔ not stale: they name the commit they were measured on, which is the anchored form this repo asks for.
What is actually red now — a different leg, a different exit code
pnpm check:eager-closure still fails, but every byte ceiling passes and the membership half passes. The failing leg is the exhausted-headroom sensitivity check: headroom 4,546 = 0.0499x against the gate's 0.10x floor. So it exits 2 — an error about the GAUGE, where it used to exit 1 — a verdict about the bundle.
⇒ The decision is no longer 「raise the ceiling or get under it」. It is: restore at least ~4,568 gzipped bytes of headroom on the framework row.
⭐ And this PR's own contribution is a net removal: the pair falls 617 gzipped bytes, the whole eager closure falls 822, the eager chunk count is unchanged at 329 of 2309. The row went thin because the bytes were returned to the line that weighs them — which is this repair working.
The two options, and why one is refused by the gate's own words
- SHRINK —
packages/types/src/zod/**is 27.6% of the eagerframeworkchunk while its only live consumer sits in a lazy chunk. Giving it its own group takes those bytes out of the eager closure: bracketed at 20,695 – 26,322 gzipped, 4.5×–5.8× what the floor needs.⚠️ Reproducible onorigin/maintoo, so it is its own card, now filed as objectui#10065. - RAISE — the gate's own printed prohibition, quoted: 「⛔ Never raise a ceiling because this leg noticed it is full — that is how a budget gets switched off rather than met.」 A raise to 104,568 would make the gate blind to 9,114 bytes of growth in the chunk that holds the whole engine; the aggregate would still catch a regression that size but would no longer attribute it, which is the exact capability the per-chunk lines exist to add.
⛔ What this seat will NOT decide
Whether this PR may land with Bundle Analysis red on that one leg. The dev recommends landing it on condition the shrink card exists and is linked — objectui#10065 now exists and is linked from here. But 「⛔ 永不合红的 PR」 is a red line for this seat, and an exit 2 about the gauge is still a red check.
⇒ The maintainer's call, and it is a narrow one: land objectui#9488 with the sensitivity leg red and carry the debt on objectui#10065, or hold this PR until that card lands — the cost of holding being that the membership pin, the only thing in the tree that can see a whole package leaving a budgeted chunk, stays unlanded while it waits.
⛔ Nothing was flipped, ⛔ no label touched, ⛔ the PR stays draft. The only push was the merge of origin/main needed to measure (d2ac39197..86d130c6d), which the dispatch authorised and the dev declared.
Generated by Claude Code
|
Two conditions under this PR moved today, and both narrow the decision it is parked on. Posted by the 1. objectui#10065 is no longer a thing to wait forIts delivery is PR #10082 — head 2.
|
Three conflicts, each resolved to keep BOTH rulings: - apps/console/vite.config.ts: this branch's `includeDependenciesRecursively: false` on `data-adapter` (objectui#9345) AND main's `types-zod` group at priority 82 (objectui#10065, #10082). The data-adapter note's anchored reading is relabelled as the historical reading it is. - scripts/check-eager-closure-budget.mjs: the membership half from this branch, and main's RETIRED exhausted-headroom block (objectui#10148) in place of the leg it replaced. - scripts/__tests__/check-eager-closure-budget.test.ts: this branch's options-capturing group parse, which subsumes main's `[,}]` tail and still sees `types-zod`; the parse note names both groups that carry an option. The membership declaration still says `packages/types` lands wholly in `framework`, which #10082 makes false by design; the next commit declares the split. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
…hip pin, exactly objectui#10082 moved `packages/types/src/zod/**` into a lazy `types-zod` group on purpose, so this branch's membership pin — `packages/types` lands WHOLLY in `framework` — is false by ruling after the merge. The pin now declares the split instead of dropping the package or loosening it. `PER_CHUNK_MEMBERSHIP_CARVE_OUTS` names each exception: a package-relative subtree of a declared package and the ONE chunk it must land in. A carved package is judged per directory: every module under the subtree in the carve-out's chunk, every other module in the declared chunk, nowhere else, and each side holding at least one module. That is what tells the healthy split apart from the failure #10082's first build produced — shared `src/` neighbours absorbed into `types-zod` along an import — which a per-package count reads as the same thing. The keys stay limited to budgeted chunks. A carve-out's destination may carry no ceiling only while it is absent from the SAME build's eager closure (read from `eager-closure.json`); a carve-out into an unbudgeted eager chunk is an error, since that is objectui#9345's incident written down as a ruling. Nested, self-targeting, undeclared-package and malformed carve-outs are errors too, as are a carved subtree or remainder holding nothing and directory counts that do not add up to the package counts. `chunk-membership.json` goes to v2 and adds `directories` (package -> package-relative directory -> chunk -> count), so a stale v1 artifact is refused. The emitter reads no declaration. A unit cross-check against the console config requires the carve-out group's own test to take the subtree and miss the rest of the package, the declared chunk's test to claim the subtree too, the carve-out group to outrank it, and to declare `includeDependenciesRecursively: false`. No ceiling, baseline or exemption constant moves. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
✅ Console Performance Budget
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
Size Limits
|
… pronoun blindness (objectui#9727) (objectstack-ai#9752) Fixes objectstack-ai#9727 Clause-②: no The first step on this card is not repairing 53 changeset entries. It is rebuilding the instrument, because both tables the card carries are transcribed and the filing seat marked them NOT MEASURED: the producing script was cleaned up with its author's worktree, so no number on that card is re-derivable. **This pull request edits no changeset body.** It adds a measurement instrument, its pins, and one adjudication. ## The rebuilt instrument `pnpm census:changeset-polarity` (`scripts/changeset-polarity-census.mjs`), report-only, wired into no workflow — `census:*` is this tree's spelling for runnable, reported, not blocking. **P, the population.** Every sentence of every pending `.changeset/*.md` except `README.md`, matched when the sentence names a schema symbol and carries a present-tense declaration verb. Bare past `declared` is excluded on purpose: a historical sentence cannot rot, which is the distinction objectui#9713 turns on. Present-tense passive (`is declared`) is included, because it is a live claim and rots like the active voice. **V, the verdict.** For each (schema, key) pair a matched assertion predicates, membership is resolved against that schema's member set and compared with the sentence's polarity. A positive claim is contradicted when the key is absent; a negative claim when it is present. Four properties the rebuild has that the transcribed instrument did not: 1. **Whole-file, whitespace-tolerant read.** This corpus wraps near eighty columns, so a line-anchored probe is structurally blind to any claim that wraps — and a claim naming a schema *and* a key is long enough that most of them do. Lines are joined per paragraph, continuation prefixes (blockquote markers, list bullets, table pipes) stripped first, all whitespace runs collapsed before a sentence is cut. Backticked spans are masked during the cut so `foo.md` never splits a sentence. 2. **Assertion position distinguished from quoted position.** A count over prose is *not* invariant under quotation, and treating it as invariant is how a repaired site gets re-flagged forever. objectui#9713's repair quotes, in guillemets, the very sentence it retires. A sentence reached through a fence, a blockquote or quotation marks is counted, reported, and never flagged. Measured on this tree: that repaired 8802 sentence is the corpus's **one** quoted match, and it draws **zero** flags. 3. **A name is not a key — a key is (interface, name).** Membership is built with the TypeScript parser over `packages/types/src`, across both published faces (the `interface` and its zod mirror), with `extends` resolved transitively. The zod walk is **structural**: only the literal passed to `z.object()` / `.extend()`, the arms of a union, and the base of a `.merge()` / `.and()` count. A schema named in a member's *value* (`columns: z.array(ObjectKanbanLaneSchema)`) is a different object — walking into it read a lane's `cards` as a member of the *board*, which is the over-approximation this rule forbids, and fixing it is what removed those flags. An inherited index signature (`[key: string]: any`) is deliberately **not** membership: a key riding it is admitted and never examined, which is the condition the whole family of cards is about. 4. **The cross-sentence pronoun** — below. ## The blindness closed, and its pin The transcribed instrument missed **the site that motivated the card**. The claim in `.changeset/8802-8257-8008-kanban-gantt-family-retirement.md` reads, across a sentence boundary: > ... the only one that refused `allowCollapse` / `cardTemplates` / `columnWidths` / `titleField` / `draggable` / `onColumnAdd` / `onCardAdd` by name, and — through `columns: KanbanColumn[]` — the only one that judged a lane's `cards`. **The surviving `ObjectKanbanSchema` face declares none of them.** The object is `them`. A matcher that reads only the sentence carrying the verb finds a schema, finds no key of its own, and reports nothing. An instrument blind to the shape that produced it reports a **floor**, not a census. The rebuild resolves a pronoun object (`them`, `these`, `those`, `none of them`, `either`, `both`) from the nearest preceding sentence in the same paragraph that names any key. The pin lives in `scripts/__tests__/changeset-polarity-census.test.ts` and carries the **pre-repair** text of that site as a fixture, so it survives a shallow clone where `adf581278` is unreachable. ⭐ **The pin is stronger than "found".** Run against that pre-repair text, the rebuilt instrument returns exactly `columns`, `cardTitle`, `titleField`, `allowCollapse` — the four keys objectui#9713 adjudicated, and no others. The other keys that sentence names are genuinely absent from the surviving face, so a negative claim about them is *true* and is correctly not flagged. Reproducing a known-good human adjudication from a fixture is the strongest available evidence that the rebuild reads correctly. Both halves are asserted: the flags are found, and every one of them is reached `viaPronoun`. ## The re-derived count, against the card's 154 Read these from a run, not from this paragraph (commandment objectstack-ai#9). At `1cfdff814` plus this branch, `pnpm census:changeset-polarity` prints: | | card (transcribed, `dea17b469`) | this rebuild | |---|---|---| | entries scanned | 1642 | 1654 | | matched, assertion position | 199 across 169 | 218 across 176 | | matched, quoted position | not distinguished | 1 (reported, never flagged) | | object resolved across a sentence boundary | 0 — the blindness | 9 | | **candidate contradictions** | **154 across 53** | **134 across 58** | | claims naming a schema this tree does not declare | not reported | 61 | Controls, same corpus and same instrument, on every run — the run exits 2 and voids its own numbers if either fails: | control | probe | reading | expected | |---|---|---|---| | corpus lit | present-tense declaration verb | 931 | > 0 | | corpus absent | `zzqqNoSuchMemberZZ` | 0 | 0 | | member lit | `BaseSchema.type` | 1 | 1 | | member absent | `BaseSchema.zzqqNoSuchMemberZZ` | 0 | 0 | **It differs from 154, and it differs in both directions.** Flags are **down** (154 → 134) while entries are **up** (53 → 58), which is not a contradiction — they move for different reasons: - **Down**, from precision. A backticked PascalCase token is a *type* name (`ObjectGridComponentProps`, `GanttConfig`), never a key, and reading one as a key inflated the count. The structural zod walk stopped a referenced schema's keys counting as the referrer's. And a sentence in quoted position is no longer flagged at all. - **Up**, from sensitivity. The whole-file whitespace-tolerant read and the pronoun resolution both find sites a line-anchored, sentence-local matcher could not see — nine flags on this tree are reached across a sentence boundary alone. The corpus also grew by 12 entries between the two readings. ⛔ 154 was never reproduced by construction, and the rebuild was not tuned toward it. The two numbers are readings of different instruments over different trees, and the card's own statement stands: its number was a floor. ## The ten adjudications Sample rule, fixed before looking: **the first ten distinct flagged entries in sorted filename order**. No cherry-picking. Seventeen flags across those ten entries. For each, the first question is whether the flag is a **true positive** at all. Only a true positive can be ROTTED or BORN FALSE — a sentence that is *true today* is neither, and saying otherwise would be a false record of its own. Write-time refs are given for all ten so any reader can re-derive; all ten lie inside this clone's shallow window (boundary `c35fed098`, 2026-08-07), and every reading here is an affirmative one, so no negative leg needs a control. | # | entry | flags | verdict | established at | |---|---|---|---|---| | 1 | `5632-svg-host-dom-passthrough.md` | `SpinnerSchema.icon`, `SpinnerSchema.color` | **TRUE POSITIVE → BORN FALSE** | write-time ref `e304a4ef7` | | 2 | `6011-tosortitems-order-spelling.md` | `SortUISchema.toSortItems`, `.direction` | false positive ×2 | `c5fbe0b99`; true at `1cfdff814` | | 3 | `6051-gantt-flat-config-declared-keys.md` | `ObjectGanttSchema.startDateField`, `.endDateField`, `.titleField` | false positive ×3 | `75bd83d6d`; true at `1cfdff814` | | 4 | `6067-component-meta-derive-from-canonical.md` | `ComponentMetaSchema.tier`, `.namespace`, `.skipFallback`, `.labelling` | false positive ×4 | `44d075ba0`; true at `1cfdff814` | | 5 | `6121-retire-report-data-source.md` | `ReportComponentSchema.data` | false positive | `6414dfd45`; true at `1cfdff814` | | 6 | `6132-undeclared-action-props.md` | `DropdownMenuSchema.onOpenChange` | false positive | `e28fbf92f`; true at `1cfdff814` | | 7 | `6150-undeclared-but-consumed-keys.md` | `TreeViewSchema.value` | false positive | `2c45966ff`; true at `1cfdff814` | | 8 | `6169-chatbot-authoring-face-type.md` | `ChatbotSchema.chatbot` | false positive | `52a43ded8`; true at `1cfdff814` | | 9 | `6235-mergedsort-wrap.md` | `ObjectGridSchema.sort` | false positive | `9caa7d474`; true at `1cfdff814` | | 10 | `6247-option-visiblewhen-metadata-admin.md` | `SelectOptionSchema.unrecognized_keys` | false positive | `0ea559e7c`; true at `1cfdff814` | **Entry 1, the one true positive, in full.** The sentence is: > `IconSchema` and `SpinnerSchema` declare only `icon` / `size` / `color`, and both renderers already consume all three by name, so the SDUI pass-through list withholds nothing they need. On `main` at `1cfdff814`, `SpinnerSchema` declares `type` and `size`, plus `body` / `children` as retirement tombstones. It declares neither `icon` nor `color` — those are `IconSchema`'s. The sentence is false of `SpinnerSchema`. **The verdict is BORN FALSE, and it is established at the write-time ref, not today.** At `e304a4ef7` (2026-09-03), the commit that added this entry, `SpinnerSchema` was exactly `{ type, size }`. Nothing later falsified the sentence; it was false the day it was written. Re-derived mechanically by running the rebuilt census with `--corpus` and `--types` both taken from that ref — two flags, `SpinnerSchema.icon` and `SpinnerSchema.color`, with all four controls passing at that ref.⚠️ That historical run is also what found a defect **in this instrument**, and it is recorded because it is this card's own class wearing the instrument's clothes. The member lit control was first spelled `ObjectKanbanSchema.groupBy` — a member objectui#7322 added. At any ref older than that card the control read 0, the run exited 2, and every historical reading voided itself — and a historical reading is *exactly* what establishing BORN FALSE requires. **A control pinned to a fact a later card moves is the defect this card is about.** It is now `BaseSchema.type`, the protocol's own recursion point, and the choice is pinned with its reason. **The nine false positives, and the three limits they name.** All are recorded in the script header so the next reader does not re-derive them: - **Window pairing** (entries 3, 4, 7, 8, 9, 10). The sentence names schema S and key K and predicates K of something else: a registry-local type (`ComponentMeta` in `Registry.ts`, objectstack-ai#4), another node's schema (`TextSchema.value`, objectstack-ai#7; the `list-view` node's `sort`, objectstack-ai#9), a spec schema (`GanttConfigSchema`, objectstack-ai#3), a registration name rather than a key (`chatbot`, objectstack-ai#8), or a zod issue code rather than a key at all (`unrecognized_keys`, objectstack-ai#10). This is the limit the card's own author stated first and it is still the largest source. - **Polarity by keyword** (entries 3, 5, 6, 9). Polarity is read from negation words anywhere in the sentence, so an unrelated clause inverts the verdict. Entry 6 is the clearest: *"None of the three menu schemas declares any event slot (`DropdownMenuSchema` declares `onOpenChange` and nothing else)"* — the parenthetical asserts exactly what the member set says, and `None` flipped it. - **Top-level membership only** (entry 2). `SortUISchema` declares `sort?: Array<{ field; direction }>`, so `direction` is a member of the inline sort *item*, not of the schema — and *"`direction` is the key `SortUISchema` legitimately declares"* is true in context. Descending into inline member objects would re-create the "a name is not a key" failure one level down, so the narrow reading is deliberate and the cost is named rather than paid silently. ⛔ None of the three is a reason to stop reporting a flag. They are the reason a flag is a **candidate**: each is resolved by a human reading the sentence, and none is resolvable by reading the count. ## Recommendation on the grade condition Triage's checkable condition was: true-positive rate materially non-zero ⇒ defect list, p2 confirmed, splittable; ≈ 0 ⇒ re-shape the card into "fix the instrument". **Measured: 1 of 10 entries, 2 of 17 flags.** That is not zero — there is a real, previously unrecorded BORN FALSE claim in the sample, and it is a *second* instance of a class objectui#9713 had already established twice. So the existence question stays answered and `priority:p2` is right. But it is nowhere near a defect list, and **⛔ the number must stop being carried as one**. At the measured yield, splitting the 58 flagged entries into dispatchable items would dispatch roughly fifty no-ops, each costing a history read — which is the same measurement error in a new coat. ⇒ **Re-shape the card to "fix the instrument, then triage its output".** Concretely: the instrument now exists, is re-derivable from a clean checkout, and its three false-positive sources are named and individually attackable — polarity-by-keyword is the cheapest and would clear four of the nine. The 154 (now 134) should be carried as *"whatever `pnpm census:changeset-polarity` prints"*, never as a figure in prose, which is commandment objectstack-ai#9 applied to this card. The one true positive found here is a candidate for its own small card; ⛔ it is **not** repaired in this pull request, because the fence on this dispatch is absolute. ## Boundaries honoured - ⛔ **No changeset body was edited** — not one of the 53, not any other. The only `.changeset/` file this branch touches is the one it adds. -⚠️ `.changeset/6051-gantt-flat-config-declared-keys.md` is in the candidate set and is adjudicated above as a **reading only**. It is being repaired right now by objectui#9743 on another branch, and nothing here touches it. (`.changeset/5903-objectgantt-declared-keys.md` left the candidate set entirely once PascalCase tokens stopped reading as keys.) - ⛔ No published face, mirror, declaration or accept set moved. No issues created. No labels written. - Open-PR intersection re-taken on this branch rather than inherited: nine open pull requests, of which four touch `scripts/` (objectui#9748, objectstack-ai#9584, objectstack-ai#9488, objectstack-ai#8941) and four touch `.changeset/` (objectstack-ai#9540, objectstack-ai#9391, objectstack-ai#8941, and the release PR objectstack-ai#5400). None touches any path this branch adds or modifies. The one shared file is `scripts/markdown-test-inputs.mjs`, which this branch adds one ledger row to and no open pull request edits. ## The changeset grade — measured, not assumed `.changeset/9727-changeset-polarity-instrument-rebuild.md` carries **empty frontmatter**, which is the explicit claim that nothing published moved. That claim is a gate reading: - **absent control / the real diff** — `node scripts/check-changeset-presence.mjs` on this branch: *"10 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved"*, exit **0**. - **lit control, same tree, same gate** — appending one comment line to a file under `packages/types/src` flips the same gate to exit **1** and demands an entry. Reverted in the same run and proven byte-identical by `git hash-object` before and after (`bcebba9b9` both sides), with `git diff HEAD` empty. Two zeros on one instrument would mean a broken instrument. This is one zero against a firing control. ## Gates Exit codes captured to disk before any pipe. | gate | exit | |---|---| | `node scripts/check-changeset-presence.mjs` | 0 | | `node scripts/check-changeset-overwrite.mjs` | 0 | | `pnpm changeset:check` | 0 | | `pnpm check:changeset-claims` | 0 | | `pnpm check:pending-changeset-literals` | 0 | | `pnpm check:control-bytes` | 0 | | `pnpm check:governed-queue-guard` | 0 | | `pnpm check:new-line-citations` | 0 | | `pnpm check:test-path-roots` | 0 | | `pnpm check:required-check-set` | 0 | | `pnpm lint:coverage` | 0 | | `pnpm check:lint-rule-coverage` | 0 | | `pnpm check:esm-specifiers` | 0 | | `pnpm check:node-esm-load` | 0 | | `pnpm type-check:scripts` | 0 | | `pnpm exec eslint` on the three added source files | 0, three files linted | | `pnpm exec vitest run` — the new suite plus `markdown-test-inputs`, `check-changeset-presence`, `check-changeset-claims` | 0 | The governed-surface guard was run at `scripts/check-governed-queue-guard.mjs`, not under `scripts/pm/`. ## The declared file surface, and the two increments beyond it The dispatch declared "a script under `scripts/` plus its test, and a new `.changeset/9727-*.md`". Two files land outside that, both required, both named here rather than left unexplained: - **`package.json` (+1 line)** — the `census:changeset-polarity` entry. Without it the instrument has no invocation the tree knows about, and `census:*` is the spelling that carries "runnable, reported, not blocking". It is a `scripts` key: not one of the eight publish-contract fields, and the root manifest is private, which is part of why the changeset grade measures as nothing-published. - **`scripts/markdown-test-inputs.mjs` (+10 lines)** — one ledger row. That ledger asserts every tracked markdown document is an input to some test, and the six fixture documents this change adds would otherwise be uncovered and red it. The row declares what the new test reads and that it walks a markdown tree; the scanner re-derives the same answer from the source, which is why the test spells its fixture paths as literal segment runs. Both are additive and neither is touched by any open pull request. ## The fixture names ARE the trap list They encode the failure modes this lane actually hit, not a happy path: | fixture | the trap it holds open | |---|---| | `01-pronoun-pre-repair.md` | the proven false negative — a claim whose object is a cross-sentence pronoun, carried at its pre-repair text | | `02-quotation-position.md` | a count is **not** invariant under quotation; a repaired sentence quoted in its own retirement note must never be re-flagged | | `03-wrapped-assertion.md` | prose wraps near eighty columns, and it carries its own **dark leg**: the pin asserts a line-anchored probe reads nothing on that exact file, so the lit reading proves the tolerance rather than assuming it | | `04-past-tense-only.md` | a historical sentence cannot rot and stays out of the population | | `05-referenced-schema-scoping.md` | a name is not a key — a referenced schema's members are its own | | `06-index-signature-is-not-membership.md` | a key riding `[key: string]: any` is admitted and never examined; counting it as declared erases the finding | ## ⭐ A hand-typed entry guard, caught by CI and fixed — the same defect, in this instrument The first push carried a hand-typed `process.argv[1]` entry guard, and `check:entry-guard` failed the Lint job on it. **It is this script's own defect class.** Node resolves symlinks for the module graph but leaves `process.argv[1]` as the caller typed it, so a census reached through a symlink compares two different paths, answers false, does nothing, and exits **0 with no output** — a clean-looking zero from an instrument that never ran, inside an instrument whose entire purpose is to stop false zeros being reported as measurements. A wrapper holding only `result.status` cannot tell that apart from a pass. It now routes through the one predicate, `isEntrypoint` from `scripts/invoked-as.mjs`, with the reason recorded at the guard. ⛔ `KNOWN_HAND_TYPED_GUARDS` was not touched — it is shrink-only, and adding a baseline line would have been the false record rather than the fix. | | exit | |---|---| | `node scripts/check-entry-guard.mjs` **before** | **1**, naming `scripts/changeset-polarity-census.mjs` | | `node scripts/check-entry-guard.mjs` **after** | **0** — 103 scripts, 0 hand-typed guards | | `node scripts/check-entry-guard.mjs --self-test` | 0 — 63 cases | | `node scripts/invoked-as.mjs --self-test` | 0 — 12 cases, real symlinks | | census re-run after the fix | 0, controls PASS, reading unchanged | | the instrument's own suite re-run | 0 — 47 pins | The remaining Lint-job guard steps were run locally rather than eslint alone: `check-lint-coverage`, `check-entry-guard`, `check-upstream-port-parity`, `check-bash32-floor`, `check-vi-mock-override-shape`, `check-test-path-roots`, `check-cross-repo-closer-outcome` — all exit 0.⚠️ One gate is **NOT MEASURED** here and is not this change's: `pnpm check:node-esm-load` (its own workflow, not Lint) exits 1 in this container because turbo shares one cache across every worktree of a checkout and two entries — `@object-ui/auth`, `@object-ui/react-runtime` — were replayed from a sibling agent's worktree. The gate refuses to grade artifacts this tree did not produce, which is correct behaviour; the refusal is about the shared cache, not about this diff, which touches no package source. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…stack chunk group (objectui#10920) (objectstack-ai#10921) Fixes objectstack-ai#10920 ## What broke objectstack's `build-console.sh` always sets `OBJECTSTACK_CLIENT_DIST` to the framework's own `packages/client`. That path is outside `node_modules`, so the `vendor-objectstack` group test never matched the injected client. Before objectstack-ai#9488 the recursive `data-adapter` group absorbed the client anyway, which hid the gap. Once objectstack-ai#9488 made `data-adapter` non-recursive, the client landed in `framework`. `framework` imports `data-adapter`, through `packages/react`'s `error-message.ts`. `data-adapter` then `require`s the CJS client from `framework` at its top level, before `framework` has defined it. On `9f0c84a44` the console built this way stays on the splash screen with `TypeError: u is not a function`. ## The fix `widenVendorChunkTestForClient` widens the group's test with the injected client's package directory. The spec hook already does the same for an injected spec. When nothing is injected, the test is returned unchanged, so production and CI builds do not move. ## Evidence - Reverse verification: the three new cases pass. With `vendorObjectstackTest` wired back to the bare expression, only the config-level case fails (1 red, 21 green). That case is the one that pins the wiring. - `pnpm exec vitest run scripts/__tests__/vite-objectstack-client-dist.test.ts`: 22 passed. - `pnpm type-check:scripts` and `pnpm type-check:vitest-config`: clean. - `node scripts/check-changeset-presence.mjs`: no changeset is owed, because no published source moved. - End to end, with the same change applied to objectstack's build tree at `9f0c84a44`: `chunk-membership.json` reads `client: { "vendor-objectstack": 1 }`, the console boots to `/login`, and a 95-surface sweep of the showcase and Setup apps shows no page errors. --- _Generated by [Claude Code](https://claude.ai/code/session_local_f929252f-7e71-4327-b98b-bd46424b5e05)_ Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Part of #9345 — route B, director seat decision batch 128, item 2. Six of the card's seven acceptance criteria are met on the merged head. Criterion 3 is only partly met: its clause "
frameworkback above its baseline" is not met, becauseframeworkreads 70,020 gzipped bytes against its 72,245 baseline. It cannot be met here without undoing objectui#10082's ruled split or re-pinning a baseline, and both are forbidden. The criteria and their readings are below.Round 3 of this pull request, by session
session_013RDBh5DqXd2xnLwvHLgLFr, on the claim holder's order (card comments5825422033and5826915183). This round mergesmainin, keeps both rulings, and declares objectui#10082'spackages/typessplit in the membership pin, exactly. Every table below was re-measured on the merged head48387d4d1, which ismainat2a943bf0cplus this branch. Readings from earlier rounds are kept at the end, labelled with the tree they were taken on.1. The merge — both rulings kept
ff14e29b5, andmainhad since moved 26 commits, some of them inpackages/coreandpackages/types. Those are the packages the pin names, so measuring on the stale merge would have described a tree that does not exist.git merge-tree --write-treeof the same two commits.mainwas merged again at2a943bf0c, pinned by SHA into a ref this round named itself. The result is merge commit0933e6f7f. There was no rebase and no force-push.apps/console/vite.config.tscarries this branch'sincludeDependenciesRecursively: falseondata-adapter(priority 84) and objectui#10082'stypes-zodgroup at priority 82 with its ownincludeDependenciesRecursively: false.frameworkstays at 80.scripts/check-eager-closure-budget.mjscarries the membership half andmain's RETIRED block for the exhausted-headroom leg (objectui#10148, ci(bundle-analysis): retire the exhausted-headroom leg, keep every ceiling #10151). That leg no longer exists onmain. The sensitivity half that remains passes on every leg below.scripts/__tests__/check-eager-closure-budget.test.tsuses this branch's group parse, which captures the options tail. That parse coversmain's shorter tail and still seestypes-zod.2. The
packages/typessplit, declared exactlyBefore this round the pin said "
packages/typeslands WHOLLY inframework". After objectui#10082 that is false by ruling. This round neither drops the package nor loosens the claim. It declares the exception:How a carved package is judged. A carved package is judged per DIRECTORY rather than per package:
packages/types/src/zodmust land intypes-zod;packages/typesmust land inframework;The rule stays EXACT: one stray module is a finding, named by its directory. The per-directory counts are what tell the healthy split apart from the failure objectui#10082 met on its first build, where shared
src/neighbours were absorbed intotypes-zodalong an import. A per-package count reads those two cases identically.The gate's rules still hold.
eager-closure.json. A carve-out into an unbudgeted EAGER chunk is refused as an error, because that is objectui#9345's own incident written down as a ruling. That is also why no carve-out can say "any chunk".includeDependenciesRecursively: false.Refused as errors, never passed:
The emitter.
chunk-membership.jsongoes to v2 and addsdirectories, keyed package, then package-relative directory, then chunk, then count. A stale v1 artifact is therefore refused. The emitter reads no declaration.3. Membership, from a real build: before and after this PR's config, same merged tree
turbo run build --filter='@object-ui/console^...' --concurrency=2: 34 of 34 tasks, 0 cached, exit 0. Thenpnpm --filter @object-ui/console buildran for each leg. The BEFORE leg is the merged tree withdata-adapter's narrowing deleted. That ismain's own grouping rule, with this branch's emitter present so the leg can be read. Attribution comes from each emitted chunk's ownmodules, never from the config.48387d4d1)packages/coredata-adapter, 26 inframeworkframeworkpackages/types, outsidesrc/zoddata-adapter, 10 inframeworkframeworkpackages/types/src/zodtypes-zodtypes-zodpackages/reactframeworkframeworkpackages/data-objectstackdata-adapterdata-adapterpackages/components/fieldsui-componentsui-componentsAfter the repair,
data-adapterholds exactly its declared regex and nothing else. On this tree the defect reaches 52 ofpackages/core's 78 modules, where earlier rounds saw 92 of 92.mainhas changed underneath:5e0b22c2c. At2a943bf0cthe grep for its cast reads 0, while a control grep finds the dynamic import it narrowed still present.packages/coreitself.4. Bytes, same two legs (gzipped at zlib's default level, as the emitter weighs them)
framework(eager, ceiling 100,000)data-adapter(eager, no ceiling)framework+data-adaptertypes-zod(lazy, not in the closure)data-adapteris in the eager closure. This is a re-attribution, not a payload change.data-adapterreturns to the neighbourhood of the 19.6 K that the gate's exemption rests on, as the ruling's point 3 required.frameworksits under its ceiling with 29.3 KB of headroom.eager-closure.json's owngzipBytes, and a re-gzip of the file on disk.vite buildruns, not cache restores, and theireager-closure.jsonandchunk-membership.jsonare byte-identical.pnpm check:eager-closureon the AFTER build exits 0, and every half is green:packages/typesas "13 modules inframeworkand itssrc/zodsubtree 9 intypes-zod, exactly";The freshness half is not applicable to a local run.
check:eager-locale-cataloguesexits 0. ⛔ No ceiling, baseline or exemption constant moved.5. Firing controls, proven on disk
Every mutation below went through
ablation-replace.mjsin wrap mode:HEAD's andgit diff HEADto be empty.A first attempt at leg A used a replacement that contained its own anchor, so the tool refused it as a no-op before anything was built. It was re-run in delete mode, which is the reading below.
A. The defect itself:
data-adapter's narrowing deleted, then a rebuild.The blob changed from
b9afdeae39edto15c4e3fe6fbd.check:eager-closureexited 1. Every byte ceiling was green, withframeworkat 42.0 KB and 55.7 KB of apparent headroom. The membership half fired:The static unit pin fired too: 1 failed of 177, namely "narrows
data-adapterto its own regex".Restore: the blob returned to
b9afdeae39ed, equal toHEAD, andgit diff HEADwas empty.A rebuild then exited 0 with membership
pass.B. The split declaration, mis-declared. Each case below ran against the real AFTER build's artifacts, and each was restored with the same proof.
PER_CHUNK_MEMBERSHIP_CARVE_OUTScheck:eager-closurepasspackages/typesis declared inframeworkbut 9 intypes-zodplugin-mappackages/types/src/zod9 intypes-zod(declaredplugin-map)data-adapter(eager, no ceiling)data-adapter, which is in the EAGER closure and carries no ceiling6. Verification, on
48387d4d1turbo run build --filter='@object-ui/console^...'pnpm --filter @object-ui/console build, three timestypes-zodlazy guard reports lazy and still reachable, on every buildnode scripts/check-eager-closure-budget.mjsnode scripts/check-eager-locale-catalogues.mjsenresident, 9 deferredfind/gziprecipeindex-B_Gx2D6D.js: 151,396 gzipped bytes against the 350 KB linepnpm check:sdui-registration-pinsnode scripts/render-budget-comment.mjs, fed the gate's own outputsclosure_membership_status=passpnpm exec vitest run scripts/pnpm type-check:scriptstsc -b apps/console/tsconfig.node.json --forcecheck:control-bytes,check:new-line-citations,check:test-path-roots,check:unreferenced-sourcesnode scripts/check-changeset-presence.mjsLint was narrowed, and the narrowing is declared.
eslint --no-inline-config --format jsonran over all five lintable files in this diff: four from the root config, andapps/console/vite.config.tsfrom its package. Result: 0 errors and 0 warnings. The file count comes from the JSON output. The.ymlfile is outside eslint's population. A file this diff does not touch cannot change its verdict, for two reasons:eslint.config.jsconfigures noprojectServiceorparserOptions(grep exit 1; control grep forrulesexit 0), so type-aware linting is off.eslint-rules/reads the filesystem (grep exit 1; control grep exit 0 with 11 hits).Exit codes were captured by redirecting to a file and then reading the code, never across a pipe.
NOT MEASURED:
check:docs-route-closure: it needsapps/site's build, and nothing here reaches it. Its tests ran insidescripts/.pull_requestmerge-ref environment../packages/*, while this round built the console's dependency closure.gates_weakened:NONE. Thepackages/typesassertion is replaced by an exact split. The only modules it admits outsideframeworkare the ruled subtree, and it holds that subtree to one chunk. The carve-out's destination must stay out of the eager closure, and it is refused as an error the moment it does not. No assertion was deleted and none was given headroom.7. #9345's acceptance criteria, read on the merged head
Quoted from the card's dispatch (comment
5662155820):CodeSplittingGroup.includeDependenciesRecursively, defaulttrue. Firing control A reproduces it on today's tree.packages/corelands inframeworkafter a real build, shown by the emitted chunk's own module list." ✅ 78 inframeworkand 0 anywhere else, fromchunk-membership.json, which is built fromoutput.modules.frameworkback above its baseline and under its ceiling,data-adapterback down."data-adapterback down: ✅ 46,837 → 19,098.src/zodsubtree out offrameworkinto a lazy chunk, and the 72,245 baseline was measured on3f775eeb8with those validators inside. Counting the split's two chunks together gives 98,751, but that is not the criterion's measure, and it is shown only to explain the gap.gates_weakened:NONE, or name exactly what and why." ✅ Section 6.⇒ this is
Part of. The unmet clause of criterion 3 needs one of two dispositions from the seat or the maintainer:frameworkceiling, which is wherePER_CHUNK_BASELINE.frameworkwould be revisited.⛔ This round moves neither constant.
8. Changeset: none owed, measured
node scripts/check-changeset-presence.mjsexits 0: "6 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved".packages/core/src/index.ts, applied throughablation-replace.mjs, made it exit 1 with "1 source file(s) of 1 released package(s) changed, and this change adds no changeset".HEAD,git diff HEADempty, and a re-run exits 0.The console build config is not published source under that script's rule. This round's card claim carries no
Clause-②:line, so none is written here.Scope
mainis these six files:apps/console/vite.config.ts, the grouping rule and the emitter;scripts/check-eager-closure-budget.mjsand its test;scripts/render-budget-comment.mjsand its test;.github/workflows/performance-budget.yml, one env line.apps/console/src/orpackages/app-shellchanged, and objectui#9185's dynamic import is untouched.Historical readings: anchored to the trees they were taken on, ⛔ not re-derived
These are kept as the record, and must not be read as current. The figures in force are in sections 3 and 4.
Round 1, measured on
ff1d5ea8d1:packages/corewent from 92 modules indata-adapterand 0 inframework, to 92 inframework.packages/typeswent from a 19/3 split to 22 inframework.frameworkwent 45,278 → 104,636 gzipped, which was 4,636 over its ceiling. The repair made a pre-existing overage visible.data-adapterwent 78,110 → 18,537.check:eager-closureexited 1 on theframeworkrow.Round 2, on the
86d130c6dmerge (PR comment5749288190):frameworkread 95,454, under the ceiling.check:eager-closureexited 2 on the exhausted-headroom leg, at 0.0499x against a 0.10x floor.mainhas since retired that leg (objectui#10148), and objectui#10082 took the zod validators off this chunk.frameworkchunk it weighed no longer exists in that shape.Generated by Claude Code