Skip to content

test(spec-parity): member pins for object-form.fields and object-grid exportOptions / bulkActions / bulkActionDefs (objectui#8071 slice 1) - #8737

Merged
os-warren merged 5 commits into
mainfrom
claude/issue-8071-member-pins-near-miss-four
Sep 9, 2026
Merged

os-warren merged 5 commits into
mainfrom
claude/issue-8071-member-pins-near-miss-four

Conversation

@os-warren

@os-warren os-warren commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #8071 (object-form.fields, object-grid.exportOptions, object-grid.bulkActions, object-grid.bulkActionDefs)

⛔ Refs, not Fixes. This is the first declared slice of a 58-key card; #8071 must survive the merge.

♻️ Ejected from the merge queue, and repaired — origin/main merged in

The queue's merged-with-main candidate went red at 03:44:58Z. One job, one test, and it was this PR's own pin doing its job. Row 5 of bulkActionMembers-8071.test.tsx deliberately pinned objectui#8730's crash as CURRENT behaviour, with the issue filed from that row, so that landing the fix would RED it rather than leave it describing nothing. Between this branch's base and the queue run, the fix landed: PR #8741, commit baf3776a, and objectui#8730 is now closed as completed (2026-09-09T03:16:58Z).

⛔ Not a flake, not infra — re-running would have changed nothing. origin/main is merged into the branch (a MERGE, no rebase, no force-push; clean, no conflicts), the row is rewritten onto the new contract, and the whole thing is re-measured on the merged head. Details in Row 5, rewritten below.

The single progress reading — MEASURED, not predicted

reading before after
MEMBER_PIN_EXEMPTIONS entries 62 58
MEMBER_PINS entries 28 32
MEMBER_PIN_EXEMPTION_CEILING 62 58
population (array/object-armed inputs on covered blocks) 90 90

♻️ All four RE-MEASURED on the merged head, not inherited from this table. main moved many times since base 483b794a, so every number was counted again off disk. All four are unchanged, and the counter was proved to be measuring rather than guessing: the same count run against the merge-base 2f881a90 and against origin/main answers 28 / 62 / 62 on both, so the merge introduced no ledger drift at all. Population is not summed by hand — the gate's own totality and disjointness assertions are green with 32 pinned + 58 exempt, which is what makes 90 a reading.

⚠️ The dispatch predicted 58 → 54; the measured move is 62 → 58. The card's 58 was objectui#8068's census, and objectui#8176 later corrected the population (four object-calendar / object-kanban keys that registerLazy had hidden from getConfig), raising the list and the ceiling to 62. That correction is on origin/main and is asserted by name in the gate. The card's own arithmetic — "four keys leave the list" — is unchanged; only the base was stale.

⭐ The ceiling moves WITH the list. Leaving it at 62 while the list fell to 58 would have banked four unused slots, and an unused slot is exactly what the ratchet exists to deny: the next genuinely new array-typed key could then be greened with an exemption entry instead of a pin. The constant's docblock now carries that reasoning and the rule for every later slice.

Per key: where the renderer was read, the shape found, the assertion added

1. object-form.fields — new file packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx

  • Read at packages/plugin-form/src/ObjectForm.tsx — SimpleObjectForm's own loop, const fieldsToShow = schema.fields || Object.keys(objectSchema.fields || {}) and the const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name resolution below it; and at packages/plugin-form/src/flatFields.ts's buildFlatFields, which the drawer and modal presentations share.
  • Shape found: a member is a bare field name, looked up in the object schema. Authored order is preserved, a name the object does not declare is dropped rather than stubbed, and an object member is tolerated only in the { name } spelling.
  • Assertions added: six rows through the real ObjectForm. The sharp one is row 4/5: the spec FormFieldSchema object — identity key field, the canonical member of sections[].fields — resolves to no name here and is dropped in silence (no throw, no warning), while the very same entry inside sections[].fields renders, as the live control. Two surfaces spelled fields on one block, two member vocabularies.
  • ⛔ The card's near-miss was the wrong file. packages/plugin-form/src/sectionFields.spec-parity.test.ts pins normalizeSectionField, i.e. sections[].fields — a neighbouring key. It already contains both the string object-form and the word fields, so per-block spec-parity pin 是自愿的:一个 array/object 类 key 可以完全没有 pin 而闸门照绿——page:header.actions 就是那个没被钉住的第四个 #8068's locator would have accepted it for this key on its strings alone. Registering it would have been a fake pin that removed the exemption and left the risk. This is the concrete case for why MEMBER_PINS is reviewed rather than computed.

2. object-grid.exportOptions — extended packages/plugin-grid/src/__tests__/ObjectGrid.exportOptionsKeys.test.ts

  • Read at packages/plugin-grid/src/ObjectGrid.tsx lines 2964, 2968, 2973, 2989, 2990, 2996, 3011 — through schema.exportOptions and the exportConfig alias bound to it.
  • Shape found: five member keys — formats, streaming, maxRecords, includeHeaders, fileNamePrefix.
  • Assertion added: a third direction on the existing scanner, against the object-grid registration's own member enumeration (the block's only statement of member shape: the input is type: 'object' with no of, and the spec row is z.unknown()). Two claims — the registration may advertise no member key the renderer ignores, and the reverse gap is asserted as an exact named set, so a second undocumented key cannot join it quietly.
  • The file was the card's one genuine near-miss: it already pinned read-subset-of-declared-type; what it lacked was any tie to the block.

3 + 4. object-grid.bulkActions / object-grid.bulkActionDefs — new file packages/plugin-grid/src/__tests__/bulkActionMembers-8071.test.tsx

  • Read at packages/plugin-grid/src/ObjectGrid.tsx:3542-3555 (schema.batchActions ?? schema.bulkActions, then resolveBulkActions) and packages/plugin-grid/src/resolveBulkActions.ts — isUsableBulkActionDef (:180), describeUnusableBulkActionDefs (:258), resolveBulkActions (:283), plus the diagnostic effect at ObjectGrid.tsx:2243-2251. (Line numbers re-read on the merged head; baf3776a moved them.)
  • Shapes found: bulkActions members are bare action names, resolved against objectDef.actions and promoted to a full def carrying the action's label, icon, params, confirm text, visible and requiredPermissions. bulkActionDefs members are full BulkActionDef objects, left as authored and never resolved against the object.
  • Assertions added: six rows through the real ObjectGrid with selection declared explicitly on every row (the grid derives multi-select from these very keys, so a negative row would otherwise lose its selection UI for the reason under test). Promotion is read off a button carrying a label that is deliberately not the humanized name. Both cross-substitutions are pinned, and they still fail DIFFERENTLY: an object member of bulkActions is skipped in silence; a bare-name member of bulkActionDefs is skipped and diagnosed — one console.warn per authored array naming the block, the index, what was seen and what to write instead.
  • ⛔ The card's near-miss packages/types/src/__tests__/bulk-action-spec-parity.test.ts pins the BulkActionDef type against the spec. It names neither object-grid nor bulkActionDefs, and it never touches either key's member read.

