Repository navigation
finding(ui): the schema-settled $expand gate is now four hand copies, and four more views resolve the schema without gating on it #6482
Description
Recorded while implementing objectui#6453, which is where the third hand copy of the schema-settled gate was written. This card exists so the convergence question is asked deliberately rather than answered by accretion, and so the second half — five sibling views that resolve the schema but do not gate on it — is not lost.
Filed as an observation. Nothing here is a correctness defect; objectui#6481 (ObjectTree's unkeyed latch) is filed separately as one.
Where the family stands today
| component | shape | keyed by object? |
|---|---|---|
plugin-kanban/src/ObjectKanban.tsx |
{ key, def } | null, gates the whole fetch effect (objectui#6271) |
yes |
plugin-view/src/ObjectView.tsx |
{ key, def } | null, gates the non-grid fetch effect (objectui#6419) |
yes |
plugin-calendar/src/ObjectCalendar.tsx |
{ key, def } | null, gates the object-provider branch (objectui#6453) |
yes |
plugin-tree/src/ObjectTree.tsx |
objectSchema + a bare schemaSettled boolean (objectui#6014) |
no — objectui#6481 |
Four copies. objectui#6453's triage note asked the implementer to prefer adopting a shared helper if objectui#6419's landed shape extracted one; it did not (e929c562a touched only ObjectView.tsx, one test and a changeset), so objectui#6453 wrote the third copy deliberately and declared it. The reasons it did not extract, which are the inputs to this card rather than a pre-ruling of it:
- The resolution half is genuinely common:
useState<{key, def} \| null>(null), a key,ready = res !== null && res.key === key,def = ready ? res.def : null, and an effect whose every exit settles. - The gate half is not.
ObjectCalendarkeys ondataConfig.object ?? schema.objectNamerather thanschema.objectName, and gates one provider branch rather than the whole effect, because an inlinevaluedata set issues no metadata read at all — a whole-effect gate would hold its query open on a resolution nothing was going to produce. A helper would hold roughly the resolution half and leave each component its own gate placement. - A helper would have to live in
@object-ui/react(AGENTS.md §3 bars React from@object-ui/core), which makes it a new export on a published package — a permanent public-surface commitment. All four plugins already depend on@object-ui/react, so it is feasible; the question is whether it is wanted. - Doing it would refactor two already-landed fixes (objectui#6271, objectui#6419) plus a fourth divergent copy.
The second half: five views resolve the schema but do not gate on it
Every other buildExpandFields consumer already lists the schema in its fetch effect's dependency list, so none of them carries the never-expands defect. They carry the other regime — two queries, the first unexpanded:
plugin-gantt/src/ObjectGantt.tsx:536 deps [... , objectSchema]
plugin-map/src/ObjectMap.tsx:708 deps [... , objectSchema]
plugin-timeline/src/ObjectTimeline.tsx:208 deps [... , objectDef]
plugin-list/src/ObjectGallery.tsx:354 deps [... , objectDef]
plugin-tree/src/ObjectTree.tsx:468 gated, but see objectui#6481
What that costs is not uniform, and objectui#6453 measured why. On ObjectCalendar, instrumented with three latency profiles: when the schema read is the slower of the two, the dependency version painted raw ids, reverted to the loading placeholder, then swapped — a three-step paint; when it was the faster one, the first response was discarded on arrival — a round trip bought and thrown away. ObjectKanban measured the discard case, ObjectView measured the visible-swap case. So whether gating is worth it in each of the five above is a per-component measurement, not an inference from this table.
What this card is asking for
A decision, then whatever work follows from it:
- Extract a shared settled-schema resolution hook, or keep hand copies? If extracted, the new export must be declared as a public-surface widening, not slipped in.
- Independently: measure each of the four ungated views and gate the ones where an extra re-run costs a visible wrong paint.
Related: objectui#6271, objectui#6419, objectui#6453, objectui#6481, objectui#6014.
Generated by Claude Code
Generated by Claude Code
Activity
os-zhuang commentedon Aug 27, 2026
Maintainer ruling recorded — Option A: extract the resolution half as a shared hook
Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 1 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意,并继续」 — accepting the triage recommendation (2026-08-26T03:50Z facet block) as presented.Ruled: extract the settled-schema RESOLUTION half as a shared hook exported from
@object-ui/react— the{ key, def } | nullstate, keyed readiness, settle-on-every-exit shape — declared explicitly as a public-surface widening. Gate PLACEMENT stays per-component (ObjectCalendar's provider-branch gate and itsdataConfig.object ?? schema.objectNamekey are component-private truth, per this card's own measurement). Migration of the existing copies rides subsequent cards touching those components — ⛔ no one-shot four-package refactor. The four ungated views get per-component measurement before any gating; blanket gating (option C) stays excluded.Dispatch notes: Clause-②: yes — a new published export widens the public surface; contract-review tier applies at dispatch. #6481 is re-priced to adopt the hook (keyed readiness) instead of writing a fourth hand copy, and stays
pm:blockedon this card until the hook lands.State:
needs-user-decision→pm:queue.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Claim:
domain:ui@ objectui PM seat, session8ca04858-ea8e-5b85-9182-de59aa49e00c— branchclaude/issue-6482-settled-schema-hook.Dispatching on the maintainer's 2026-08-27 ruling (「同意,并继续」), Option A, unchanged. Recording the three constraints the ruling attaches, because each one is a place a dev would naturally overreach:
{ key, def } | nullstate, keyed readiness, settle-on-every-exit shape. Gate placement stays per-component —ObjectCalendar's provider-branch gate and itsdataConfig.object ?? schema.objectNamekey are component-private truth, established by this card's own measurement. ⭐ That split is not a compromise; it is the measured boundary. Extracting the gate half too would be the wrong refactor, because an inlinevaluedata set issues no metadata read at all and a whole-effect gate would hold its query open on a resolution nothing was going to produce.ObjectCalendarmeasured a three-step paint when the schema read was slower,ObjectKanbanmeasured a discarded round trip when it was faster. Gating is a per-component measurement, and those get their own cards afterwards.Clause-② yes — a new published export widens
@object-ui/react's public surface. Contract-review tier; the widening is declared explicitly in the PR body, ⛔ never slipped in. Changesetminor. ⛔ Noneeds:contract-reviewlabel (phantom here, zero readers).⭐ This card unblocks #6481. Per the ruling, #6481 is re-priced to adopt the hook (keyed readiness) instead of writing a fourth hand copy, and stays
pm:blockedon this one until the hook lands. That is the whole point of the card:ObjectTreeused two independent pieces of state (objectSchemaplus a bareschemaSettledboolean), which structurally cannot express keyed readiness — and that is the root cause of #6481's stale$expandquery. A shared hook makes "keyed by object" impossible to omit rather than merely conventional.⇒ So the acceptance bar is not "a hook exists." It is: the hook's shape must make #6481's defect unwritable. A hook a caller can hold wrong reproduces the hand-copy problem with extra steps.
Generated by Claude Code
os-sales commentedon Aug 28, 2026
os-dev-report
{ "issue": 6482, "status": "done", "branch": "claude/issue-6482-settled-schema-hook", "pr": "https://github.com/objectstack-ai/objectui/pull/6690", "premise_still_valid": true, "summary": "Extracted the settled-schema RESOLUTION half shared by ObjectKanban / ObjectView / ObjectCalendar's fetch-gate hand copies into a new published hook, useSettledSchema, from @object-ui/react (packages/react/src/hooks/useSettledSchema.ts), per the maintainer's 2026-08-27 Option A ruling. Gate placement stays per-component and no existing hand copy (kanban/view/calendar/tree) was migrated, per the dispatch's hard limits. The hook holds one piece of state and derives readiness at render time by comparing the settled key against the current key, which makes objectui#6481's stale-key-reads-as-ready defect structurally unwritable rather than merely avoided; a dedicated test and a mutation-based ablation both demonstrate this. Changeset scored minor; new export reachability was verified through the built package entry with a positive control, not by an export keyword alone.", "tests": "Built the dependency closure (pnpm --filter '@object-ui/react^...' build, then pnpm --filter @object-ui/react build) before judging anything. pnpm exec vitest run packages/react/ -> Test Files 62 passed (62), Tests 916 passed (916) (canonical repo-root invocation; the package-cwd form is refused by this repo's own vitest guard, objectui#3378). pnpm --filter @object-ui/react type-check -> exit 0 (tsc --noEmit && tsc -p tsconfig.test.json); confirmed via tsc --listFiles that tsconfig.test.json actually includes both the new hook and its test file, not just claims to. node scripts/check-control-bytes.mjs -> OK, 5536 tracked files scanned (files were git-added first so the new files were actually in the scanned population). node scripts/check-changeset-fixed.mjs and node scripts/check-changeset-no-major.mjs -> both OK. eslint on the 3 touched files with --format json -> population 3 (read from the JSON output itself), 0 errors, 17 no-explicit-any warnings (warn severity, same class already present throughout the package, lint.yml sets no --max-warnings); pnpm --filter @object-ui/react lint (whole package) -> exit 0, 380 problems, 0 errors, all pre-existing. check:readme-exports full-repo run reports PRECONDITION NOT MET in this fresh worktree (packagesRead found 6, floor is 25 -- most sibling packages are simply unbuilt here, unrelated to this diff); reported literally rather than read as red or green. What IS measured: the gate's own scan() function called directly with packageDirs/readmes/floors overrides (the same override mechanism its own fixture suite uses), scoped to packages/react -- 0 fabricated, 0 wrong-path, useSettledSchema resolves as real. Reachability was separately probed by loading the built dist/index.d.ts through the TypeScript checker's getExportsOfModule (the same query check-readme-exports.mjs itself uses) with a positive control, useDataRefresh, resolved through the identical query alongside it -- both true. Ablation: predicted before running that mutating `const ready = resolution !== null && resolution.key === key;` down to `const ready = resolution !== null;` (dropping the key comparison, reproducing ObjectTree's actual bug shape) would turn 2 of 9 tests red. Mutation landing on disk was proven by an anchored grep -c of the exact line (1 before, 0 after) plus the mutated marker's presence, never by an editor's exit code. Observed only 1 of 9 red (the test modeled directly on #6481's real shape: settle a key successfully, THEN switch to a new key before its fetch resolves) -- the second predicted test stayed green because it switches keys before any resolution had EVER settled, so the base `resolution !== null` check already reads false under both the mutated and original code; recorded the actual observed direction rather than forcing the prediction. Restoration proven by an empty `git diff HEAD` plus a byte-identical git hash-object against the committed HEAD blob (1d89c21a1f26108a715dbe4b36eef22cffe21c12, matched before and after), using a trap on EXIT/INT/TERM anchored to an absolute REPO_ROOT path. Full suite re-run green (9/9) after restoration, then the whole gate union re-run again after the final commit (877944237) with no further changes.", "mcp_calls": 8, "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 28, 2026
ACCEPT — PR #6690, head
877944237, flipped ready; auto-merge once its table is green.All three of the ruling's hard limits held: gate placement stayed per-component, ⛔ no hand copy was migrated (kanban / view / calendar / tree untouched), ⛔ no blanket gating of the four ungated views. The dispatch asked for the hook, and the hook is what came back.
⭐ The acceptance bar was "make #6481's defect unwritable", and it was met structurally
I said a hook a caller can hold wrong just reproduces the hand-copy problem with extra steps. The answer:
That is the fix at the level the card was actually about.
ObjectTree's bug exists because it expressed readiness with two independent pieces of state (objectSchemaplus a bareschemaSettled), which cannot represent "settled, but for a different key." With one state and a render-derived comparison there is no second variable to fall out of sync — unwritable rather than merely avoided, and demonstrated by a dedicated test plus a mutation ablation rather than asserted.⭐⭐ The prediction missed, and reporting it honestly is the more useful result
Predicted: mutating
ready = resolution !== null && resolution.key === keydown toready = resolution !== null(i.e. reproducingObjectTree's actual bug shape) turns 2 of 9 tests red.Observed: 1 of 9. The one that redded is the test modelled on #6481's real shape — settle a key successfully, then switch keys before the new fetch resolves. The second stayed green because it switches keys before any resolution has ever settled, so the base
resolution !== nullcheck already reads false under both the mutated and the original code.⇒ ⭐ That is a fact about the suite's discriminating power, not a footnote: one of the two tests I would have counted as guarding the keyed-readiness property does not actually discriminate on that mutation. The dev "recorded the actual observed direction rather than forcing the prediction" — which is how the fact surfaced at all. A prediction quietly adjusted to match the run would have left a test in the file that looks like a guard and is not.
Verification discipline worth naming
exportkeyword. The dev loadeddist/index.d.tsthrough the TypeScript checker'sgetExportsOfModule— the same querycheck-readme-exports.mjsitself uses — withuseDataRefreshas a positive control resolved through the identical query. Both true. That is exactly the "anexportin a module file is not a published surface" rule, applied without being told twice.check:readme-exportsreportedPRECONDITION NOT MET(packagesRead 6 against a floor of 25 — sibling packages simply unbuilt in a fresh worktree) and was reported literally, ⛔ not booked as red and ⛔ not booked as green. Then, instead of stopping there, the dev measured what could be measured: the gate's ownscan()invoked directly with the override mechanism its own fixture suite uses, scoped topackages/react— 0 fabricated, 0 wrong-path.tsc --listFilesconfirmedtsconfig.test.jsonactually includes the new hook and its test — the config claiming coverage and the program containing the file are two different facts.git added beforecheck:control-bytesran, so the new files were genuinely in the scanned population. An untracked new file is invisible to that scanner, which is the documented blind spot in finding(scripts): check-control-bytes and check-vi-mock-specifiers scan TRACKED files only, so they print green on a new file they never opened #6545.git diff HEADplus a byte-identicalgit hash-objectagainst the committed HEAD blob (1d89c21a…, matched before and after), under a trap anchored to an absoluteREPO_ROOT.Changeset
minor; the public-surface widening is declared explicitly rather than slipped in, as clause-② requires.Next
⭐ #6481 is now unblocked once this lands — per the ruling it adopts
useSettledSchemainstead of writing a fourth hand copy. It stayspm:blockeduntil then. Migration of the existing three copies rides subsequent cards touching those components, ⛔ not a follow-up refactor card.Generated by Claude Code
refactor(views): converge on useSettledSchema and gate the gantt's du…
refactor(views): converge on useSettledSchema and gate the gantt's du…
feat(non-grid): a platform row ceiling with a loud footnote, and the …