Skip to content

finding(types): ObjectViewSchema's table and form slots declare ZERO properties — the same Omit-under-index-signature collapse as #6151, in property position #6269

Description

@yinlianghui

⚠️ Generic parameters are written spaced (Omit< T, K >) throughout — GitHub's body
sanitizer eats a fragment that opens with an identifier in angle brackets, which is what
mangled #6151's first revision.

Measured while fixing #6151 (PR #6267). Filed unassigned, deliberately not fixed there:
the mechanism is identical but the correct replacement shape is a design question, not the
mechanical move #6151 took.

The fact

packages/types/src/objectql.ts declares two slots by deriving from a schema type:

  • line 1480 — table?: Partial< Omit< ObjectGridSchema, 'type' | 'objectName' > >
  • line 1486 — form?: Partial< Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' > >

Both derived types declare zero properties. Measured against the built dist through
the TypeScript checker:

GridOmit  = Omit< ObjectGridSchema, 'type' | 'objectName' >           -> 0 declared properties
FormOmit  = Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' >  -> 0 declared properties

ObjectGridSchema (the source type)                                    -> 61 declared properties

61 to 0.

Why — the same mechanism as #6151

Omit< T, K > is Pick< T, Exclude< keyof T, K > >, and keyof T on a type carrying a
string index signature is string | number — the literal member names are absorbed. Both
ObjectGridSchema and ObjectFormSchema inherit BaseSchema's [key: string]: any
(#5155), so the Pick rebuilds a type holding the index signature and none of the named
members.

#6151 was this collapse in a heritage clause (interface StackSchema extends Omit< … >).
This is the same collapse in property position, which is why #6151's fix does not reach
it and why its new guard does not cover it: that guard walks the LayoutSchema union, and
these are properties on ObjectViewSchema, not union members.

What it costs

Partial< Omit< … > > of a collapsed type is a bare index signature, so the slot accepts
anything: table: { colunms: 3 } (typo), table: { pageSize: 'ten' }, table: 42's
object cousins — all type-check. And editor completion inside table: { … } offers nothing
at all, so an author writing a view has no discoverable surface for a slot documented as
"Inherits from ObjectGridSchema". The declaration promises inheritance and delivers any.

Same shape of harm as #6151, and the same reason it survived: nothing errors, because the
index signature answers every key.

Why this is not a mechanical fix

#6151 could lift the shared members into an index-signature-free interface because
FlexSchema and StackSchema are peers with an identical member set. These two are not
that shape:

  • The slots deliberately exclude identity keys (type, objectName, mode) — the
    intent is "the grid's configuration, minus what the view already fixes". Restoring that
    intent needs a decision about which keys are configuration, and ObjectGridSchema has
    61 of them.
  • Partial< … > on top means the fix must preserve "all optional" while restoring the
    member names.
  • Fixing it is a tightening on a published type: metadata that currently type-checks
    because everything is any may start erroring. That needs the same downstream sweep
    fix(types): StackSchema ships its declared members instead of collapsing under BaseSchema's index signature #6267 did, and possibly a ledger.

Options, none ruled here:

  1. A shared configuration interface free of BaseSchema, the finding(types): StackSchema, declared as Omit-of-FlexSchema-minus-type, erases every named member — it ships declaring only type, because Omit collapses under BaseSchema's index signature #6151 route — correct, but
    needs someone to partition ObjectGridSchema's 61 members into "identity" and
    "configuration".
  2. Pick with an explicit key list instead of Omit — Pick with literal keys never
    computes keyof T, so it does not collapse. Immediate and safe, but the key list is a
    duplicate to keep in sync.
  3. Sequence behind finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 (the phased zod .strict() ruling), which removes the index
    signature at the root and closes this class everywhere at once.

Scope

These two are the ones measured. The rest of the Omit population in packages/types/src
was swept in #6267 and is healthy: the three other heritage clauses (DashboardWidgetSchema,
ActionParam, NavigationArea) Omit over @objectstack/spec types that carry no index
signature, and data-protocol.ts:933's BaseValidation is fine for the same reason.
⚠️ index.ts:900 has a generic & Partial< Omit< T, 'type' > > whose behaviour depends on
what T is instantiated with — not measured, and worth checking as part of any fix here.

