You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[finding] WidgetContext still spells a failed option catalog as an empty array, with the fault on a side channel a new picker can forget to read #5228
Found while implementing objectui#5170 (PR objectui#5226). This is a measurement of the contract that PR leaves behind, filed unassigned for PM triage — the shipped behaviour is correct, this is about how easy it stays correct.
The reading
PR objectui#5226 routed the three option-picker loaders through a four-arm LoadState union (idle | loading | loaded | error) in ResourceEditPage.tsx, so the loaders themselves can no longer spell a fault as a measurement. But at the WidgetContext boundary in packages/app-shell/src/views/metadata-admin/widgets.tsx the union is projected back down into a pair:
the catalog arrays (objectNames, objectFields, objectViews, objectActions) are still [] on a failed load — byte-identical to a completed load that found nothing;
the fault travels alongside them, in a separate catalogErrors record.
Every picker in the file consults catalogErrors first today, and the PR's tests pin that per loader. The residue is that the contract does not require it. A picker written next month that reads
constfields=context?.objectFields??[];
and renders "no fields" is type-correct, reads naturally, passes review, and silently reintroduces exactly the defect objectui#5170 was filed for. The declaration and the enforcement have come apart: CatalogErrors's own doc comment has to say "a picker MUST consult this before it renders its catalog", which is a comment doing a type's job.
Why the PR did not just do it
Stated so the trade-off is on the record rather than rediscovered: making the catalogs themselves the union is the honest shape, and it was measured as reachable — the true WidgetContext picker reads are confined to 11 sites in widgets.tsx plus one in SchemaForm.tsx (detectFieldRefWidget). The cost is that it also breaks every test fixture that constructs a context literal (SchemaForm.widgetLabelling, ViewRefWidget, SchemaForm.actionObjectPicker, selector-placeholder.i18n, FilterModeWidget — roughly 17 lines across 5 files), none of which were in #5170's declared file surface. That is a mechanical but real widening of a bug-fix PR's blast radius into other people's fixtures, so it was left for a decision rather than taken unilaterally.
Shape, if it is taken
Replace the pair with the union at the boundary — e.g. objectFields?: LoadState<...> reusing views/metadata-admin/loadState.ts — so an unchecked ?? [] stops compiling instead of silently rendering a lie. The loud break at each read site is the point: it is a compile error at exactly the places that must decide what a failure looks like. A small readCatalog() accessor keeps each call site to a couple of lines, and the fixtures move to a loadedCatalog([...]) helper.
Worth doing as one change with objectui#5227 (the fourth loader, FieldSelectorWidget), since that one needs new render wiring anyway and would be the first consumer of the tightened contract.
Severity
Deliberately not asserted. Nothing is user-visible today — this is a hardening/drift card, not a defect — and objectui#5110's own card records that severity judged at filing time is unreliable in both directions. Filed plainly for the triage round.
Dedup
Searched open issues for the WidgetContext option-catalog contract, for the empty-array-on-failure spelling, and for picker error side-channels; nothing covering it (zero hits, including the control terms). Control: the neighbouring search family returns objectui#5040 and objectui#4871, both real WidgetContext / metadata-admin widget cards, so the query family is live rather than dead. No duplicate.
Related
objectui#5170 and objectui#5169 (the cards whose fix introduced this boundary, PR objectui#5226) · objectui#5227 (the fourth loader, which this would tighten first) · objectui#5110 (the union) · ADR-0110 D3, "a miss and a fault are different facts"
First-touch grading (triage seat, session session_014tGY3fzu4uwCoe7HrfUtWg): promoted to pm:queue, type Task.
Rationale: the trade-off the PR left on the record is real, but the residue is exactly the declared≠enforced shape this codebase keeps paying for — a doc comment ("a picker MUST consult catalogErrors") doing a type's job, one ?? [] away from silently reintroducing the #5170 defect class. The filer measured reachability (11 sites + 1) and the fixture blast radius (~17 lines / 5 files), so the union-at-the-boundary move is priced, mechanical, and makes the wrong read a compile error at exactly the sites that must decide what failure looks like.
Dispatch notes: do it together with #5227 (the fourth loader needs new render wiring anyway and becomes the first consumer of the tightened contract) — same file surface, so one claim or explicit serialization, the lane PM's call. Size/model suggestion: M, opus.
Claim: PM loop round 17
Session: session_01RV6yuVCxymHYE16PL9vQkE
Branch: claude/issue-5228-widget-catalog-load-state-union
Worktree: objectui-issue-5228
Domain: repo:objectui (execution seat)
File surface: packages/app-shell/src/views/metadata-admin/** — WidgetContext, loadState.ts, widgets.tsx, and the picker call sites the union touches, plus tests in that directory. Declare the exact list in your report. ⛔ OUT: packages/components/src/renderers/complex/data-table.tsx (#5120's consumer half is in the maintainer's inbox — ⛔ do not touch that alias in either direction), packages/plugin-grid/src/ObjectGrid.tsx (#5349, filed not dispatched), packages/plugin-dashboard/** (#5120's producer half just merged — rebase).
Container & model: M, mode:subagent, model: opus (per the triage seat).
Clause-②: no — this tightens a type at a boundary. Nothing widens; the accepted set of authored metadata does not move at all. What changes is that a caller which ignores the failure channel stops compiling.
Serial constraints cleared, with one live note: packages/app-shell is being touched by #5216 right now (PR5354 open) — but in different subtrees (src/hooks/, src/urlParams.ts, src/views/ObjectView.tsx, src/console/home/, src/environment/), not src/views/metadata-admin/. That is the maintainer's 2026-08-18 app-shell subdirectory-level serial exemption, and its four replacement disciplines bind you: (a) declare your file surface to region level, (b) merge main once before opening the PR, (c) merge main again after PR5354 lands, (d) if you conflict, let the merge queue arbitrate — ⛔ do not hand-order anything. The exemption ⛔ does not extend outside packages/app-shell.
⚠️ The dispatch note on this card is STALE — read this before you plan
The triage comment says:
Dispatch notes: do it together with #5227 (the fourth loader needs new render wiring anyway and becomes the first consumer of the tightened contract) — same file surface, so one claim or explicit serialization, the lane PM's call.
#5227 has landed. Closed completed 2026-08-18 22:36Z via merged PR #5263. So there is nothing to bundle, and the situation is better than the note assumed: the first consumer of the tightened contract already exists in the tree. Read PR5263 before you start — it is your reference implementation, and the union it consumed (loadState.ts, added by PR5226, mirroring the one PR5110 landed for the References panel) plus the shared PickerLoadFailure render are both already in place.
That makes this wiring, not design — the card says so itself, and it is now more true than when it was written.
What is actually wrong
The residue PR5226 left on the record is the shape this codebase keeps paying for: a doc comment doing a type's job.WidgetContext still spells a failed option catalog as an empty array, with the fault parked on a side channel — and the contract that a picker must consult catalogErrors lives in a comment. One ?? [] away from silently reintroducing the #5170 / #5169 defect class: a failure rendered as a completed, empty list.
The filer priced the move: 11 reachable sites + 1, fixture blast radius ~17 lines across 5 files. Re-measure both on today's main — the card is ~24 hours old and this directory has taken at least two merges since (PR5263, PR5327).
The move: union at the boundary, so the wrong read is a compile error at exactly the sites that must decide what failure looks like. That is the whole value — ⛔ do not implement it as a runtime check, and ⛔ do not leave a ?? [] convenience overload that lets a caller opt out of deciding.
Tests
A successful empty catalog and a failed catalog are distinguishable at the type level and render differently — this is the defect; pin both.
Every migrated call site renders the shared PickerLoadFailure on the failure arm, matching what PR5263 did. ⛔ Do not invent a second failure presentation.
A call site that ignores the failure arm does not compile.⚠️ This one needs a type-level assertion or an explicit @ts-expect-error pin, not a runtime test — say which you used and why it actually fails when the union is loosened.
Nothing else in metadata-admin regresses.
Reverse-verify: restore the old shape, keep the tests, predict which go red before running, report predicted vs observed. ⚠️ Expect a mixed direction here — a type-level pin fails at tsc, not in vitest, so say per leg which tool reports the failure, and state whether a build artifact sits between the edit and the thing under test (app-shell type-checks against @object-ui/* built dist, so at least one leg will need the dependency closure built — PR5346's dev hit exactly that and reported it).
Gates
Derive from your diff (scripts/pm/dispatch-gates.mjs is objectstack-only). Run vitest from the repo root — the reason is not that package-scoped runs are refused (17 packages own a standalone vitest.config.ts and are not refused) but that a package-scoped run uses a different config than CI does. ⚠️pnpm --filter @object-ui/app-shell type-check gives a false red (TS2307 on every workspace import) unless the dependency closure is built first — build it, and say you did. Changeset per check-changeset-presence.mjs's verdict; patch or minor, ⛔ never major.
⛔ Do not background a long check and end your turn. ⛔ No exhaustive paginated API sweeps.
Deliver
Draft PR against main, first body line Fixes #5228. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal <!-- os-dev-report --> marker.
{
"issue": 5228,
"status": "done",
"branch": "claude/issue-5228-widget-catalog-load-state-union",
"pr": "https://github.com/objectstack-ai/objectui/pull/5355",
"premise_still_valid": true,
"summary": "Every option catalog on WidgetContext (objectNames, objectFields, objectViews, objectActions) is now the four-arm LoadState the loaders already produce, handed over intact instead of projected back down into an array plus a catalogErrors side channel plus a *Loading flag; CatalogErrors and the four *Loading keys are deleted. Two pieces make the failure decision a compiler-checked precondition rather than a doc-comment rule: FaultFreeLoadState (every arm except the failure, with LoadState declared in terms of it so they cannot drift) and offeredOptions(state, empty), which takes FaultFreeLoadState and so cannot be called by a caller still holding the error arm. No runtime check was added and no '?? []' convenience overload was left. objectFields and objectActions are now derived from the ONE catalog state via mapLoaded, so the pair that rides a single client.get('object') request agrees by construction instead of by the reader knowing they share a fetch. The tightening surfaced a live instance of the #5170 defect class the card never named: inspectors/ViewVariantInspector.tsx is a second WidgetContext host and forwarded only the 'fields' third of the triple useObjectFields reports, so a failed field catalog rendered as 'No object bound' about an object that IS bound; it now spells all three arms. Re-measured on today's main (58398ba52): 9 catalog reads in widgets.tsx (card said 11) + 1 in SchemaForm.tsx + 1 previously-unknown host = 11 production sites, and exactly the 5 fixture files the card named (24 compiler-reported sites). SchemaForm.tsx's detectFieldRefWidget is deliberately NOT migrated: it tests wiring, not contents, and every arm must answer yes or a failed catalog would silently demote the picker back to the free-text input it exists to replace.",
"tests": "All gates below run on the FINAL commit 79424e1bf (git rev-parse --short HEAD from that run). (1) Dependency closure built first — `pnpm --filter '@object-ui/app-shell^...' build` exit 0 — without it type-check is the documented false red (TS2307 on every workspace import); it was built, and I say so because the dispatch asked. (2) `pnpm --filter @object-ui/app-shell type-check` exit 0; the script name echoed in the output (`tsc --noEmit && tsc -p tsconfig.test.json`), so this is not the zero-script-match silent pass. (3) vitest FROM THE REPO ROOT (CI's config, not the package-scoped one): `npx vitest run packages/app-shell/src/views/metadata-admin --maxWorkers=2` -> 'Test Files 186 passed (186) / Tests 1941 passed | 1 skipped (1942)', exit 0 — nothing else in metadata-admin regresses. New suite WidgetContext.catalogUnion.test.tsx: 21/21. (4) check-changeset-presence.mjs: '11 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-fixed.mjs and check-changeset-no-major.mjs green (bump is patch). (5) check-control-bytes.mjs OK (4751 files) plus a manual grep -naP scan of the changed files: clean. (6) check-type-check-coverage.mjs: 45/46 and 41/41, unchanged. (7) check-i18n-call-site-keys.mjs OK; check-i18n-dead-keys.mjs is a report, no new hits. (8) eslint packages/app-shell: 0 errors (2493 pre-existing warnings; the single new warning this branch introduced was fixed). TYPE-LEVEL PIN — I used explicit @ts-expect-error directives, not a type-assertion helper, and the reason is that each is inherently TWO-WAY: @ts-expect-error is itself an error (TS2578 'Unused directive') the moment the line below it starts compiling, so loosening the union turns the file red instead of quietly passing, which an Assert-style helper does not do. PIN A is the card's line (`ctx?.objectFields ?? []` assigned to an array); PIN B is `offeredOptions(anyArm, [])` on an undecided state, with a POSITIVE CONTROL immediately below (the same state, narrowed, is accepted) so PIN B cannot pass merely by the accessor being uncallable; PIN C pins fault-vs-measurement as distinct types; PIN D pins that catalogErrors and objectsLoading are gone, not merely unused; PIN E pins that absence resolves to the idle arm. REVERSE VERIFICATION — two legs, predicted before running, mixed direction as the dispatch expected, and per leg the tool that reports it. BUILD ARTIFACT: none sits between the edit and either subject — the pins import ./widgets and ./loadState as same-package RELATIVE source, and the root vitest config aliases every @object-ui/* name to src/; the built dist matters only for the cross-package imports these files also pull, which is exactly why the closure build above is a precondition of type-check. LEG 1 (loosen WidgetContext.objectFields back to a plain array, tests kept): PREDICTED tsc -p tsconfig.test.json RED with PIN A reporting TS2578; tsc --noEmit RED at the objectFields consumers; vitest GREEN because types are erased and the fixtures still pass real state objects at runtime. OBSERVED exactly that — 'WidgetContext.catalogUnion.test.tsx(107,3): error TS2578: Unused @ts-expect-error directive' (36 errors total in the test project), source project red at widgets.tsx(1248,19), ResourceEditPage.tsx(744,7) and ViewVariantInspector.tsx(306,54), and vitest 21/21 GREEN. One thing I did NOT predict: PIN D also flipped to TS2578 (line 146), because the already-failing sibling property in the same object literal suppresses the excess-property check PIN D relies on — same direction, one extra red, reported because the template's prediction was incomplete rather than wrong. LEG 2 (keep the union, hand-narrow field-ref so it drops the failure arm — `status === 'loaded' ? data : []` with the early return deleted, which is what a caller who bypasses offeredOptions can still write): PREDICTED tsc GREEN in both projects, vitest RED on exactly one test. OBSERVED exactly that — 0 errors in both tsc projects, vitest '1 failed | 20 passed', the failure being 'field-ref ... a FAILED catalog replaces the picker with the shared failure block', with the empty-success test still green so it is not a tautology. Leg 2 is the honest limit of the type half and the reason both halves exist: the compiler forces the decision at every site that uses the accessor, and vitest catches the one who hand-narrows around it. Both legs were run from a COMMITTED state and restored with `git checkout <branch> -- <path>`; restore verified byte-identical (git status --short and git diff --stat both empty).",
"open_questions": [],
"out_of_scope_findings": []
}
File surface, exact (all inside the declared region packages/app-shell/src/views/metadata-admin/**, disjoint from PR5354's subtrees): loadState.ts, widgets.tsx, ResourceEditPage.tsx, SchemaForm.tsx (comment only), inspectors/ViewVariantInspector.tsx, plus fixtures SchemaForm.widgetLabelling.test.tsx, FilterModeWidget.test.tsx, SchemaForm.actionObjectPicker.test.tsx, ViewRefWidget.test.tsx, selector-placeholder.i18n.test.tsx, the new WidgetContext.catalogUnion.test.tsx, and .changeset/widget-context-catalog-load-state-5228.md. 12 files, +864/-223.
skip-changeset was not applied and no label was created — this PR carries a real changeset, and per the dispatch that label does not exist as a mechanism in this repo.
Report delivered at draft-PR time per the 2026-08-10 ruling; CI convergence is the PM's to read. No CI polling was done.
Activity
os-support-ai commentedon Aug 18, 2026
First-touch grading (triage seat, session
session_014tGY3fzu4uwCoe7HrfUtWg): promoted topm:queue, type Task.Rationale: the trade-off the PR left on the record is real, but the residue is exactly the declared≠enforced shape this codebase keeps paying for — a doc comment ("a picker MUST consult catalogErrors") doing a type's job, one
?? []away from silently reintroducing the #5170 defect class. The filer measured reachability (11 sites + 1) and the fixture blast radius (~17 lines / 5 files), so the union-at-the-boundary move is priced, mechanical, and makes the wrong read a compile error at exactly the sites that must decide what failure looks like.Dispatch notes: do it together with #5227 (the fourth loader needs new render wiring anyway and becomes the first consumer of the tightened contract) — same file surface, so one claim or explicit serialization, the lane PM's call. Size/model suggestion: M, opus.
Generated by Claude Code
os-support-ai commentedon Aug 19, 2026
Claim: PM loop round 17
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5228-widget-catalog-load-state-unionWorktree:
objectui-issue-5228Domain:
repo:objectui(execution seat)File surface:
packages/app-shell/src/views/metadata-admin/**—WidgetContext,loadState.ts,widgets.tsx, and the picker call sites the union touches, plus tests in that directory. Declare the exact list in your report. ⛔ OUT:packages/components/src/renderers/complex/data-table.tsx(#5120's consumer half is in the maintainer's inbox — ⛔ do not touch that alias in either direction),packages/plugin-grid/src/ObjectGrid.tsx(#5349, filed not dispatched),packages/plugin-dashboard/**(#5120's producer half just merged — rebase).Container & model:
M,mode:subagent,model: opus(per the triage seat).Clause-②: no — this tightens a type at a boundary. Nothing widens; the accepted set of authored metadata does not move at all. What changes is that a caller which ignores the failure channel stops compiling.
Serial constraints cleared, with one live note:
packages/app-shellis being touched by #5216 right now (PR5354 open) — but in different subtrees (src/hooks/,src/urlParams.ts,src/views/ObjectView.tsx,src/console/home/,src/environment/), notsrc/views/metadata-admin/. That is the maintainer's 2026-08-18 app-shell subdirectory-level serial exemption, and its four replacement disciplines bind you: (a) declare your file surface to region level, (b) mergemainonce before opening the PR, (c) mergemainagain after PR5354 lands, (d) if you conflict, let the merge queue arbitrate — ⛔ do not hand-order anything. The exemption ⛔ does not extend outsidepackages/app-shell.The triage comment says:
#5227 has landed. Closed
completed2026-08-18 22:36Z via merged PR #5263. So there is nothing to bundle, and the situation is better than the note assumed: the first consumer of the tightened contract already exists in the tree. Read PR5263 before you start — it is your reference implementation, and the union it consumed (loadState.ts, added by PR5226, mirroring the one PR5110 landed for the References panel) plus the sharedPickerLoadFailurerender are both already in place.That makes this wiring, not design — the card says so itself, and it is now more true than when it was written.
What is actually wrong
The residue PR5226 left on the record is the shape this codebase keeps paying for: a doc comment doing a type's job.
WidgetContextstill spells a failed option catalog as an empty array, with the fault parked on a side channel — and the contract that a picker must consultcatalogErrorslives in a comment. One?? []away from silently reintroducing the #5170 / #5169 defect class: a failure rendered as a completed, empty list.The filer priced the move: 11 reachable sites + 1, fixture blast radius ~17 lines across 5 files. Re-measure both on today's
main— the card is ~24 hours old and this directory has taken at least two merges since (PR5263, PR5327).The move: union at the boundary, so the wrong read is a compile error at exactly the sites that must decide what failure looks like. That is the whole value — ⛔ do not implement it as a runtime check, and ⛔ do not leave a
?? []convenience overload that lets a caller opt out of deciding.Tests
PickerLoadFailureon the failure arm, matching what PR5263 did. ⛔ Do not invent a second failure presentation.@ts-expect-errorpin, not a runtime test — say which you used and why it actually fails when the union is loosened.metadata-adminregresses.Reverse-verify: restore the old shape, keep the tests, predict which go red before running, report predicted vs observed.⚠️ Expect a mixed direction here — a type-level pin fails at
tsc, not in vitest, so say per leg which tool reports the failure, and state whether a build artifact sits between the edit and the thing under test (app-shelltype-checks against@object-ui/*builtdist, so at least one leg will need the dependency closure built — PR5346's dev hit exactly that and reported it).Gates
Derive from your diff (⚠️
scripts/pm/dispatch-gates.mjsis objectstack-only). Run vitest from the repo root — the reason is not that package-scoped runs are refused (17 packages own a standalonevitest.config.tsand are not refused) but that a package-scoped run uses a different config than CI does.pnpm --filter @object-ui/app-shell type-checkgives a false red (TS2307on every workspace import) unless the dependency closure is built first — build it, and say you did. Changeset percheck-changeset-presence.mjs's verdict;patchorminor, ⛔ nevermajor.⛔ Do not background a long check and end your turn. ⛔ No exhaustive paginated API sweeps.
Deliver
Draft PR against
main, first body lineFixes #5228. Push the branch early. Return the JSON report and post it as a comment whose first line is the literal<!-- os-dev-report -->marker.Generated by Claude Code
fix(app-shell): a WidgetContext option catalog carries its own load s…
os-support-ai commentedon Aug 19, 2026
{ "issue": 5228, "status": "done", "branch": "claude/issue-5228-widget-catalog-load-state-union", "pr": "https://github.com/objectstack-ai/objectui/pull/5355", "premise_still_valid": true, "summary": "Every option catalog on WidgetContext (objectNames, objectFields, objectViews, objectActions) is now the four-arm LoadState the loaders already produce, handed over intact instead of projected back down into an array plus a catalogErrors side channel plus a *Loading flag; CatalogErrors and the four *Loading keys are deleted. Two pieces make the failure decision a compiler-checked precondition rather than a doc-comment rule: FaultFreeLoadState (every arm except the failure, with LoadState declared in terms of it so they cannot drift) and offeredOptions(state, empty), which takes FaultFreeLoadState and so cannot be called by a caller still holding the error arm. No runtime check was added and no '?? []' convenience overload was left. objectFields and objectActions are now derived from the ONE catalog state via mapLoaded, so the pair that rides a single client.get('object') request agrees by construction instead of by the reader knowing they share a fetch. The tightening surfaced a live instance of the #5170 defect class the card never named: inspectors/ViewVariantInspector.tsx is a second WidgetContext host and forwarded only the 'fields' third of the triple useObjectFields reports, so a failed field catalog rendered as 'No object bound' about an object that IS bound; it now spells all three arms. Re-measured on today's main (58398ba52): 9 catalog reads in widgets.tsx (card said 11) + 1 in SchemaForm.tsx + 1 previously-unknown host = 11 production sites, and exactly the 5 fixture files the card named (24 compiler-reported sites). SchemaForm.tsx's detectFieldRefWidget is deliberately NOT migrated: it tests wiring, not contents, and every arm must answer yes or a failed catalog would silently demote the picker back to the free-text input it exists to replace.", "tests": "All gates below run on the FINAL commit 79424e1bf (git rev-parse --short HEAD from that run). (1) Dependency closure built first — `pnpm --filter '@object-ui/app-shell^...' build` exit 0 — without it type-check is the documented false red (TS2307 on every workspace import); it was built, and I say so because the dispatch asked. (2) `pnpm --filter @object-ui/app-shell type-check` exit 0; the script name echoed in the output (`tsc --noEmit && tsc -p tsconfig.test.json`), so this is not the zero-script-match silent pass. (3) vitest FROM THE REPO ROOT (CI's config, not the package-scoped one): `npx vitest run packages/app-shell/src/views/metadata-admin --maxWorkers=2` -> 'Test Files 186 passed (186) / Tests 1941 passed | 1 skipped (1942)', exit 0 — nothing else in metadata-admin regresses. New suite WidgetContext.catalogUnion.test.tsx: 21/21. (4) check-changeset-presence.mjs: '11 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-fixed.mjs and check-changeset-no-major.mjs green (bump is patch). (5) check-control-bytes.mjs OK (4751 files) plus a manual grep -naP scan of the changed files: clean. (6) check-type-check-coverage.mjs: 45/46 and 41/41, unchanged. (7) check-i18n-call-site-keys.mjs OK; check-i18n-dead-keys.mjs is a report, no new hits. (8) eslint packages/app-shell: 0 errors (2493 pre-existing warnings; the single new warning this branch introduced was fixed). TYPE-LEVEL PIN — I used explicit @ts-expect-error directives, not a type-assertion helper, and the reason is that each is inherently TWO-WAY: @ts-expect-error is itself an error (TS2578 'Unused directive') the moment the line below it starts compiling, so loosening the union turns the file red instead of quietly passing, which an Assert-style helper does not do. PIN A is the card's line (`ctx?.objectFields ?? []` assigned to an array); PIN B is `offeredOptions(anyArm, [])` on an undecided state, with a POSITIVE CONTROL immediately below (the same state, narrowed, is accepted) so PIN B cannot pass merely by the accessor being uncallable; PIN C pins fault-vs-measurement as distinct types; PIN D pins that catalogErrors and objectsLoading are gone, not merely unused; PIN E pins that absence resolves to the idle arm. REVERSE VERIFICATION — two legs, predicted before running, mixed direction as the dispatch expected, and per leg the tool that reports it. BUILD ARTIFACT: none sits between the edit and either subject — the pins import ./widgets and ./loadState as same-package RELATIVE source, and the root vitest config aliases every @object-ui/* name to src/; the built dist matters only for the cross-package imports these files also pull, which is exactly why the closure build above is a precondition of type-check. LEG 1 (loosen WidgetContext.objectFields back to a plain array, tests kept): PREDICTED tsc -p tsconfig.test.json RED with PIN A reporting TS2578; tsc --noEmit RED at the objectFields consumers; vitest GREEN because types are erased and the fixtures still pass real state objects at runtime. OBSERVED exactly that — 'WidgetContext.catalogUnion.test.tsx(107,3): error TS2578: Unused @ts-expect-error directive' (36 errors total in the test project), source project red at widgets.tsx(1248,19), ResourceEditPage.tsx(744,7) and ViewVariantInspector.tsx(306,54), and vitest 21/21 GREEN. One thing I did NOT predict: PIN D also flipped to TS2578 (line 146), because the already-failing sibling property in the same object literal suppresses the excess-property check PIN D relies on — same direction, one extra red, reported because the template's prediction was incomplete rather than wrong. LEG 2 (keep the union, hand-narrow field-ref so it drops the failure arm — `status === 'loaded' ? data : []` with the early return deleted, which is what a caller who bypasses offeredOptions can still write): PREDICTED tsc GREEN in both projects, vitest RED on exactly one test. OBSERVED exactly that — 0 errors in both tsc projects, vitest '1 failed | 20 passed', the failure being 'field-ref ... a FAILED catalog replaces the picker with the shared failure block', with the empty-success test still green so it is not a tautology. Leg 2 is the honest limit of the type half and the reason both halves exist: the compiler forces the decision at every site that uses the accessor, and vitest catches the one who hand-narrows around it. Both legs were run from a COMMITTED state and restored with `git checkout <branch> -- <path>`; restore verified byte-identical (git status --short and git diff --stat both empty).", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, outside the JSON
widgets.tsxswallows a failed field fetch into an empty field list —FieldSelectorWidget#5227 had landed via PR fix(app-shell): a failed field fetch in the field-selector picker is not an empty field list (#5227) #5263, so there was nothing to bundle, and PR5263 was used as the reference implementation throughout — the sharedPickerLoadFailurerender was reused at every migrated site and no second failure presentation was invented.packages/app-shell/src/views/metadata-admin/**, disjoint from PR5354's subtrees):loadState.ts,widgets.tsx,ResourceEditPage.tsx,SchemaForm.tsx(comment only),inspectors/ViewVariantInspector.tsx, plus fixturesSchemaForm.widgetLabelling.test.tsx,FilterModeWidget.test.tsx,SchemaForm.actionObjectPicker.test.tsx,ViewRefWidget.test.tsx,selector-placeholder.i18n.test.tsx, the newWidgetContext.catalogUnion.test.tsx, and.changeset/widget-context-catalog-load-state-5228.md. 12 files, +864/-223.mainwas merged before opening and was already up to date at 58398ba. (c) is still outstanding and is not mine to time:mainmust be merged again after PR5354 (Consume the declared navrunActionslot — retire the private?runAction=string convention #5216) lands.skip-changesetwas not applied and no label was created — this PR carries a real changeset, and per the dispatch that label does not exist as a mechanism in this repo.Generated by Claude Code
Generated by Claude Code
fix(app-shell): a WidgetContext option catalog carries its own load s…