Repository navigation
Commit dc51811
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
- .changeset
- packages/spec/liveness
- state-counts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
50 | 52 | | |
51 | 53 | | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
56 | 59 | | |
57 | 60 | | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
62 | 67 | | |
63 | 68 | | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
68 | 74 | | |
69 | 75 | | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
74 | 81 | | |
75 | 82 | | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
80 | 88 | | |
81 | 89 | | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
86 | 95 | | |
87 | 96 | | |
88 | 97 | | |
89 | 98 | | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
94 | 104 | | |
95 | 105 | | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
100 | 111 | | |
101 | 112 | | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
106 | 118 | | |
107 | 119 | | |
108 | 120 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
0 commit comments