Repository navigation
Commit cdd9f0a
fix(spec)!: refuse an object that declares one action name twice; Studio renames to a free name (#26874)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB9P9QpYQKquVrFuRFUPNpZf)_
Fixes #26725
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — the refusal itself names the action, every index and
the fix at every door that parses an object, and no `skills/**` file
teaches a shape that repeats an action name
(`skills/objectstack-ui/rules/actions.md` says nothing about name
uniqueness either way)
## 维护者速读
- 改了什么:对象的 `actions` 数组里同名的两个动作,现在在对象 schema 解析时就被拒绝(保存门返回 422
`INVALID_METADATA`,指出重名和每个下标);Studio 动作面板里把动作改名成已占用的名字时,自动写成第一个空闲的
`NAME_N`。
- 为什么改:动作名是动作在对象上的运行键,之前保存门和发布门都接受同名的两个动作(实测保存 200、发布
200、两个都被服务),到底跑哪个是未定义的;Studio 的名称输入框会直接写出这种重名。
- 风险与回滚:这是一次收窄(BREAKING),已存的重名对象仍能加载,但带 `[metadata_spec_invalid]`
诊断,下次保存被拒;`os migrate meta --stored` 会按对象列出它们。回滚即 revert 本 PR。
-
先看:`packages/spec/src/data/duplicate-action-names.ts`(唯一判定与文案)、`packages/spec/src/data/object.zod.ts`
的
`refuseDuplicateActionNames`、`ui/packages/app-shell/src/views/studio-design/ObjectActionsPanel.tsx`
的 `patchSelected` 改名分支。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/data/object-duplicate-action-name.test.ts`,预期 7 个用例全绿,其中重名用例断言路径
`actions.1` 与首句 "Object 'probe_item' declares the action 'dup' twice in
actions: actions[0] and actions[1]."
- 需要你做的:无,席位落地。
## What changes
- **Spec (`@objectstack/spec`, minor, BREAKING).** `ObjectSchema`'s
existing `superRefine` gains `refuseDuplicateActionNames`: one `custom`
issue per repeated action name, at `actions[j]` of its second
declaration, naming the name and every index (`Object 'X' declares the
action 'Y' twice in actions: actions[i] and actions[j]. …`). The
predicate and message live in one module,
`data/duplicate-action-names.ts`, read by the refinement and by the v18
stored-row listing. No new check node, so the JSON Schema projection is
unchanged (`dropped-refinements` baseline untouched; spec build green).
- **One judge per pair.** `defineStack`'s duplicate action-key check
already refused two embedded twins on one object; on the strict path the
object parse now refuses them first (`STACK_SCHEMA_INVALID`), so the
cross-reference walk keeps the pairs that span two collections (bound
standalone + embedded, two standalones). Its docblock says so; row (d)
of `stack-duplicate-action-key.test.ts` and the "identical copy beside a
differing twin" case now assert the parse envelope (`code`, `status`,
first sentence).
- **ADR-0087: registered.** New D3 semantic entry
`object-duplicate-action-name-refused` (step 18) and a
`REFUSED_STORED_SHAPES` detector, so `os migrate meta --stored` lists
each stored object that repeats a name, under its row, at
`objects[0].actions[j]`. Nothing rewrites it: which declaration keeps
the name only the owner knows. The listing's `from` carries only the
name and the indexes (an action body can carry request headers).
- **Studio (`@object-ui/app-shell`, patch).** The actions panel's create
path already derived a free name (`nextActionName`). The duplicate came
from the editor's Name field, which wrote whatever was typed, so the QA
run's three `call_api` entries were renames. `patchSelected` now writes
a rename to a name another listed action holds as the first free
`NAME_N`, and selects it.
- **Out of scope, by design.** A standalone `action` item bound to the
object by `objectName` is a second item: one stack's pair stays
`defineStack`'s duplicate-key refusal, and two packages' pair stays the
runtime's bound-action collision report, which its own card ruled
report-first (warn, not refuse). The object schema sees only its own
array.
## Tests
- `packages/spec/src/data/object-duplicate-action-name.test.ts` (new):
pair, three-way, one issue per name, byte-identical twin, distinct-name
control, stored-body reads.
- `packages/spec/src/conversions/refused-shapes.test.ts`: the new id in
the registered list; a stored row with a pair is listed once at
`objects[0].actions[1]`, not rewritten, and still fails the parse at the
same site; distinct names are not listed.
- `packages/spec/src/stack-duplicate-action-key.test.ts`: rows above
move to the parse envelope.
-
`packages/metadata-protocol/src/protocol.object-duplicate-action-name-refusal.test.ts`
(new): `saveMetaItem` answers `INVALID_METADATA` / 422 with one issue at
`actions.1`, stores nothing; distinct-name control saves.
`scripts/engine-double-contract.pinned.json` learned its doubles
(`--write`).
-
`ui/packages/app-shell/src/views/studio-design/ObjectActionsPanel.renameTaken-26725.test.tsx`
(new): rename to a taken name writes `call_api_2`, skips a taken suffix
(`call_api_3`), free-name control writes as typed; each written draft
parses through `ObjectSchema`.
## Acceptance notes
- Fixtures, examples and seeds: a JSON scan of the tracked tree found no
object that repeats an action name (1292 files, 0 hits), and every
`examples/**` stack already went through `defineStack`'s strict pass,
which refused embedded twins before this change. The full spec,
metadata-protocol and dogfood suites stayed green, so no TS fixture
repeated one either.
- The local app-shell run was narrowed to the 169 test files that load
the actions panel, the Studio design surface or the Data pillar (the
1,765-file package suite did not finish in 60 minutes on the shared
box); CI's `Test UI` runs the rest.
## Verification
All readings on head `76f9014ff` (base `main` `c8583f80a`), on a shared
container; the lock's VERDICT lines are quoted.
<details><summary>Reproduction before the fix (main c8583f8, pnpm
dev:crm boot)</summary>
- `PUT /api/v1/meta/object/dup_item?mode=draft&package=com.qa.dup` with
two actions named `dup` (labels "Dup A", "Dup B") → `200`, `Saved object
'dup_item' … state=draft`.
- `POST /api/v1/meta/object/dup_item/publish` → `200`, `Published
draft`; the served object lists `dup/Dup A`, `dup/Dup B`.
- The standalone action door also saved an `action` named `dup` bound to
the object (`200`): a second item, out of scope here (see What changes).
</details>
<details><summary>Tests</summary>
- `pnpm --filter @objectstack/spec test` → Test Files 735 passed (735),
Tests 22027 passed | 1 todo; `pnpm --filter @objectstack/spec typecheck`
green (tsc, scripts-typecheck, test-typecheck).
- `pnpm --filter @objectstack/metadata-protocol test` → Test Files 270
passed | 3 skipped (273), Tests 21618 passed | 19 skipped. VERDICT
command-exit 0.
- app-shell, narrowed to the 169 files that load `ObjectActionsPanel`,
`StudioDesignSurface` or `DataPillar` → Test Files 169 passed (169),
Tests 1349 passed; `pnpm --filter @object-ui/app-shell type-check`
green. VERDICT command-exit 0.
- Dogfood battery, full (`pnpm --filter @objectstack/dogfood test`) →
Test Files 314 passed | 3 skipped (317), Tests 2609 passed | 20 skipped.
VERDICT command-exit 0.
</details>
<details><summary>Ablations (scripts/ablation-replace.mjs, restore
proven blob == HEAD)</summary>
- Spec: the `refuseDuplicateActionNames(...)` call replaced by a no-op →
`object-duplicate-action-name.test.ts` + `refused-shapes.test.ts`: Tests
5 failed | 74 passed. Restored.
- Studio: the rename guard's condition replaced by `false` →
`ObjectActionsPanel.renameTaken-26725.test.tsx`: Tests 2 failed | 1
passed (the free-name control stays green). Restored.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 98 derived families, every one run and exit
0; `--ran` → `98 derived famil(ies) accounted for — 98 run, 0
NOT-MEASURED`.
- 89 of them ran on the tree of `fe0b8e175`; the only later commit,
`76f9014ff`, adds the engine-double ledger rows, and the families it
brought in plus `check:engine-double-contract` (red before `--write`,
exit 0 after) were run on `76f9014ff`. `check:type-check-debt` first
exited 3 (lock not granted while the test runs held it) and was re-run
to exit 0.
- `node scripts/check-adr-0087-registration.mjs --base origin/main` →
`registered object-duplicate-action-name-refused (new here:
object-duplicate-action-name-refused)`.
- `pnpm --filter @objectstack/spec check:generated` → all 14 generated
artifacts up to date.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzeqb8AXHib3Hx7Fpeb
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 161e00e commit cdd9f0a
13 files changed
Lines changed: 575 additions & 20 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- spec/src
- conversions
- data
- migrations/entries/semantic
- scripts
- ui/packages/app-shell/src/views/studio-design
| 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 | + | |
| 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 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| |||
661 | 662 | | |
662 | 663 | | |
663 | 664 | | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
| |||
478 | 480 | | |
479 | 481 | | |
480 | 482 | | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
481 | 508 | | |
482 | 509 | | |
483 | 510 | | |
| |||
488 | 515 | | |
489 | 516 | | |
490 | 517 | | |
| 518 | + | |
491 | 519 | | |
492 | 520 | | |
493 | 521 | | |
| |||
| 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 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 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 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
0 commit comments