Repository navigation
Commit 39fd5fb
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8G6QZjYQCXktarYR4vyuASc)_
Fixes #26835
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:Studio 钩子表单新增「Refusals」一行,可以逐条填写拒绝码、提示语和状态码;表单的 record
编辑器新增一个通用的「只用键」模式,任何 record 字段声明后都能用。
- 为什么改:原来的 record 编辑器会把键名写进每个条目,而钩子的拒绝条目是封闭对象,写进去就被 spec 拒收,所以 Studio
里根本没法配置可翻译的拒绝提示。
- 风险与回滚:只有声明了 `widget: 'key-only-record'` 的字段走新逻辑,其余 record
字段行为不变(有测试钉住);回滚即撤销本 PR。
- 先看:`ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx` 的
RecordField 改动(键名不写入、改名逻辑);`packages/spec/src/data/hook.form.ts` 新增的一行。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/metadata-admin/SchemaForm.keyOnlyRecord-26835.test.tsx`,预期
8 个测试全绿。
- 需要你做的:无,席位落地。
## What changes
- **Key-only record face** (`SchemaForm.tsx`, `RecordField`). A `type:
'record'` form row that declares `widget: 'key-only-record'`
(`KEY_ONLY_RECORD_WIDGET`) edits a map of closed entries: add, rename,
remove and reorder keys, and edit each entry's fields, without writing
the key into the entry. A key is checked against the record's served key
rule (`propertyNames.pattern`, which the server derives from the spec's
key schema), so the rule is never restated in the UI. A rename moves the
entry in place once the typed key is valid, so an invalid intermediate
spelling stays typeable. A record row with no such widget is untouched:
the key is still mirrored into the entry.
- **Hook form** (`packages/spec/src/data/hook.form.ts`). The Body
section offers `refusals` with that control, with `message` (textarea,
required) and `status` (number) per entry.
- **Gate ledger** (`metadata-form-zod-reconciliation.test.ts`). The
`hook` root omission row for `refusals` that the hook refusal catalogue
PR recorded is removed: the form now offers the key, and the
reconciliation reads `refusals.message` / `refusals.status` against
`HookRefusalSchema`.
- **i18n.** `engine.form.key` (en `Key`, zh `键名`) names the key column
when the form declares no key label.
- **Translations.** The generated metadata-form bundles carry the three
refusals rows, translated in zh-CN, ja-JP and es-ES; three
platform-objects census pins and one cli coverage filter move with them.
- **Changeset.** Patch for `@objectstack/spec`,
`@objectstack/platform-objects`, `@object-ui/app-shell`,
`@object-ui/i18n`.
## Why
After the hook refusal catalogue landed, a hook can declare `refusals: {
CODE: { message, status? } }` so its refusal messages are translatable,
but the Studio hook form could not author it: the generic record editor
stamps each key into its entry (`name` by default), and a refusal entry
is closed at `message` / `status`, so the stamped key is refused. The
card asked for a generic key-only record control, opted into by
declaration, and for the hook form to use it.
## Tests
New file `SchemaForm.keyOnlyRecord-26835.test.tsx` (8 tests). Everything
it reads comes from the spec: the form row is `hookForm`'s own, the
field schema is the served derivation of `HookSchema` (`z.toJSONSchema`
with `io: 'input'`), and the key rule is checked equal to
`HOOK_REFUSAL_CODE_PATTERN.source`.
- the hook form declares `refusals` as a record on the key-only face;
- adding an entry writes no key into it;
- a key that breaks the served rule (`clean_version`) is refused and the
value is unchanged;
- a rename moves the entry in place, unstamped, through an invalid
intermediate spelling; a rename onto an existing key is refused;
- round trip: two refusals authored in the form save a body `HookSchema`
accepts, the parsed `refusals` serialise byte-identical to the saved
ones, and the saved body reloads into the same rows in the same order;
- control: a record row with no widget still mirrors the key, and
`HookSchema` refuses that body with `unrecognized_keys` `['name']` at
the entry.
Ablation, one-off, restored by `scripts/ablation-replace.mjs` (blob back
to HEAD `892c144b`, `git diff HEAD` empty): `const keyOnly = widget ===
KEY_ONLY_RECORD_WIDGET;` replaced with `const keyOnly = false;` (the
hook row falls back to the stamping editor) turns 4 of 8 red, the round
trip among them; the 4 that stay green are the declaration, the
served-rule and the default-face pins, which do not read the face.
## Acceptance notes
- The control refuses a bad key with the generic
`engine.form.keyPattern` text naming the served pattern, not with the
sentence `HookSchema` attaches to the record key ("A refusal code is
SCREAMING_SNAKE …"). That sentence is inline in `hook.zod.ts` and the
served JSON Schema does not carry it, so a generic control has no way to
read it without restating it. The row's own help text states the
spelling.
- `keyField.field`, `regex` and `immutable` are not read on the key-only
face (nothing is stamped, and a key is always renamable). Declaring
`keyField` on such a row would also make the reconciliation gate count
`keyField.field` as an offered member, so the hook row declares none.
## Verification
Local, on the branch head named in the report: the new file 8/8; `ui`
vitest over `packages/app-shell/src/views/metadata-admin/` and
`packages/i18n/` 727 files green; spec vitest over `src/system/`,
`src/data/hook*.test.ts`, `src/kernel/repeater-item-titles.test.ts` 65
files / 2208 tests green; `type-check` for `@object-ui/app-shell`,
`@object-ui/i18n`, `@objectstack/spec` and `type-check:scripts` exit 0.
The derived gate list (84 commands) is reconciled with `dispatch-gates
--ran` in the report; `check:i18n` and `check:dual-build-cjs-loads` need
a whole-repo build and are left to CI's Lint and Repo Gates.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (CI).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: no generated artifact moves (a form
row, no schema change).
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9dacccf commit 39fd5fb
15 files changed
Lines changed: 409 additions & 42 deletions
File tree
- .changeset
- packages
- cli/test
- platform-objects/src/apps/translations
- spec/src
- data
- system
- ui/packages
- app-shell/src/views/metadata-admin
- i18n/src/namespaces/studio
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
82 | 85 | | |
83 | | - | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
84 | 89 | | |
85 | 90 | | |
86 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
701 | 701 | | |
702 | 702 | | |
703 | 703 | | |
| 704 | + | |
| 705 | + | |
704 | 706 | | |
705 | | - | |
| 707 | + | |
706 | 708 | | |
707 | 709 | | |
708 | 710 | | |
| 711 | + | |
| 712 | + | |
709 | 713 | | |
710 | 714 | | |
711 | 715 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
937 | 937 | | |
938 | 938 | | |
939 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
940 | 952 | | |
941 | 953 | | |
942 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
937 | 937 | | |
938 | 938 | | |
939 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
940 | 952 | | |
941 | 953 | | |
942 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
782 | 782 | | |
783 | 783 | | |
784 | 784 | | |
785 | | - | |
| 785 | + | |
| 786 | + | |
786 | 787 | | |
787 | | - | |
| 788 | + | |
788 | 789 | | |
789 | 790 | | |
790 | 791 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
937 | 937 | | |
938 | 938 | | |
939 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
940 | 952 | | |
941 | 953 | | |
942 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1233 | 1233 | | |
1234 | 1234 | | |
1235 | 1235 | | |
1236 | | - | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
1237 | 1239 | | |
1238 | 1240 | | |
1239 | 1241 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
937 | 937 | | |
938 | 938 | | |
939 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
940 | 952 | | |
941 | 953 | | |
942 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
49 | 62 | | |
50 | 63 | | |
51 | 64 | | |
| |||
0 commit comments