Skip to content

Commit dc51811

Browse files
fix(spec): the page liveness ledger grades the app page record binding live, citing its console readers (#30611)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuKB8Sg6c6sVpL96ruVvdxaf)_ Fixes #30094 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`packages/spec/liveness/page.json` 里 app 页面 `record` 绑定及其全部子键(object/view/filter 及 field/operator/value、sort 及 field/order)从 `planned` 改判 `live`,每行引用控制台里真正读取它的代码;同步重生成 `state-counts/page.md`(live 35→42,planned 9→2),并加一条 `@objectstack/spec` patch changeset。 - 为什么改:旧行写着「还没有渲染器读页面级 `record`」,但 `ui/` 里的 PageView 已经按这个绑定读出首行并作为 `record` 交给每个组件的 `visibleWhen`;过期的 `planned` 会让作者和 AI 以为这个键不起作用。 - 风险与回滚:只改台账数据与生成计数,不动 schema、不动 `ui/`、无运行时行为变化;回滚即 revert 本 PR。 - 先看:`packages/spec/liveness/page.json` 的 `record` 块(容器行的 evidence 与 note);`packages/spec/liveness/state-counts/page.md`;`.changeset/30094-page-record-liveness-regrade.md`。 - 两分钟验证:`pnpm --filter @objectstack/spec check:liveness`,预期末行 `✓ … every path#symbol anchor names a symbol its file contains …` 且 `state-counts/ is current`,退出码 0。 - 需要你做的:无,席位落地。 ## What changes Each `props.record.*` row of the `page` ledger is regraded from the renderer in this checkout, following the README regrade recipe: a `path#symbol` citation at the reader, `verifiedAt: 2026-11-05`, `evidenceScope: in-repo`, and a `producer` where the read depends on a second input (the page's adapter). No row stays `planned`: every key has a reader. | Row | Old | New | Reader citation | |---|---|---|---| | `record` | planned | live | `PageView.tsx#appPageRecordBinding` (an `app` page only, a binding that names its object) → the AppPageRecord element `PageView.tsx#PageView` mounts around the page body → `PageView.tsx#AppPageRecordRead` (first row, `$top: 1`) → `ExpressionProvider.tsx#PageRecordScope` (publishes it as `record`) → `SchemaRenderer.tsx#buildNodeEvaluationScope` (spread into every component's `visibleWhen`) | | `record.object` | planned | live | `PageView.tsx#AppPageRecordGate` lands it on `objectName` (`element-data-source.ts#composeElementDataSource`); `PageView.tsx#AppPageRecordRead` calls `dataSource.find(object, params)` | | `record.view` | planned | live | `useElementDataSource.ts#useElementDataSource` reads `config.view`, lists the saved views and resolves the name (an unresolved name draws the gate's error panel, never a wider read); `element-data-source.ts#composeElementDataSource` AND-combines the view filter and takes the view sort when the binding has none | | `record.filter` | planned | live | `ElementDataSourceGate.tsx#useElementDataSourceSchema` (the page mapping's `filter: true` lowers it with `toFilterNodeSafely`); `PageView.tsx#AppPageRecordRead` resolves placeholder tokens and sends it as `$filter` | | `record.filter.field` | planned | live | `filter-converter.ts#viewFilterRuleToNode` (left operand of the lowered comparison) | | `record.filter.operator` | planned | live | `filter-converter.ts#viewFilterRuleToNode` (normalised by `normalizeFilterOperator`) | | `record.filter.value` | planned | live | `filter-converter.ts#viewFilterRuleToNode` (right operand) | | `record.sort` | planned | live | `PageView.tsx#AppPageRecordRead` sends it as `params.$orderby` beside `$top: 1`; `element-data-source.ts#composeElementDataSource` (binding sort overrides the view's) | | `record.sort.field` | planned | live | `data-objectstack/src/index.ts#serializeOrderBy` (`field` / `-field` on the wire `sort`) | | `record.sort.order` | planned | live | `data-objectstack/src/index.ts#serializeOrderBy` (`desc`, case-insensitive, selects `-field`) | All `ui/` paths are rooted at `ui/packages/`: `app-shell/src/views/PageView.tsx`, `app-shell/src/providers/ExpressionProvider.tsx`, `react/src/SchemaRenderer.tsx`, `react/src/hooks/useElementDataSource.ts`, `react/src/element-data-source/ElementDataSourceGate.tsx`, `core/src/data-scope/element-data-source.ts`, `core/src/utils/filter-converter.ts`, `data-objectstack/src/index.ts`. **The predicate scope the old note said was missing.** The old note measured objectui `main` @79cb8c03eb, where `record` came only from `RecordContext`, which a record block provides to its own body. The page host now mounts `PageRecordScope`, which adds the page row as `record` to the ambient predicate scope; `buildNodeEvaluationScope` spreads that scope first, so every component predicate on the page reads it, and a record block that binds its own row still overrides `record` for its own subtree. **Changeset.** `packages/spec/package.json` `files[]` lists `liveness`, so `liveness/page.json` ships in the npm tarball: `@objectstack/spec` `patch`, with both declaration lines. ## Why A stale `planned` row tells authors and agents that the key does nothing. The ledger is what the gate, the enforce-or-remove channel and the AI authoring surfaces read as the capability catalogue, and this row was written before the console's reader landed. ## Tests No code changed, so no test was added. The reader is already pinned by `ui/packages/app-shell/src/views/__tests__/PageView.appPageRecord-12931.test.tsx` (tier and count predicates, one read per page open with the org token resolved in `$filter`, no binding and a non-app page read nothing, 0 rows, 403 read as no row, failure fails closed). That test covers `object` and the `filter` rule keys. `view` and `sort` are cited at their readers only. ## Acceptance notes - **The schema's prose is stale too, left alone here.** `packages/spec/src/ui/page.zod.ts` still says "Planned in the renderer: until the console mounts it, `record` stays unbound on an app page" in the `record` key's `.describe()` and in the `PageRecordBindingSchema` TSDoc. The same file's `visibleWhen` describe already states the binding is read. The ruling for this card is no schema change, so this is a follow-up for the spec owner (a `.describe()` edit regenerates `check:docs` artifacts). The ledger row wins over a describe marker, so the gate's verdict is unaffected. - The README `page` Notes cell ("live, plus the one dead tombstone below") is now exactly true for the `record` subtree and was not edited. The 2 rows still `planned` in `page` are outside `record`. - Gate sweep: the `--commands` families derived on the committed head, exit codes captured to disk, `--ran` reconciled. Readings are in Verification. ## Verification All readings on head `7a3032f3b`, the final commit; exit codes captured before any pipe. <details><summary>check:liveness — exit 0</summary> `pnpm --filter @objectstack/spec check:liveness` before regenerating the counts: exit 1, the only failure being the stale `page` shard (`35 | 0 | 0 | 1 | 9` became `42 | 0 | 0 | 1 | 2`). `gen:liveness-counts` rewrote 1 shard (`page.md`). Re-run: exit 0, with every `path#symbol` anchor resolving, every cited file naming its key, and `state-counts/ is current`. </details> <details><summary>Liveness vitest — 12/12 files, 359/359 tests</summary> `bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/spec gen:migration-registry && pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness'` printed `Test Files 12 passed (12)`, `Tests 359 passed (359)` and `VERDICT command-exit 0`. </details> <details><summary>Derived gate families — 57 derived, 57 run, 0 NOT-MEASURED (derived zero)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 57 commands from the change set (3 paths, 105 changed lines). Every one exited 0, and `--ran` with `command :: exit N` records printed `57 derived, 57 run, 0 NOT-MEASURED, 0 UNRUN` (a DERIVED zero: all 57 carry an exit code, none of them 3). Three needed a second run. `check:generated`: in the first sweep the `check:test-typecheck` lane's tsc died with no diagnostics, because the sweep ran without `NODE_OPTIONS`. Run alone with `--max-old-space-size=4096`, that gate held (52 files / 240 errors / 131 pinned signatures, unchanged), and `check:generated` re-ran with all 14 artifacts up to date. `check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET). After `turbo run build` (120/120 tasks) both exited 0. Local-only families: `check:dev-prereqs` exit 0, `check:skill-frame-freshness` exit 0. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. (None added.) - [x] 3. No formatter ran; `pnpm lint` passes. (No lintable source changed; CI runs it.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed (`state-counts/page.md`; `check:generated` 14/14). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (except the 维护者速读 section, which is written in Chinese by design). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTqqeHSwZYJmApbDCa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aad1603 commit dc51811

3 files changed

Lines changed: 64 additions & 41 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the liveness ledger grades an app page's `record` binding `live`, citing the console reader for each of its keys
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `liveness/page.json` graded `record` and every key under it `planned`, with the note that nothing read a page-level `record` yet. The console now reads it: an app page's binding is read once per page open as the first row under `sort`, and every component's `visibleWhen` sees that row as `record`. `record` and every key under it (`object`, `view`, `filter` with `field` / `operator` / `value`, `sort` with `field` / `order`) are now `live`, each citing the code that reads it.
11+
- Nothing to do on upgrade: only the shipped `liveness/page.json` grades and evidence text change. No key, schema shape or export changes.

‎packages/spec/liveness/page.json‎

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -43,66 +43,78 @@
4343
"note": "record-page binding + interfaceConfig source object — objectui PageView / InterfaceListPage."
4444
},
4545
"record": {
46-
"status": "planned",
47-
"verifiedAt": "2026-10-18",
48-
"evidence": "packages/spec/src/ui/page.zod.ts#checkPageRecordBindingPlacement (the parse refuses `record` on every page that is not `type: 'app'`, so the key reaches only the one page type that opens no row of its own); packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema (the strict one-row query shape: `object`, `view`, `filter`, `sort`, and no `limit`)",
49-
"note": "PLANNED, the renderer half pending — consumer card objectstack-ai/objectui#12931: measured on objectui `main` @79cb8c03eb, nothing reads a page-level `record` yet. `SchemaRenderer`'s `configEvaluationScope` binds `record` from `RecordContext` (`boundRecord = recordContext?.data`), and on an `app` page the only provider of that context is a record block's own one-row binding (`plugin-detail/src/renderers/recordBinding.tsx`), which wraps that block's BODY — so neither the block's own `visibleWhen` nor a sibling's sees the row. The renderer half mounts the page binding with that same one-record semantics at the page root, so every component predicate reads it as `record`. Until then `record` is unbound on an `app` page exactly as before the key existed: a predicate reading it faults, and a faulting `visibleWhen` renders the component. Flip to `live` when the console mounts it, citing the read point.",
46+
"status": "live",
47+
"verifiedAt": "2026-11-05",
48+
"evidenceScope": "in-repo",
49+
"evidence": "ui/packages/app-shell/src/views/PageView.tsx#appPageRecordBinding (takes `page.record` on a `type: 'app'` page only, and only a binding that names its object, through isElementDataSourceConfig); ui/packages/app-shell/src/views/PageView.tsx#PageView mounts `<AppPageRecord binding={appPageRecordBinding(page)}>` around the page body; ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordRead reads the FIRST row of the bound set (`$top: 1`) and hands it to ui/packages/app-shell/src/providers/ExpressionProvider.tsx#PageRecordScope, which publishes it as `record` on the predicate scope; ui/packages/react/src/SchemaRenderer.tsx#buildNodeEvaluationScope spreads that scope into every component's evaluation, so each `visibleWhen` reads it as `record` (a record block's own bound row still wins for its own subtree)",
50+
"producer": "ui/packages/app-shell/src/views/PageView.tsx#PageView — the read runs on the page's own adapter (`useAdapter()`), passed to AppPageRecord beside the binding; there is no second input: the author's binding is the query",
51+
"note": "RE-GRADED planned → live 2026-11-05 (#30094) by reading the renderer in this checkout: the renderer half the previous note waited on (objectui#12931, measured then on objectui `main` @79cb8c03eb) has landed in `ui/`. The page-level scope the old note said was missing is PageRecordScope, mounted by the page host, not RecordContext: the page row is `record` to every component predicate on the page, and a record block that binds its own row still provides that row to its own subtree. The three states a predicate sees: pending (body not drawn), no row or a refused read (`record` is null — absent and not-yours read the same), any other failure (body not drawn; the record page's failed-read state with Retry). Pinned by ui/packages/app-shell/src/views/__tests__/PageView.appPageRecord-12931.test.tsx (tier and count predicates, the one read per open with the org token resolved, no binding and non-app page read nothing, 0 rows, 403, failed closed). The parse half is unchanged: packages/spec/src/ui/page.zod.ts#checkPageRecordBindingPlacement refuses `record` on every page that is not `type: 'app'`, and packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema is the strict one-row shape (no `limit`). The schema's own describe() and TSDoc still say 'Planned in the renderer' — stale prose left for a spec change, not this ledger regrade.",
5052
"children": {
5153
"object": {
52-
"status": "planned",
53-
"verifiedAt": "2026-10-18",
54-
"evidence": "packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema (declares `object` on the strict one-row query shape)",
55-
"note": "The object the page row is read from. Planned with its container: no renderer reads a page-level `record` yet."
54+
"status": "live",
55+
"verifiedAt": "2026-11-05",
56+
"evidenceScope": "in-repo",
57+
"evidence": "ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordGate lands the binding's object on `objectName` (ui/packages/core/src/data-scope/element-data-source.ts#composeElementDataSource: `object: config.object`) and ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordRead queries it: `dataSource.find(object, params)`",
58+
"note": "The object the page row is read from. Required to bind at all: #appPageRecordBinding treats a binding with no object as no binding, and the page renders with `record` unbound."
5659
},
5760
"view": {
58-
"status": "planned",
59-
"verifiedAt": "2026-10-18",
60-
"evidence": "packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema (declares `view` on the strict one-row query shape)",
61-
"note": "The named view whose filter AND-combines with `filter` — the element `dataSource` semantics. Planned with its container."
61+
"status": "live",
62+
"verifiedAt": "2026-11-05",
63+
"evidenceScope": "in-repo",
64+
"evidence": "ui/packages/react/src/hooks/useElementDataSource.ts#useElementDataSource (reads `config.view`, lists the object's saved views through the adapter and resolves the name with resolveSavedView; an unresolved name answers `missing`, which ElementDataSourceGate draws as its error panel in place of the page body, never a wider read); ui/packages/core/src/data-scope/element-data-source.ts#composeElementDataSource (the named view's filter AND-combined with the binding's; the view's sort when the binding declares none)",
65+
"producer": "ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordGate passes the page's adapter to ElementDataSourceGate as its `dataSource`, which is what lists the saved views",
66+
"note": "The named view whose filter AND-combines with `filter` — the element `dataSource` semantics, through the same gate a block's binding uses."
6267
},
6368
"filter": {
64-
"status": "planned",
65-
"verifiedAt": "2026-10-18",
66-
"evidence": "packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema (declares `filter` on the strict one-row query shape)",
67-
"note": "ViewFilterRule array form, the one orthography every `filter` door shares. Planned with its container.",
69+
"status": "live",
70+
"verifiedAt": "2026-11-05",
71+
"evidenceScope": "in-repo",
72+
"evidence": "ui/packages/react/src/element-data-source/ElementDataSourceGate.tsx#useElementDataSourceSchema (the page's mapping sets `filter: true`: the composed filter is lowered by toFilterNodeSafely and lands on the bound schema); ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordRead resolves its placeholder tokens (useResolvedFilter over useFilterScope) and sends it as `$filter`",
73+
"note": "ViewFilterRule array form, the one orthography every `filter` door shares. A filter the lowering refuses withholds the read and the page body (the malformed-filter panel), never an unfiltered read.",
6874
"children": {
6975
"field": {
70-
"status": "planned",
71-
"verifiedAt": "2026-10-18",
72-
"evidence": "packages/spec/src/ui/view.zod.ts#ViewFilterRuleSchema (declares `field` on the rule the binding's `filter` array holds)",
73-
"note": "`field` of one `filter` item on the page record binding. Planned with its container: no renderer reads a page-level `record` yet."
76+
"status": "live",
77+
"verifiedAt": "2026-11-05",
78+
"evidenceScope": "in-repo",
79+
"evidence": "ui/packages/core/src/utils/filter-converter.ts#viewFilterRuleToNode (each rule of the landed `filter` array lowers to the comparison `[rule.field, operator, rule.value]`, the operator through normalizeFilterOperator; a rule it cannot lower is refused as a FilterOperatorError, which ui/packages/react/src/element-data-source/ElementDataSourceGate.tsx#ElementDataSourceGate draws as its malformed-filter panel in place of the page body)",
80+
"note": "`field` of one `filter` item on the page record binding: the left operand of the lowered comparison."
7481
},
7582
"operator": {
76-
"status": "planned",
77-
"verifiedAt": "2026-10-18",
78-
"evidence": "packages/spec/src/ui/view.zod.ts#ViewFilterRuleSchema (declares `operator` on the rule the binding's `filter` array holds)",
79-
"note": "`operator` of one `filter` item on the page record binding. Planned with its container: no renderer reads a page-level `record` yet."
83+
"status": "live",
84+
"verifiedAt": "2026-11-05",
85+
"evidenceScope": "in-repo",
86+
"evidence": "ui/packages/core/src/utils/filter-converter.ts#viewFilterRuleToNode (each rule of the landed `filter` array lowers to the comparison `[rule.field, operator, rule.value]`, the operator through normalizeFilterOperator; a rule it cannot lower is refused as a FilterOperatorError, which ui/packages/react/src/element-data-source/ElementDataSourceGate.tsx#ElementDataSourceGate draws as its malformed-filter panel in place of the page body)",
87+
"note": "`operator` of one `filter` item on the page record binding: normalised, then the comparison operator."
8088
},
8189
"value": {
82-
"status": "planned",
83-
"verifiedAt": "2026-10-18",
84-
"evidence": "packages/spec/src/ui/view.zod.ts#ViewFilterRuleSchema (declares `value` on the rule the binding's `filter` array holds)",
85-
"note": "`value` of one `filter` item on the page record binding. Planned with its container: no renderer reads a page-level `record` yet."
90+
"status": "live",
91+
"verifiedAt": "2026-11-05",
92+
"evidenceScope": "in-repo",
93+
"evidence": "ui/packages/core/src/utils/filter-converter.ts#viewFilterRuleToNode (each rule of the landed `filter` array lowers to the comparison `[rule.field, operator, rule.value]`, the operator through normalizeFilterOperator; a rule it cannot lower is refused as a FilterOperatorError, which ui/packages/react/src/element-data-source/ElementDataSourceGate.tsx#ElementDataSourceGate draws as its malformed-filter panel in place of the page body)",
94+
"note": "`value` of one `filter` item on the page record binding: the right operand; placeholder tokens such as `{current_org_id}` resolve before the read (#AppPageRecordRead)."
8695
}
8796
}
8897
},
8998
"sort": {
90-
"status": "planned",
91-
"verifiedAt": "2026-10-18",
92-
"evidence": "packages/spec/src/ui/page.zod.ts#PageRecordBindingSchema (declares `sort` on the strict one-row query shape)",
93-
"note": "Decides which row is FIRST — the page row. Planned with its container.",
99+
"status": "live",
100+
"verifiedAt": "2026-11-05",
101+
"evidenceScope": "in-repo",
102+
"evidence": "ui/packages/app-shell/src/views/PageView.tsx#AppPageRecordRead sends the landed sort as `params.$orderby` beside `$top: 1`, so the first row under it is the page row; ui/packages/core/src/data-scope/element-data-source.ts#composeElementDataSource (the binding's sort overrides the named view's)",
103+
"note": "Decides which row is FIRST — the page row.",
94104
"children": {
95105
"field": {
96-
"status": "planned",
97-
"verifiedAt": "2026-10-18",
98-
"evidence": "packages/spec/src/shared/enums.zod.ts#SortItemSchema (declares `field` on the item the binding's `sort` array holds)",
99-
"note": "`field` of one `sort` item on the page record binding. Planned with its container: no renderer reads a page-level `record` yet."
106+
"status": "live",
107+
"verifiedAt": "2026-11-05",
108+
"evidenceScope": "in-repo",
109+
"evidence": "ui/packages/data-objectstack/src/index.ts#serializeOrderBy (each `{ field, order }` item of `$orderby` becomes `field` or `-field` on the wire `sort` parameter of the adapter's find)",
110+
"note": "`field` of one `sort` item on the page record binding: the column the read orders by, through the shipping ObjectStack adapter."
100111
},
101112
"order": {
102-
"status": "planned",
103-
"verifiedAt": "2026-10-18",
104-
"evidence": "packages/spec/src/shared/enums.zod.ts#SortItemSchema (declares `order` on the item the binding's `sort` array holds)",
105-
"note": "`order` of one `sort` item on the page record binding. Planned with its container: no renderer reads a page-level `record` yet."
113+
"status": "live",
114+
"verifiedAt": "2026-11-05",
115+
"evidenceScope": "in-repo",
116+
"evidence": "ui/packages/data-objectstack/src/index.ts#serializeOrderBy (`order` compared case-insensitively to `desc` decides `-field` against `field`)",
117+
"note": "`order` of one `sort` item on the page record binding: `desc` sorts descending; anything else ascending."
106118
}
107119
}
108120
}

‎packages/spec/liveness/state-counts/page.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.
1212

1313
| Type | live | exp | elsewhere | dead | planned | classified |
1414
|---|---|---|---|---|---|---|
15-
| `page` | 35 | 0 | 0 | 1 | 9 | 45 |
15+
| `page` | 42 | 0 | 0 | 1 | 2 | 45 |

0 commit comments

Comments
 (0)