Repository navigation
feat(spec): an action param can name the widget that draws it (permission-ref picker) - #30321
Conversation
…sion-ref picker) ActionParamSchema gains an optional `widget`, the same key, vocabulary and meaning as FieldSchema.widget, so a param can ask for the permission-set picker ADR-0131 D4 gives the grant column. `type` stays the value's type; an unregistered name is kept and degrades to the type renderer. The action form's Params repeater offers the key, labelled in four locales. The console reader is #30260. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhmTfrR3b13itPTmu3
…e the two row-label counts for params.widget Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhmTfrR3b13itPTmu3
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 170 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 1415a8d34b0e555834586b1fcd261ef5d979e359 && git checkout 1415a8d34b0e555834586b1fcd261ef5d979e359
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cfb602c4595795c696dd07763abca3104dd4cf80 1d215c5caf8c191915f424e74dda7715c01af0fc && git checkout -B drift-repro cfb602c4595795c696dd07763abca3104dd4cf80 && git merge --no-ff 1d215c5caf8c191915f424e74dda7715c01af0fc
node scripts/docs-audit/affected-docs.mjs --json cfb602c4595795c696dd07763abca3104dd4cf80
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 792c7ab · 2026-11-05T06:04Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe surface choice is sound and the smallest one that keeps ADR-0104 D2 intact and reuses the field key's vocabulary and degradation. The widening is honest and fully pinned, and every generated surface a new optional key moves is regenerated. The two defects are declaration and record hygiene, not design: the protocol line is malformed and blocks the level gate, and the ADR-0049-style reader obligation is held by an unscanned sentence rather than by the ledger. Implemented-by: VERDICT: FAIL |
…with an author warning, declare Metadata-protocol: added The contract review asked for the drill: each ActionParamSchema member gets its own verdict with in-repo evidence (options drilled too), action/params leaves the undrilled baseline, and params.widget is planned + authorWarn naming #30260. The key's describe says it is not read until #30260. The changeset's Metadata-protocol line takes the closed-set value added. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhmTfrR3b13itPTmu3
check:doc-authoring refuses an issue id in author-facing prose; the pointer to the reader card moves to a // comment beside the key. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhmTfrR3b13itPTmu3
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 1d215c5 · 2026-11-05T07:35Z Delta review of fix round 1 ( |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe surface choice stands as reviewed. The fix round turned the reader obligation from an unscannable sentence into 22 measured verdicts, each anchored on the symbol that reads the member, with the one planned member warning its authors through the shipped ledger and a pin that keeps it warning; the declaration axis is well-formed and green. The remaining gap is hand-off hygiene on the successor issue, not this diff. Implemented-by: VERDICT: PASS |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审两轮。
席位核对 — ACCEPT · domain:spec#3 · head
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 41989486500 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Merge-queue failure on run 41989486500 is not this PR's. |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 41990912046 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
#30321 and #30259 landed first. The four metadata-forms catalogs auto-merged and `check-i18n-bundles --write` on the merged tree reproduces them byte for byte. The echo-ledger positive control is main's 702 plus this branch's thirty view row labels: 732. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzjGQVFPCizmQreyKkv
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Requested by Yibo Wang · project thread
domain:spec#3 · os-support-ai · session_01WGufzhmTfrR3b13itPTmu3
Fixes #30259
Part of #30117 (this is slice 1, the spec half; #30260 adds the console reader, and #30117 keeps the server switch of Create User's param)
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed once #30260 lands (the skills lane is paused, and the key does nothing in the console until then, so teaching it now would teach an inert key). Owed lines: skills/objectui/rules/protocol.md:309, the inline-param widget-config list (
options,multiple,accept, …) gainswidget; skills/objectstack-ui/rules/actions.md:174-175 says a field-backed param takes its "widget" from the object metadata, which the console does not do today (its resolver copies no fieldwidgetonto a param, measured on #30117), so that line is #30260's to make true or to correct.Prior rulings read: permission-ref,actionparamschema,action param widget → 1 hits; ADR-0104 D2; thread: none; repo: objectstack-ai/objectstack
ADR-0087: not needed. A new optional key; nothing is removed, renamed or narrowed, and no stored param changes meaning, so no conversion is owed.
维护者速读
ActionParamSchema)新增可选键widget,与字段上的widget同名、同词汇、同含义:指定用哪个已注册的控件来画这个参数,type仍是值的类型。Studio 的动作表单参数列表多一列「控件」,四种语言都有标签。permission-ref选择器(ADR-0131 D4),但动作参数没有任何方式请求这个选择器。这是拆分后的第一片:先让规范能表达。type的默认控件,和字段一致。控制台在 ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260 落地前不读它,所以现在声明了也只是照旧显示文本框;键的说明写明了这一点,os validate/os lint也会提醒写了它的作者。回滚即还原本 PR。packages/spec/src/ui/action.zod.ts(新键与 TSDoc);packages/spec/src/ui/action-param-widget.test.ts(与字段键逐值同判的钉);packages/spec/liveness/action.json(params钻取后的逐成员判定,widget为 planned 并提醒作者)。pnpm --filter @objectstack/spec exec vitest run src/ui/action-param-widget.test.ts,预期17 passed。What changes
@objectstack/spec:ActionParamSchema.widget,z.string().optional(), placed aftertype. Same key, vocabulary and meaning asFieldSchema.widget(data/field.zod.ts): it names a registered field component, looked up asfield:plus the name, that draws the param instead of thetypedefault.typestays the value's type. The TSDoc cites ADR-0131 D4 and names the case:widget: 'permission-ref'on atextparam lists the security catalog's permission sets and submits the chosen machine name. The.describe()carries the same contract for the generated docs and JSON Schema.ui/action.form.ts): theparamsrepeater offerswidgetas a row property (labelWidget), and@objectstack/platform-objects' metadata-form bundles label it in en / zh-CN / ja-JP / es-ES, reusing each locale's existing word for the field form'swidgetrow (控件覆盖,ウィジェット指定,Control del campo).action/paramsdrilled:liveness/action.json'sparamsrow now carries achildrenmap, one verdict per member, andaction/paramsleaves thecontainerslist ofscripts/liveness/undrilled-containers.baseline.json(the debt shrinks by 19 child keys). 18 members arelive, each with dated in-repo evidence anchored on the consuming symbol: the console resolverresolveActionParam, the dialog (ActionParamDialog,filterVisibleParams,CarryOverParam,serializeParamValues),paramToField, and the dispatcher's ADR-0104 D2 path (resolveDeclaredActionParams,validateActionParams,summarizeActionParams);requiresFeatureciteslowerRequiresFeature, its parse-time lowering, as the action-level row does.optionsis itself a container, so it is drilled too (label,value,visibleWhen, alllive).widgetisplannedwithauthorWarnand anauthorHintsaying the dialog does not read it yet (the row's note names ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260 as the reader; author-facing text carries no issue id, percheck:doc-authoring), soos validate/os lintwarn an author who writes it (ruleliveness-planned-property); its note records what each reader does with it today.state-counts/action.mdmoves 51/2/54 → 70/3/74 (live/planned/classified). Two measured notes ride on rows:objectOverrideis read by the console only (the dispatcher resolves a field-backed param against the action's own object), andoptions.visibleWhenis client-side only..describe()(and so the generated reference) now says it is not read yet: until the console's dialog passes it through, every param is drawn with itstyperenderer. A//comment beside the key says the sentence comes off with ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260.authorable-surface/ui.jsongainsui/ActionParam:widget;content/docs/references/ui/action.mdxgains the row (both regenerated, not hand-edited).action-body-panel-echo-decisions.test.ts(params children 18 → 19) andobject-lifecycle-panel-echo-decisions.test.ts(positive control 701 → 702), each with a dated comment line.sys-user.object.ts. ItspermissionSetparam switches towidget: 'permission-ref'in Create User: the permission set is a free text box that only accepts the machine name #30117's server PR, once ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260 gives the key a reader; declaring it now would ship a key the console ignores.BulkActionParamSchemadoes not get the key: no bulk action asks for a picker of this kind today.The choice:
widgeton the param, notpermission-refas a paramtypeMeasured on
origin/main959c26be:permission-refis a widget hint, not a field type. It is not inFieldType. The grant column declares it asField.text({ widget: 'permission-ref' })(plugin-security/src/objects/sys-user-permission-set.object.ts:104), the console registers it asfield:permission-refbesideobject-refandposition-ref, and both the console's changelog andui/packages/app-shell/src/utils/paramValueShape.ts:181say it is "widget-hint only — not a declarable param type". A paramtypereadsFieldType, so thetyperoute means either addingpermission-reftoFieldType(which also makes it a declarable FIELD type, against ADR-0131 D4's text-column-plus-widget shape) or forking a param-only type vocabulary.typeroute would loosen the server's value check. ADR-0104 D2 (the one prior ruling the check found) makestypethe enforced value contract: the dispatcher validates each param throughvalueSchemaFor(type). Atypeit does not know falls to the explicitly-open branch (field-value.zod.ts:797,z.unknown()), sotype: 'permission-ref'would stop checking that the value is a string. Withwidget,typestaystextand D2's check is unchanged.widgetis one key with the field's own meaning, so an author who knows the field key already knows this one, and the console's resolver can pass it through to the samefield:WIDGETlookup the record form uses.Value verdict: kept, as on the field.
FieldSchema.widgetis an openz.string(): an unregistered name parses and degrades to thetyperenderer. The param key makes the same promise, and the pin checks seven values (three registered names, one unregistered, a number, a boolean,null) get the same verdict on both schemas.Who reads action params, and what each does with the key today
widgetbefore #30260ActionParamSchema(strict) behindPUT /meta/action,MetadataManager.validate,defineStack,os validateui/packages/fields/src/resolveActionParams.tsActionParamDeffrom named members only;ActionParamDefhas nowidget, so the key is not carried. The param is still resolved and drawn.ui/packages/app-shell/src/utils/paramToField.tswidgetonly forboolean(checkbox); otherwise draws thetyperenderer. Atextparam stays a text box and its value is submitted as before. Nothing is dropped.packages/runtime/src/action-execution.ts→validateActionParams){ name, type, multiple, required, options }(ResolvedActionParam) and validates the value bytype(ADR-0104 D2).widgetis not read; the value check is unchanged.service-aiaction-tools.ts#buildParametersSchema, theparamsrow's evidence)os i18n extract(packages/cli/src/utils/i18n-extract.ts)label/helpText/placeholder/optionsleaves only;widgetis a machine name, not translatable copy.@object-ui/typesActionParamz.inputtype, so it gains the member with no edit; its parity pins are subset (direction) checks. Not run locally (ui is out of scope here); CI's UI job runs them.Tests
New
packages/spec/src/ui/action-param-widget.test.ts(17 cases): acceptspermission-refon an inlinetextparam and keeps it withtypeunchanged; accepts it on a field-backed param; it is a declared key (nounrecognized_keys); per-value parity withFieldSchema.widgetover seven values; an unregistered name is kept; a non-string is refused on thewidgetpath (invalid_type); five existing param shapes (Create User's current inlinepermissionSetamong them) parse with nowidgetmember added.New pin in
packages/lint/src/lint-liveness-properties.test.ts: an action whose params namewidgetdraws exactly oneliveness-planned-propertywarning (through the shipped ledger and the array fan-out); its live siblings stay silent; an action whose params name no widget draws nothing.Suites, under the verify lock:
pnpm --filter @objectstack/spec test: Test Files 813 passed (813), Tests 25592 passed, 1 todo, on the drilled tree (4fe77e8's content).pnpm --filter @objectstack/spec typecheck: exit 0 (first head).1d215c5c(the describe and hint rewording):lint-liveness-properties.test.ts113/113;action-param-widget.test.ts+action.test.ts168/168.pnpm --filter @objectstack/metadata-protocol test: Test Files 312 passed, 4 skipped (316), Tests 22354 passed, 26 skipped (first head; the later commits touch no metadata-protocol input).pnpm --filter @objectstack/platform-objects test: the first run had 2 failures, both count pins the new form row moves (action-body-panel-echo-decisions.test.ts18 → 19,object-lifecycle-panel-echo-decisions.test.ts701 → 702). Both moved in the second commit; the two files then pass (57/57), and the other 108 files passed.ablation-replace.mjs, see Verification). The ledger drill is data, so it is not ablated; the lint pin is its red-without-it check.Derived gates on the final head 1d215c5: 107 derived, 107 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 107 families against merge base 959c26b; a runner recordedCMD :: exit Nfor each (withNODE_OPTIONS=--max-old-space-size=4096, after a fullturbo run build), and--ranreconciled: "107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED".check:liveness: "every governed-type property, at every depth the ledger drills, is classified … state-counts/ is current".check:dual-build-cjs-loads: "116 published require entry point(s) across 71 package(s) load; 2456 emitted CommonJS file(s) parse" (NOT MEASURED on the first head; measured here after the full build).check:doc-authoring: "21066 customer-facing string(s) … clean — no internal issue-id references". Its first run on4fe77e88refused the describe'sobjectstack#30260;1d215c5cmoved that pointer to a//comment and out of theauthorHint.check:docs: "240 generated files in sync with packages/spec".check-changeset-no-major --base origin/main: nomajorbump; the declaration axis reads this body in CI.Acceptance notes
792c7abe) wrotewidget's planned status into the note of the undrilledparamsrow, which no check scans and no lint reads. The contract review (6651175841) asked for the drill instead, and this head does it: 22 rows (19 members plusoptions' 3), each verdict from its own reader, none fanned out from the parent. ui: resolveActionParams and paramToField pass a param widget through, so an action dialog draws the permission-set picker (#30117 slice 2) #30260 flipsparams.widgettoliveand drops itsauthorWarnwhen the pass-through lands.resolveDeclaredActionParamsdoes not readobjectOverride, so a cross-object field-backed param's server-side value check inherits from the action's own object's field of that name (or is open when there is none), while the console resolves it against the override object. Recorded on the row's note; it is outside this card.Verification
Metadata-protocol: added(corrected from the malformedadditivethe first head carried, whichCheck Changesetread as no declaration) is read off this body by CI'sCheck Changeset; locallycheck-changeset-no-majorfound nomajorbump. The changeset bumps@objectstack/specand@objectstack/platform-objectsminor and carries both declaration lines.792c7abethroughscripts/ablation-replace.mjs(spec tests import the schema fromsrc, so no build sits between): renaming the key towidgetAblated(anchor 1 → 0, blob8cd40792→893ac68c) turnsaction-param-widget.test.tsred, 9 failed / 8 passed of 17 (the 8 are the five existing-param cases and the three values both schemas refuse). Restored: blob equals HEAD8cd40792andgit diff HEADis empty.FieldSchema.widgetever narrows to an enum the per-value cases go red rather than silently diverging.Human Minimum
packages/specholds no runtime logic.pnpm lintis left to CI (the repo-wide run is CI's).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzhmTfrR3b13itPTmu3