Repository navigation
feat(app-shell): a key-only record control, and the Studio hook form offers refusals with it (#26835) - #27160
Conversation
…offers refusals with it Claude-Session: https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3 Co-authored-by: Claude <noreply@anthropic.com>
…refusals round trip Claude-Session: https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b98ad52a5f038e34296d673e40523449e576cd3c && git checkout b98ad52a5f038e34296d673e40523449e576cd3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b242df34f343e86d5fbee4513e3822f0d2e5b89 b88c9b6025de9ca8349f16bda4962736f8a75e0b && git checkout -B drift-repro 9b242df34f343e86d5fbee4513e3822f0d2e5b89 && git merge --no-ff b88c9b6025de9ca8349f16bda4962736f8a75e0b
node scripts/docs-audit/affected-docs.mjs --json 9b242df34f343e86d5fbee4513e3822f0d2e5b89 |
…hook form's refusals rows Regenerated with scripts/check-i18n-bundles.mjs --write and translated in zh-CN, ja-JP and es-ES; the three census pins that count hook-form children and translated row labels move with the new rows, and the cli flow-refusal coverage pin is scoped to the flows bucket. Claude-Session: https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3 Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT · seat 席位意见:Studio 的钩子表单现在可以编写
Generated by Claude Code |
Resolve the platform-objects census pin in object-lifecycle-panel-echo-decisions.test.ts: main's #26602 polarity row (690) plus this branch's three hook-form refusals rows gives 693; both dated comments kept. Claude-Session: https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3 Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · addendumACCEPT stands · seat The merge queue removed this PR for a merge conflict. The dev merged Generated by Claude Code |
Requested by Warren · project thread
Fixes #26835
Clause-②: no
Metadata-protocol: none
维护者速读
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
SchemaForm.tsx,RecordField). Atype: 'record'form row that declareswidget: '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.packages/spec/src/data/hook.form.ts). The Body section offersrefusalswith that control, withmessage(textarea, required) andstatus(number) per entry.metadata-form-zod-reconciliation.test.ts). Thehookroot omission row forrefusalsthat the hook refusal catalogue PR recorded is removed: the form now offers the key, and the reconciliation readsrefusals.message/refusals.statusagainstHookRefusalSchema.engine.form.key(enKey, zh键名) names the key column when the form declares no key label.@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 (nameby default), and a refusal entry is closed atmessage/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 ishookForm's own, the field schema is the served derivation ofHookSchema(z.toJSONSchemawithio: 'input'), and the key rule is checked equal toHOOK_REFUSAL_CODE_PATTERN.source.refusalsas a record on the key-only face;clean_version) is refused and the value is unchanged;HookSchemaaccepts, the parsedrefusalsserialise byte-identical to the saved ones, and the saved body reloads into the same rows in the same order;HookSchemarefuses that body withunrecognized_keys['name']at the entry.Ablation, one-off, restored by
scripts/ablation-replace.mjs(blob back to HEAD892c144b,git diff HEADempty):const keyOnly = widget === KEY_ONLY_RECORD_WIDGET;replaced withconst 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
engine.form.keyPatterntext naming the served pattern, not with the sentenceHookSchemaattaches to the record key ("A refusal code is SCREAMING_SNAKE …"). That sentence is inline inhook.zod.tsand 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,regexandimmutableare not read on the key-only face (nothing is stamped, and a key is always renamable). DeclaringkeyFieldon such a row would also make the reconciliation gate countkeyField.fieldas an offered member, so the hook row declares none.Verification
Local, on the branch head named in the report: the new file 8/8;
uivitest overpackages/app-shell/src/views/metadata-admin/andpackages/i18n/727 files green; spec vitest oversrc/system/,src/data/hook*.test.ts,src/kernel/repeater-item-titles.test.ts65 files / 2208 tests green;type-checkfor@object-ui/app-shell,@object-ui/i18n,@objectstack/specandtype-check:scriptsexit 0. The derived gate list (84 commands) is reconciled withdispatch-gates --ranin the report;check:i18nandcheck:dual-build-cjs-loadsneed a whole-repo build and are left to CI's Lint and Repo Gates.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (CI).main, one pull request.packages/spectouched: no generated artifact moves (a form row, no schema change).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQwxbaFd8s4eYs6HGFghK3
Generated by Claude Code