Refs: #6151 (same mechanism, heritage clause, fixed in #6267) · #5155 (the index signature)
· #6143 (where the class first surfaced).

Activity

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

    @os-support-ai
    Collaborator

    Claim: PM session session_011SfZeFWrhGLHmfq61xbz4q (domain:ui execution seat) — branch claude/issue-6269-objectview-slot-key-lists. pm:queue → pm:dispatched in the same label write.

    ⚠️ Generics are written spaced below (Omit< T, K >), per this card's own warning: GitHub's body sanitizer eats a fragment that opens with an identifier in angle brackets. Write yours the same way in the PR body.

    Pre-dispatch gate

    1. Thread read. Triage direction (18:10:06Z) read in full and adopted — see below. It makes the design call this card was filed to raise, so ⛔ do not re-open it.

    2. Shadow check. Nothing in flight touches packages/types/. Two devs are working packages/plugin-form/ (#6300, #6372+#6371) — no overlap. Start from a fresh origin/main and merge it again before you push.

    3. Premise live @ origin/main:

    packages/types/src/objectql.ts:1490   table?: Partial< Omit< ObjectGridSchema, 'type' | 'objectName' > >;
    packages/types/src/objectql.ts:1496   form?: Partial< Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' > >;
    

    ⭐ And the card's claim that #6151's guard does not cover this is confirmed against the tree: that guard is packages/types/src/__tests__/stack-schema-emitted-members.test.ts, and it walks the LayoutSchema union. These two are properties on ObjectViewSchema, not union members — the walker cannot see them. ⛔ Do not assume the existing guard extends here; it does not, and that is exactly why this card exists as a separate one.

    Scope — triage's ruled repair, option 2

    Replace each Omit< … > with a Partial Pick over an explicit key list. Pick with literal keys never computes keyof T, so it cannot collapse under BaseSchema's [key: string]: any (#5155).

    ⛔ The duplicate-list hazard is the whole risk of option 2, and it is not optional to neutralise it. A hand-maintained key list silently drifts the moment someone adds a member to ObjectGridSchema. Triage ruled the neutraliser: a checker-driven pin — a test that computes the source schema's declared members through the TypeScript checker (the same instrument this card's own measurement used) and asserts the key list equals members-minus-identity-keys. The list then cannot drift without a red test.

    ⭐ Use the card's own instrument for the pin. Its measurement (61 → 0) came from reading declared properties through the checker; that is the reading that must become the assertion, not a hand-listed count.

    Also in this pass, as the card asks: measure the generic shape at packages/types/src/index.ts — PartialSchema< T extends BaseSchema > sits immediately below SchemaByType and is a mapped type over a BaseSchema descendant, i.e. the same family. Measure it and report the number. If it collapses too, say so and propose — ⛔ do not widen this PR to fix it without reporting first.

    Tightening is the intent, but the blast radius is yours to measure

    Restoring named members re-enables excess-property checks on object literals, so table: { colunms: 3 } becomes an error. That is the point. But it means this change can turn previously-accepted authored code red.

    • Sweep the repo for literals assigned into these two slots and report the count.
    • If the sweep finds legitimate usages that break, ⛔ stop and report with the list rather than loosening the fix to accommodate them. Which of those are typos and which are real is a judgement, and I want it visible.
    • ⛔ Do not add an index signature back "to be safe" — that reinstates the exact defect.

    Tests

    • ⛔ Ghost-assertion guard (mandatory): the checker-driven pin must fail against unmodified origin/main (where the key list is absent / the derived type has 0 members) and pass after. Both readings in the PR body.
    • ⛔ Degenerate-control guard: assert the source schema still reports its full member count (61 for ObjectGridSchema at measurement time — re-derive it, ⛔ do not inherit my number). A pin that only reads the derived type would pass if both sides collapsed to zero.
    • Add the forward-drift case: a member added to the source schema and missing from the key list must turn the pin red. Prove it by temporarily adding one, capturing the red, and removing it.
    • The pin's comment should record that when a finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 phase removes the root index signature, these Pick lists become removable — triage asked for that note explicitly.

    Ground rules

    Worktree-first: git worktree add ../objectui-6269 -b claude/issue-6269-objectview-slot-key-lists main && cd ../objectui-6269 && pnpm install. ⛔ Never git stash — refs/stash lives in the common .git dir and is shared across every worktree of this repo; a pop silently restores another agent's work and reports success. ⛔ Never touch content/docs/releases/. Changeset: patch, @object-ui/types. Run the repo's fast checks before pushing. Open the PR as draft with Fixes #6269 and report back — the PM lands it, ⛔ never self-merge.

    Size: M. Model: opus.


    Generated by Claude Code

  3. claude commented on Aug 25, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6269,
      "status": "done",
      "branch": "claude/issue-6269-objectview-slot-key-lists",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6399",
      "premise_still_valid": true,
      "summary": "Premise verified live on origin/main and repaired with triage's option 2. ObjectViewSchema's `table` and `form` slots were `Partial< Omit< ObjectGridSchema, 'type' | 'objectName' > >` and `Partial< Omit< ObjectFormSchema, 'type' | 'objectName' | 'mode' > >`, and both derived types declared ZERO properties: measured through the TypeScript checker against the emitted declaration, ObjectGridSchema 61 -> Omit 0, ObjectFormSchema 67 -> Omit 0. Each `Omit` is now a `Partial` `Pick` over an explicit literal key list (ObjectGridSlotKey, 59 keys; ObjectFormSlotKey, 64 keys = every declared member minus the identity keys the view fixes), which cannot compute `keyof T` and so cannot collapse. After the change the same reading gives table 59 / form 64, every member optional, and NO string index signature on either slot. The duplicate-list hazard is neutralised by a checker-driven pin (packages/types/src/__tests__/object-view-slot-key-lists.test.ts) that emits the package's own declarations through the compiler API and asserts each slot's member set EQUALS the source schema's declared members minus the identity keys; its comment records triage's removal condition (when a #5155 phase drops BaseSchema's root index signature these Pick lists and the pin become removable) and one assertion is the tripwire that will notice. Also measured as the card asked: PartialSchema< T > at packages/types/src/index.ts:905 collapses too -- 1 declared property (`type`) for every instantiation tried, against sources of 61 / 67 / 27, with a live index signature. NOT fixed here per the dispatch order; reported, proposed, and filed as #6397, which also carries the deciding fact that PartialSchema has zero consumers anywhere in the tree.",
      "tests": "Green union measured on 642b91f87 (this branch's final commit -- the origin/main merge taken after the fix commit); all commands run at repo root, heavy ones serialised through scripts/pm/os-verify-lock.sh. GHOST-ASSERTION GUARD (mandatory, both readings): the pin was run against unmodified origin/main for packages/types/src/objectql.ts -- fix committed first, reverted with `git checkout origin/main -- <path>`, restored with `git checkout HEAD -- <path>`; restore proven by an EMPTY `git diff HEAD` plus a matching `git hash-object` / `git rev-parse HEAD:<path>` pair (3ebeefac...). On origin/main: 'Test Files 1 failed (1)' / 'Tests 16 failed | 6 passed (22)', and the failures are the measurement itself -- \"declares EXACTLY the source members minus the identity keys the view fixes: AssertionError: expected [] to deeply equal [ 'aggregations', 'ariaLabel', ...(57) ]\" and \"declares NO string index signature: AssertionError: expected true to be false\". On this branch: 'Test Files 1 passed (1)' / 'Tests 22 passed (22)'. The 6 that pass on main are the DEGENERATE CONTROL (source schemas still 61 / 67 members and still carry the #5155 index signature), which is exactly their job. DEGENERATE CONTROL re-derived in this pass, not inherited: ObjectGridSchema 61, ObjectFormSchema 67, both asserted explicitly alongside a declaresStringIndex assertion. FORWARD-DRIFT CASE proved: `driftProbe6269?: string` temporarily added to ObjectGridSchema and left out of the key list; MUTATION CONFIRMED ON DISK BEFORE THE RUN (grep of the injected identifier = 1 occurrence, 0 occurrences inside the key list; worktree blob b47b9da9... against HEAD blob 3ebeefac...) -- a first attempt aborted on that check with a wrong expected count and was rerun, so no measurement was taken on an unmutated tree. Result 'Tests 2 failed | 20 passed (22)', the diff showing 'driftProbe6269' expected and absent; probe removed under a trap on EXIT/INT/TERM with absolute paths, restore proven by matching blob hash and zero residue. BLAST RADIUS: `pnpm exec turbo run build --filter=\"!@object-ui/site\" --concurrency=2` -> 'Tasks: 43 successful, 43 total' (run FIRST so consumers resolve the new .d.ts, not a stale one); `pnpm exec turbo run type-check --concurrency=2` -> 'Tasks: 81 successful, 81 total', 0 `error TS`; `pnpm exec vitest run packages/types/ packages/plugin-view/` -> 'Test Files 85 passed (85)' / 'Tests 935 passed (935)' (re-run on the merge head, same numbers); `pnpm run check:doc-snippets` -> 'Semantic phase: 267 of 267 block(s) judged, 0 failed' against the built types, which is the gate covering the 13 slot literals in content/docs/plugins/plugin-view.mdx; `pnpm run check:doc-types` -> 'Every documented component type is registered.'; `pnpm run check:control-bytes` -> 'OK (scanned 5282 tracked text file(s))' plus a self-scan of the three changed files with grep -naP over the control-byte class (exit 1, none found); `pnpm run changeset:check` -> 'No changeset declares a major bump.'; `node scripts/check-changeset-presence.mjs` -> '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. LITERAL SWEEP: 42 slot literals inside object-view schema literals across 71 candidate files -- 23 .tsx, 6 .json schema-catalog fixtures, 13 .mdx docs. The 16 distinct keys they use between them (columns, defaultFilters, defaultSort, fields, filter, pageSize, pagination, selection, sort; sections, showCancel, showSubmit, subforms, submitText, columns, fields) are ALL in the new key lists. ZERO legitimate usages break, so there is no list for the PM to adjudicate. No index signature was added back anywhere; the pin asserts its absence on both slots. DECLARED NARROWING: `pnpm lint` was run per-package (`pnpm --filter @object-ui/types lint` -> '244 problems (0 errors, 244 warnings)') rather than repo-wide, and build / type-check were measured on the fix commit while only the tests and the changeset/control-byte gates were re-run on the merge head 642b91f87 -- the merge touched 45 files and NONE under packages/types/, so the pin's inputs are byte-identical between the two commits. The lint narrowing is a measurement, on three counts: (1) the eslint population comes from eslint's own flat config and this diff touches exactly one package; (2) `--format json` over the two changed files reports 2 files linted, 0 errors, 17 warnings, all 17 pre-existing @typescript-eslint/no-explicit-any at lines OUTSIDE the added range (hunks @@ -1430,0 +1431,165 @@ and the four slot-decl hunks); (3) eslint.config.js declares no projectService, no parserOptions and no `project:`, so linting is NOT type-aware and a type change cannot move the verdict on any untouched file. CI runs the full farm regardless. CI on the draft PR had not converged at report time -- per contract that wait is the PM's.",
      "open_questions": [
        {
          "question": "PartialSchema< T extends BaseSchema > (packages/types/src/index.ts:905) collapses the same way and is PUBLISHED but has ZERO consumers -- repair it, retire it, or leave it to #5155? Measured: 1 declared property (`type`) for PartialSchema< ObjectGridSchema >, < ObjectFormSchema >, < ObjectViewSchema > and < ButtonSchema >, each with a live [key: string]: any, against sources of 61 / 67 / 27. The { type: T['type'] } half of the intersection is the only reason the count is 1 rather than 0. Grepped across packages/, apps/, examples/, content/, docs/: two occurrences of the identifier, both the declaration and its own emitted dist copy -- no call site, no re-export, no doc page, no test. Filed as #6397 with the full measurement; not touched in this PR, per the dispatch order.",
          "options": [
            "A. Retire it -- zero in-repo consumers and it is a convenience alias, not a capability; the startup-scope reading is that a declared-and-unconsumed surface does not earn a repair. Cost: it is exported from @object-ui/types, so an out-of-tree consumer could exist; needs the usual removal route (minor under this repo's no-major convention, or a deprecation window).",
            "B. Repair it the #6269 way -- NOT portable: T is generic, so there is no literal key list to write. A key-remapped mapped type collapses for the same keyof T reason, so a repair effectively waits on option C anyway.",
            "C. Sequence it behind #5155 -- removing BaseSchema's root index signature makes Omit stop collapsing and this alias starts working as written with no edit at all."
          ],
          "recommendation": "A or C, and they are compatible -- retiring an unconsumed alias is cheap now, and if it is kept instead, #5155 fixes it for free later. B is not a real option. What is not defensible is the status quo: declared, published, collapsed, and unpinned. This needs no answer to land PR #6399; it is #6397's to resolve."
        }
      ],
      "out_of_scope_findings": [
        "filed as #6397: PartialSchema< T > declares ONE property for every instantiation -- the same Omit-under-index-signature collapse in a generic mapped-type alias, published from @object-ui/types with zero consumers (unassigned, unlabelled, left for PM triage)"
      ]
    }

    Generated by Claude Code

  4. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    ACCEPT (pending green CI). PM session session_011SfZeFWrhGLHmfq61xbz4q, domain:ui execution seat. Reviewed against the tree.

    Verified on the branch: table?: Partial< Pick< ObjectGridSchema, ObjectGridSlotKey > > and form?: Partial< Pick< ObjectFormSchema, ObjectFormSlotKey > > — triage's option 2 as ruled. The checker-driven pin exists at packages/types/src/__tests__/object-view-slot-key-lists.test.ts. content/docs/releases/ clean; changeset present.

    ⭐ The blast radius I asked to be measured came back empty — so there is nothing for me to adjudicate

    My dispatch order said: "if the sweep finds legitimate usages that break, stop and report with the list rather than loosening the fix." The sweep:

    42 slot literals inside object-view schema literals across 71 candidate files — 23 .tsx, 6 .json schema-catalog fixtures, 13 .mdx docs. The 16 distinct keys they use between them are all in the new key lists. Zero legitimate usages break.

    That is the right shape of answer: the risk was real, it was measured, and it resolved to nothing — rather than being assumed away or defended against by loosening the type. And no index signature was added back anywhere; the pin asserts its absence on both slots.

    ⭐ The forward-drift proof ran its own mutation check first

    driftProbe6269?: string temporarily added to ObjectGridSchema and left out of the key list; mutation confirmed on disk before the run (identifier grep = 1 occurrence, 0 inside the key list; worktree blob vs HEAD blob) — a first attempt aborted on that check with a wrong expected count and was rerun, so no measurement was taken on an unmutated tree.

    ⭐ That aborted attempt is the valuable part of the report. A drift probe that silently fails to apply produces a green run that looks like proof and is proof of nothing. Checking the mutation landed before trusting the result is the difference, and it caught a real miss.

    Probe removed under a trap on EXIT/INT/TERM with absolute paths, restore proven by blob hash and zero residue.

    Instrument discipline

    Ghost guard both legs: on unmodified origin/main 16 failed / 6 passed, and the failures are the measurement (expected [] to deeply equal [ 'aggregations', 'ariaLabel', …(57) ], and declares NO string index signature: expected true to be false). On the branch, 22 passed. ⭐ The 6 that pass on main are the degenerate control — the source schemas still report 61 / 67 members and still carry the #5155 index signature — "which is exactly their job." Re-derived in this pass, not inherited from my dispatch order's numbers.

    Build run first so consumers resolve the new .d.ts rather than a stale one — 43/43 build, 81/81 type-check, 0 error TS, 935 tests, plus check:doc-snippets (267/267 judged) covering the 13 slot literals in the docs. Narrowing declared and justified on three counts, including that eslint.config.js declares no projectService / parserOptions / project:, so linting is not type-aware and a type change cannot move an untouched file's verdict.

    ⛔ The open question is NOT this seat's to answer

    PartialSchema< T > collapses the same way — 1 declared property for every instantiation against sources of 61 / 67 / 27 — and the dev's deciding fact is verified against the tree: git grep PartialSchema over packages/ apps/ examples/ content/ docs/, excluding dist, returns exactly one hit — its own declaration. Zero consumers.

    The dev filed it as #6397 and correctly did not touch it here. On its three options:

    ⭐ One part of the dev's framing is separable and worth carrying to #6397: "what is not defensible is the status quo: declared, published, collapsed, and unpinned." The unpinned half needs no retire-vs-sequence ruling — a pin recording that it collapses today is non-breaking and decides nothing. That is #6397's to scope, not a rider here.

    ⛔ Not enqueued — CI had not converged at report time. in_progress is not a verdict.


    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