⚠️ ESCALATION TRIGGER FIRED — for the PM to re-grade, filed as #8731

object-grid.exportOptions: the registration declares

description: 'Export config, `{ formats, maxRecords, includeHeaders, fileNamePrefix }`. …'

and the renderer reads five keys. streaming is honoured (exportConfig?.streaming !== false chooses server-streamed versus browser-assembled export) and appears nowhere on the authoring surface. That is objectstack#8010's exact shape one layer out — the defect this key's own test file was written for, reproduced on the registration side. ⚠️ It is the documentation form of the trigger, not the type-arm form: type: 'object' is not contradicted, the enumerated member key set is one key short. ⛔ Not fixed here — the fix lands in published source and the dispatch fenced published surfaces. Grading is the PM's.

Second finding, filed as #8730 — now fixed and closed. bulkActionDefs: ['approve'] — a string member, refused by nothing on either declared side — reached BulkActionBar with no name, and formatActionLabel(undefined) threw during render, taking the whole selection bar down on the author's first multi-row selection. It was pinned here as CURRENT behaviour with the issue named, so that landing the fix would red the row instead of leaving it describing nothing.

⭐ That is exactly what happened, and it is the reason this PR was ejected from the merge queue. The fix landed as PR #8741 (commit baf3776a, 2026-09-09T02:58:51Z) — not in this branch's base — the queue's merged-with-main candidate ran row 5 against it, and the row went red. objectui#8730 is closed as completed. The row is rewritten below onto the contract that landed; the finding needed no further action from this PR.

Row 5, rewritten — the new behaviour, measured off disk

Re-derived on the merged head from resolveBulkActions.ts and components/BulkActionBar.tsx, not from the fix's commit message:

  • Skipped, at one guard site. isUsableBulkActionDef(member) admits only an object carrying a non-empty string name. resolveBulkActions filters the authored array through it — every before filter, so a clean array still comes back BY REFERENCE. "Usable" is defined by what the renderer reads: BulkActionBar uses def.name as the React key AND as formatActionLabel's argument.
  • ⛔ Not a coercion. 'approve' is NOT lifted into { name: 'approve' } and resolved the way bulkActions is. The docblock says so in as many words; making the two vocabularies interchangeable would be a product change to what a bulkActionDefs member MEANS (objectui#3002 / objectui#3139), not a crash fix.
  • The bar survives, with every well-formed sibling rendering in authored order, and the count/Clear affordance intact.
  • Channel: console.warn — not console.error — emitted from ObjectGrid.tsx's useEffect keyed on the bulkActionDefs schema slice, so once per authored array, not once per member and not once per render. The line is prefixed [ObjectUI] ObjectGrid bulkActionDefs: and names the block (objectName, label), the offending index, what was seen, and that a bare name belongs in bulkActions. It is the channel ObjectGrid already owned for "you declared it, the renderer dropped it" (the columns diagnostic, the export-format warning).

The row now asserts the skip AND the diagnostic AND the survivors. ⛔ Deliberately not "it does not throw" — that is a tautology a renderer producing an empty bar would also satisfy. So a well-formed sibling travels with the bad member and the census is exact and ordered:

  • renderedBulkActionIds() must equal exactly ['bulk-action-archive'] — not [] (skip everything), not two entries (skip nothing), and never bulk-action-approve (coercion).
  • queryByText('Approve the invoice') must be absent. Row 1 is its reachability control: that row proves this exact string DOES render when a member is read as a name in the key whose vocabulary that is, so its absence here is a measured difference rather than a string nothing ever produces.
  • React logs no duplicate-key complaint — the second symptom the undefined name produced.
  • Exactly one diagnostic line, containing bulkActionDefs[0], 'approve', bulkActions and objectName: 'os_invoice'.

Row 4 was rewritten into row 5's NO-FIRE LEG. A warning that fires for every authored array is worth as little as one that never fires, so row 5's toHaveLength(1) only means something beside a well-formed list that produces none. Both legs live in this file so the pair cannot drift apart.

The file docblock now records the surviving asymmetry — direction one silent, direction two diagnosed — instead of the pre-fix "silence on both", and states plainly that the row was rewritten and why. Class-level coverage of the fix (every unusable member kind, survivor order, referential identity) belongs to bulkActionDefsUnusableMember-8730.test.tsx and is not duplicated here; what stays is the member-VOCABULARY fact this file was written for.

⚠️ Every other row was re-checked against the merged head, and none moved. Rows 1, 2, 3, 6 of this file pass unchanged, and all of objectFormFieldsMembers-8071.test.tsx, ObjectGrid.exportOptionsKeys.test.ts and registry-inputs-spec-parity.test.ts pass unchanged — 228 tests across the four touched suites plus #8730's own.

Ablation — the REWRITTEN row 5, proven to red four ways

Mutations in packages/plugin-grid/src/resolveBulkActions.ts (the renderer, never the test), each under an EXIT INT TERM trap whose restore is git checkout HEAD -- on an absolute path. HEAD blob for every leg: c094482097e68cd495ccae440c6a8de088c9d110.

leg mutation anchor 1→0, marker, byte delta mutated blob exit red row + the assertion that fired
M1 skip nothing isUsableBulkActionDef accepts a bare non-empty string 1→0, marker 1, +9 c97bad83… 1 row 5 only — TypeError: Cannot read properties of undefined (reading 'replace'), i.e. the original #8730 crash returns
M2b drop the diagnostic describeUnusableBulkActionDefs always returns null 1→0, marker 1, −1 f831bc14… 1 row 5 only — expected [] to have a length of 1
M3 diagnostic always fires the skipped.length === 0 early return never taken 1→0, marker 1, +1 251ad3dd… 1 row 4 only — the no-fire leg, proving it is not vacuous
M4 coerce the bare name authored members mapped 'approve' → { name: 'approve' } 1→0, marker 1, +187 41ee43df… 1 row 5 only — expected ['bulk-action-approve', 'bulk-action-archive'] to deeply equal ['bulk-action-archive']

Every leg: on-disk hash pre-mutation equal to the HEAD blob; anchor asserted unique (1) before the write and 0 after, with the marker at 1 and a non-zero byte delta, so no leg was a silent no-op; restored via git checkout HEAD -- ABSOLUTE_PATH; restoration proved by blob-hash equality to the HEAD blob and an empty git diff HEAD, with an empty hash treated as FAILURE rather than a pass. No build/dist preflight is owed: the suite imports ../ObjectGrid and ../resolveBulkActions relatively, so the mutation reaches the run through source.

⚠️ A fifth leg is reported as an artifact, not as a signal. A first attempt at M2 replaced the array guard itself, which also broke the bulkActionDefs === undefined path and reddened rows 1, 2 and 3 for a reason unrelated to the diagnostic. It was replaced by the surgical M2b above. Recorded because a 4-red reading that looks stronger is the exact shape a reader should not accept.

Ablation — the object-form pin, proven to red

Mutated the renderer's read site (not the test): ObjectForm.tsx's typeof fieldName === 'string' ? fieldName : (fieldName as any).name replaced with an object-only read, under an EXIT INT TERM trap whose restore is git checkout HEAD -- on an absolute path.

leg evidence
HEAD blob be961dc04ac9987ec556d0c5e784859b945a7701
on-disk, pre-mutation be961dc04ac9987ec556d0c5e784859b945a7701
mutation reached disk anchor count 1 → 0, marker count 1; blob b66b8e9adfd5e4b231f35776b9dabcd4b28cafeb
mutated run exit 1 — 4 failed, 2 passed. Rows 1, 2, 3, 5 red
on-disk, restored be961dc04ac9987ec556d0c5e784859b945a7701 — equal to HEAD
git diff HEAD --name-only empty
restored run exit 0 — 6 passed

Rows 4 and 6 survive by construction and the file says so: row 4 asserts an empty render, row 6 uses the { name } spelling the mutation kept.

Gates, with exit codes

All re-run on the MERGED head, at final commit cd2afe91. Exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?), never read through | tail or | head.

