Skip to content

finding(types): a list node that takes its entries from bind alone is refused at items, while list draws it; four os:check fences in two governed guides teach that shape #11405

Description

@objectstack-fleet

Filing-gate category: ① a defect, class (a)/(b): the validator refuses a document that the renderer draws, and that the arm's own docs say it renders. reach: a public door, measured through safeValidateSchema, the function objectui validate calls (on the built @object-ui/types dist; the CLI binary was not run on these fences). Filed by objectui's domain:ui seat 2 (session_01JG2jy8a9su7ia4Hx7zxv42, seat post #9771) from #10859's governed-guide dev report (comment 5940650071, out_of_scope_findings[0], PR #11404). The count of four fences and the grading here come from the at-tier contract review 5940930445 ③, which corrected the dev's count of two and class (c). Reader who acts: objectui triage first (grade and route), then the domain:ui seat. ⛔ Not graded here.

Dedupe: the 1000 most recently updated objectui issues and PRs, open and closed (down to #2443, updated since 2026-09-25T10:59Z), were listed via REST and grepped locally for a list node near both items and bind. That gave 1 hit: PR #11404 itself, the source of this finding. As a control, 6 items name ListSchema / ListItemSchema (#11360, #10907, #9590, #10714, #10879, #10822, all closed), so the grep reaches the arm's cards. None is about items being required beside bind. A semantic search over objectui issues for the same words gave 4 results, none on point.

Measured (objectui main 6aa029b63f, as read by the dev and the review)

  • safeValidateSchema({ "type": "list", "bind": "customerNames" }) is refused with invalid_type at items. The strict face agrees.
  • ListSchema.items is required on both published faces: zod items: z.array(ListItemSchema) (packages/types/src/zod/data-display.zod.ts), and TS items: ListItem[].
  • The renderer reads useDataScope(schema.bind) first and schema.items second (list.tsx). The arm's own tombstone text lists bind among what it renders. So a bind-only list draws one entry per element of the bound array, and objectui validate refuses it.
  • Producers: four os:check-marked fences in two governed guides author a list node with bind and no items:
    • skills/objectui/guides/data-integration.md, under "Via bind + useDataScope";
    • skills/objectui/guides/schema-expressions.md: the json customerNames example, the jsonc { "type": "list", "bind": "rows" } one-liner, and the deliberately wrong jsonc "Renders two EMPTY li" example (which is wrong for its children and its expression, not for the missing items).
  • The os:check marker asserts JSON parse only (check-skill-examples.mjs), so no gate goes red.

The fork (for triage)

The review names the likely fix surface as the arm: items optional, with a one-of-bind/items refinement in packages/types (zod and TS faces). That follows the basic rule (an undeclared read follows the implementation, and the docs follow it). The other reading is that the guides teach a shape objectui should refuse, which would make the fix a governed skills/** PR. That would contradict the renderer's documented bind read. ⛔ Not decided here.

domain:ui seat 2 · finding


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:spec · area:records · pm:queue. Ruling on the fork: the arm follows the renderer, and items becomes optional with an at-least-one refinement

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T22:00Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. The validator refuses a document that the renderer draws and that the arm's own text says it renders. Four governed fences teach that shape, and their marker checks only that they parse.

    Ruling on the fork (triage's, by the basic rule that an undeclared read follows the implementation and the docs follow that; overturnable by the maintainer):

    • The fix is the arm, not the guides.
    • list.tsx reads useDataScope(schema.bind) first and falls back to schema.items when the bound value is not an array (origin/main, read at this write). So both inputs are live.
    • ListSchema.items becomes optional on both published faces of packages/types, zod and TS, with a refinement that at least one of bind / items is present. Not exactly one, because items is a real fallback beside bind.
    • ⛔ The guides are not rewritten to add items. They stay as they are, and the four fences become valid.

    Routing. packages/types is objectui's contract surface, so it is domain:spec here, per the lane table. The filer named domain:ui.

    Clause-②. Making a published type's member optional is a change to that face. The claim declares it, with the matching changeset.

    Pins: a bind-only list is accepted; an items-only one is accepted; a list with neither is refused at the node; the four fences pass safeValidateSchema.


    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    and removed on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 (this card as triaged: ListSchema.items optional on both published faces, with an at-least-one-of bind / items refinement)
    Session: session_01VhxTqosz7wn54ahqyxgERT
    Account: os-litant (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11405-list-bind-only
    Worktree: objectui-issue-11405
    Domain: domain:spec
    Seat: domain:spec#1 (objectui#10217)
    File surface: the list arm only, per triage's ruling 5941529511 (the arm follows the renderer, and the guides stay as they are).

    • packages/types: ListSchema.items becomes optional on both published faces: zod in zod/data-display.zod.ts and TS in data-display.ts. A refinement requires at least one of bind / items, ⛔ not exactly one, because items is the renderer's fallback beside bind. The strict face follows.
    • Pins:
      • a bind-only list is accepted;
      • an items-only list is accepted;
      • a list with neither is refused at the node;
      • the four os:check fences in skills/objectui/guides/data-integration.md and schema-expressions.md pass safeValidateSchema, read from the guides, ⛔ not edited.
    • Readers of ListSchema['items'] that assumed it present: typed in place, region-local, only where the optional member makes them fail to compile.
    • Plus: docs (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2) and one changeset per released package changed.

    ⛔ No edit to skills/**: the governed guides stay as they are, and their fences become valid. ⛔ No renderer behaviour change in list.tsx. Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; no path-derived mandate)
    Clause-②: yes
    Why yes: a required member of a published type becomes optional, and the validator starts accepting a bind-only list it refused. The at-tier contract record is owed on the landing head before enqueue (objectstack#21187 ruling A: a contract face).
    Thread-read: 5941529511
    Serial constraints cleared: read 2026-10-01T23:11Z at objectui origin/main 7728c67c8, over all open PRs' file lists and every open pm:dispatched claim. The seat runs serially under the maintainer's word at 2026-10-01T23:08Z, 「继续上班,只处理 p0 p1 p2任务,串行派发」, so this is the only card in flight.

    • data-display.ts / zod/data-display.zod.ts: no open PR edits them.
    • zod-mirror-parity.test.ts: PR objectui#11406 (objectui#11276, another seat) edits its EXCLUSIONS block. This card opens no parity row if both faces move together. If one opens, edit only that row, and whichever PR lands second merges main.
  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 11405,
     "status": "done",
     "branch": "claude/issue-11405-list-bind-only",
     "pr": "https://github.com/objectstack-ai/objectui/pull/11407",
     "session": "session_01VhxTqosz7wn54ahqyxgERT",
     "premise_still_valid": true,
     "summary": "ListSchema.items is optional on both published faces of @object-ui/types. The zod arm ends in .superRefine(listHasAnEntrySource) with a when-always-true option. A list with neither bind nor items is refused once, at the node (code custom, params.code LIST_ENTRIES_REQUIRED, message naming both keys). Bind-only, items-only and both are accepted through the arm, safeValidateSchema and the strict face. Zone 2 readings: H1 was confirmed on 7728c67c8 (bind-only and neither were both refused with invalid_type at items, on all three faces). H2: the refinement keeps the arm a valid discriminated-union member, and a TS union spelling was probed and not taken: the reader type is identical, TS2312 blocks extends, its error names only bind, and three precedent arms keep optional plus a mirror refinement. H3: a type-checker census over 5787 files finds one reader, list.tsx, which is Array.isArray-guarded. H4: nothing reads required from the registration, so it is untouched. H5: the four guide fences are read off disk with scanSkillFences; three pass and the one marked wrong is refused only at children. H6: no parity row is needed; the parity ratchet fires if the TS face moves alone. The pin, the list.mdx docs, a minor changeset and one ledger row in scripts/markdown-test-inputs.mjs (see deviations) land with it. skills/** and list.tsx are untouched.",
     "tests": "Final union on landing head 8b317ec61, under os-verify-lock: types build exit 0; `pnpm --filter @object-ui/types type-check` exit 0 (three legs; tsc -p tsconfig.test.json --listFilesOnly includes the new pin); `pnpm exec vitest run packages/types/` Test Files 321 passed (321), Tests 8560 passed (8560). Then `pnpm --filter '@object-ui/components^...' build` exit 0 and `pnpm --filter @object-ui/components type-check` exit 0, against a dist grep-confirmed to carry `items?: ListItem[]` and LIST_ENTRIES_REQUIRED. Consumer and ledger suites, 15 files (components list and bind tests, skill-guide-data-table-binding, markdown-test-inputs, check-skill-examples, and the changeset-script suites that name the ledger): Test Files 15 passed (15), Tests 494 passed (494). An earlier full types run at 24c2c1e1e was red, 1 failed of 8560: base-bind-declared's single-bind-declaration scan matched the refinement's `bind?:` destructuring annotation. db0a0537a fixed it by reading the node as a plain record. Ablation used objectstack's ablation-replace in WRAP mode, with fix committed and anchors hit x1 then x0, and blob restored equal to HEAD with git diff HEAD empty. It ran at 24c2c1e1e; db0a0537a and 8b317ec61 touch none of the three anchors. M1 (zod items required): 11 failed, 1 passed of 12, with only the counter-probe green. M2 (refinement removed): 5 failed, 7 passed, the neither, nested and beside-a-fault rows. The first M2 attempt was a no-op: the replacement count did not rise, so the tool refused before the command ran. It was re-run with a --delete anchor. M3 (TS items required): tsc -p tsconfig.test.json exit 2 with TS2741 at the pin's BIND_ONLY literal, plus the zod-mirror-parity ratchet (TS2322 on data-display.zod.ts#ListSchema and items). Control on unmutated HEAD: 12/12 passed, tsc exit 0. No dist build was needed, because the pin imports ../zod/index.zod and ../data-display from src.",
     "mcp_calls": "0",
     "api_writes": "3 — every one a relay dispatch POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectui/pulls (#11407, draft forced; read-back 9052 bytes identical); (2) label-write --assign os-litant → POST /repos/objectstack-ai/objectui/issues/11407/assignees (read-back MATCHES); (3) post-stamped os-dev-report → POST /repos/objectstack-ai/objectui/issues/11405/comments. git push x4 to claude/issue-11405-list-bind-only (empty probe, then three commits) is not REST.",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: c · reach: public door — safeValidateSchema on both list fences in content/docs/guide/expressions.md ('Scoped Data', 'Index in Loops') answers invalid_type at items, and StrictAnyComponentSchema adds unrecognized_keys for itemTemplate. The page teaches items as the string ${users}, an itemTemplate and per-item ${item.*} / ${index}, which the published skills/objectui expressions guide refutes ('No per-item template iteration'). · evidence: scripts/check-doc-expression-carriage.mjs already lists both sites (list.items ${users}) in its report-only census, and its header names the family's open cards objectui#7440, #7444, #7838; I did not verify which one owns this page. Family member: merge it into that family's closing card, ⛔ no single-point card. · dedupe words: expressions.md itemTemplate; list items ${users}; Scoped Data Index in Loops; per-item template",
      "carrier: none needed — tree-view, the other useDataScope reader in @object-ui/components, was measured: { type: tree-view, bind: rows } is already accepted on both faces, so the same defect is absent there · noted, not filed"
     ],
     "files_changed": [
      ".changeset/11405-list-bind-only.md (+22/-0)",
      "content/docs/components/data-display/list.mdx (+18/-1)",
      "packages/types/src/__tests__/list-bind-only-11405.test.ts (+179/-0)",
      "packages/types/src/data-display.ts (+18/-3)",
      "packages/types/src/zod/data-display.zod.ts (+41/-2)",
      "scripts/markdown-test-inputs.mjs (+13/-0)"
     ],
     "gates": {
      "head": "8b317ec61 (gate family run at this sha; hand-derived because dispatch-gates.mjs derives nothing for objectui)",
      "passed_exit_0": [
       "check:control-bytes",
       "check:new-line-citations (0 new citations)",
       "check:test-path-roots",
       "check:vi-mock-specifiers",
       "check:phantom-deps",
       "check:self-import",
       "check:unreferenced-sources",
       "check:spec-symbols",
       "check:changeset-claims (13 pending changesets name a touched file; each list-related paragraph read, none made false)",
       "check:pending-changeset-literals",
       "check-changeset-no-major",
       "check-changeset-presence (1 released package, 1 changeset)",
       "check:doc-types",
       "check:doc-fences",
       "check:doc-example-ids",
       "check-doc-links",
       "check-doc-expression-carriage (report-only)",
       "check:doc-example-readers",
       "check:component-surface-parity --type list (report-only)",
       "check:prompt-keys",
       "check:pre-install-import-graph",
       "check-lint-coverage",
       "check-type-check-coverage",
       "markdown-test-inputs --audit (110 candidates, all adjudicated)",
       "check-governed-queue-guard --test over the 6 paths: NOT GOVERNED"
      ],
      "not_measured": [
       "check:doc-examples — exit 2 (could not run: needs the whole-workspace dist build); check:doc-snippets — same build closure, not run. Narrowed substitute: the list.mdx ts fence compiled --strict against the rebuilt @object-ui/types dist, exit 0, with a failing control in the same harness (TS2322, exit 2). CI builds their closure."
      ],
      "lint": "narrowed and proven. (1) Population from eslint's own config: 4 changed files are linted; the .mdx and changeset report 'no matching configuration'. (2) Count 4 from --format json; 0 errors. data-display.ts has 30 no-explicit-any warnings, the same 30 as its base blob through --stdin. (3) eslint.config.js has no parserOptions.project or projectService, so no type-aware linting, and the diff cannot move an untouched file's verdict.",
      "ci": "in_progress — not awaited, per the dispatch contract"
     },
     "line_budget": "n/a — no skills/** or ratcheted ledger with a ceiling touched; diff +291/-6 over 6 files against base 7728c67c8",
     "deviations": [
      "File surface: scripts/markdown-test-inputs.mjs gains one ADJUDICATED row registering the new pin as a reader of skills/objectui/guides/data-integration.md and schema-expressions.md. The row is outside the claim's declared surface. It is required: the claimed pin reads the guides off disk (H5), `markdown-test-inputs.mjs --audit` answered 'unadjudicated-test' for it, and scripts/__tests__/markdown-test-inputs.test.ts asserts auditTree(...) equals [] on the live tree. The seat owes the claim's file-surface addition. The script's own suite and its importers ran green.",
      "PR body amendment (seat to write if wanted): its Ablation section names no sha. The legs ran at 24c2c1e1e, and the later commits db0a0537a and 8b317ec61 touch none of the three anchors.",
      "Clause-② line: copied as `Clause-②: yes` with em-dash reasoning and no parenthetical arm. Measured: the change only widens, because neither was already refused before, at items."
     ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: report 5942839913, PR #11407 at 8b317ec61, ready for the at-tier contract review (Fixes #11405). From the domain:spec @ objectui seat (objectui#10217), session session_01VhxTqosz7wn54ahqyxgERT, 2026-10-01T23:46Z. The seat runs serially, so this PR's review is the one subagent in flight.

    Checked against the PR, not the report's prose

    • check-governed-merges reads NOT governed (0 of 6 paths). Size: +291 / -6.
    • git merge-tree against main 7728c67c8 is clean.
    • The PR opens with Fixes #11405 / Clause-②: yes. That is right on the diff's face:
      • a bind-only list goes from refused to accepted;
      • a list with neither key was refused before, at items, and is refused now, at the node;
      • nothing goes from accepted to refused.
    • The changeset is @object-ui/types minor.
    • Triage's ruling 5941529511 is executed as written:
      • items is optional on both faces, with an at-least-one refinement (listHasAnEntrySource, !== undefined, when: () => true, params.code LIST_ENTRIES_REQUIRED);
      • skills/** and list.tsx are untouched;
      • the four guide fences are read off disk.

    Deviation accepted: scripts/markdown-test-inputs.mjs gains one adjudicated row, outside the claim's surface. It is forced: the pin reads the governed guides off disk, and markdown-test-inputs.test.ts asserts that the live tree's audit is empty. That path is not governed.

    Prose faces, judged by this seat (objectstack main 8dea55d31, contract-review.md: .changeset and docs prose are the ACCEPT's). Each sentence was read against the diff:

    • .changeset/11405-list-bind-only.md:
      • the renderer's read order (list.tsx, useDataScope(schema.bind) first, then Array.isArray(schema.items));
      • the three faces' old refusal of a bind-only literal;
      • all four rows of the before/after table;
      • "at least one, not exactly one";
      • presence !== undefined, so bind: '' and items: [] count;
      • the node-level issue reported beside other issues (when: () => true);
      • the TS reader ListItem[] | undefined, whose one reader is Array.isArray-guarded;
      • "the TypeScript face cannot express the at-least-one rule".
        All hold.
    • content/docs/components/data-display/list.mdx:
      • the illustrative interface's bind? / items? lines;
      • the at-least-one paragraph;
      • the bind-only example;
      • the LIST_ENTRIES_REQUIRED refusal;
      • the TS-face caveat.
        All hold. The snippet's pre-existing items?: SchemaNode[] (the real member type is ListItem[]) predates this PR and is noted, not owed.

    Carried, not filed: the dev's class-(c) finding. content/docs/guide/expressions.md teaches list.items: '${users}' with an itemTemplate, which the validator refuses and the published skills guide refutes. It belongs to the family that scripts/check-doc-expression-carriage.mjs already censuses (objectui#7440 / #7444 / #7838). ⛔ No single-point card.

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11407 merged through the queue as 9547063da (Fixes #11405). From the domain:spec @ objectui seat (objectui#10217), session session_01VhxTqosz7wn54ahqyxgERT, 2026-10-02T00:14Z.

    Verification

    • Merge content matches the PR: the merge commit's git patch-id --stable equals the PR's net diff (080ff8c82558 on both sides).
    • Reviewed head is the landed head: the at-tier record 5942964923 (PASS) names 8b317ec61, and nothing was pushed after it. Its scope was the contract faces only (objectstack main 8dea55d31, contract-review.md). The changeset and docs prose were judged in the ACCEPT 5942857572.
    • CI on that head: 40 success and 3 skipped by design.

    Delivered

    • A bind-only list is accepted. ListSchema.items is optional on both faces of @object-ui/types, and the zod arm requires at least one of bind / items, per triage's ruling 5941529511.
    • A list with neither key is refused at the node, with params.code LIST_ENTRIES_REQUIRED.
    • Nothing that was accepted is now refused.
    • The four os:check fences in the governed skills/objectui guides pass safeValidateSchema as written. The pin reads them off disk.
    • Changeset: minor.

    Carried, not filed: content/docs/guide/expressions.md teaches list.items: '${users}' with an itemTemplate. It belongs to the family check-doc-expression-carriage already censuses (objectui#7440 / #7444 / #7838).

    The leftover pm:dispatched is stripped in the same act.

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions