Skip to content

Commit 01fafde

Browse files
docs(spec): the page element's api data source is described as rendered, and visibleWhen names where data is mounted (#24460)
Fixes #24300 Clause-②: no The page element's `api` data source (`ElementApiDataSourceSchema`) is rendered by the console at its pin, and `@objectstack/spec` now says so. Before this change, its describe opened with `[PLANNED — not yet rendered]`, and `PageComponentSchema.visibleWhen` said that no renderer reads the binding. Both told an author, and an AI author first, not to use a binding the console renders. The change is describe and TSDoc text only: no accept set moves, and every refusal keeps its issue `code` and `path`. ## What the console renders, measured at the pin The pin is `.objectui-sha` `0707b4ae37` on `main`. It carries objectui `c4dd7078ed`: `git merge-base --is-ancestor c4dd707 0707b4a` exits 0 in `../objectui`, an exit 0 that proves itself. Every read below is `git show 0707b4a:PATH`, not objectui `main`. - `packages/react/src/SchemaRenderer.tsx` calls `useElementApiDataSource(schema)` for every node (`:1157`). Once the read is `ready`, the node's evaluation scope is `{ ...ambientScope, data: apiData }` (`:1231`), and this scope evaluates its expression holes and its `visibleWhen` (`:1982`). The same value goes to the subtree through `PredicateScopeProvider` (`:1161`, `:2524`). While the read is `loading` the node draws a loading panel, and when the binding is `refused` or hits an `error` it draws an error panel. Both are drawn before visibility, so `visibleWhen` is decided only once the value is in (`:1285`, `:2165`). - `packages/react/src/element-data-source/useElementApiDataSource.ts` parses the binding with `ElementApiDataSourceSchema` first (`refused` carries the schema's own message, and nothing is read). It then sends one `GET` through `SchemaRendererContext.apiFetch`, the console's signed-in fetch, and selects `path`. - Page regions render each node through `SchemaRenderer`: `packages/components/src/renderers/layout/page.tsx` passes the region's components to `PageBlockGrid` (`page-block-grid.tsx:56`), and `toRenderableSchema` is identity for an object. - The generic `hero`, `stat`, `usage-bar`, `checklist` and `tile-grid` blocks are registered in `packages/app-shell/src/console/blocks/productBlocks.tsx` (`:360`–`:408`). - The mounted-roots sentence holds at the pin: `buildExpressionScope` (`packages/app-shell/src/providers/ExpressionProvider.tsx:181`) returns `current_user`, `user`, `ctx.user`, `os.user` and `features`, and `configEvaluationScope` (`SchemaRenderer.tsx:668`) adds `record` (only when there is a row) and `page`. No root is claimed that is not mounted. - The `page:tabs` item evaluator (`packages/components/src/renderers/layout/containers.tsx:645`) writes `data: recordData` after spreading the ambient scope. So on that surface `data` stays the row, even inside an element an `api` data source binds. - The framework's own console is built from that pin: `scripts/build-console.sh` builds `@object-ui/console` "at the SHA pinned in .objectui-sha", and `scripts/sdui-manifest.record.json` records `objectuiSha` `0707b4ae37…`. So no "which console" caveat is needed. ## What changed - `packages/spec/src/ui/page.zod.ts` - `ElementApiDataSourceSchema`: its describe drops the marker and says what the console renders. Its docblock section "Planned, not yet rendered" becomes "Rendered by the console, on a page element", with the read points cited by the `objectui:` realm marker. - `PageComponentSchema.visibleWhen`, describe and TSDoc: the renderer mounts no `data` root on an element that no `api` data source binds, where a `data.*` read still faults and the gate never gates. On an element one binds, `data` is the bound value, there and on every element nested inside it. The TSDoc's measured-scope sentence names that one added root. The section heading is now "`data` — only where an `api` data source binds it". - `PageComponentSchema.dataSource` TSDoc: "(planned in the renderer …)" now reads "rendered by the console". - Rider `6259026494`: the binding's refusal, unknown-key surface and sibling describe sentences no longer name a page element. For example, "`read.url` of an `api` data source must be …", the surface "this `api` data source", and the `write`, `body`, `method` and `path` messages. The same change also made door-neutral the `object` guidance, which told an author to "keep `object`": the navigation entry that PR #24427 adds admits no object binding, so that advice is now scoped to "where the `dataSource` also takes the object binding (a page element's does)". Each refusal keeps its issue `code` and `path`. The objectui test that reads the refusal text asserts only `same-origin` (`useElementApiDataSource-12197.test.tsx:141`), and the new wording keeps it. - `packages/spec/src/ui/component.zod.ts`: only the `page:tabs` item `visibleWhen` TSDoc face that the pin reads. It now says that the component-node evaluator mounts no `data` root, except on an element an `api` data source binds, and that on this surface `data` stays the row. Patch round 2 (claim amendment `6259996449`): the same key's `.describe()` parenthetical now agrees with it: "(the component-node evaluator mounts no `data` root save on an element an `api` data source binds, where `data` is that binding's value)". - `packages/spec/src/ui/expression-scope-data-root.pin.test.ts`: re-trued, not deleted. Rule (2) now admits a `data` sentence only if it says "NO `data`", ties `data` to `provider: 'api'`, or is about `page:tabs`. A sentence that mounts `data` without the binding is still refused. The "two texts agree" block becomes "three texts agree": the binding's describe and both `visibleWhen` faces carry no planned reading (six matchers), and both `visibleWhen` faces name the binding as the one `data` carrier. Two DARK controls are added: one shows that an overclaiming sentence fails rule (2), and one shows that the three replaced planned texts are caught by the planned matcher. The pin has 16 cases (13 before). - `packages/spec/liveness/page.json`: the `_note` sentence "PLANNED RECORD (#23016 …)" becomes a "LIVE RECORD", measured at the pin, with the `objectui:` read points. Only the `_note` changes; the `regions` row that PR #24442 edits is untouched. - `content/docs/references/ui/page.mdx` and `content/docs/references/ui/component.mdx`: regenerated by `gen:docs`. `content/docs/references/ui/app.mdx`: regenerated in patch round 3, because PR #24427's navigation entry embeds `ElementApiDataSourceSchema` by reference (18 embeddings × the `provider`, `read` and `path` rows). - `content/docs/ui/pages.mdx` (hand-written; claim amendment `6260050788`, declared to `domain:devx` on #20163): the "Not rendered yet" bullet of "The API binding's rules" now says the console renders the binding and names the blocks that read it. No other edit to the page. - `.changeset/24300-api-binding-rendered.md`: `@objectstack/spec` `patch`. Patch round 3 added one bullet for the `page:tabs` item `visibleWhen` describe parenthetical. Not touched, by the claim's fence: `packages/spec/src/ui/app.zod.ts` (its navigation `dataSource` stays planned), the other `[PLANNED]` markers in `component.zod.ts`, and the binding's accept set. `view.zod.ts:170` and the `view-data-api-provider-retired` migration texts call this binding "the governed door" read with the signed-in fetch. They never call it planned, so they stay as they are (dispatch hypothesis H3, refuted for them). ## Verification (at head `8951d79063`, unless a line names another sha) - Patch round 3, head `f0ce6bc1e3`: `origin/main` `ff4840756d` (carrying PR #24427 and PR #24442) was merged with `bash scripts/pm/os-regen-merge.sh`, with no rebase or force-push. `component.mdx` and `app.mdx` were regenerated with `check:generated --fix`. The hop proof compares the `+`/`-` lines of `git diff -U0 9112bcd 0ea4d85` and `git diff -U0 ff48407 f0ce6bc`, both outside `content/docs/references` and `.changeset`. It is empty, so the hop moves only generated pages and the changeset. At `f0ce6bc1e3`, `vitest run src/ui/` gave `Test Files 132 passed (132)`, `Tests 5200 passed (5200)`, and `check:generated` and `check:docs` exited 0. - Reverse verification at `1dad7bbe4b` (the pin file is byte-identical at the final head), one shot, expected direction red. `page.zod.ts` was restored to the merge base `9112bcd887` (`hash-object` matched the base blob `448b9617…`; the marker count was 1), and the re-trued pin ran against it: `Tests 4 failed | 12 passed (16)`. The four failing cases were the docblock's binding sentence, the describe's binding sentence, the binding's "renders" sentence and the no-planned-reading sweep. The file was then restored with `git checkout HEAD --`: `hash-object` matched the HEAD blob `646f9a43…`, and `git diff HEAD` was empty. The observed direction was red. - `pnpm --filter @objectstack/spec test` at `1dad7bbe4b`: `Test Files 697 passed (697)`, `Tests 21271 passed | 1 todo`. The final commit `8951d79063` changes one TSDoc paragraph. At that head, every test that reads `page.zod.ts` source was re-run: 6 files / 362 tests passed, and `scripts/file-description.test.ts` plus `scripts/schema-index.test.ts` gave 2 files / 130 tests passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0 (`tsc --noEmit`, `check:scripts-typecheck`, `check:test-typecheck` OK). - `pnpm --filter @objectstack/spec check:generated`: the only stale artifact was `content/docs/references/**`, regenerated with `gen:docs`. `check:authorable-surface`, `check:api-surface`, `check:liveness` and `check:strictness-ledger` were all green. - `node scripts/pm/dispatch-gates.mjs --commands` at `8951d79063` derived 111 commands, and all were run locally. Reconciliation result: `--ran`: "111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED". 110 exited 0. Six had first refused with `PREREQUISITE NOT MET` and passed once `lint`, `formula`, `client-react` and `objectql` were built. One exited 1, `pnpm check:platform-checklist`, and that red predates this branch: `docs/qa/platform-checklist/areas/access-security.json` cites `packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired`, which is absent there on the merge base `9112bcd887` and on `origin/main` `7e1ce9d945`. The symbol now lives in `packages/plugins/plugin-auth`, since `b2e78701f7`. This diff touches no file under `packages/core` or `docs/qa`. `main`'s `ec951e29e4` (#24463) re-pointed that anchor, and the check reads `OK — 16 areas, 327 items` at `f0ce6bc1e3`. - Verdict lines: `check:docs` "227 generated files in sync"; `check:api-surface` "public API surface + factory signatures unchanged"; `check:doc-authoring` "19075 customer-facing string(s) … clean"; `check:issue-citations` "every citation this change adds resolves"; `check:nul-bytes` "no raw ASCII control bytes"; `check:empty-changeset` and `check:adr-0087-registration` green. - Local-only `pnpm check:dev-prereqs`: "@objectstack/spec … built from the sources on disk". ## Acceptance notes - ~~The `page:tabs` item `visibleWhen` describe was left for the seat.~~ Done in patch round 2: the seat widened the claim's surface (`6259996449`), and the describe now agrees with its TSDoc face. - `carrier: none` · The committed SDUI manifest (`sdui.manifest.json`, `objectuiSha` `0707b4ae37`, 107 components) lists none of the five product blocks the pin registers (`hero`, `stat`, `usage-bar`, `checklist`, `tile-grid`). The JSX page gate validates components against that manifest. This is a read-only inference: no `os validate` run measured it, so it is noted, not filed. - `carrier: none` · The docblock's "## The ruling" section still justifies `method: 'GET'` through `@object-ui/core`'s `ApiDataSource`, which the shipped reader does not use (its own `GET` is cited in the new section). The sentence stays true about that adapter. It sits outside the fence and is left as it was. - `main` was merged in once, at `ff4840756d` (patch round 3). PR #24427 reuses `ElementApiDataSourceSchema` on the navigation entry, so this branch's describe change also moves `content/docs/references/ui/app.mdx`, which the queue's `check:docs` would otherwise find stale. --- _Generated by [Claude Code](https://claude.ai/code/session_01WnW7HvfnfHbLg1fpkHX3Mu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fb9c22a commit 01fafde

9 files changed

Lines changed: 258 additions & 145 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
A page element's `api` data source is described as rendered, and `visibleWhen` says where `data` is mounted
6+
7+
Clause-②: no (describe and TSDoc text only: `ElementApiDataSourceSchema` and `PageComponentSchema` accept exactly what they accepted before, and every refusal keeps its issue `code` and `path`)
8+
9+
- `ElementApiDataSourceSchema`'s describe dropped its `[PLANNED — not yet rendered]` marker. It now says what the console renders: the console reads the binding on every page element and binds the value `path` selects as `data`, on that element and on every element nested inside it. The element's `visibleWhen` is decided once the read answers. While the read is in flight, or when the binding is refused or the read fails, the element draws a loading or error panel instead.
10+
- `PageComponentSchema.visibleWhen` (its describe and its TSDoc) no longer says that no renderer reads the binding. The renderer mounts no `data` root on an element that no `api` data source binds, where a `data.*` read still faults and the gate never gates. On an element that one binds, `data` is the bound value.
11+
- The `page:tabs` item `visibleWhen` describe's parenthetical now reads that the component-node evaluator mounts no `data` root "save on an element an `api` data source binds", where `data` is that binding's value. That surface's own evaluator still binds `data` to the record row.
12+
- The binding's refusal and unknown-key messages no longer name a page element. For example, "`read.url` of a page element's `api` data source must be …" now reads "`read.url` of an `api` data source must be …", and the unknown-key surface is "this `api` data source". The `object` guidance names the object binding only where the `dataSource` takes one. Each message keeps its issue `code` and `path`.

‎content/docs/references/ui/app.mdx‎

Lines changed: 54 additions & 54 deletions
Large diffs are not rendered by default.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,7 @@ View filter rule
15431543
| :--- | :--- | :--- | :--- |
15441544
| **label** | `string \| Record<string, string>` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
15451545
| **icon** | `string` | optional | Lucide icon name rendered in the tab trigger, left of the label. Read on this component — the renderer draws it via `LazyIcon`; contrast the item `key` beside it, which no read point takes and which the alias table answers with `value`. |
1546-
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user`), `page.<var>`. ⚠️ NOT the same environment as page-component `visibleWhen`: this surface's own evaluator binds `data` to the record ROW (the component-node evaluator mounts no `data` root) and also spreads the row's bare fields — renderer behaviour, NOT contract-guaranteed. ADR-0089 canonical name — `visible`/`showWhen`/`visibility`/`visibleOn` are all rejected here (not folded in), each with a pointer at this key. |
1546+
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE; the renderer falls back to the first visible tab when the active one is hidden. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user`), `page.<var>`. ⚠️ NOT the same environment as page-component `visibleWhen`: this surface's own evaluator binds `data` to the record ROW (the component-node evaluator mounts no `data` root save on an element an `api` data source binds, where `data` is that binding's value) and also spreads the row's bare fields — renderer behaviour, NOT contract-guaranteed. ADR-0089 canonical name — `visible`/`showWhen`/`visibility`/`visibleOn` are all rejected here (not folded in), each with a pointer at this key. |
15471547
| **value** | `string` | optional | Stable `?tab=` URL token for this tab (default: index-derived `tab-<i>`, which is not durable across item-list changes) |
15481548
| **count** | `integer` | optional | Badge count shown next to the tab label (default: derived from `record:related_list` descendants) |
15491549
| **children** | `any[]` | ✅ | Child components |

‎content/docs/references/ui/page.mdx‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,22 @@ const result = ElementApiDataSourceSchema.parse(data);
2424

2525
## ElementApiDataSource
2626

27-
[PLANNED — not yet rendered] API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.
27+
API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console renders it on every page element: the value is `data` on that element and on every element nested inside it, the element's `visibleWhen` is decided once the read answers, and while the read is in flight, or when the binding is refused or the read fails, the element draws a loading or error panel instead. The console's generic `stat`, `usage-bar` and `checklist` blocks are built to take it.
2828

2929
### Properties
3030

3131
| Property | Type | Required | Description |
3232
| :--- | :--- | :--- | :--- |
33-
| **provider** | `'api'` | ✅ | Binds the element to one same-origin API endpoint instead of an object |
34-
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the element binds |
35-
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the element binds, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the element binds the whole response |
33+
| **provider** | `'api'` | ✅ | Binds one same-origin API endpoint — the discriminator of an `api` data source |
34+
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the binding reads |
35+
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the binding takes, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the binding takes the whole response |
3636

3737
### Nested Shape: `ElementApiDataSource.read`
3838

3939
| Property | Type | Required | Description |
4040
| :--- | :--- | :--- | :--- |
41-
| **url** | `string` | ✅ | Same-origin API path the element reads — it opens with a single `/`, e.g. `/api/v1/cloud/home`. An absolute URL of any scheme, a protocol-relative `//host`, a backslash read as a slash, a space or control character, and a relative path are refused at parse. No credential is written here: the console reads the endpoint with its own signed-in fetch. |
42-
| **method** | `'GET'` | optional | HTTP method of the read — `GET`, the only method a page element's read is sent with |
41+
| **url** | `string` | ✅ | Same-origin API path the binding reads — it opens with a single `/`, e.g. `/api/v1/cloud/home`. An absolute URL of any scheme, a protocol-relative `//host`, a backslash read as a slash, a space or control character, and a relative path are refused at parse. No credential is written here: the console reads the endpoint with its own signed-in fetch. |
42+
| **method** | `'GET'` | optional | HTTP method of the read — `GET`, the only method the read of an `api` data source is sent with |
4343

4444

4545
---
@@ -311,7 +311,7 @@ View filter rule
311311
| **style** | `Record<string, string>` | optional | Inline styles or utility classes |
312312
| **className** | `string` | optional | CSS class names |
313313
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
314-
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root here: a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record` — the record a record page opens, or the row an app page's `record` binding reads (none without one; a block's own `dataSource` binds no `record` for its siblings). A count or a plan is a field on that row (a `summary` roll-up: `record.environment_count > 1`; `record.plan != 'free'`), never `features.*`, which are the deployment's public auth flags, the same for every organisation. `data` is declared only for an element whose `dataSource` is the PLANNED API binding (`provider: 'api'`), which reads its bound value as `data` once that binding's renderer ships. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
314+
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root on an element that no `api` data source binds: there a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record` — the record a record page opens, or the row an app page's `record` binding reads (none without one; a block's own `dataSource` binds no `record` for its siblings). A count or a plan is a field on that row (a `summary` roll-up: `record.environment_count > 1`; `record.plan != 'free'`), never `features.*`, which are the deployment's public auth flags, the same for every organisation. `data` is mounted only on an element whose `dataSource` is an `api` data source (`provider: 'api'`): there, and on every element nested inside it, `data` is the value the binding selects, and the element's `visibleWhen` is decided once the read answers. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
315315
| **visibility** | `never` | optional | [REMOVED] `page.components[].visibility` was removed in @objectstack/spec 18 (ADR-0089 D2) — it was the page component's deprecated spelling of the conditional-visibility predicate, which the parse folded into `visibleWhen` until this release. Use `visibleWhen`. Rename the key; the value (a CEL predicate) is unchanged. Where a component writes both, `visibleWhen` is the one that was in effect: delete `visibility`. |
316316
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
317317
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -338,13 +338,13 @@ View filter rule
338338

339339
### Nested Shape: `PageComponent.dataSource[option 2]`
340340

341-
[PLANNED — not yet rendered] API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.
341+
API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console renders it on every page element: the value is `data` on that element and on every element nested inside it, the element's `visibleWhen` is decided once the read answers, and while the read is in flight, or when the binding is refused or the read fails, the element draws a loading or error panel instead. The console's generic `stat`, `usage-bar` and `checklist` blocks are built to take it.
342342

343343
| Property | Type | Required | Description |
344344
| :--- | :--- | :--- | :--- |
345-
| **provider** | `'api'` | ✅ | Binds the element to one same-origin API endpoint instead of an object |
346-
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the element binds |
347-
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the element binds, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the element binds the whole response |
345+
| **provider** | `'api'` | ✅ | Binds one same-origin API endpoint — the discriminator of an `api` data source |
346+
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the binding reads |
347+
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the binding takes, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the binding takes the whole response |
348348

349349
### Nested Shape: `PageComponent.aria`
350350

@@ -483,7 +483,7 @@ Sort field and direction pair
483483
| **style** | `Record<string, string>` | optional | Inline styles or utility classes |
484484
| **className** | `string` | optional | CSS class names |
485485
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
486-
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root here: a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record` — the record a record page opens, or the row an app page's `record` binding reads (none without one; a block's own `dataSource` binds no `record` for its siblings). A count or a plan is a field on that row (a `summary` roll-up: `record.environment_count > 1`; `record.plan != 'free'`), never `features.*`, which are the deployment's public auth flags, the same for every organisation. `data` is declared only for an element whose `dataSource` is the PLANNED API binding (`provider: 'api'`), which reads its bound value as `data` once that binding's renderer ships. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
486+
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root on an element that no `api` data source binds: there a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record` — the record a record page opens, or the row an app page's `record` binding reads (none without one; a block's own `dataSource` binds no `record` for its siblings). A count or a plan is a field on that row (a `summary` roll-up: `record.environment_count > 1`; `record.plan != 'free'`), never `features.*`, which are the deployment's public auth flags, the same for every organisation. `data` is mounted only on an element whose `dataSource` is an `api` data source (`provider: 'api'`): there, and on every element nested inside it, `data` is the value the binding selects, and the element's `visibleWhen` is decided once the read answers. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
487487
| **visibility** | `never` | optional | [REMOVED] `page.components[].visibility` was removed in @objectstack/spec 18 (ADR-0089 D2) — it was the page component's deprecated spelling of the conditional-visibility predicate, which the parse folded into `visibleWhen` until this release. Use `visibleWhen`. Rename the key; the value (a CEL predicate) is unchanged. Where a component writes both, `visibleWhen` is the one that was in effect: delete `visibility`. |
488488
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
489489
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

0 commit comments

Comments
 (0)