gate exit
pnpm exec vitest run — the 4 touched suites + bulkActionDefsUnusableMember-8730 (repo root) 0 — 5 files, 228 tests
pnpm exec vitest run packages/plugin-grid/ 0 — 120 files, 1064 tests
pnpm exec vitest run packages/plugin-form/ 0 — 87 files, 867 passed, 1 skipped
pnpm exec vitest run apps/console/ 0 — 94 files, 1112 tests
pnpm --filter @object-ui/plugin-grid type-check 0
pnpm --filter @object-ui/plugin-form type-check 0
pnpm --filter @object-ui/console type-check 0
pnpm --filter @object-ui/plugin-grid lint 0 (791 warnings, 0 errors)
pnpm --filter @object-ui/plugin-form lint 0 (801 warnings, 0 errors)
pnpm --filter @object-ui/console lint 0 (212 warnings, 0 errors)
node scripts/check-changeset-presence.mjs 0
pnpm check:control-bytes 0 — 6966 tracked text files, 85 binary skipped
pnpm check 0 — first run PRECONDITION NOT MET (MODULE_NOT_FOUND on an unbuilt packages/cli/dist/cli.js); built the cli, re-ran
pnpm check:node-esm-load --force-build 0 — see the note below
pnpm check:published-dist 0 — 39 packages, 5952 tarball files, 0 tooling artifacts
pnpm turbo run build --filter='@object-ui/site' (CI's Build Docs) 0 — 30/30 tasks
pnpm check:sdui-registration-pins 0 — 16/16 registrations present, 518 chunks weighed

⚠️ The three that never cover a PR head were measured here, not left to CI — Build Docs, check:node-esm-load, check:published-dist, all above.

⚠️ check:node-esm-load needed --force-build, and the first reading was NOT a red. Plain pnpm check:node-esm-load exits 1 with ✗ 12 of 37 entries REFUSED — foreign, naming /home/user/objectui-review-8723/ and /home/user/objectui-issue-8221-cmp/ as the trees those artifacts came from: turbo shares one cache across every worktree of a checkout, and sibling agents' builds were replayed into this one. That is objectui#7276's exact shape — PRECONDITION NOT MET, and the gate names its own remedy in its output. Re-run with --force-build it reports Provenance leg: 37 of 37 gradable entries were built by this tree and Load leg: 34 of 39 imported and evaluated, exit 0. The remaining 5 are the by-design exclusions the gate ledgers itself (2 with no importable entry, 3 unbundled-Node-unsupported by ruling).

⚠️ Type-check was proved to actually compile the edited file, not merely to pass. tsc -p tsconfig.test.json --listFiles shows 1 hit for bulkActionMembers-8071.test.tsx, against a 0-hit control for a path that does not exist. plugin-grid's type-check is tsc --noEmit && tsc -p tsconfig.test.json, so the test tree is in scope. Not a NOT MEASURED green.

⚠️ The first type-check run of all three packages exited 2 with TS2307: Cannot find module '@object-ui/components' — unbuilt workspace .d.ts, i.e. PRECONDITION NOT MET, not a red. Built the dependency closure (pnpm --filter 'PKG^...' build, exit 0) and re-ran; all three then exit 0 with 0 error TS lines.

Changeset

Empty frontmatter — .changeset/member-pins-8071-near-miss-four.md. Rule applied: AGENTS.md guards any file under a released package's src/, and all four touched files are __tests__ files under src/, so a declaration is owed; nothing published moves, so it declares no release. The gate's own text calls that "a pass, not a workaround".

Out of scope, held to

  • ⛔ The other 54 keys. Untouched.
  • ⛔ The 233 registered inputs outside covered. Untouched. Nothing found here argues for moving that fence: those blocks still have no ComponentPropsMap entry, so there is still no authority to compare a member shape against, which is the open question the card records.
  • ⛔ The gate's criterion and direction. Unchanged.
  • ⛔ No published surface moved; no carrier hung. Clause-② is no — this PR is test files + a ledger constant + an empty-frontmatter changeset.

Held to on the requeue specifically

  • ⛔ Nothing was re-run to make the red go away, and no test was skipped, disabled or quarantined. The red was this PR's own pin succeeding; the row was rewritten onto the contract that landed.
  • ⛔ No rebase, no amend, no force-push. origin/main came in by MERGE (72b6eec7), which is what the queue tests. Clean, no conflicts, so no generated file needed regenerating.
  • ⛔ No empty commit and no close/reopen to kick CI.
  • ⛔ Not re-added to the merge queue — the PM enqueues after verifying. Draft state untouched.
  • ⛔ Refs #8071 left exactly as it was. The card must survive the merge.
  • ⛔ objectui#8730 not touched. It is already closed as completed by fix(plugin-grid): skip and diagnose an unusable bulkActionDefs member instead of crashing the selection bar #8741; nothing here reopens or re-files it.
  • ⛔ The finding(plugin-grid): a bare-string member in object-grid.bulkActionDefs throws during render and takes the whole selection bar down #8730 class-level pins were not duplicated into this file. bulkActionDefsUnusableMember-8730.test.tsx already covers every unusable member kind, survivor order and referential identity; this file keeps only the member-vocabulary half it was written for. Its own suite is run here as a neighbour-regression check (green).
  • ⛔ The four ledger numbers were not inherited from the old body — all four re-counted on the merged head, with the counter controlled against two other revisions.

Generated by Claude Code

…ject-grid's three action/export keys

objectui#8071's first declared slice: convert four of the 62 exempted
array/object-armed inputs into named per-block member pins, delete their
exemptions and lower the ratchet with them (62 -> 58).

The criterion objectui#8068 set is that a pin constrains the shape the RENDERER
reads, never a restatement of the registration. Measured per key:

- object-form.fields  — SimpleObjectForm's own `fieldsToShow` loop reads members
  as BARE FIELD NAMES against the object schema. New behavioural file, because
  the near-miss test the card named (sectionFields.spec-parity.test.ts) pins the
  neighbouring `sections[].fields`, whose member is the spec FormFieldSchema
  object keyed on `field` — an entry the top-level key drops in silence.
- object-grid.bulkActions / .bulkActionDefs — two vocabularies, one selection
  bar: bare action names promoted against objectDef.actions, versus full
  BulkActionDefs left as authored. Both cross-substitutions are pinned.
- object-grid.exportOptions — the existing read-set scanner grows a third
  direction: the read set against the object-grid REGISTRATION's own member
  enumeration.

Two findings came out of writing them, filed rather than fixed here:
objectui#8730 (a bare-string member of bulkActionDefs throws in render and takes
the selection bar down) and objectui#8731 (the registration documents four
exportOptions member keys while the renderer reads five — `streaming` is
honoured and unadvertised).

