Skip to content

Commit c13ef04

Browse files
fix(spec)!: an action's newTabUrl is a backend path starting with /; dashboard read-refusal and citation prose (#27252)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBAaQW5g4DKEMyfrDWjAqdoh)_ Fixes #26924 Fixes #27068 Fixes #26973 Ruling-ref: none (seat choice A on #26924, per the card's options and triage 6454574648) Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: updated here — `skills/objectstack-ui/rules/actions.md` (the one `skills/**` sentence that taught `newTabUrl`, "For external deep-links … a direct URL template", now teaches a backend path and points an external page at `openIn`); no eval under `skills/objectstack-ui/evals/` exercises `newTabUrl` (0 hits), so an eval case is follow-up owed if the seat wants one. `skills/**` carries no north-star citation (0 hits). **Tier H: this PR edits `skills/**`.** ## 维护者速读 - 改了什么:动作的 `newTabUrl` 只能写本后端上以 `/` 开头的路径,绝对 URL 在解析处拒收,提示「写后端路径,外部页面改用 `type: 'url'` + `openIn: 'new-tab'`」;JSDoc、describe、表单帮助(四种语言)和技能文档一处句子统一说法。另两项只改文字:仪表盘磁贴说明写明「非设计模式下,服务器以读权限拒绝(403)的数据集磁贴对该用户不显示,`requiresObject` 仍只是安装门」;五处指向不存在的 `north-star.mdx` 章节的引用改指真实出处或删除。 - 为什么改:控制台把 `newTabUrl` 当路径拼在后端地址后面,写成 `https://…` 会打开坏标签页,而规范之前不拒收(#26924 选 A);#27065 已上线磁贴隐藏行为但规范未说明(#27068);引用指向从未存在的章节(#26973)。 - 风险与回滚:收窄(BREAKING,minor)。仓库调查 20 处 `newTabUrl` 取值、0 处不以 `/` 开头。已存的 `sys_metadata` 行原样保存,不做转换(外部 URL 推不出后端路径):照常加载注册,启动计为 invalid 并打印 `[metadata_spec_invalid]`,读取带 `_diagnostics`,下次保存被拒直到改成路径;渲染端不改。含 `skills/**`,属 Tier H。回滚即 revert 本 PR。 - 先看:`packages/spec/src/ui/action.zod.ts`(`newTabUrl` 的 regex 与提示)、`packages/metadata-protocol/src/protocol.stored-conversions.test.ts`(已存行处置)、`skills/objectstack-ui/rules/actions.md`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ui/action-newtaburl-path.test.ts`,预期 7 个用例全绿:`https://` 与 `http://` 值、无前导 `/` 的值在 `newTabUrl` 处拒收并带外部页面的处方,`/api/v1/invoice/{recordId}/pdf` 原样通过。 - 需要你做的:本 PR 触及 `skills/**`(Tier H),席位 ACCEPT 后需要你批准。 ## What changes - **#26924, `@objectstack/spec` (minor, BREAKING).** `ActionSchema.newTabUrl` is `z.string().regex(/^\//, …)`: a value that does not start with `/` is refused at `newTabUrl` with `` `newTabUrl` is a path on this backend and must start with `/` (e.g. `/sso-open/{recordId}`): the console prefixes the backend origin to it, so an absolute or relative URL opens a broken tab. To open an external page, use a `type: 'url'` action with `target: 'https://…'` (`${ctx.recordId}` where the record id goes) and `openIn: 'new-tab'`. `` A native string check, so it projects to the published JSON Schema as a `pattern` and adds no dropped-refinement site (the spec build is unchanged on that ledger). The external-page remedy is measured, not assumed: `type: 'url'` is the action type whose `target` takes an `http(s)` URL, and `openIn: 'new-tab'` is its new-tab switch (`action.zod.ts` `target` / `openIn`); the test parses that exact shape. The JSDoc, `.describe()` and the action form's help text (`action.form.ts`) now all say "a path on this backend, starting with /", and the four `*.metadata-forms.generated.ts` bundles carry the new help text (en from source; zh-CN / ja-JP / es-ES hand-translated, keys unchanged). No renderer change: `consoleServerAction.ts` already prefixes the backend origin. - **ADR-0087: `not-required (no-migration-prescription)`.** No backend path can be derived from an external URL, so nothing converts a stored value. What load does to one, measured on this head (new pins in `protocol.stored-conversions.test.ts`): `loadMetaFromDb` → `{ loaded: 1, errors: 0, invalid: 1 }`, the row registers with its stored `newTabUrl`, and boot prints `[Protocol] [metadata_spec_invalid] stored action/export_invoice fails the current spec schema even after conversion (newTabUrl: …the prescription…). Registered anyway so it stays serveable and fixable …`; `getMetaItem` serves the bytes unchanged with `_diagnostics.valid: false` locating `newTabUrl`; a CONTROL row with a path loads `invalid: 0`. Save, measured with a one-off probe through `saveMetaItem` (deleted after use): the absolute value answers 422 `INVALID_METADATA` `action/export_invoice failed spec validation: newTabUrl: …the prescription…`; the same body with `/export/{recordId}` saves. The same sentence is in the JSDoc, the changeset and the disposition marker. - **Liveness.** `liveness/action.json` `newTabUrl`: verdict unchanged (`live`), now with in-repo evidence (`consoleServerAction.ts#createConsoleServerActionHandler`) and a note stating the path rule. - **Survey (metadata in the repository, base `835b2d1c`).** Every `newTabUrl` value written anywhere in the tree (CHANGELOGs excluded): **20** literal sites (`packages/spec` tests 8, `ui/` tests 11, `skills/**` 1), **0** that do not start with `/`; `examples/**` 0, `ui/examples/**` 0, no JSON fixture carries the key. The cloud SSO producers live in `objectstack-ai/cloud`, which this session cannot read; the card names `/sso-open` as their endpoint, a path. A deployed database's `sys_metadata` rows cannot be surveyed from here; an absolute value there behaves as measured above. - **#27068, `@objectstack/spec` (prose only, no key).** Measured on `main` first: `DashboardRenderer.tsx` (`refusedTiles`, and `renderWidget`'s `if (!designMode && refusedTileKey && refusedTiles.has(refusedTileKey)) return null`) leaves out a dataset-bound tile once `DatasetWidget`'s `onForbidden` fires, which it does only when `classifyLoadError(e) === 'forbidden'` (PR #27065, merged as `4aeb0543`). The `DashboardWidgetSchema` docblock and `requiresObject`'s JSDoc and `.describe()` now say: outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 `PERMISSION_DENIED`) is left out for that viewer, with no key; in design mode it stays and shows its no-access state; `requiresObject` / `requiresService` keep their install-only meaning. `liveness/dashboard.json` `requiresObject` note gains the same sentence. No accept-set change. - **#26973, `@objectstack/spec` + `@objectstack/formula` (prose only).** `content/docs/concepts/architecture-overview.mdx` (the renamed page) has the headings Principles, Runtime Shape, Environment Artifact, Request Resolution, Non-Goals. Each citation: - `environment-artifact.zod.ts` "§6.3 … runtime-inputs boundary" → `architecture-overview.mdx` § "Environment Artifact", which states it ("It is not enough to deploy by itself; the host still supplies deployment config"). - `expression.zod.ts`, `packages/formula/src/index.ts`, `packages/formula/README.md` "§8 No private expression DSL" → no heading on the renamed page carries it; ADR-0032 does (its title: "… one CEL language …"), so the three cite `docs/adr/0032-unified-expression-layer.md`. This is the card's "the section that actually states the claim" rather than a bare drop; say if the seat wants the drop instead. - `hook-body.zod.ts` "Metadata-only runtime" → no such heading anywhere; the pointer is dropped (its docblock already states the claim in full). - `git grep -n 'north-star.mdx' -- packages/` → 0 (positive control: `git grep -n architecture -- packages/spec/src` hits). The references `shared/expression.mdx` and `system/environment-artifact.mdx` are regenerated. - **`skills/objectstack-ui/rules/actions.md` (Tier H).** The sentence "For external deep-links with no server round-trip, add `newTabUrl` — a direct URL template" would steer an author into the refused value; it now says a backend path starting with `/`, and points an external URL at `openIn` above. Readings: the file 204 → 204 lines, 2259 → 2258 tokens (ceiling 2259); the bundle (every `SKILL.md`) 4615 → 4615 lines; the whole shipped tree 215844 → 215843 tokens. Paid by deleting words in the same paragraph, no re-wrap credit. - **Changesets.** `@objectstack/spec` minor (the narrowing, with the ADR-0087 marker; the dashboard and citation prose ride in it), `@objectstack/formula` patch (README and docblock citation), `@objectstack/platform-objects` patch (the four form-translation bundles). ## Tests - `packages/spec/src/ui/action-newtaburl-path.test.ts` (new, 7): `https://` and `http://` values and a relative `sso-open/{recordId}` are refused at `newTabUrl` (`invalid_format`, one issue), the first sentence and the `type: 'url'` / `openIn: 'new-tab'` prescription asserted; refused through the registered `action` metadata schema too; `/api/v1/invoice/{recordId}/pdf` parses byte-identically; a query-string path passes; the remedy shape (`type: 'url'`, `https://` target, `openIn: 'new-tab'`) parses. - `packages/metadata-protocol/src/protocol.stored-conversions.test.ts` (3 new): the stored absolute row is served unchanged with `_diagnostics` naming `newTabUrl`; boot counts it `invalid: 1` and still registers it with the stored value; CONTROL: the same row with a path loads valid. - Ablation (one-off, not kept): `scripts/ablation-replace.mjs` turned the rule into `regex(/^/, …)` → the 4 refusal pins red, the 3 acceptance pins green; restored, blob == HEAD and `git diff HEAD` empty. ## Acceptance notes - `//host` and `/\host` start with `/` and pass. The console concatenates them after the backend origin, so they open a path on the backend, not another origin; refusing them was not asked and would be a wider narrowing. - The console substitutes only the first `{recordId}` (`String.replace` with a string pattern). No value in the tree repeats the placeholder; not changed here (no renderer change by order). - The action form's `newTabUrl` label stays "New-tab URL" (zh-CN "新标签页 URL 模板"); only the help text moved. - `content/docs/protocol/objectui/actions.mdx` calls `newTabUrl` "a zero-roundtrip new-tab target template"; still true under the rule, so left alone. ## Verification All readings on a shared container, base `main` `d745bb5e5` after merging it (no overlap with this diff); the lock's VERDICT lines are quoted. <details><summary>Tests and generated artifacts (head 7131b81)</summary> - `pnpm --filter @objectstack/spec build` → VERDICT command-exit 0 (dropped-refinements ledger unchanged; the published `ui/Action.json` now carries `newTabUrl: { type: string, pattern: '^\/' }`). - `pnpm --filter @objectstack/spec check:generated --fix` → only `check:docs` was stale (6 reference pages regenerated, committed); VERDICT command-exit 0. - `pnpm --filter @objectstack/spec test` → Test Files 744 passed (744), Tests 22270 passed | 1 todo; `typecheck` → exit 0. - `pnpm --filter '@objectstack/metadata-protocol^...' build` then `pnpm --filter @objectstack/metadata-protocol test` → Test Files 277 passed | 4 skipped (281), Tests 21753 passed | 26 skipped. - `pnpm --filter @objectstack/formula test` → Test Files 57 passed (57), Tests 1522 passed; `typecheck` → exit 0. - `pnpm --filter @objectstack/platform-objects test` → Test Files 96 passed (96), Tests 1361 passed. - One locked run held all of the above: VERDICT command-exit 0 · held the lock 648s. - Single files: `action-newtaburl-path.test.ts` → 7 passed; `protocol.stored-conversions.test.ts` → 19 passed (the boot line quoted under What changes is this run's stderr). - `node scripts/check-skills-token-ratchet.mjs` → `skills/objectstack-ui/rules/actions.md is 2258 tokens (ceiling 2259; headroom 1)`. </details> <details><summary>Full dogfood (head 7131b81)</summary> - `pnpm --filter '@objectstack/dogfood^...' build` then `pnpm --filter @objectstack/dogfood test` (no shard, no path), one locked run → VERDICT command-exit 0 · held the lock 1961s. Test Files 348 passed | 3 skipped (351), Tests 2804 passed | 20 skipped. No failure. </details> <details><summary>Derived gates (head 7131b81)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 115 families (26 paths, 288 changed lines vs merge base `d745bb5e5`). - Run serially: 113 exit 0 on the first pass; `spec check:skill-examples` and `check:dual-build-cjs-loads` exit 3 (PREREQUISITE NOT MET: no `client-react` / plugin dists). After building those dists (VERDICT command-exit 0 · 280s): `check:skill-examples` exit 0 (270 prose examples type-check across 3 surfaces), `check:dual-build-cjs-loads` exit 0 (112 entry points across 69 packages load). - `dispatch-gates --ran` → `115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED`, exit 0. - `GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event FILE` (this body) → exit 0. - CI check-runs: in progress at open; the seat reads them. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzhJoJYPHSvBpq8euSq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8bb2c31 commit c13ef04

26 files changed

Lines changed: 254 additions & 34 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: an action's `newTabUrl` is a path on this backend and must start with `/`; an absolute URL is refused at parse
6+
7+
`newTabUrl` is the zero-roundtrip target of the pre-opened-tab flow (`opensInNewTab: true`): on click the console drives the pre-opened tab to the backend origin + `newTabUrl`, with `{recordId}` substituted, and posts nothing. Its JSDoc called it a path template, while its `.describe()` and the action form's help text called it a URL template, and nothing refused an absolute value. So `newTabUrl: 'https://example.com/export/{recordId}'` parsed clean and opened a tab at `https://BACKEND/https://example.com/export/…`. `ActionSchema.newTabUrl` now refuses any value that does not start with `/`, and the JSDoc, `.describe()` and the form help (all four form-translation locales) say the same thing: a path on this backend.
8+
9+
Clause-②: no (narrowing) — `ActionSchema.newTabUrl` (every action door: `defineAction`, the `action` metadata type, an object's `actions[]`) refuses a value that does not start with `/`. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an action's `newTabUrl` may no longer be an absolute URL or a relative path without a leading `/`)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: newTabUrl keeps its name, shape and meaning, and now refuses the values that never opened the page they named. No backend path can be derived from an external URL, so there is no mechanical rewrite; a stored action row holding one loads unchanged, is counted invalid with a [metadata_spec_invalid] boot line and still registered, is served with _diagnostics locating newTabUrl, and its next save is refused until the author rewrites it. -->
14+
15+
**BREAKING** accept-set narrowing on `ActionSchema.newTabUrl` (`@objectstack/spec`).
16+
17+
- **Before.** `newTabUrl: 'https://example.com/export/{recordId}'` (or `sso-open/{recordId}`) parsed, and the console opened the backend origin + that text: a broken tab.
18+
- **Now.** The parse refuses it at `newTabUrl` with: `` `newTabUrl` is a path on this backend and must start with `/` `` and names the external-page remedy.
19+
- **Unchanged.** A path template such as `/sso-open/{recordId}` or `/api/v1/invoice/{recordId}/pdf` parses byte-identically. No renderer changes: the console already prefixes the backend origin, so the declared meaning and the behaviour now agree.
20+
- **A row stored before this release.** Nothing converts it. It loads unchanged and is still registered, counted `invalid` with a `[metadata_spec_invalid]` boot line; the metadata read serves it with `_diagnostics` locating `newTabUrl`; it keeps opening the same broken tab; and its next save is refused until the value is a path.
21+
- **Who sees it.** An author who wrote an absolute URL in `newTabUrl`. No example, template, fixture or platform object in this repository does.
22+
23+
**The remedy.** For an endpoint on this backend, write its path: `newTabUrl: '/sso-open/{recordId}'`. For an external page, drop `opensInNewTab` and `newTabUrl` and declare a `type: 'url'` action instead: `target: 'https://example.com/export/${ctx.recordId}'`, `openIn: 'new-tab'`.
24+
25+
Also in `@objectstack/spec`, prose only, no accept-set change:
26+
27+
- The `DashboardWidget` docblock and `requiresObject`'s `.describe()` say that, outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 `PERMISSION_DENIED`) is left out of the dashboard for that viewer, and that `requiresObject` stays an install gate, never a viewer gate.
28+
- Three shipped citations of `content/docs/concepts/north-star.mdx` "§8" / "§6.3" (sections that page never had; the page is now `concepts/architecture-overview`) are corrected: `expression.zod.ts` cites ADR-0032 ("one CEL language"), `environment-artifact.zod.ts` cites the architecture overview's "Environment Artifact" section, and `hook-body.zod.ts` drops a pointer to a heading that does not exist. The generated references follow.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the action form's `newTabUrl` help text says it is a path on this backend starting with `/`, in all four locales (en, zh-CN, ja-JP, es-ES), and names the `type: 'url'` action for an external page — matching the spec, which now refuses an absolute `newTabUrl`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/formula': patch
3+
---
4+
5+
docs(formula): the README and the package docblock cite ADR-0032 ("one CEL language") instead of `content/docs/concepts/north-star.mdx` "§8 No private expression DSL", a section that page never had (the page is now `concepts/architecture-overview`, which carries no such heading)
6+
7+
Clause-②: no
8+
Metadata-protocol: none

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ const result = ApiMethod.parse(data);
511511
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
512512
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
513513
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
514-
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
514+
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
515515
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
516516
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
517517
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ const result = MetadataBulkResultSchema.parse(data);
356356
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
357357
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
358358
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
359-
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
359+
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
360360
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
361361
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
362362
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

