Repository navigation
fix(app-shell): the hook object picker flags and says "publish" only from an answered roster (objectui#10585) - #10644
Conversation
…from an answered roster `useObjectOptions` now publishes `error` beside `options` / `loading`, spelled the way `useObjectFields` spells it, so a failed fetch is no longer indistinguishable from a catalog that answered with no objects. `HookDefaultInspector` reads the roster through `rosterFrom`: a selected object is flagged "(not published)" and the empty list prints the "publish an object" copy only once the roster answered. In flight the selection is drawn bare and the list says it is loading; on failure the selection is drawn bare and a status notice names the cause, with the shared localized title. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
…oading Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
… roster Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
CI note from the
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsFamily rule. All three states are implemented and pinned in The hook. Census. Verified at source on main. Pins re-run in a scratch worktree (borrowed Coverage gap. CI ran the full sharded suite on the merge ref. Seven shards green; ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS — landable only after shard 2/8 is re-run green and the draft is marked ready; neither is a defect of this PR. Generated by Claude Code |
…ction editable The changeset now says what the action and page-block object pickers read: `options` only. Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…rosterFrom Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10585
Clause-②: no
The Hook inspector's object picker made two claims from its object catalog off the list alone: a selected object the catalog does not list was flagged
(not published), and an empty list printed "No objects found — publish an object, then pick it here." The list is[]while the fetch is in flight and after it fails, exactly as for a catalog with no objects, so both claims fired before the roster answered, and permanently on a failure. This applies the objectui#8862 / objectui#9651 rule: only an ANSWERED roster may make a claim.What changed
previews/useObjectOptions.ts: returns a third fact,error: string | null, beside the unchangedoptions/loading. Spelled the wayuseObjectFieldsspells it:err?.message ?? String(err)in thecatch.erroris reset tonullat the start of every fetch. The answer branch does not touch it, so it staysnullthrough an answer.rosterFrom({ loading, error })therefore reads both hooks alike. The header comment is rewritten to be true: a failure still leavesoptionsempty (the action and page-block pickers still fall back to free text), and it now also setserror.inspectors/HookDefaultInspector.tsx: reads the roster throughrosterFrom.engine.form.loadingOptions("Loading options…").role="status"notice rendersengine.form.optionsLoadFailedTitleplus the cause.InspectorSelectFieldrenders (same role, tone, title key and cause span). That notice is inlined in the primitive in_shared.tsxand not exported,_shared.tsxis read-only for this card, and this picker is a checkbox list, not a select. No new i18n key..changeset/10585-hook-object-roster-state.md:'@object-ui/app-shell': patch.Census (the card's H2, measured)
previews/useObjectOptions.tshas exactly three importers on main:ActionDefaultInspector,HookDefaultInspectorandPageBlockInspector(git grepfor the import specifier).DatasetDefaultInspectorandViewVariantInspectorimport a different same-name hook frominspectors/useDatasetFields.ts.ObjectFieldInspectordeclares a third, file-localuseObjectOptions(locale).ObjectPickergates on{options.length === 0 ? (InspectorTextField…, so an empty roster renders a free-text input. It shows no flag and no empty-state copy. A throwaway probe (real hook, stubbed client, not committed) read:INPUTholdingghost_object, flag false, "publish" copy false;ghost_object (not found), which is a correct claim.ObjectPickerFieldhas the same gate,if (options.length === 0) {returnInspectorTextField…. This one was not probed at runtime; the conclusion rests on that line.Tests
New:
HookDefaultInspector.rosterFailure.test.tsx(8 cases, mirroringViewColumnInspector.rosterFailure.test.tsx) andpreviews/useObjectOptions.test.tsx(3 cases).b1030c71awith the new tests kept. The on-disk blobs matched the base blobs, and the markersrosterFrom/setErrorcounted 0. Result at heade5a135708:Tests 9 failed | 2 passed (11). The two that pass are the answered-arm CONTROLS, as designed. The editability case is red on base because its bare-named checkbox does not exist there (the base label is flagged). Restore usedgit checkout HEAD --, andgit diff HEADwas empty afterwards.e5a135708:Test Files 2 passed (2) · Tests 11 passed (11). The editability case alone, verbose:✓ … leaves the selection EDITABLE after the failure — un-ticking it reaches the draft 207ms(Tests 1 passed | 7 skipped (8)). Atcab28b435, the run with the existingHookDefaultInspector.flag.i18n-10448.test.tsxreadTest Files 3 passed (3) · Tests 13 passed (13).cab28b435against the 7-case suite, usedablation-replace.mjs. Every anchor hit exactly once, every mutation was proven on disk by a blob change, and every restore was proven (blob equals HEAD,git diff HEADempty):rosterAnswered ? flagUnknownValue(…) : nbecomes always-flag):2 failed | 5 passed. The failed-arm "not published" row and the in-flight bare row went red.{rosterAnsweredbecomes{true):1 failed | 6 passed. "does not tell the author to publish an object while loading" went red.true &&):1 failed | 6 passed. The failed-arm row "a failed request is no longer in flight" went red.{false && ():1 failed | 6 passed. "tells the author the roster failed, and names the cause" went red.invoked-as.mjs(MODULE_NOT_FOUND), so nothing mutated. It was re-run with that module present, and the readings above come from the re-run.Local verification
a1c3d2984): one changeset sentence changed, from "The Hook inspector reads the three throughrosterFrom" to "The Hook inspector foldsloadinganderrorthroughrosterFrom". The frontmatter md5 is4e8ce55021fe3b14329c8b4cb3f9f354both before and after.check-changeset-presence,check-changeset-no-majorandcheck:control-bytesall exit 0.git merge-treeagainstf99f9cde7exit 0.e5a135708) changed only a test case and one changeset sentence. At this head:check-changeset-presence: "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"check-changeset-no-major: "No changeset declares amajorbump"check:control-bytes: OKtype-checkcovers the new case.cab28b435.pnpm --filter @object-ui/app-shell type-check: exit 0. That coverstsc --noEmitplustsc -p tsconfig.test.json.--listFilesOnlyconfirms both new test files and both edited sources are in the programs. The dependency closure was built first withpnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' run build: exit 0.HookDefaultInspector,ActionDefaultInspector,PageBlockInspector,useObjectOptionsordefault-inspector-registrypassed:Test Files 54 passed (54) · Tests 528 passed (528).vitest run packages/app-shell/is NOT MEASURED. I stopped it myself at 24m56s holding the shared verify lock (exit 143) because it was starving four queued sibling agents.inspectors/index.tsbarrel, without naming them. CI runs the full sharded suite.cab28b435, each exit 0 with its own verdict line:check-changeset-presence: "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"check-changeset-claims,check-changeset-no-majorandcheck-changeset-overwritecheck:new-line-citations: "0 new citation(s)"check:control-bytescheck:vi-mock-specifiers,check:vi-mock-inheritandcheck:vi-mock-override-shapecheck:test-path-roots,check:i18n-keys,check:pending-changeset-literalsandcheck:unreferenced-sourceseslint.config.js's scope, and the--format jsonrun returned 4 results with no ignore notice.react-hooks/set-state-in-effecton the effect's openingsetLoading(true), and threeno-explicit-anyin the mapper) are present on the base version of the file too, read through--stdin. The inspector has 0 findings before and after.eslint.config.jsconfigures noparserOptions.projectorprojectService, so linting is not type-aware and this diff cannot move a verdict on an untouched file.git merge-tree --write-tree origin/main HEAD: exit 0 againsteca276070in round 1 and againstf99f9cde7in round 2. Main did not move in any file this PR touches, so no merge commit was made.Pending changesets (H6)
git grep -l -E "HookDefaultInspector|useObjectOptions|ActionDefaultInspector|PageBlockInspector" origin/main -- .changesetreturns 10 pending changesets. None describes the object picker's roster behaviour, and no sentence in them is falsified by this change.The closest is
10448-designer-invalid-not-published-flags.md: "the Hook inspector's object picker flagged a selected object the live catalog does not list(not published)". It stays true, because the flag still fires once the catalog has answered. The 8862 and 9651 family changesets describeInspectorSelectFieldcall sites, which this change does not touch.Acceptance notes
inspectors/useDatasetFields.tshas the same missing failure state. Itscatchsets{ options: [], loading: false }, and nothing else. The probe read a failed fetch and an answered-empty fetch as byte-identical{"options":[],"loading":false}.DatasetDefaultInspector's "Base object" andViewVariantInspector's object picker, renderInspectorComboField. That field draws a stored value raw, with no absence flag.emptyText"No match — keep typing to use a custom value.", with no failure notice.InspectorComboFieldwithout a fault arm. Out of scope here, and not merged with this hook.ActionDefaultInspectorandPageBlockInspectordegrade to a text box with no failure notice. They make no false claim, but the failure goes unsaid: this is the objectui#9651 "silence" concern, a distinct and lesser class. Not fixed here.ObjectFieldInspector, was assessed by the contract reviewer as not reachable with this defect: it feeds a datalist input and renders no flag and no empty-state copy.Generated by Claude Code