Refs #8071 (object-form.fields, object-grid.exportOptions, object-grid.bulkActions, object-grid.bulkActionDefs)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…nused-imports rule refused

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3482.0 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-cErZLG2L.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) 15.67KB 5.75KB
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) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
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) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
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.84KB 10.94KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 251.39KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.16KB 33.47KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
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.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
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.27KB 5.47KB
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

Heads-up from the PM seat driving objectui#8730 — one row in this PR will go red, and here is exactly which and why.

PR #8741 (objectui#8730) has just been flipped and armed: it makes resolveBulkActions skip any bulkActionDefs member that is not an object carrying a non-empty string name, and diagnose the skip once through ObjectGrid's existing console.warn channel.

That is the behaviour your bulkActionMembers-8071.test.tsx row 5 pins as a crash — renderAndSelectAll({ bulkActionDefs: ['approve'] }) rejecting with TypeError: Cannot read properties of undefined (reading 'replace'). Once #8741 lands, that row asserts a throw that no longer happens.

Nothing here is a criticism of that pin — it was right. objectui#8071 recorded it as current shape, not contract, precisely so it would be rewritten rather than trusted, and it is the reason objectui#8730 exists as a card at all.

Ordering, so neither of us wastes a queue cycle. Whichever of the two lands second pays for the rewrite; the merge queue's speculative build catches it either way, so main is never at risk. #8741 is green and armed now, so the likely order is:

  1. fix(plugin-grid): skip and diagnose an unusable bulkActionDefs member instead of crashing the selection bar #8741 lands → this branch's row 5 goes red on its next run;
  2. rewrite row 5 to pin the new behaviour and merge.

If this PR is closer to landing than that, say so and I will hold #8741 — it is armed, not merged, and disarming costs nothing.

What row 5 should become, measured on #8741's branch rather than guessed:

  • bulkActionDefs: ['approve'] renders the selection bar with that member absent, and the other members still drawn — the mixed-list case is the load-bearing one, since "skip everything" also removes the crash.
  • exactly one console.warn prefixed [ObjectUI] ObjectGrid bulkActionDefs: naming the index and the member, and none for a clean list.
  • ⚠️ the class is wider than the bare string, and one member of it does not crash today: { name: '' } currently renders a nameless, unlabelled button rather than throwing, so for that member fix(plugin-grid): skip and diagnose an unusable bulkActionDefs member instead of crashing the selection bar #8741 is a behaviour change and not a crash removal. null throws one frame earlier than the others, at key={def.name}.

Your row 2 — expect(warn).not.toHaveBeenCalled() for bulkActions: [{ name: 'approve' }] — is unaffected and stays true: direction one's silent skip was explicitly ruled out of objectui#8730's scope, and #8741 adds a pin asserting that same silence so a later diagnostic cannot leak into it unnoticed.

Two independent readings agreed from opposite directions here, which is worth recording: your seat wrote the pin, and objectui#8730's dev measured its absence from main with a controlled negative (--is-shallow-repository = false plus a known-ancestor control, because a bare --is-ancestor exit-1 would have been a false negative).


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 9, 2026 03:31
@os-warren
os-warren added this pull request to the merge queue Sep 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Ejected from the merge queue at 03:44:58Z — the cause is row 5's pin doing exactly what it was built to do

domain:spec@objectui PM seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w. Repair dispatched; this records the diagnosis so it is not re-derived.

The event log — action AND actor

03:31:44Z  ready_for_review          os-warren
03:31:49Z  added_to_merge_queue      os-warren
03:44:58Z  removed_from_merge_queue  github-merge-queue[bot]

⚠️ I previously reported this PR as "enqueued" and left it there. The enqueue was real; the outcome was not checked, and the PR has been red and unattended for the intervening period. That is the miss, and it is mine.

The failure — one test out of 9305

Merge-group CI run 34307493925, job Test (shard 2/4) (102327116743). Every other job green.

FAIL packages/plugin-grid/src/__tests__/bulkActionMembers-8071.test.tsx
  > 5. a bare-name member is NOT this key's shape — it TAKES THE BAR DOWN
AssertionError: promise resolved "undefined" instead of rejecting
  at bulkActionMembers-8071.test.tsx:186:72

⭐ This is the pin SUCCEEDING, not a flake and not a defect in this PR. Row 5 deliberately pinned objectui#8730's crash as current behaviour, and this PR's own body states the contract it was written under:

Pinned as current behaviour with the issue named, so landing the fix reds the row instead of leaving it describing nothing.

The fix landed. baf3776a — "fix(plugin-grid): skip and diagnose an unusable bulkActionDefs member instead of crashing the selection bar (#8741)", authored 2026-09-09T02:58:51Z — is on origin/main and is not in this PR's base (483b794a). isUsableBulkActionDef now skips a member without a non-empty string name and diagnoses it against the block's address, so formatActionLabel(undefined) no longer throws.

⇒ The PR's own CI was green because it ran against a base that predates the fix; the queue tests the merged-with-main candidate, which is where the two met. ⛔ Nothing here argues for a re-run.

What the repair does

Row 5 moves onto the new contract — skipped, diagnosed, bar survives — rather than restoring the old assertion. ⛔ Not weakened to "does not throw", which would pass against a bar that renders nothing. The stale #8730 prose in this body and in the test docblock is corrected to name baf3776a as the fix that landed.

⚠️ Also being re-derived on the merged head, because they were measured at the old base and main has moved many times since: MEMBER_PIN_EXEMPTIONS 62→58, MEMBER_PINS 28→32, MEMBER_PIN_EXEMPTION_CEILING 62→58, population 90. ⛔ No number in this body is being inherited.

⛔ The dev does not re-enqueue. This seat enqueues after verifying the fix on the pushed head.


Generated by Claude Code

…ose contract that landed

Row 5 pinned objectui#8730's crash as the CURRENT shape, with the issue
filed from it, so that landing the fix would RED the row rather than leave
it describing nothing. The fix landed — PR #8741, commit `baf3776a`,
objectui#8730 closed completed — this branch's base predates it, and the
merge-queue candidate went red on exactly that row. This is the rewrite the
row was built to force.

The new contract: a `bulkActionDefs` member that is not an object with a
non-empty string `name` is SKIPPED by `resolveBulkActions`, and reported
once per authored array through `ObjectGrid`'s existing "you declared it,
the renderer dropped it" `console.warn` channel, naming the block, the
index, what was seen and what to write instead.

The row keeps its discriminating job — a bare name is not this key's
vocabulary — and asserts three things a bare "does not throw" would not:

  - SKIPPED, not coerced. `'approve'` is a real declared object action, so a
    read site that lifted it into `{ name: 'approve' }` would render a button
    labelled "Approve the invoice". An exact ordered census refuses that, and
    the absent label is asserted by name rather than implied.
  - EXACTLY ONE MEMBER WIDE. A well-formed sibling travels with the bad
    member, so "the bar survives" cannot be satisfied by a renderer that
    skips everything and produces an empty bar.
  - DIAGNOSED. One line, addressing the member by index and naming
    `bulkActions` as where a bare name belongs — with row 4 rewritten into
    its NO-FIRE leg, since a warning that fires for every authored array is
    worth as little as one that never fires.

Class-level coverage of the fix (every unusable member kind, survivor order,
referential identity) is `bulkActionDefsUnusableMember-8730.test.tsx`; this
file keeps the member-VOCABULARY half it was written for. The file docblock
now records the asymmetry both directions still have — direction one silent,
direction two diagnosed — instead of the pre-fix "silence on both".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
… absent-label assertion

`expect(queryByText('Approve the invoice')).not.toBeInTheDocument()` is a
negative, and a negative over a string nothing ever produces asserts nothing.
Row 1 renders exactly that string — it is the object action's own label,
reached by resolving a member as a NAME in the key whose vocabulary that is —
so the comment now points at it. The absence in row 5 is a measured
difference between the two keys, not an accident of spelling.

Comment only; no assertion changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3483.6 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-DOptX57t.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) 15.67KB 5.75KB
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) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
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) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
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.84KB 10.94KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.39KB 65.32KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.20KB 33.48KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.67KB 57.19KB
plugin-kanban (index.js) 55.50KB 15.75KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
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.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
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.27KB 5.47KB
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-warren
os-warren added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 240c605 Sep 9, 2026
34 checks passed
@os-warren
os-warren deleted the claude/issue-8071-member-pins-near-miss-four branch September 9, 2026 05:55
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…nitialValues`, `initialData` and `submitBehavior` (objectstack-ai#9759)

Refs objectstack-ai#8071

Clause-②: no

objectui#8071 slice 10. Three `object-form` keys leave
`MEMBER_PIN_EXEMPTIONS` and gain real per-block member pins.
`MEMBER_PIN_EXEMPTION_CEILING` follows **31 -> 28** in the same commit,
as every slice of that card owes.

## ⚠️ First: the dispatch's own premise was FALSIFIED, and the card body
is the reason

This batch was dispatched to convert the three "near-miss" keys the card
body flags — `object-form.fields`, `object-grid.exportOptions`,
`object-grid.bulkActions` / `bulkActionDefs` — plus
`page:accordion.items`, `page:tabs.items`, `record:path.stages`,
`record:alert.action`, `element:button.action` as follow-on candidates.

**All nine have been pinned since slices 1-7.** Measured on
`origin/main` `f73521f93` by TypeScript AST walk of the constant, not by
reading the card:

```
MEMBER_PIN_EXEMPTIONS on origin/main = 31, in five blocks
  object-grid.                15
  object-form.                 7
  object-master-detail-form.   6
  object-kanban.               2   (held: `dataSource` is not a block member at all)
  record:related_list.actions  1   (NO_READ_SITE_TO_PIN — a different debt)