‎content/docs/references/shared/expression.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ SQL fragments (analytics joins, partial indexes) are intentionally **not**
5252
routed through this schema — they stay driver-native because their security
5353
posture and portability story differ.
5454

55-
See also: content/docs/concepts/north-star.mdx §8 "No private expression DSL"
55+
See also: docs/adr/0032-unified-expression-layer.md — "one CEL language"
5656

5757
<Callout type="info">
5858
**Source:** `packages/spec/src/shared/expression.zod.ts`

‎content/docs/references/system/environment-artifact.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ identity (`commitId`, `checksum`).
4242
`metadata`, outside the `checksum` digest. Absent ≠ `{}` — see the
4343
key's docblock.
4444

45-
See `content/docs/concepts/north-star.mdx` §6.3 for the
46-
runtime-inputs boundary.
45+
See `content/docs/concepts/architecture-overview.mdx` § "Environment
46+
Artifact" for the runtime-inputs boundary.
4747

4848
### History (#4740)
4949

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const result = ActionSchema.parse(data);
6666
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
6767
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
6868
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
69-
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
69+
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
7070
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
7171
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
7272
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const result = DashboardSchema.parse(data);
6767
| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | optional (default: `"metric"`) | Visualization type |
6868
| **chartConfig** | `{ title?: string \| Record<string, string>; subtitle?: string \| Record<string, string>; description?: string \| Record<string, string>; colors?: string[] \| Record<string, string>; … }` | optional | Chart appearance: title, subtitle, description, colors, height, legend, data labels, annotations, interaction. The bound dataset decides which series exist and which column each one reads (ADR-0021), so `type` (the widget's own `type` wins), `xAxis`, `yAxis` and `series` are refused here — select `dimensions` / `values` instead. |
6969
| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming |
70-
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered |
70+
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered — an install gate, not a viewer gate. Outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 PERMISSION_DENIED) is left out of the dashboard for that viewer with no key. |
7171
| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered |
7272
| **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) |
7373
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |
@@ -167,7 +167,7 @@ Dashboard header action
167167
| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | optional (default: `"metric"`) | Visualization type |
168168
| **chartConfig** | `{ title?: string \| Record<string, string>; subtitle?: string \| Record<string, string>; description?: string \| Record<string, string>; colors?: string[] \| Record<string, string>; … }` | optional | Chart appearance: title, subtitle, description, colors, height, legend, data labels, annotations, interaction. The bound dataset decides which series exist and which column each one reads (ADR-0021), so `type` (the widget's own `type` wins), `xAxis`, `yAxis` and `series` are refused here — select `dimensions` / `values` instead. |
169169
| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming |
170-
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered |
170+
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered — an install gate, not a viewer gate. Outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 PERMISSION_DENIED) is left out of the dashboard for that viewer with no key. |
171171
| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered |
172172
| **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) |
173173
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |

‎packages/formula/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Canonical expression engine for ObjectStack.
66
- **Dialect: `js`** — sandboxed L2 hook bodies (delegated).
77
- **Dialect: `cron`** — job schedules (delegated).
88

9-
See `content/docs/concepts/north-star.mdx` §8 "No private expression DSL" and `ROADMAP.md` M9.
9+
See ADR-0032 (`docs/adr/0032-unified-expression-layer.md`, "one CEL language") and `ROADMAP.md` M9.

0 commit comments

Comments
 (0)