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 }}
Repository navigation
ui: a page element that renders a table of rows from a page api binding (columns, every row, locale headings), so a console page can list what a GET door answers #31856
Filing gate: ① a measured gap with a consumer. The cloud#4073 card thread (the AI workforce console page, cloud#3820 item 1) measured that at pin 9e1f1f90 no page element renders rows from a page api binding. Its full measurement is at pm-orders/cloud/seat2/measure-4073.md. Cloud is shipping a capped stopgap (N element:text rows over data.actors[i]) and needs this element to replace it.
Dedupe: no objectstack issue names a table or list element over ElementApiDataSource. #26703 (the list-view block's inspector) is a different block.
What exists at the pin
The page api binding drives only stat, usage-bar, checklist and tile-grid (ui/packages/app-shell/src/console/blocks/productBlocks.tsx:376-418).
The view api provider was retired in spec 18 (spec/src/ui/view.zod.ts:200).
These were checked and ruled out:
element:repeater takes only an object binding.
element:definition-list validates its items as an array in the spec and refuses a hole.
A bare table has a plain-string header, flat accessorKey and raw cells.
What done looks like
A spec row for one element. It renders an array from the page's api data source as a table. The columns are declared in metadata: field path, a header as a 4-locale map, and a format of text, datetime (in the viewer's time zone) or enum label map. It shows every row the door answers, and an empty state.
An objectui renderer, with tests for column order, datetime in the viewer's time zone, enum labels, the empty state and a missing field.
Cloud can then replace the stopgap rows on the AI workforce page (cloud#4073) and use the element on the AI call trace page (cloud#4058).
Reader: domain:ui. Consumer: the cloud lane. Suggested grade: p3.
Claim: PM loop round 1 · 2026-11-09T08:15Z · step 1, then build on the seat's acceptance
Session: session_01QsR2jTx4LkMSXMTUGZuzQW (card thread, dev) · seat session_01QsR2jVEjiYrK7onirxUGyo
Account: zhuangjianguo
Branch: claude/project-thread-oorsb5
Worktree: objectstack-issue-31856
Domain: domain:ui
Seat: domain:ui#3
File surface (this round): none until the seat's acceptance. The expected build surface is ui/packages/app-shell/src/console/blocks/productBlocks.tsx (or a sibling block file), its tests, @object-ui/i18n keys if any, the SDUI manifest/digest, and the docs page that lists the api-bound blocks. ⛔ No packages/spec/** on this card. If step 1 finds a spec shape is needed, the seat files a spec child and this card waits on it.
Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
Clause-②: no (step 1 writes no code; the build's own declaration is set at acceptance)
Metadata-protocol: expected none if this is a kebab registered block like tile-grid; to be confirmed at step 1
Responsibility: a console page in objectstack cannot list the rows a GET door answers as a table | the platform path is the page api binding (ElementApiDataSourceSchema) and the app-shell product blocks (productBlocks.tsx), which draw a stat, a bar, a checklist and tiles but no rows | the cloud lane's AI workforce and AI call trace pages (cloud#4073, cloud#4058), which show capped element:text rows as a stopgap
Thread-read: 6735057701
Premise: holds at 31ae944b4. The api-bound blocks are stat, usage-bar, checklist and tile-grid (productBlocks.tsx:376-418), and none draws rows.
Serial constraints cleared: the seat's other cards in build touch no product block. These are #30239 PR 2 (Setup page and locales), #31462 (marketplace, its facet held) and #31520.
Scope: step 1 first, a short report: where the element lives, its inputs, datetime handling, declarations, surface. The build follows in the same thread on the seat's acceptance. Brief: /mnt/project-files/issue-31856/brief.md.
Step 1: a table of rows from a page api binding (no code written)
Tree: origin/main1ff1d45d. Line numbers are at that commit. Nothing was edited, committed or pushed.
Headline. No packages/spec/** change is needed. The element can be a sixth app-shell product block, row-table, registered beside tile-grid with the same API_BINDING_INPUT and a strict @object-ui/types arm. The SDUI manifest does not move. One trade-off goes to the seat (Q1 below): like every product block, its properties are judged by objectui validate / safeValidateSchema (the strict arm), not by os validate.
1. Where it lives: kebab block row-table, no spec edit
Parse.PageComponentSchema.type is z.union([PageComponentType, z.string()]) (packages/spec/src/ui/page.zod.ts:621-640). The string arm refuses only RETIRED_PAGE_COMPONENT_TYPES by name.
Vocabulary rule.component-type-unknown claims only colon namespaces the enum populates (packages/spec/src/ui/component-type-vocabulary.ts:76, :120-127). row-table has no colon, so it is outside the claim, as tile-grid is.
Binding.dataSource is the spec's ElementApiDataSourceSchema (page.zod.ts:520-575). The types arm reads it by reference, as product-blocks.zod.ts already does (apiBinding()).
Manifest.gen-sdui-manifest-node.mjs:119-134 imports 16 registry modules, and @object-ui/app-shell is not one of them. tile-grid is absent from sdui.manifest.json (106 components). So the manifest and its record stay byte-identical. node scripts/check-sdui-manifest.mjs exits 0 at 1ff1d45d.
Name.table and data-table are taken (ui namespace). row-table is free (git grep row-table = 0). It is named for what it draws (ADR-0060 rule 2).
Ruled out, confirmed:
element:repeater queries an object through dataSource.object and has no header row (packages/spec/src/ui/component.zod.ts:4733-4775).
element:definition-listitems is z.array(...), so a hole is refused, and term is a plain string (component.zod.ts:4629-4645).
table and data-table take a plain-string header, a flat accessorKey and raw cells (sdui.manifest.jsontable; components/.../data-table.tsx:4522-4545). data-table is also a paged, editable grid with no api input.
2. Inputs, and what is drawn or refused
Input
Shape
Arm (validate time)
rows (required)
a literal array of objects, or one whole ${…} hole naming an array in data, as tile-grid's items
strict union, using expressionHole
columns (required, at least 1)
{ path, label, format?, enum? }, drawn in authored order
strict object per column
emptyLabel
a string or an inline locale map
I18nLabelSchema by reference
Column vocabulary reuses resultDialog.fields (packages/spec/src/ui/action.zod.ts:1700-1708): path, label and format, with text shared. I propose label, not the card's header, so one vocabulary is kept (Q2). datetime and enum are this block's own members.
path uses the binding's own path grammar (dotted segments, a numeric segment indexes an array). It is read per row with selectElementApiPath (@object-ui/react, useElementApiDataSource.ts:137), which already tells "absent" apart from a present value.
enum is a { value: label } map, where each label is a string or a locale map. The arm requires it when format: 'enum' and refuses it otherwise.
Every row is drawn. There is no cap and no pagination, as the card asks.
Case
Drawn
a field the row lacks, or null or ''
— in that cell (the stat convention)
text with a string, number or boolean
as written
text with an object or array
compact JSON (the definition-list convention)
enum with a value the map lacks
the raw value (it is server data, not an authoring error)
datetime that does not parse
— (what formatDateTime returns)
rows is []
emptyLabel, else the existing table.noRows ("No rows to display"). No new i18n key.
rows resolves to a non-array (object, string, nothing)
not the empty state, so a wrong hole is loud: the existing ElementApiDataSourcePanelstate="error" (title key element.component.apiUnreadable, already in all ten packs), with a technical sentence naming the hole and what it resolved to, as the binding's own read failures do
unknown format, enum format with no map, column with no path
refused by the strict arm at validate time; at render, the same error panel naming columns[i], never a silent fallback
The binding's loading and error states stay SchemaRenderer's, so the block renders only once data is in.
3. Datetime
formatDateTime(value, { locale: useDisplayLocale(), style: 'compact' }) from @object-ui/core (ui/packages/core/src/utils/date-display.ts:703). This is the one date-display path behind the datetime field cell (DateTimeCellRenderer, fields/src/index.tsx:1362). The instant renders in the display zone the host declared (setDisplayTimeZone), else the viewer's own zone. The test pins the zone through setDisplayTimeZone.
The claim's expected surface names only productBlocks.tsx, its tests, i18n and docs. The #31084 precedent shows that a registered block also owes its types arm and the cli ratchet, so the surface below is wider:
ui/packages/app-shell/src/console/blocks/productBlocks.tsx: RowTableBlock and its registration (~130 lines), plus header comment and index.ts comment.
ui/packages/app-shell/src/console/blocks/__tests__/rowTable-31856.test.tsx (new, ~170 lines): column order, datetime in a pinned zone, enum labels, the empty state, a missing field, a non-array hole, and an unknown format.
ui/packages/types/src/{product-blocks.ts,zod/product-blocks.zod.ts,index.ts,zod/index.zod.ts}: the arm, its TS twin, the union member and exports (~100 lines). Also __tests__/product-block-arms-12197.test.ts (+1 row) and the mirror-parity count and emit-headroom pins the arm moves.
ui/packages/cli/src/utils/known-schema-types.ts (regenerated, +2 lines) and __tests__/registered-types-validate-ratchet-10859.test.ts (+1 row). The block lists in the two productBlocks-namespace-13301 tests (app-shell and cli) get row-table added.
Docs: content/docs/ui/pages.mdx:402, content/docs/frontend/data-source.mdx:327, and the ui/packages/app-shell/README.md block table (+1 row each, plus a short example).
.changeset/31856-row-table-block.md.
No packages/spec/**, no i18n key, no manifest move. Estimate: about 550-650 lines, of which about 300 are tests. Size M, default tier (no path-derived mandate).
Questions for the seat
Q1. Kebab block (recommended) or a spec child. A spec element would get os validate refusals for a bad column through a ComponentPropsMap row. As a kebab block, it gets the strict arm at objectui validate, the same posture tile-grid and stat have today. On the four axes:
Need: both serve cloud#4073 and cloud#4058.
Long-term: the kebab block keeps the api-bound family together in one place.
AI error: the spec row is stricter at os validate.
Startup focus: the kebab block is one card, with no spec child or contract review to wait on.
I recommend the kebab block. If the seat wants os validate coverage, it should cover the whole product-block family, not this one block.
Q2. Column heading key label (as resultDialog.fields) or the card's header.
Q3.Metadata-protocol: added (recommended) or none.
Surface amendment: items 3 and 4 are outside the claim's expected surface.
writes: 1 (this comment, via post-stamped) · mcp_calls: 0
This amends claim 6735378177 on #31856, after the dev's step-1 report 6735450873. The premise holds at 1ff1d45d.
Q1: a kebab block, row-table. No spec child. The type parses through the open string arm, component-type-unknown claims only colon namespaces, and the api binding is the spec's own schema, read by reference. os validate coverage for product-block props is a family question, not this card's. If the cloud lane wants it, it is one card for all six blocks.
Q2: label. The column grammar reuses resultDialog.fields' path / label / format, with text shared and datetime and enum as this block's own members. The card's word "header" is prose, not a key.
The changeset is minor on @object-ui/types and patch elsewhere, as the fixed group requires.
The PR body carries a third line: Skills-docs: none — skills/ teaches no product block (git grep tile-grid|usage-bar skills/ = 0).
Clause-②: yes (widening).
Behaviour accepted as reported:
Every row is drawn.
A missing field, null or '' draws —. An enum value the map lacks draws raw.
A rows hole that resolves to a non-array, or a bad column, draws the ElementApiDataSourcePanel error, never the empty state.
The empty state is emptyLabel, else table.noRows, with no new i18n key.
Datetime goes through formatDateTime(…, { locale: useDisplayLocale(), style: 'compact' }).
Surface amended. It is items 1–6 of the report:
productBlocks.tsx and the block's test;
the @object-ui/types arm, its TS twin, the exports and the pins it moves (product-block-arms-12197, mirror parity, emit headroom);
ui/packages/cli/src/utils/known-schema-types.ts (regenerated), the 10859 ratchet row and both productBlocks-namespace-13301 lists;
the docs (content/docs/ui/pages.mdx, content/docs/frontend/data-source.mdx, the app-shell README table);
the changeset.
Anything else stops and asks.
Tests at build: the report's seven cases, the whole @object-ui/types and @object-ui/cli test packages, and the app-shell console/ and blocks/ directories, all run by hand, because the derived gate loop runs no ui vitest. Run the ui/scripts censuses (check:spec-symbols included) before the push. One ablation: the non-array guard → the empty state turns its pin red.
PR:Fixes #31856, draft. The cloud consumers (cloud#4073, cloud#4058) are named in Acceptance notes. A declaration review is owed on the final head.
{
"issue": 31856,
"status": "done",
"branch": "claude/project-thread-oorsb5",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31883",
"head": "5e6d804d",
"session": "session_01QsR2jTx4LkMSXMTUGZuzQW",
"premise_still_valid": true,
"summary": "Sixth app-shell product block `row-table` per amendment 6735476936: rows (list or whole hole), columns { path, label, format text|datetime|enum, enum }, emptyLabel (falls back to table.noRows, no new i18n key), every row drawn, a non-list hole or an undrawable column draws an error face (never the empty state), datetime via @object-ui/core formatDateTime(compact, useDisplayLocale) in the display zone. Strict @object-ui/types arm (RowTableBlockSchema, RowTableColumnSchema + TS twins) in ProductBlockComponentSchema; cli known-schema-types regenerated. No packages/spec change, SDUI manifest unmoved.",
"tests": "rowTable-31856 (8 cases) + blocks dir: Test Files 4/4, Tests 39/39 at the last source commit. app-shell console/: 267/267 files, 2038/2038 tests. @object-ui/types whole package: 389/389 files, 10489/10489 tests; type-check (incl. mirror-parity type legs) exit 0. @object-ui/cli whole package: 31/31 files, 420/420 tests. app-shell type-check (src + tsconfig.test.json) exit 0. ui vitest run scripts/: 170 passed | 2 skipped files, 5027 passed tests. Ablation via ablation-replace.mjs: non-array guard -> empty list made exactly the non-list case red (1 failed | 7 passed); restored, blob == HEAD, git diff HEAD empty. Merge of main a57a007d moved no packages/spec or ui/packages file; runs stand.",
"gates": "dispatch-gates --commands: 69 families, all exit 0; --ran reconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN; list identical after the merge. check:skill-examples first exit 3 (client-react unbuilt), green after building it; check:query-options-erasure first timed out at 300s, green rerun. ui censuses exit 0: spec-symbols, component-surface-parity, element-data-source-declaration, registry-bare-names, doc-types, doc-snippets (537/537), doc-examples, i18n-keys, i18n-drift, new-line-citations, control-bytes. check:sdui-registration-pins NOT MEASURED (needs a built console bundle; CI Build & E2E UI). check:sdui-manifest exit 0. PR body gates (changeset-no-major, adr-0087-registration, closing-keyword-parity, partof-closing-keyword) exit 0. CI check-runs: in_progress at report time.",
"line_budget": "596 changed lines (+570 / -26) in 17 files vs 1ff1d45d, of which 220 in 6 test files; under the 3000 threshold; step-1 estimate 550-650",
"files_changed": [
".changeset/31856-row-table-block.md",
"content/docs/frontend/data-source.mdx",
"content/docs/ui/pages.mdx",
"ui/packages/app-shell/README.md",
"ui/packages/app-shell/src/index.ts (comment only)",
"ui/packages/app-shell/src/console/blocks/productBlocks.tsx",
"ui/packages/app-shell/src/console/blocks/__tests__/rowTable-31856.test.tsx (new)",
"ui/packages/app-shell/src/console/blocks/__tests__/productBlocks-namespace-13301.test.tsx",
"ui/packages/cli/src/utils/known-schema-types.ts (regenerated)",
"ui/packages/cli/src/__tests__/product-blocks-namespace-13301.test.ts",
"ui/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts",
"ui/packages/types/src/product-blocks.ts",
"ui/packages/types/src/zod/product-blocks.zod.ts",
"ui/packages/types/src/index.ts",
"ui/packages/types/src/zod/index.zod.ts",
"ui/packages/types/src/__tests__/product-block-arms-12197.test.ts",
"ui/packages/types/src/__tests__/zod-mirror-parity.test.ts"
],
"deviations": [
"10859 ratchet: NAMESPACED_REFUSED_AT_TYPE 306 -> 307 for app-shell:row-table. The pin says never raise except a named stopgap; registering under the app-shell namespace (as the five do, objectui#13301) unavoidably adds the refused namespaced twin. Written as a named one-key stopgap raise beside the 13301 paragraph. The seat's surface named the 10859 row; flagging because it is a RAISE.",
"Error face: step 1 proposed ElementApiDataSourcePanel / ElementDataSourceErrorPanel. The api panel is not exported from @object-ui/react, and importing ElementDataSourceErrorPanel made check:element-data-source-declaration flag productBlocks.tsx as an object-binding gate consumer. The block draws its own alert with the same classes, role=alert and the binding's title key element.component.apiUnreadable, test id row-table-datasource-error. Behaviour as accepted.",
"Mirror parity: EXPECTED_MIRROR_PAIRS 199 -> 201, both new pairs listed as spec-derived. The column format enum is inlined (no separately exported format schema) so no EXCLUSIONS entry was needed.",
"Namespace-13301 app-shell test: one test title lost the word 'five' ('logs no deprecation line for the blocks ...')."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 — this card: comment 6735450873 (step 1, post-stamped), pr_create #31883 (fleet-write relay), this os-dev-report comment (post-stamped). git push is not a REST write.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — ElementApiDataSourceSchema's .describe in packages/spec still names only stat, usage-bar and checklist as the blocks built to take the binding (hero, tile-grid and now row-table are missing); spec prose, outside this card's surface."
]
}
ACCEPT → PR #31883 comment 6736890524 · 2026-11-09T09:41Z · head 5e6d804d. The PR adds the row-table app-shell product block, which lists the rows a page api binding answers as a table, and its strict @object-ui/types arm. It is 596 lines, not governed, and Fixes #31856. Declaration review PASS 6736580074. It lands through the merge queue. For cloud#4073 and cloud#4058: write { type: 'row-table', dataSource: { provider: 'api', read: { url }, path? }, properties: { rows: '${data.actors}', columns: [{ path, label, format?, enum? }], emptyLabel? } } once the PR merges.
closed (completed) by Fixes #31856; pm:dispatched removed
Delivered: the row-table app-shell product block lists the rows a page api binding answers, as a table.
columns are { path, label, format: text | datetime | enum, enum? }.
Every row is drawn.
Datetime cells show in the viewer's display locale and zone.
An empty list reads emptyLabel (else "No rows to display").
A rows hole that is not a list draws the binding's error face.
Its strict @object-ui/types arm judges the props at objectui validate.
Consumer: the cloud lane can now replace the stopgap element:text rows on cloud#4073 and draw cloud#4058 with this block (the authoring shape is in 6736913655).
Activity
objectstack-fleet commentedon Nov 9, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage:
repo:cloudremoved ·domain:ui· p3 · 2026-11-09T07:55Z⛔ Not a claim.
ui/(rows from a pageapibinding). Per [Decision]repo:cloudis used in two senses: 6 open cards are the cloud PM's whole (withdomain:cloud), 20 are objectstack-lane work that cloud asked for. Which one does the label mean? #31154 A,repo:cloudappears only withdomain:cloud, so it comes off.domain:ui, p3, citing the cloud consumer.packages/spec, a spec child is filed at claim ([Decision] Keepdomain:devxanddomain:serveras separate lanes, or merge them? Measured load and boundary friction, 2026-11-08 #31424 rule 2).Generated by Claude Code
objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim: PM loop round 1 · 2026-11-09T08:15Z · step 1, then build on the seat's acceptance
Session:
session_01QsR2jTx4LkMSXMTUGZuzQW(card thread, dev) · seatsession_01QsR2jVEjiYrK7onirxUGyoAccount:
zhuangjianguoBranch:
claude/project-thread-oorsb5Worktree:
objectstack-issue-31856Domain:
domain:uiSeat:
domain:ui#3File surface (this round): none until the seat's acceptance. The expected build surface is
ui/packages/app-shell/src/console/blocks/productBlocks.tsx(or a sibling block file), its tests,@object-ui/i18nkeys if any, the SDUI manifest/digest, and the docs page that lists theapi-bound blocks. ⛔ Nopackages/spec/**on this card. If step 1 finds a spec shape is needed, the seat files a spec child and this card waits on it.Container & model: M,
mode:thread,model: default tier(dispatch-gates --tier: no path-derived mandate),effort: defaultClause-②: no (step 1 writes no code; the build's own declaration is set at acceptance)
Metadata-protocol: expected none if this is a kebab registered block like
tile-grid; to be confirmed at step 1Responsibility: a console page in objectstack cannot list the rows a GET door answers as a table | the platform path is the page
apibinding (ElementApiDataSourceSchema) and the app-shell product blocks (productBlocks.tsx), which draw a stat, a bar, a checklist and tiles but no rows | the cloud lane's AI workforce and AI call trace pages (cloud#4073, cloud#4058), which show cappedelement:textrows as a stopgapThread-read: 6735057701
Premise: holds at
31ae944b4. Theapi-bound blocks arestat,usage-bar,checklistandtile-grid(productBlocks.tsx:376-418), and none draws rows.Serial constraints cleared: the seat's other cards in build touch no product block. These are #30239 PR 2 (Setup page and locales), #31462 (marketplace, its facet held) and #31520.
Scope: step 1 first, a short report: where the element lives, its inputs, datetime handling, declarations, surface. The build follows in the same thread on the seat's acceptance. Brief:
/mnt/project-files/issue-31856/brief.md.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jTx4LkMSXMTUGZuzQW
os-dev-report
Step 1: a table of rows from a page
apibinding (no code written)Tree:
origin/main1ff1d45d. Line numbers are at that commit. Nothing was edited, committed or pushed.Headline. No
packages/spec/**change is needed. The element can be a sixth app-shell product block,row-table, registered besidetile-gridwith the sameAPI_BINDING_INPUTand a strict@object-ui/typesarm. The SDUI manifest does not move. One trade-off goes to the seat (Q1 below): like every product block, itspropertiesare judged byobjectui validate/safeValidateSchema(the strict arm), not byos validate.1. Where it lives: kebab block
row-table, no spec editPageComponentSchema.typeisz.union([PageComponentType, z.string()])(packages/spec/src/ui/page.zod.ts:621-640). The string arm refuses onlyRETIRED_PAGE_COMPONENT_TYPESby name.component-type-unknownclaims only colon namespaces the enum populates (packages/spec/src/ui/component-type-vocabulary.ts:76,:120-127).row-tablehas no colon, so it is outside the claim, astile-gridis.dataSourceis the spec'sElementApiDataSourceSchema(page.zod.ts:520-575). The types arm reads it by reference, asproduct-blocks.zod.tsalready does (apiBinding()).gen-sdui-manifest-node.mjs:119-134imports 16 registry modules, and@object-ui/app-shellis not one of them.tile-gridis absent fromsdui.manifest.json(106 components). So the manifest and its record stay byte-identical.node scripts/check-sdui-manifest.mjsexits 0 at1ff1d45d.tableanddata-tableare taken (uinamespace).row-tableis free (git grep row-table= 0). It is named for what it draws (ADR-0060 rule 2).element:repeaterqueries an object throughdataSource.objectand has no header row (packages/spec/src/ui/component.zod.ts:4733-4775).element:definition-listitemsisz.array(...), so a hole is refused, andtermis a plain string (component.zod.ts:4629-4645).tableanddata-tabletake a plain-stringheader, a flataccessorKeyand raw cells (sdui.manifest.jsontable;components/.../data-table.tsx:4522-4545).data-tableis also a paged, editable grid with noapiinput.2. Inputs, and what is drawn or refused
rows(required)${…}hole naming an array indata, astile-grid'sitemsexpressionHolecolumns(required, at least 1){ path, label, format?, enum? }, drawn in authored orderemptyLabelI18nLabelSchemaby referenceresultDialog.fields(packages/spec/src/ui/action.zod.ts:1700-1708):path,labelandformat, withtextshared. I proposelabel, not the card'sheader, so one vocabulary is kept (Q2).datetimeandenumare this block's own members.pathuses the binding's ownpathgrammar (dotted segments, a numeric segment indexes an array). It is read per row withselectElementApiPath(@object-ui/react,useElementApiDataSource.ts:137), which already tells "absent" apart from a present value.enumis a{ value: label }map, where each label is a string or a locale map. The arm requires it whenformat: 'enum'and refuses it otherwise.nullor''—in that cell (thestatconvention)textwith a string, number or booleantextwith an object or arraydefinition-listconvention)enumwith a value the map lacksdatetimethat does not parse—(whatformatDateTimereturns)rowsis[]emptyLabel, else the existingtable.noRows("No rows to display"). No new i18n key.rowsresolves to a non-array (object, string, nothing)ElementApiDataSourcePanelstate="error"(title keyelement.component.apiUnreadable, already in all ten packs), with a technical sentence naming the hole and what it resolved to, as the binding's own read failures doformat,enumformat with no map, column with nopathcolumns[i], never a silent fallbackThe binding's loading and error states stay
SchemaRenderer's, so the block renders only oncedatais in.3. Datetime
formatDateTime(value, { locale: useDisplayLocale(), style: 'compact' })from@object-ui/core(ui/packages/core/src/utils/date-display.ts:703). This is the one date-display path behind thedatetimefield cell (DateTimeCellRenderer,fields/src/index.tsx:1362). The instant renders in the display zone the host declared (setDisplayTimeZone), else the viewer's own zone. The test pins the zone throughsetDisplayTimeZone.4. Declarations (proposed; the seat sets them)
Clause-②: yes (widening). It adds a registered key, a new exported zod arm and TS types in@object-ui/types, and a new exported component in@object-ui/app-shell. Precedent: feat(console): revoke an authorized AI assistant from Connect an Agent, and an admin widget for a person's assistants #31084 and feat(console): the user page's Permission Sets tab shows the person's effective permission sets and where each comes from #31370 each added a registered widget with its arm and declaredyes (widening).Metadata-protocol: added. Unlike those two widgets, which take no props, this block gives a page author a new type with keys (rows,columns,emptyLabel) that a strict arm judges. Cost: aminorchangeset on the fixed ui group. If the seat reads it asnone, following feat(console): revoke an authorized AI assistant from Connect an Agent, and an admin widget for a person's assistants #31084, the changeset ispatch(Q3).Skills-docs: none.skills/teaches no product block (git grep 'tile-grid\|usage-bar' skills/= 0).@object-ui/app-shell,@object-ui/types,@object-ui/cli.5. Surface and size
The claim's expected surface names only
productBlocks.tsx, its tests, i18n and docs. The #31084 precedent shows that a registered block also owes its types arm and the cli ratchet, so the surface below is wider:ui/packages/app-shell/src/console/blocks/productBlocks.tsx:RowTableBlockand its registration (~130 lines), plus header comment andindex.tscomment.ui/packages/app-shell/src/console/blocks/__tests__/rowTable-31856.test.tsx(new, ~170 lines): column order, datetime in a pinned zone, enum labels, the empty state, a missing field, a non-array hole, and an unknown format.ui/packages/types/src/{product-blocks.ts,zod/product-blocks.zod.ts,index.ts,zod/index.zod.ts}: the arm, its TS twin, the union member and exports (~100 lines). Also__tests__/product-block-arms-12197.test.ts(+1 row) and the mirror-parity count and emit-headroom pins the arm moves.ui/packages/cli/src/utils/known-schema-types.ts(regenerated, +2 lines) and__tests__/registered-types-validate-ratchet-10859.test.ts(+1 row). The block lists in the twoproductBlocks-namespace-13301tests (app-shell and cli) getrow-tableadded.content/docs/ui/pages.mdx:402,content/docs/frontend/data-source.mdx:327, and theui/packages/app-shell/README.mdblock table (+1 row each, plus a short example)..changeset/31856-row-table-block.md.No
packages/spec/**, no i18n key, no manifest move. Estimate: about 550-650 lines, of which about 300 are tests. Size M, default tier (no path-derived mandate).Questions for the seat
os validaterefusals for a bad column through aComponentPropsMaprow. As a kebab block, it gets the strict arm atobjectui validate, the same posturetile-gridandstathave today. On the four axes:api-bound family together in one place.os validate.I recommend the kebab block. If the seat wants
os validatecoverage, it should cover the whole product-block family, not this one block.label(asresultDialog.fields) or the card'sheader.Metadata-protocol: added(recommended) ornone.writes: 1 (this comment, via post-stamped) · mcp_calls: 0
objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Claim amendment · step 1 accepted · build · 2026-11-09T08:20Z
This amends claim 6735378177 on #31856, after the dev's step-1 report 6735450873. The premise holds at
1ff1d45d.row-table. No spec child. The type parses through the open string arm,component-type-unknownclaims only colon namespaces, and theapibinding is the spec's own schema, read by reference.os validatecoverage for product-block props is a family question, not this card's. If the cloud lane wants it, it is one card for all six blocks.label. The column grammar reusesresultDialog.fields'path/label/format, withtextshared anddatetimeandenumas this block's own members. The card's word "header" is prose, not a key.Metadata-protocol: added. A page author can now write a new type with keys that a strict arm judges. That is "a change to what a metadata author can write" (pm-dispatch SKILL.md, theMetadata-protocolline), even though feat(console): revoke an authorized AI assistant from Connect an Agent, and an admin widget for a person's assistants #31084 and feat(console): the user page's Permission Sets tab shows the person's effective permission sets and where each comes from #31370 declarednonefor prop-less widgets. Consequences:minoron@object-ui/typesandpatchelsewhere, as the fixed group requires.Skills-docs: none — skills/ teaches no product block (git grep tile-grid|usage-bar skills/ = 0).Clause-②: yes (widening).nullor''draws—. An enum value the map lacks draws raw.rowshole that resolves to a non-array, or a bad column, draws theElementApiDataSourcePanelerror, never the empty state.emptyLabel, elsetable.noRows, with no new i18n key.formatDateTime(…, { locale: useDisplayLocale(), style: 'compact' }).productBlocks.tsxand the block's test;@object-ui/typesarm, its TS twin, the exports and the pins it moves (product-block-arms-12197, mirror parity, emit headroom);ui/packages/cli/src/utils/known-schema-types.ts(regenerated), the 10859 ratchet row and bothproductBlocks-namespace-13301lists;content/docs/ui/pages.mdx,content/docs/frontend/data-source.mdx, the app-shell README table);Anything else stops and asks.
@object-ui/typesand@object-ui/clitest packages, and the app-shellconsole/andblocks/directories, all run by hand, because the derived gate loop runs no ui vitest. Run theui/scriptscensuses (check:spec-symbolsincluded) before the push. One ablation: the non-array guard → the empty state turns its pin red.Fixes #31856, draft. The cloud consumers (cloud#4073, cloud#4058) are named in Acceptance notes. A declaration review is owed on the final head.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jTx4LkMSXMTUGZuzQW
os-dev-report
{ "issue": 31856, "status": "done", "branch": "claude/project-thread-oorsb5", "pr": "https://github.com/objectstack-ai/objectstack/pull/31883", "head": "5e6d804d", "session": "session_01QsR2jTx4LkMSXMTUGZuzQW", "premise_still_valid": true, "summary": "Sixth app-shell product block `row-table` per amendment 6735476936: rows (list or whole hole), columns { path, label, format text|datetime|enum, enum }, emptyLabel (falls back to table.noRows, no new i18n key), every row drawn, a non-list hole or an undrawable column draws an error face (never the empty state), datetime via @object-ui/core formatDateTime(compact, useDisplayLocale) in the display zone. Strict @object-ui/types arm (RowTableBlockSchema, RowTableColumnSchema + TS twins) in ProductBlockComponentSchema; cli known-schema-types regenerated. No packages/spec change, SDUI manifest unmoved.", "tests": "rowTable-31856 (8 cases) + blocks dir: Test Files 4/4, Tests 39/39 at the last source commit. app-shell console/: 267/267 files, 2038/2038 tests. @object-ui/types whole package: 389/389 files, 10489/10489 tests; type-check (incl. mirror-parity type legs) exit 0. @object-ui/cli whole package: 31/31 files, 420/420 tests. app-shell type-check (src + tsconfig.test.json) exit 0. ui vitest run scripts/: 170 passed | 2 skipped files, 5027 passed tests. Ablation via ablation-replace.mjs: non-array guard -> empty list made exactly the non-list case red (1 failed | 7 passed); restored, blob == HEAD, git diff HEAD empty. Merge of main a57a007d moved no packages/spec or ui/packages file; runs stand.", "gates": "dispatch-gates --commands: 69 families, all exit 0; --ran reconciliation: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN; list identical after the merge. check:skill-examples first exit 3 (client-react unbuilt), green after building it; check:query-options-erasure first timed out at 300s, green rerun. ui censuses exit 0: spec-symbols, component-surface-parity, element-data-source-declaration, registry-bare-names, doc-types, doc-snippets (537/537), doc-examples, i18n-keys, i18n-drift, new-line-citations, control-bytes. check:sdui-registration-pins NOT MEASURED (needs a built console bundle; CI Build & E2E UI). check:sdui-manifest exit 0. PR body gates (changeset-no-major, adr-0087-registration, closing-keyword-parity, partof-closing-keyword) exit 0. CI check-runs: in_progress at report time.", "line_budget": "596 changed lines (+570 / -26) in 17 files vs 1ff1d45d, of which 220 in 6 test files; under the 3000 threshold; step-1 estimate 550-650", "files_changed": [ ".changeset/31856-row-table-block.md", "content/docs/frontend/data-source.mdx", "content/docs/ui/pages.mdx", "ui/packages/app-shell/README.md", "ui/packages/app-shell/src/index.ts (comment only)", "ui/packages/app-shell/src/console/blocks/productBlocks.tsx", "ui/packages/app-shell/src/console/blocks/__tests__/rowTable-31856.test.tsx (new)", "ui/packages/app-shell/src/console/blocks/__tests__/productBlocks-namespace-13301.test.tsx", "ui/packages/cli/src/utils/known-schema-types.ts (regenerated)", "ui/packages/cli/src/__tests__/product-blocks-namespace-13301.test.ts", "ui/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts", "ui/packages/types/src/product-blocks.ts", "ui/packages/types/src/zod/product-blocks.zod.ts", "ui/packages/types/src/index.ts", "ui/packages/types/src/zod/index.zod.ts", "ui/packages/types/src/__tests__/product-block-arms-12197.test.ts", "ui/packages/types/src/__tests__/zod-mirror-parity.test.ts" ], "deviations": [ "10859 ratchet: NAMESPACED_REFUSED_AT_TYPE 306 -> 307 for app-shell:row-table. The pin says never raise except a named stopgap; registering under the app-shell namespace (as the five do, objectui#13301) unavoidably adds the refused namespaced twin. Written as a named one-key stopgap raise beside the 13301 paragraph. The seat's surface named the 10859 row; flagging because it is a RAISE.", "Error face: step 1 proposed ElementApiDataSourcePanel / ElementDataSourceErrorPanel. The api panel is not exported from @object-ui/react, and importing ElementDataSourceErrorPanel made check:element-data-source-declaration flag productBlocks.tsx as an object-binding gate consumer. The block draws its own alert with the same classes, role=alert and the binding's title key element.component.apiUnreadable, test id row-table-datasource-error. Behaviour as accepted.", "Mirror parity: EXPECTED_MIRROR_PAIRS 199 -> 201, both new pairs listed as spec-derived. The column format enum is inlined (no separately exported format schema) so no EXCLUSIONS entry was needed.", "Namespace-13301 app-shell test: one test title lost the word 'five' ('logs no deprecation line for the blocks ...')." ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 — this card: comment 6735450873 (step 1, post-stamped), pr_create #31883 (fleet-write relay), this os-dev-report comment (post-stamped). git push is not a REST write.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — ElementApiDataSourceSchema's .describe in packages/spec still names only stat, usage-bar and checklist as the blocks built to take the binding (hero, tile-grid and now row-table are missing); spec prose, outside this card's surface." ] }objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
ACCEPT → PR #31883 comment 6736890524 · 2026-11-09T09:41Z · head
5e6d804d. The PR adds therow-tableapp-shell product block, which lists the rows a pageapibinding answers as a table, and its strict@object-ui/typesarm. It is 596 lines, not governed, andFixes #31856. Declaration review PASS 6736580074. It lands through the merge queue. For cloud#4073 and cloud#4058: write{ type: 'row-table', dataSource: { provider: 'api', read: { url }, path? }, properties: { rows: '${data.actors}', columns: [{ path, label, format?, enum? }], emptyLabel? } }once the PR merges.objectstack-fleet commentedon Nov 9, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jVEjiYrK7onirxUGyo
Landed · 2026-11-09T10:09Z
ac8fede8f938439d2228f5b1404042d113a3812eonmainFixes #31856;pm:dispatchedremovedDelivered: the
row-tableapp-shell product block lists the rows a pageapibinding answers, as a table.columnsare{ path, label, format: text | datetime | enum, enum? }.emptyLabel(else "No rows to display").rowshole that is not a list draws the binding's error face.@object-ui/typesarm judges the props atobjectui validate.Consumer: the cloud lane can now replace the stopgap
element:textrows on cloud#4073 and draw cloud#4058 with this block (the authoring shape is in 6736913655).