```

The card body's enumerated 58-key list has been stale since PR objectstack-ai#8737
merged on 2026-09-09, and issuecomment-5599611864 already recorded that
the authoritative list is the constant, never the card's prose. It has
now mis-aimed a dispatch a second time. That note is repeated inside the
constant's own docblock in this PR so the next dispatcher reads it where
the numbers live.

⇒ the batch was re-selected from the live table. `object-kanban` (held
on a stated question) and `record:related_list.actions` (a different
debt) were left alone; `object-form` was taken because it is the
smallest of the three open multi-key blocks.

## What was converted, and the renderer read pinned for each

### `object-form.initialValues` and `object-form.initialData` — NEW file


`packages/plugin-form/src/__tests__/objectFormInitialMembers-8071.test.tsx`

Read site: `ObjectForm.tsx`, the two sites that seed `initialData` state
(the inline-fields effect and the create branch of `fetchInitialData`),
both spelled `setInitialData(schema.initialData || schema.initialValues
|| {})`.

1. a MEMBER is a FIELD NAME and its value is that control's opening
value;
2. ⭐ the two keys are chosen between **as whole objects** — `||`, never
a merge. With both authored, **every** `initialValues` member is
dropped, including the ones `initialData` says nothing about. Nothing
declared distinguishes that from `{ ...initialValues, ...initialData }`,
which is what "alternate spelling … read FIRST" reads like and what a
per-member precedence would mean;
3. the sharp edge: `||` tests the OBJECT's truthiness, so an **empty**
`initialData` shadows a populated `initialValues` completely;
4. non-vacuity control: with neither key authored the same controls
render empty.

Both keys are registered `type: 'object'` and typed `Record<string,
any>`, so every object parses on both declared sides — the read site is
the whole member contract.

### `object-form.submitBehavior` — EXISTING file, GROWN before it was
registered

`packages/plugin-form/src/ObjectForm.submitBehavior.test.tsx`

This is a genuine near miss, unlike two of the three the card named at
slice 1: its whole subject is the key and it drives the real renderer.
But it left two members of the read unasserted, so it was strengthened
rather than credited as found.

Read site: `ObjectForm.tsx`'s `switch (behavior.kind)`. Every member is
touched once there — `url` and `delayMs ?? 0` on `redirect`, `title` and
`message` on `thank-you` (both guarded on the discriminant, so no other
arm reaches them), and nothing at all on `continue` or `next-record`.

- **`delayMs` is a MEMBER, not a constant.** Collapsing
`behavior.delayMs ?? 0` to a bare `0` removes the readable pause without
changing any destination, so every other row in this file and in
`ObjectForm.submitRedirect.test.tsx` stays green. Pinned on the number
the wait is armed with, with the **same declaration minus the member as
the lit control** — without it, an empty `armed` would be
indistinguishable from a spy that never sees this renderer's timers.
- **`next-record` is a declared arm that reads NO members.** It shares
the confirmation-panel body with `thank-you` by falling through, so an
edit giving it its own arm or dropping it into `continue`'s no-op would
leave a spec-legal authored value doing something else with nothing red.

## Ablations — every pin shown FAILING, restore proven by blob

Each from the committed state, `trap … EXIT INT TERM`, restore by `git
checkout HEAD -- path` (never a bare checkout), on-disk proof read
**before** any result.

| leg | mutation | anchor before/after | result |
|---|---|---|---|
| 1 | `schema.initialData \|\| schema.initialValues \|\| {}` → `{
...(schema.initialValues ?? {}), ...(schema.initialData ?? {}) }` (the
plausible "improvement") | 2 → 0, injected 2 | **exit 1** — rows **3 and
4** red, rows 1/2/5 green **by construction** |
| 2a | `delayMs: behavior.delayMs ?? 0` → `delayMs: 0` | 1 → 0, injected
1 | **exit 1** — the `delayMs` row red, 6 passed |
| 2b | `case 'next-record'` moved into `continue`'s no-op arm | verified
at `:1105-1106` | **exit 1** — the `next-record` row red, 6 passed |

Blobs: HEAD `42b7a9b0855c5a53276af4f1d9b3842ad6a0899d`; mutated
`30e2c464…` / `e50a99be…` / `ce8521db…`; restored `42b7a9b0…` = the HEAD
blob in all three legs, `git diff HEAD` empty each time. Restored runs:
pin A **5 passed**, pin B **7 passed**.

⚠️ Rows 1, 2 and 5 of pin A survive leg 1 **by construction** and the
file says so — a merge and a `||` agree when only one key is authored.
Not every row is presented as if it reddened.

## Exemption ledger — measured by AST, not by arithmetic

TypeScript AST walk (`ts.createSourceFile` → `VariableDeclaration` →
initializer unwrapped through `as` / `satisfies` / parens → **direct**
`PropertyAssignment` children):

```
                                origin/main (f73521f)   this branch
MEMBER_PIN_EXEMPTIONS                    31                   28
MEMBER_PIN_EXEMPTION_CEILING             31                   28
MEMBER_PINS                              60                   63
NEWLY_JUDGED_UNPINNED_MEMBERS             2                    2
```

Exemptions **−3**, pins **+3** — they move together, so the three keys
were **converted**, not dropped. Instrument control:
`nonPropertyAssignmentMembers` is `[]` on both trees, so no spread,
getter or shorthand hides inside a span the counter treats as flat.

`object-form` is **not** closed: `customFields`, `dataSource`, `mobile`
and `sections` remain — the two-bite shape slice 9 said every remaining
block now has. `NEWLY_JUDGED_UNPINNED_MEMBERS` and
`record:related_list.actions` are untouched by this slice.

## Gates — exit codes captured to disk before any pipe

| gate | exit |
|---|---|
| `apps/console` `registry-inputs-spec-parity.test.ts` (the gate this
card serves) | **0** — 199 passed |
| `pnpm --filter @object-ui/console test` | **0** — 101 files, 1158
tests |
| `pnpm --filter @object-ui/plugin-form test` | **0** — 93 files, 902
passed / 1 skipped |
| `pnpm --filter @object-ui/plugin-form type-check` | **0** |
| `pnpm --filter @object-ui/console type-check` | **0** |
| `pnpm --filter @object-ui/plugin-form lint` · `@object-ui/console
lint` | **0** · **0** (0 errors) |
| the repo's 61 `check:*` scripts | **60 × 0**, one caveat below |
| `check-governed-queue-guard.mjs --test` over the four changed paths |
**0** — NOT GOVERNED, so no maintainer-summary section is owed |

⚠️ **`check:node-esm-load` exited 1 on the first run, and it is not this
diff.** Its own provenance leg refused 2 of 37 entries because turbo's
shared cache replayed artifacts built by a *different worktree*
(`/home/user/objectui-issue-9659` — `@object-ui/auth`,
`@object-ui/react-runtime`), neither of which this diff touches. The
script names that condition and prescribes the remedy itself; re-run
with its own `--force-build`: **exit 0, 37 of 37 gradable entries built
by this tree.**

**Typecheck coverage proven, not assumed:** `tsc -p tsconfig.test.json
--listFiles` lists both `ObjectForm.submitBehavior.test.tsx` and
`objectFormInitialMembers-8071.test.tsx`, so the greens are not the
excluded-tests trap.

**Control bytes:** `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over
all four changed files — no match (and `check:control-bytes` exit 0).

