Repository navigation
Commit c13ef04
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
File tree
- .changeset
- content/docs/references
- data
- kernel
- system
- ui
- packages
- formula
- src
- metadata-protocol/src
- platform-objects/src/apps/translations
- spec
- liveness
- src
- data
- system
- ui
- skills/objectstack-ui/rules
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
511 | 511 | | |
512 | 512 | | |
513 | 513 | | |
514 | | - | |
| 514 | + | |
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
356 | 356 | | |
357 | 357 | | |
358 | 358 | | |
359 | | - | |
| 359 | + | |
360 | 360 | | |
361 | 361 | | |
362 | 362 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
| 55 | + | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
46 | | - | |
| 45 | + | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
| 69 | + | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | | - | |
| 70 | + | |
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | | - | |
| 170 | + | |
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
0 commit comments