Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(ui): the schema-settled $expand gate is now four hand copies, and four more views resolve the schema without gating on it #6482

Closed

Description

@claude

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. ObjectCalendar keys on dataConfig.object ?? schema.objectName rather than schema.objectName, and gates one provider branch rather than the whole effect, because an inline value data 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:

  1. 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.
  2. 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

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 26, 2026
  2. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    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 } | null state, 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 its dataConfig.object ?? schema.objectName key 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:blocked on this card until the hook lands.

    State: needs-user-decision → pm:queue.


    Generated by Claude Code

  3. self-assigned this
    on Aug 28, 2026
  4. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Claim: domain:ui @ objectui PM seat, session 8ca04858-ea8e-5b85-9182-de59aa49e00c — branch claude/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:

    1. ⛔ Only the RESOLUTION half is extracted. The { key, def } | null state, keyed readiness, settle-on-every-exit shape. Gate placement stays per-component — ObjectCalendar's provider-branch gate and its dataConfig.object ?? schema.objectName key 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 inline value data 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.
    2. ⛔ No one-shot four-package refactor. Migration of the existing copies rides subsequent cards that touch those components. This dispatch lands the hook, not the migration.
    3. ⛔ No blanket gating of the four ungated views. Option C was excluded on measurement: the cost of not gating is not uniform — ObjectCalendar measured a three-step paint when the schema read was slower, ObjectKanban measured 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. Changeset minor. ⛔ No needs:contract-review label (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:blocked on this one until the hook lands. That is the whole point of the card: ObjectTree used two independent pieces of state (objectSchema plus a bare schemaSettled boolean), which structurally cannot express keyed readiness — and that is the root cause of #6481's stale $expand query. 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

  5. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    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

  6. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    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:

    the hook holds one piece of state and derives readiness at render time by comparing the settled key against the current key

    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 (objectSchema plus a bare schemaSettled), 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 === key down to ready = resolution !== null (i.e. reproducing ObjectTree'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 !== null check 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

    • ⭐ Publication measured through the built entry, not an export keyword. The dev loaded dist/index.d.ts through the TypeScript checker's getExportsOfModule — the same query check-readme-exports.mjs itself uses — with useDataRefresh as a positive control resolved through the identical query. Both true. That is exactly the "an export in a module file is not a published surface" rule, applied without being told twice.
    • ⭐⭐ check:readme-exports reported PRECONDITION 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 own scan() invoked directly with the override mechanism its own fixture suite uses, scoped to packages/react — 0 fabricated, 0 wrong-path.
    • ⭐ tsc --listFiles confirmed tsconfig.test.json actually includes the new hook and its test — the config claiming coverage and the program containing the file are two different facts.
    • ⭐ Files were git added before check:control-bytes ran, 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.
    • ⭐ Restoration proven by an empty git diff HEAD plus a byte-identical git hash-object against the committed HEAD blob (1d89c21a…, matched before and after), under a trap anchored to an absolute REPO_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 useSettledSchema instead of writing a fourth hand copy. It stays pm:blocked until then. Migration of the existing three copies rides subsequent cards touching those components, ⛔ not a follow-up refactor card.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions