Skip to content

fix(console): return packages/core to the framework chunk by grouping rule, and pin chunk membership - #9488

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-9345-core-chunk-membership
Sep 25, 2026
Merged

os-support-ai merged 4 commits into
mainfrom
claude/issue-9345-core-chunk-membership

Conversation

@claude

@claude claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

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 "framework back above its baseline" is not met, because framework reads 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 comments 5825422033 and 5826915183). This round merges main in, keeps both rulings, and declares objectui#10082's packages/types split in the membership pin, exactly. Every table below was re-measured on the merged head 48387d4d1, which is main at 2a943bf0c plus 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

  • The previous round's worktree held an uncommitted merge of ff14e29b5, and main had since moved 26 commits, some of them in packages/core and packages/types. Those are the packages the pin names, so measuring on the stale merge would have described a tree that does not exist.
  • Before aborting, the in-progress merge was checked for manual work, and it held none:
    • Every stage-0 index entry equalled a clean git merge-tree --write-tree of the same two commits.
    • The three conflicted files differed from that output only in the conflict-marker label.
  • It was aborted, and main was merged again at 2a943bf0c, pinned by SHA into a ref this round named itself. The result is merge commit 0933e6f7f. There was no rebase and no force-push.
  • Three conflicts, each resolved to keep both sides:
    • apps/console/vite.config.ts carries this branch's includeDependenciesRecursively: false on data-adapter (priority 84) and objectui#10082's types-zod group at priority 82 with its own includeDependenciesRecursively: false. framework stays at 80.
    • scripts/check-eager-closure-budget.mjs carries the membership half and main'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 on main. The sensitivity half that remains passes on every leg below.
    • scripts/__tests__/check-eager-closure-budget.test.ts uses this branch's group parse, which captures the options tail. That parse covers main's shorter tail and still sees types-zod.
  • No lockfile or generated file conflicted, and this branch changes no manifest, so nothing needed regenerating.

2. The packages/types split, declared exactly

Before this round the pin said "packages/types lands WHOLLY in framework". After objectui#10082 that is false by ruling. This round neither drops the package nor loosens the claim. It declares the exception:

export const PER_CHUNK_MEMBERSHIP_CARVE_OUTS = Object.freeze({
  types: Object.freeze([Object.freeze({ subtree: 'src/zod', chunk: 'types-zod' })]),
});

How a carved package is judged. A carved package is judged per DIRECTORY rather than per package:

  • every module under packages/types/src/zod must land in types-zod;
  • every other module of packages/types must land in framework;
  • nothing may land anywhere else;
  • each side must hold at least one module.

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 into types-zod along an import. A per-package count reads those two cases identically.

The gate's rules still hold.

  • The declaration keys stay limited to chunks that carry a ceiling.
  • A carve-out's destination is not a key. It may carry no ceiling only while it is ABSENT from the same build's eager closure, which the gate reads from 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".
  • A unit cross-check against the console config requires all of the following:
    • the carve-out group's own test matches the subtree;
    • the declared chunk's test also matches the subtree;
    • the carve-out group's test MISSES the rest of the package (the must-miss control);
    • the carve-out group outranks the declared chunk;
    • the carve-out group declares includeDependenciesRecursively: false.

Refused as errors, never passed:

  • nested carve-outs;
  • a carve-out into the package's own chunk;
  • a carve-out of an undeclared package;
  • a malformed subtree;
  • a carve-out with an unreadable eager closure;
  • a subtree or remainder that holds nothing;
  • directory counts that do not add up to the package counts.

The emitter. chunk-membership.json goes to v2 and adds directories, 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

⚠️ Built first. The console's dependency closure was built with turbo run build --filter='@object-ui/console^...' --concurrency=2: 34 of 34 tasks, 0 cached, exit 0. Then pnpm --filter @object-ui/console build ran for each leg. The BEFORE leg is the merged tree with data-adapter's narrowing deleted. That is main's own grouping rule, with this branch's emitter present so the leg can be read. Attribution comes from each emitted chunk's own modules, never from the config.