## Changeset grade — MEASURED from a build, with both controls

Empty-frontmatter changeset. Both packages were built and their
published `files[]` output grepped:

| marker | corpus | hits |
|---|---|---|
| `objectFormInitialMembers` | `packages/plugin-form/dist` | **0** |
| `Saved — next one`, `from initialValues` | `packages/plugin-form/dist`
| **0** |
| `MEMBER_PIN_EXEMPTION_CEILING`, `AWAITING_A_PIN` | `apps/console/dist`
| **0** |
| `submitBehavior` (positive control) | `packages/plugin-form/dist` |
**7 files** |
| `object-form` (positive control) | `apps/console/dist` | **12 files**
|

Two zeros with a lit control on the same instrument and corpus. One
near-miss chased down: `registry-inputs-spec-parity` matches **1** file
in `apps/console/dist`, and it is `vendor-objectstack-*.js` —
`@objectstack/spec`'s own prose naming this gate file, not a byte of
this diff.

## Serial constraint — re-taken immediately before the first write

Enumerated over the **11** open PRs (the dispatch's table listed 3; this
lane's tables have been short before). Intersection at FILE level:
**empty**. Nothing open touches
`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` or any
`packages/plugin-form/src/**`. PR objectstack-ai#8941 touches
`packages/plugin-form/package.json` only. Root `package.json` is held by
PR objectstack-ai#9752 in the queue — this batch does not need it.

## Out of scope — handed back, not filed and not fixed

- The **233** array/object inputs on blocks with no spec contract entry
at all are explicitly not this card. Nothing here widens into them.
- ⚠️ An **empty `initialData` silently discarding a populated
`initialValues`** is pinned as the renderer's behaviour and deliberately
not endorsed. Changing it is a renderer change, and Clause-② for this
batch is `no` — so it is reported to the dispatching seat rather than
filed or repaired here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…he pending 8171 body (objectstack-ai#9823)

Fixes objectstack-ai#9781

Clause-②: no

Scope: one sentence of prose in the pending
`.changeset/8171-calendar-sort-input.md`. Frontmatter untouched, and the
rest of that paragraph (the `object-calendar.sort` exemption, the
objectui#8176 backlog ceiling) is not assessed here.

## The defect, and why the reading is DATED rather than corrected

The paragraph ended with: `MEMBER_PIN_EXEMPTION_CEILING` is untouched at
62.

That is two claims with different lifetimes:

- **"is untouched"** — a bounded statement about what objectui#8171
itself did. True when written, and nothing anyone does later can falsify
it. ⭐ KEPT, word for word.
- **"at 62"** — an absolute reading of a SHARED constant. ROTTED:
somebody else's change falsifies it, and one did.

⇒ The repair DATES the reading at its write-time commit and names the
falsifier. ⛔ It does not restate the constant at today's value — that
number is true today and false again at the next slice, i.e. this same
defect authored a second time. The rule the repaired prose now carries:
**state your OWN change's bounded transition ("lowers 62 to 58",
"follows 31 to 28 in the same change") or state that you did not touch
the constant, and never state what its value currently is.**

## Every number, with the instrument that produced it and the ref it was
taken at

Established in this worktree, ⛔ not inherited from the card. The
repository was NOT shallow when measured (`git rev-parse
--is-shallow-repository` prints `false`).

1. **Today's reading — 24, at
`42234b1a2cd7540a5d53907a95438a888a6b5021`** (this branch's base,
`origin/main` fetched 2026-09-18T06:56Z):
`git grep -n -E 'const MEMBER_PIN_EXEMPTION_CEILING = [0-9]+'
42234b1 --
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`
prints `...:3144:const MEMBER_PIN_EXEMPTION_CEILING = 24;`. ⚠️ That
value moves often; it appears here only as the reason the old sentence
is false, and ⛔ it is deliberately not written into the changeset.
2. **The write-time commit — `35e63669d6`, 2026-09-07 (pull request
objectstack-ai#8223)**:
`git log --oneline -S'is untouched at 62' --
.changeset/8171-calendar-sort-input.md` returns exactly that one commit,
and `git log --oneline --reverse --
.changeset/8171-calendar-sort-input.md` shows it is the only commit that
has ever touched the file.
3. **62 WAS the value there, so the sentence was TRUE when written**:
`git show
35e6366:apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
| grep -nE 'const MEMBER_PIN_EXEMPTION_CEILING = [0-9]+;'` prints
`2267:const MEMBER_PIN_EXEMPTION_CEILING = 62;`. ⇒ ROTTED, ⛔ not BORN
FALSE.
4. **The FIRST falsifier — `240c605939`, 2026-09-09 (pull request objectstack-ai#8737,
objectui#8071 slice 1)**:
`git log --oneline --reverse -G'^const MEMBER_PIN_EXEMPTION_CEILING = '
35e6366..42234b1 --
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`
enumerates, in order, the 11 commits that move that line between write
time and this base; `240c605939` is the first of them. At it the
constant reads 58 (`git show
240c605:apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
| grep -nE ...` prints `2437:const MEMBER_PIN_EXEMPTION_CEILING = 58;`)
⇒ the transition that falsified the reading is 62 to 58. Why that one
and not a later one: the enumeration is ordered and complete for that
line over that range, so no earlier commit in the range touched it.
5. **Ancestry legs**, so that range really is the path from write time
to this base: `git merge-base --is-ancestor 35e6366 42234b1` exits
**0**; `git merge-base --is-ancestor 240c605 42234b1` exits **0**;
the control leg in the opposite direction, `git merge-base --is-ancestor
42234b1 35e6366`, exits **1**, as it must.

## Before/after probe of the changed prose — whitespace-tolerant, both
controls, one corpus

⚠️ Changeset prose wraps near eighty columns, so a line-anchored `grep
-F` reads 0 on text that is plainly there. The instrument normalizes
whitespace first (`tr -s '[:space:]' ' '`) and then counts fixed-string
occurrences (`grep -o -F | wc -l`). One corpus: the changeset file, base
copy vs working copy.

| probe | before | after |
|---|---|---|
| LIT control, spans a wrap, untouched by this change: `the shrink-only
objectui#8176 backlog ceiling drops 16 to 15` | 1 | 1 |
| ABSENT control: `MEMBER_PIN_EXEMPTION_CEILING is untouched at 24` | 0
| 0 |
| REMOVED, spans a wrap: `rather than a member-pin exemption.
MEMBER_PIN_EXEMPTION_CEILING is untouched at 62.` | 1 | 0 |
| ADDED, spans a wrap, the dated reading: `MEMBER_PIN_EXEMPTION_CEILING
is untouched by this change; it read 62 at the commit that wrote this
paragraph (35e6366, 2026-09-07)` | 0 | 1 |
| ADDED, spans a wrap, the falsifier: `objectui#8071 slice 1
(240c605, 2026-09-09) lowered it 62 -> 58` | 0 | 1 |
| KEPT, the original sentence quoted verbatim inside the closing note:
`MEMBER_PIN_EXEMPTION_CEILING is untouched at 62` | 1 | 1 |

⭐ The lit control itself spans a wrap, so it proves the instrument
rather than merely accompanying it: the same needle under a
line-anchored `grep -c -F` reads **before=0, after=0** — the false
negative this table exists to avoid.

⚠️ Read the paragraph, not the line: the true half stays as the live
sentence, the rotted half is preserved verbatim as a dated quotation,
and the still-true claim is never deleted along with the false reading.

## Changeset grade for this change — MEASURED, with both controls

`node scripts/check-changeset-presence.mjs`, in a worktree with `pnpm
install` completed.

- **ABSENT control, this change as committed** — exit **0**: "1 file(s)
changed, 0 of them published source of a package the release covers, 0
of them a manifest whose published contract moved, 0 under a package
changesets ignores, 0 changeset(s) added", then "No source or published
contract of a released package changed in this range, so no changeset is
owed."
- **LIT control** — one comment line appended to
`packages/plugin-calendar/src/ObjectCalendar.tsx`, proven to reach disk
(marker occurrences 0 then 1): exit **1**, "2 file(s) changed, 1 of them
published source of a package the release covers", "1 source file(s) of
1 released package(s) changed, and this change adds no changeset". ⇒ the
gate can fail on this corpus, so the green above is a measurement rather
than a silence.
- **Restore leg** — `git checkout HEAD --
packages/plugin-calendar/src/ObjectCalendar.tsx`; `git hash-object` on
the path equals the HEAD blob
`daa84297c2ac6c4a61059a05cc86974750c7295e`, `git diff HEAD` for it
prints nothing, and the gate re-reads exit **0**.

⛔ No changeset is added for this change, and ⛔ the `skip-changeset`
label is not applied — in this repository nothing reads it; the gate's
own verdict line above is the authority.

## Gate exit codes — each captured to disk BEFORE any pipe

Written with the redirect first and the status read from the file
afterwards, never through a pipe.

| gate | exit |
|---|---|
| `node scripts/check-changeset-overwrite.mjs` (case 2: a pre-existing
changeset modified on purpose; report-only) | 0 |
| `node scripts/check-changeset-presence.mjs` | 0 |
| `node scripts/check-changeset-claims.mjs` | 0 |
| `node scripts/check-changeset-fixed.mjs` | 0 |
| `node scripts/check-changeset-no-major.mjs` | 0 |
| `node scripts/check-pending-changeset-literals.mjs` | 0 |
| `node scripts/check-control-bytes.mjs` | 0 |
| `node scripts/check-new-cross-file-line-citations.mjs` | 0 (0 new
citations) |
| `pnpm exec vitest run` over the four `scripts/__tests__/` changeset
suites, under the shared verify lock, slot `issue-9781` | 0 — 4 files,
126 tests passed |

The overwrite gate classifies this change as its **case 2** and prints
both sides of the declaration: "declared at base:
@object-ui/plugin-calendar: minor" and "declares now:
@object-ui/plugin-calendar: minor" — frontmatter untouched, which is
exactly what case 2 asks of a deliberate prose correction.

Gate family: the workflows whose path filters include `.changeset/**`
are `changeset-guard.yml` (no-major plus overwrite), `ci.yml`,
`lint.yml` and `live-e2e.yml`. The repository-wide farm belongs to CI;
the targeted set above is what this diff owes locally.

## The window

Release pull request objectstack-ai#5400 was open at claim time, re-read **open, not
merged** immediately before the first write, and again immediately
before the push (2026-09-18T07:03Z). ⛔ Nothing in this branch touches it
or any file it owns. If it merges before this lands, the card is
re-triaged: the remedy channel becomes a docs-only pull request on a
release-owned tree, and the published CHANGELOG copy is already out of
reach.

Serial constraint, re-taken over the open-pull-request set immediately
before the first write: 19 open pull requests enumerated with their full
changed-file lists (objectstack-ai#5400 paginated, 1732 files). ⛔ Only objectstack-ai#5400 touches
`.changeset/8171-calendar-sort-input.md`, and that is the WINDOW, not a
conflict for this lane to settle. The sibling card objectui#9772 works a
different pending file; no file is shared, and no verdict here was
established by analogy to it.

## Acceptance notes

- Placement: the in-paragraph sentence stays short and the provenance
goes into a closing note at the end of the body, so the paragraph that
follows ("That member pin is deliberately NOT ...") keeps the antecedent
sitting directly above it. Two inserted paragraphs would have separated
them.
- Observed, not filed:
`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` carries
docblock comments that state this same ceiling in the present tense —
"and `MEMBER_PIN_EXEMPTION_CEILING` stays 62", "the reason
`MEMBER_PIN_EXEMPTION_CEILING` reads 62" — each attributed to a named
past card, so a careful reader can date them from context, but the tense
reads live while the constant reads otherwise today. In-tree comments,
not pending changeset prose, and outside this card's surface. Carrier:
whoever moves that ceiling next.

Session: `https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L`

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L)_

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.

3 participants