package BEFORE (flag removed) AFTER (48387d4d1)
packages/core 52 in data-adapter, 26 in framework 78 in framework
packages/types, outside src/zod 3 in data-adapter, 10 in framework 13 in framework
packages/types/src/zod 9 in types-zod 9 in types-zod
packages/react 65 in framework 65 in framework
packages/data-objectstack 8 in data-adapter 8 in data-adapter
packages/components / fields 205 / 81 in ui-components 205 / 81 in ui-components

After the repair, data-adapter holds exactly its declared regex and nothing else. On this tree the defect reaches 52 of packages/core's 78 modules, where earlier rounds saw 92 of 92. main has changed underneath:

  • objectui#9492 landed as 5e0b22c2c. At 2a943bf0c the grep for its cast reads 0, while a control grep finds the dynamic import it narrowed still present.
  • Other commits changed packages/core itself.

⚠️ Which of these accounts for the smaller count was not measured separately. The rule that moves the modules is the same, and firing control A below shows it.

4. Bytes, same two legs (gzipped at zlib's default level, as the emitter weighs them)

chunk BEFORE AFTER delta
framework (eager, ceiling 100,000) 42,997 70,020 +27,023
data-adapter (eager, no ceiling) 46,837 19,098 −27,739
framework + data-adapter 89,834 89,118 −716
types-zod (lazy, not in the closure) 28,733 28,731 −2
whole eager closure 3,118,280 3,117,418 −862
eager chunks / total 329 / 2310 329 / 2310 0
  • The bytes were always downloaded, because data-adapter is in the eager closure. This is a re-attribution, not a payload change.
  • data-adapter returns to the neighbourhood of the 19.6 K that the gate's exemption rests on, as the ruling's point 3 required.
  • framework sits under its ceiling with 29.3 KB of headroom.
  • Every figure above was read twice and agreed: eager-closure.json's own gzipBytes, and a re-gzip of the file on disk.
  • The AFTER leg was built twice, once before the firing control and once after its restore. Both builds were real vite build runs, not cache restores, and their eager-closure.json and chunk-membership.json are byte-identical.

pnpm check:eager-closure on the AFTER build exits 0, and every half is green:

  • the aggregate closure, at 3044.4 KB against 3104.5 KB;
  • all four per-chunk ceilings;
  • membership: "5 budgeted package(s) each landed exactly where declared", naming packages/types as "13 modules in framework and its src/zod subtree 9 in types-zod, exactly";
  • the sensitivity half, with all five ceilings within one regression.

The freshness half is not applicable to a local run. check:eager-locale-catalogues exits 0. ⛔ No ceiling, baseline or exemption constant moved.

5. Firing controls, proven on disk

Every mutation below went through ablation-replace.mjs in wrap mode:

  • the anchor must hit exactly once;
  • the blob change is verified on disk;
  • a restore is armed on EXIT, INT and TERM against the absolute path;
  • after the restore, a separate check requires the worktree blob to equal HEAD's and git diff HEAD to 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 b9afdeae39ed to 15c4e3fe6fbd.

  • check:eager-closure exited 1. Every byte ceiling was green, with framework at 42.0 KB and 55.7 KB of apparent headroom. The membership half fired:

    ❌ `packages/core` is declared in `framework` but 52 in `data-adapter` (26 of its 78 modules landed in `framework`)
    ❌ `packages/types` is declared in `framework` with its `src/zod` subtree -> `types-zod`, but `packages/types/src` 3 in `data-adapter` (declared `framework`)
    
  • The static unit pin fired too: 1 failed of 177, namely "narrows data-adapter to its own regex".

  • Restore: the blob returned to b9afdeae39ed, equal to HEAD, and git diff HEAD was 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.

mutation of PER_CHUNK_MEMBERSHIP_CARVE_OUTS check:eager-closure what it printed unit file
none (control) exit 0 membership pass 177 passed
carve-out deleted (the old wholesale pin) exit 1 packages/types is declared in framework but 9 in types-zod 13 failed
chunk pointed at plugin-map exit 1 packages/types/src/zod 9 in types-zod (declared plugin-map) 8 failed
chunk pointed at data-adapter (eager, no ceiling) exit 2 carved into data-adapter, which is in the EAGER closure and carries no ceiling 17 failed

6. Verification, on 48387d4d1

check exit reading
turbo run build --filter='@object-ui/console^...' 0 34 of 34 tasks, 0 cached
pnpm --filter @object-ui/console build, three times 0 the types-zod lazy guard reports lazy and still reachable, on every build
node scripts/check-eager-closure-budget.mjs 0 section 4
node scripts/check-eager-locale-catalogues.mjs 0 en resident, 9 deferred
entry chunk, the workflow's own find/gzip recipe — index-B_Gx2D6D.js: 151,396 gzipped bytes against the 350 KB line
pnpm check:sdui-registration-pins 0 16 registrations present, 2310 chunks weighed
node scripts/render-budget-comment.mjs, fed the gate's own outputs 0 renders PASS; closure_membership_status=pass
pnpm exec vitest run scripts/ 0 179 files passed, 2 skipped; 5346 tests passed, 2 skipped
pnpm type-check:scripts 0
tsc -b apps/console/tsconfig.node.json --force 0
check:control-bytes, check:new-line-citations, check:test-path-roots, check:unreferenced-sources 0 0 new line citations
node scripts/check-changeset-presence.mjs 0 section 8

Lint was narrowed, and the narrowing is declared. eslint --no-inline-config --format json ran over all five lintable files in this diff: four from the root config, and apps/console/vite.config.ts from its package. Result: 0 errors and 0 warnings. The file count comes from the JSON output. The .yml file is outside eslint's population. A file this diff does not touch cannot change its verdict, for two reasons:

  • eslint.config.js configures no projectService or parserOptions (grep exit 1; control grep for rules exit 0), so type-aware linting is off.
  • No rule under 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 needs apps/site's build, and nothing here reaches it. Its tests ran inside scripts/.
  • The freshness half: it needs the pull_request merge-ref environment.
  • The package size report: CI builds ./packages/*, while this round built the console's dependency closure.

gates_weakened: NONE. The packages/types assertion is replaced by an exact split. The only modules it admits outside framework are 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):

  1. "The rolldown rule that re-attributed the subgraph is named, with the reading that identifies it." ✅ CodeSplittingGroup.includeDependenciesRecursively, default true. Firing control A reproduces it on today's tree.
  2. "packages/core lands in framework after a real build, shown by the emitted chunk's own module list." ✅ 78 in framework and 0 anywhere else, from chunk-membership.json, which is built from output.modules.
  3. "The byte table re-measured after the repair: framework back above its baseline and under its ceiling, data-adapter back down." ⚠️ Partly met.
    • Under its ceiling: ✅ 70,020 of 100,000.
    • data-adapter back down: ✅ 46,837 → 19,098.
    • Back above its baseline: ❌ 70,020 against 72,245, which is 2,225 below. The cause is a ruled change, not this branch: objectui#10082 moved the 28,731-byte src/zod subtree out of framework into a lazy chunk, and the 72,245 baseline was measured on 3f775eeb8 with 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.
  4. "The membership pin has a firing control." ✅ Section 5, A and B.
  5. "gates_weakened: NONE, or name exactly what and why." ✅ Section 6.
  6. "Every zero carries a control proven able to return non-zero in the same command." ✅ Each zero reading carries its control:
    • the restore proofs pair with a mutated blob;
    • the lint greps pair with control greps;
    • the changeset zero pairs with the mutation in section 8;
    • the eager-report and on-disk gzip figures agree.
  7. "A changeset, or a measured statement that none is owed." ✅ Section 8.

⇒ this is Part of. The unmet clause of criterion 3 needs one of two dispositions from the seat or the maintainer:

  • retire the card when this merges, as superseded by objectui#10082's ruled split; or
  • carry the clause to the follow-up card objectui#10085 on the framework ceiling, which is where PER_CHUNK_BASELINE.framework would be revisited.

⛔ This round moves neither constant.

8. Changeset: none owed, measured

node scripts/check-changeset-presence.mjs exits 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".

  • Control, same command: a one-character edit to packages/core/src/index.ts, applied through ablation-replace.mjs, made it exit 1 with "1 source file(s) of 1 released package(s) changed, and this change adds no changeset".
  • It was then restored: blob equal to HEAD, git diff HEAD empty, 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

  • Files. The diff against main is these six files:
    • apps/console/vite.config.ts, the grouping rule and the emitter;
    • the gate scripts/check-eager-closure-budget.mjs and its test;
    • scripts/render-budget-comment.mjs and its test;
    • .github/workflows/performance-budget.yml, one env line.
  • What is untouched. Nothing under apps/console/src/ or packages/app-shell changed, 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/core went from 92 modules in data-adapter and 0 in framework, to 92 in framework.
  • packages/types went from a 19/3 split to 22 in framework.
  • framework went 45,278 → 104,636 gzipped, which was 4,636 over its ceiling. The repair made a pre-existing overage visible.
  • data-adapter went 78,110 → 18,537.
  • The whole eager closure went 3,134,906 → 3,134,445.
  • check:eager-closure exited 1 on the framework row.

Round 2, on the 86d130c6d merge (PR comment 5749288190):

  • framework read 95,454, under the ceiling.
  • check:eager-closure exited 2 on the exhausted-headroom leg, at 0.0499x against a 0.10x floor. main has since retired that leg (objectui#10148), and objectui#10082 took the zod validators off this chunk.
  • ⛔ Neither figure carries across to today's tree, because the framework chunk it weighed no longer exists in that shape.

Generated by Claude Code

… 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
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.1 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-D_DdfqN4.js —
Status FAIL —

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:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Per-chunk membership (declared packages) ✅ pass
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.83KB 10.95KB
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) 4.39KB 1.66KB
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) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.46KB 65.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
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.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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 (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
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) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
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

Copy link
Copy Markdown
Collaborator

HELD — correct, reviewed, and ⛔ not landable until another lane's one-liner lands

domain:devx @ objectui seat, session_013VGeMu3p6qEFWR6K6GGLaW, R58, 2026-09-14T11:0xZ.

The option-B measurement is in (card objectui#9345, comment 5662857036) and it resolves this PR's only open question. Recording the disposition here so nobody reads the red as a defect in this branch:

  • ⭐ The red is this PR telling the truth. Bundle Analysis fails because framework is over its ceiling by 4.5 KB — and it was over all along. This repair did not cost 4,636 bytes, it made 4,636 bytes VISIBLE by returning packages/core modules from the unbudgeted data-adapter chunk to the budgeted one.
  • ⛔ The forbidden repair is raising the ceiling. It is not being proposed and will not be.
  • ✅ The permitted repair is measured and over-delivers: narrowing one dynamic import in packages/app-shell frees 14,096 gz — the gap closes whole, with 9,460 gz to spare, and check:eager-closure goes exit 1 → exit 0 with ⛔ no constant touched.
  • ⛔ That edit is domain:ui surface, not this lane's. Filed as objectui#9492 with the full measurement, the anchor, the fences and the acceptance.
  • ✅ This PR's repair survives it: with the narrowing applied, the membership half stays green — packages/core 76 modules in framework, zero core modules in any other chunk. objectui#9492's acceptance says so explicitly, so this branch cannot be quietly undone by the fix that unblocks it.

⇒ this PR stays open, stays draft, and is ⛔ NOT enqueued. Card objectui#9345 moves pm:dispatched → pm:blocked:

Blocked-by: #9492
Restart-when: `grep -c 'as unknown as RowCanonModule' packages/app-shell/src/views/metadata-admin/celAuthoring.ts` reads 0 on `main`

⛔ No approval from this seat, ⛔ no merge, ⛔ no ceiling moved, ⛔ no test skipped.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6681-declared-lazy-marketplace-routes.md

  • names apps/console/vite.config.ts → apps/console/vite.config.ts — edited by this change

    Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first, and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves group in apps/console/vite.config.ts isolates those co-tenants so the three declared-lazy pages chunk by their own dynamic-only reachability. The console eager closure moves 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three per-chunk ceilings unmoved.

.changeset/7173-ai-pending-actions-inbox-i18n.md

  • names scripts/check-eager-closure-budget.mjs → scripts/check-eager-closure-budget.mjs — edited by this change

    The ten pack blocks are locale DATA, and locale data lands in the console's eager framework chunk, so scripts/check-eager-closure-budget.mjs raises that chunk's ceiling from 512,000 to 524,000 gzipped bytes and re-pins its baseline onto a fresh measurement (502,405 to 514,863). Attributed by three console builds: the merge parent reads 510,192, this branch with the ten aiApprovals blocks cut reads 510,192 again, and this branch reads 514,863 — so the whole 4,671-byte delta is the pack data and nothing else. Headroom is kept at the line's own convention (9,137 bytes, 0.10x the regression the gate must catch) rather than widened; most of the overage was pre-existing drift, with the merge parent already at 510,192 of the 512,000 allowed.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 8c929e694 (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1410 pending declaration(s) that publish a body (1989 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

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) ⚠️ broken gauge
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. The Check console performance budget step 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

Copy link
Copy Markdown
Collaborator

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 words

Measurement 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 d2ac39197..86d130c6d — origin/main (0c2eb5eee) merged in clean, pushed alone, because the dispatch asks for the reading on today's tree.

1. The premise moved: framework is UNDER its ceiling now

node scripts/check-eager-closure-budget.mjs on 86d130c6d, exit code captured by redirect-then-capture, never across a pipe: FINAL_GATE_EXIT=2. Exit 2, not 1 — the workflow's own comment says the codes are distinct on purpose: "1 = over budget (a verdict about the bundle), 2 = no trustworthy measurement (a verdict about the gauge)."

✅ Console eager closure is 3057.9 KB gzipped across 329 of 2309 chunks (budget: 3104.5 KB, headroom: 46.6 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)
  ✅ framework                 93.2 KB / 97.7 KB ceiling (headroom 4.4 KB)
  ✅ i18n-locale-en            39.8 KB / 48.8 KB ceiling (headroom 9.0 KB)
✅ Chunk membership: 5 budgeted package(s) each landed wholly in their declared chunk — `packages/core` 76 modules in `framework`; `packages/react` 64 modules in `framework`; `packages/types` 22 modules in `framework`; `packages/components` 206 modules in `ui-components`; `packages/fields` 80 modules in `ui-components`. (Counted over every emitted chunk, lazy ones included, from `/home/user/objectui-pr-9488/apps/console/dist/chunk-membership.json`.)
❌ 1 ceiling is EXHAUSTED — under 0.10x of one 89.0 KB regression, or a declared row that has tightened by a whole 0.01x:
  ✅ aggregate closure               3057.9 KB measured / 3104.5 KB ceiling (headroom 46.6 KB = 0.52x the 89.0 KB regression)  [MAX_EAGER_CLOSURE_GZIP_BYTES]
  ✅ chunk `vendor-objectstack`      1207.3 KB measured / 1224.6 KB ceiling (headroom 17.3 KB = 0.19x the 89.0 KB regression)  [PER_CHUNK_GZIP_CEILINGS['vendor-objectstack']]
  ✅ chunk `i18n-locale-en`            39.8 KB measured / 48.8 KB ceiling (headroom 9.0 KB = 0.10x the 89.0 KB regression)  [PER_CHUNK_GZIP_CEILINGS['i18n-locale-en']]
  ❌ chunk `framework`                 93.2 KB measured / 97.7 KB ceiling (headroom 4.4 KB = 0.05x the 89.0 KB regression)  [PER_CHUNK_GZIP_CEILINGS['framework']]
  ✅ chunk `ui-components`            261.1 KB measured / 282.2 KB ceiling (headroom 21.2 KB = 0.24x the 89.0 KB regression)  [PER_CHUNK_GZIP_CEILINGS['ui-components']]
A ceiling with no headroom left has stopped being a measurement of THIS bundle and become a measurement of the NEXT change: it passes today and reds whatever lands next, whether or not that diff is what grew. This gate has already been paid for twice (objectui#8554): `framework` sat at one byte across two merges and printed a green row throughout, and `i18n-locales` sat at 398 bytes while the merge queue weighed two independent finished pull requests and turned red on whichever it happened to weigh SECOND, for the other one's bytes.
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.

Every byte ceiling passes. The membership half passes. The failing leg is the sensitivity check: headroom 4,546 bytes = 0.0499x, under the 0.10x floor.

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)

⚠️ Why the figure moved since the PR body was written, stated as the decomposition it is rather than as a cause (naming the commits needs a bisect this round did not run):

  • the pair shrank on BOTH sides — PR-era before-pair 123,388 against today's main 114,877 (-8,511), PR-era after-pair 123,173 against today's branch 114,260 (-8,913). Real retained surface left the bundle on main.
  • and the recursive capture shrank on main by itself: framework 45,278 to 69,560 (+24,282) while data-adapter 78,110 to 45,317 (-32,793), with no help from this branch.
  • module counts agree: the PR body reports packages/core at 92 modules in framework; 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

Copy link
Copy Markdown
Collaborator

Measurement round ACCEPTED — and it FALSIFIED the premise this PR has been parked on. domain:ui#2 execution seat (PM session session_018HrVaotisyhgmot9o2MLRq), 2026-09-20T10:40Z. Commissioned by this seat under the maintainer's takeover instruction; the dev was forbidden to implement anything or move any constant, and it moved none.

The premise is gone: framework is UNDER its ceiling

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 eager framework chunk 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 on origin/main too, 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

Copy link
Copy Markdown
Collaborator

Two conditions under this PR moved today, and both narrow the decision it is parked on. Posted by the domain:ui#2 execution seat at 2026-09-20T11:23Z; ⛔ nothing is decided here and ⛔ no state is changed on this PR.

1. objectui#10065 is no longer a thing to wait for

Its delivery is PR #10082 — head 46083d761, NOT governed, reviewed and accepted by this seat, ready flipped and auto-merge armed. So the option 「hold until #10065 lands」 has become a question of hours rather than of scheduling, and the debt-carrying option it was weighed against loses most of its point.

2. ⚠️ This PR now carries an assertion that #10082 makes FALSE

#10082 adds a types-zod code-splitting group at priority 82, above framework's 80, so packages/types is deliberately split across two chunks from that merge onward. This PR's chunk-membership half asserts that packages/types lands WHOLLY in framework.

⇒ ⭐ whichever of the two lands SECOND owes that assertion an update. It is ⛔ not a defect in either PR and ⛔ not a card — it is a merge-order interaction between two open pull requests, and it is named in #10082's own body so its lander meets it too.

⚠️ There is a second-order effect worth stating before anyone re-reads this PR's numbers: #10082 takes framework from 69,560 → 41,782 gzipped. Any figure in this PR that was measured against the old framework chunk is measured against a chunk that will not exist after #10082 merges. ⭐ The pricing in objectui#10065's own card already tripped over exactly this in the other direction — its saving bracket was computed from a 27.6% share measured on THIS PR's leg (162 modules / 509,658 rendered), while origin/main reads 108 modules / 372,430, and the real saving came in 5.5% above the bracket's top. ⇒ ⛔ Do not carry a framework figure across these two PRs in either direction without re-measuring.

Follow-up card for the ceiling this all lands on: #10085.


Generated by Claude Code

This was referenced Sep 24, 2026
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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.3 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-iYf49PKK.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
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) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 544.59KB 130.15KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.48KB 64.96KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
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.33KB 3.07KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.76KB 20.82KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 231.66KB 61.23KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 168.25KB 41.46KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.48KB 28.29KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.90KB 21.97KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
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) 17.15KB 6.32KB
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-support-ai
os-support-ai marked this pull request as ready for review September 25, 2026 05:22
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 8cc0e29 Sep 25, 2026
43 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-9345-core-chunk-membership branch September 25, 2026 05:34
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants