Repository navigation
fix(spec)!: page action rows carry ActionSchema's declarative-update rules, read from one judge - #23494
Conversation
…hared module, texts byte-equal The mixing rule between `operation: 'update'` and the rest of the action vocabulary now lives in `action-declarative-update.ts`, parameterised by a surface (the executor key, whether it defaults to 'script', the onSuccess subject, whether params has a static-values object form). ActionSchema applies it with its own surface; every issue it raises is unchanged (152-case corpus, 572 issues, same sha256 before and after). Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…rules from the one judge
action:button, action:icon and each action:group / action:menu member now
apply the shared declarative-update judge on their own surface: patch only
beside operation 'update'; beside it no executor but 'script', no target,
method, bodyExtra, bodyShape or (action:button) recordIdField, something to
write, and no list_toolbar location. A row's patch is ActionSchema's own
record by reference. The rows' url-target, executor-alias and onSuccess
executor judgments stand down beside the operation, which the runner reads
first. Landed pins that put api / a host handler beside the update now name
a valid executor; the unknown-members ledger drops patch for patch{}.
Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy
Co-authored-by: Claude <noreply@anthropic.com>
…-update rules Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…n and patch Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…w-declarative-update-rules
…e version cut wrote it The merge followed main's rename into .changeset/pre/ and carried an edit into consumed stock; that text now lives in packages/spec/CHANGELOG.md (18.0.0-next.1), which a code PR does not edit. Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…w-declarative-update-rules
…w-declarative-update-rules
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 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 cb634d5858bded54450fe0f02ff7d52aae1643bc && git checkout cb634d5858bded54450fe0f02ff7d52aae1643bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 873151021f640a7a2b8675f2c38ff10ec8a7230b d4f80dc9db96796d8ce8623979140d2ec4cac0e8 && git checkout -B drift-repro 873151021f640a7a2b8675f2c38ff10ec8a7230b && git merge --no-ff d4f80dc9db96796d8ce8623979140d2ec4cac0e8
node scripts/docs-audit/affected-docs.mjs --json 873151021f640a7a2b8675f2c38ff10ec8a7230b
|
席位核对ACCEPT · The PR queues after one 1. Shape
2. Premises measured
3. Gates
4. Deviations accepted
5. Out of scope
6. Landing: Tier S, ordinary queue, after the contract review's PASS on this head and an all-green head. Generated by Claude Code |
Contract reviewServed-tier: Inputs: card #23427 (body; comments Check-runs on the head as found, not awaited: 39 in all — 24 success, 5 skipped (Console Pin Gate, Temporal Conformance, the Dogfood matrix, Dogfood Verify CLI, Packed-tarball smoke), 10 in progress (Test Core 1-6, Build Core, Type Check · workspace, · consumer gates, · debt ledger), 0 failures. Green and load-bearing here: Check Changeset (the job that hosts the ADR-0087 disposition step), Governed Surface Queue Guard, Lint & Repo Gates, Spec property liveness, the two claim checks. ① Derived judgments(a) The four rows refuse the card's five shapes, plus the carried keys — right. The judge
(b) One place — right; (c) The sixth narrowing — in scope, correctly disclosed. (d) Standing down — nothing widens. Three row judgments now return early on (e) Runtime alignment — matches. At the pin ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23427
Clause-②: no (narrowing)
The page action rows are
action:button,action:icon, and eachaction:group/action:menumember. They now applyActionSchema's declarative-update rules through one judge that both read. That closes the five members this closing card names and gives every rule in the set a verdict on the rows. Direction of record: the family's triage (6154972534, consistent with #23168 answer C6148462268) and this card's triage (6164739115).What changed
packages/spec/src/ui/action-declarative-update.tsholds the declarative-update rules and their texts. It replaces the table and refinement that lived privately inaction.zod.ts.ActionSchemacalls it with its own surface, and each row calls it with the row's surface.type/actionType), whether that key defaults to'script', theonSuccesssubject, and whetherparamshas a static-values object form.action-on-success-executor.ts, and is not in theuibarrel.ActionSchemais byte-identical. A one-time corpus dump compared before and after: 152 action shapes covering every rule, every refused key, everytype, and their combinations. Before (at7cfc3400c) and after the extraction, both dumps hold 572 issues with sha256c359828f357004883ce1b0ac4df598190839bcfdaa87898d094eb7469f54027d, andcmpreports the two files byte-identical.action-row-update.test.tsis unchanged and green.component.zod.ts) run the judge first, as the runner readsoperationfirst:.superRefine(rowDeclarativeUpdate(...))on all four rows. A row'spatchis nowActionSchema's own record, by reference (ActionSchema.in.shape.patch.unwrap()), the same way spec(ui): a page action row withoperation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332 tookoperation.operation: 'update', the rows' url-target judgment, the'navigation'rename and the executor half of theonSuccessrule stand down. The judge refuses the keys they would judge (target, any executor but'script',onSuccess) whole. So a row is never told both to writeactionType: 'url'and to drop the key. Nothing becomes accepted: each standing-down case is refused at the same key by the judge.H1: every rule of
ActionSchema's set, with its verdict on the rowsThe rules are enumerated from the code (
refuseDeclarativeUpdateContradictionsand its refused-key table at7cfc3400c), not from the docblock.ActionSchemarulepatchwithoutoperation: 'update'patch'script'typeactionTypeon a node andtypeon a member (card member 3). Reasoned difference, text only: a row's key has no default, so the text says the key "names that route or nothing" where the action's says "stays at its default'script'". An empty row key names nothing, aseffectiveRowExecutoralready reads it. A host-handler name is refused too, because the runner never dispatches it beside the update.targettargetmethod,bodyExtra,bodyShaperecordIdFieldaction:button. Not applicable onaction:iconand members, which do not declare it, so the strict row refuses it as an unknown key.onSuccessonSuccessbody,recordIdParam,outcomeMessages,opensInNewTab,newTabUrlunrecognized_keysbefore any refinement runs (pinned)patchaction:button/action:icona non-empty objectparamscounts. On a node,paramsISproperties.params, the static values the renderer forwards (readStaticParamValues), and the runner's update merges them overpatch(executeUpdateOperation). On a member an objectparamsnever reaches a non-apiexecutor, so it does not count.list_toolbarlocationlocations.Nlocations(card member 5)objectName(deliberately not judged in the schema;defineStack's walk judgesstack.actions)objectNameor on the page's object, which the row cannot see. See Acceptance notes.patchkey's own schema, a recordpatchpatchas something to write, which holds only for a record. The runner drops any otherpatchbefore the write.Neighbours outside the set that read
operation:bodyortarget" rule has nothing to judge on a row, because a row with no executor takes the runner's name leg.undoablerule is not carried by any row; that is outside this family (Acceptance notes).H2: how the rows spell things (measured)
actionTypeand a member spells ittype. A node'stypeis the component discriminator.operation,patch,params,target,method,bodyExtra,bodyShape,locationsandonSuccess;action:buttonalso forwardsrecordIdField. Measured at objectui's.objectui-shapinb9814b334: the forward blocks inaction-button.tsx,action-icon.tsx,action-group.tsxandaction-menu.tsx(first linetype: schema.actionType/type: action.type).locations. All four rows declare it, the sameActionLocationSchema.ActionRunner.executereadsoperationbefore any executor (:1384-1388at the pin, byte-identical at objectuimain524136e).executeUpdateOperationposts through the registeredscriptdispatch and mergespatchonly when it is a plain object. The platform action route treatsoperation: 'update'as the write "whatevertypesays" (isDeclarativeUpdateAction).H3: why not call
refuseDeclarativeUpdateContradictionsas-isIts paths are relative to the refined schema, so a member's issue lands at
actions.N.KEY. Four things still kept it from being called on a row unchanged:type;onSuccesstext opens with the action's subject;type: 'api'/type: 'script', which a node must spellactionType: '…', because on a page componenttypeis the component;So the judge is extracted and parameterized. Each text is a function of the surface, and on
ActionSchema's surface it is the action's text byte-for-byte (proof above). The rows' pins derive the expected text fromActionSchema's own parse; they do not restate it.H4: producer sweep (counts)
examples/**patchoroperationapp-showcaseui/pages/index.ts,ui/actions/index.ts), and neither has either key. The 6operation: 'update'hits arebulkActionDefsand a permission set.packages/cli/src/lint/corpus.ts)packages/qa/dogfood)packages/**test fixtures'script'executor beside the updatespeccomponent-action-row-on-success-executor.test.ts: itsBESIDElist hadapi/run, giving 16 cases plus 4 "ONE TEXT" cases that read anapirow.lintvalidate-component-props-row-on-success.test.ts: 2it.eachover 4 rows withapi. Both now name'script', and the other executors beside the update are pinned as refused in the new file. The unknown-members ledger movespatchtopatch{}(records).skills/**, hand-written docsgit fetch)b9814b334and atmain524136e(same list at both), 63 files mentionoperation: 'update', and 15 of them also mention a page action row. None parses such a row against the spec; the spec reads are key-set reads andsafeParse({ size }). One renderer test,action-button-icon-inputs-11168.test.tsx, mounts{ actionType: 'script', patch: { stage: 'done' } }(member 1's shape) at runtime as a control that nothing is written. No spec parse judges it, so no objectui test goes red at the next spec bump. objectui's ownActionDefaultInspectoralready lists the same refused keys andlist_toolbar(UPDATE_OPERATION_REFUSES).ADR-0087
18.ui-page-action-row-declarative-update-rules; the changeset marker isregistered, and the gate reports it new here.operation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332's18.ui-page-action-row-operation-declaredcovers only theoperationvalue andonSuccess, so it is notalready-registered.ui-page-action-row-declarative-update-rulesrefuses it".Tests
All readings come from the verify lock on a shared box. Each exit code was captured before any pipe.
At the final head
d4f80dc9d(after mergingmainat1baf976cf):@objectstack/specunit layer (vitest run --project local --maxWorkers=2): 677 files, 20,691 passed, 1 todo, exit 0.@objectstack/spectypecheck(tsc + scripts + test-typecheck): exit 0.@objectstack/lintunit (vitest run --maxWorkers=2): 139 files, 6,726 passed, exit 0. This layer includes the props-gate door pins.turbo run build --filter=!@objectstack/docs --concurrency=2): 75/75 tasks, exit 0.At
2541bab8c(after merging #23454's step-18 entry):@objectstack/cliunit layer (--project unit): 284 of 287 files passed. The other three were not a reading of this diff:published-subpath-console.pinandpublished-subpath-hook-body.pinrefused becausepackages/cliitself was unbuilt (a prerequisite);hook-timeout-override-refusaltimed out twice at 5 s while spawning vitest on the loaded box.pnpm --filter @objectstack/cli build, those three files: 33/33 passed.The new pins:
component-action-row-declarative-update.test.ts, every row, with code and path asserted at the parse:ActionSchema's own parse;unrecognized_keys;paramsas the source of values, andpatchas the action's record by identity;Six targeted files (the new pins, #23332's pins,
action-row-update.test.ts, the unknown-members ledger and two neighbours): 434 passed.Ablation (one, on the rows' call)
On the committed tree,
scripts/ablation-replace.mjsreplacedaction:button'ssuperRefine(rowDeclarativeUpdate('actionType', …))withsuperRefine(() => undefined).54e3895c3d86→5f55315f502d). The tests resolvecomponent.zod.tsby relative import fromsrc, so nodistwas involved.operation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332's pin file.action:buttonrefusal pins: the five members, ONE TEXT, the table keys, the executor edges,params, the stand-down, and spec(ui): a page action row withoperation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332's update pairing.HEAD(54e3895c3d86) andgit diff HEADis empty, checked both by the tool and by an absolute-path trap. The same two files then ran 282 passed.Gates
Derived set.
node scripts/pm/dispatch-gates.mjs --commands(no paths; the change set comes from the merge base) gives 111 families, run at the final headd4f80dc9dwith the tool's capture idiom.--ranreconciles them as111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN(a derived zero: every family recorded an exit code).2541bab8c:check:skill-examplesandcheck:dual-build-cjs-loads. After the full build, both exit 0 atd4f80dc9d.Targeted spec gates.
pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.node scripts/check-adr-0087-registration.mjs --base origin/main:registered ui-page-action-row-declarative-update-rules (new here).Stale tree, declared.
origin/mainmoved 8 commits past this head while the battery ran, with no file in this PR's surface. They changed four inputs of the derivation (scripts/check-durability-degradation-log-level.mjs,scripts/measure-durability-swallow-family.mjs,scripts/engine-double-contract.pinned.json, and onescripts/adr-anchors/JSON). CI's runs on the merge ref are the reading for those families.Deviations from the claim's file surface
packages/lint/src/validate-component-props-row-on-success.test.ts(fixture only). The dispatch fencespackages/lint/**off; os-dev.md's fixture-triage clause requires sweeping by the rule's consumption radius.apibesideoperation: 'update', card member 3 itself, so they would go red. They now name'script'.18.ui-page-action-row-operation-declared.ts: the one-clause amendment above.patchby reference. An in-place fix of the same defect class (rows accept whatActionSchemarefuses on the declarative update), applied under os-dev's four conditions:operation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332 shape;patch: 'done'row that writes nothing.Acceptance notes (observations, not filed)
packages/spec/CHANGELOG.mdunder18.0.0-next.1(lines 559 and 565 at08ea7e368) carries spec(ui): a page action row withoperation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332's FROM → TO row and example{ actionType: 'api', operation: 'update', patch: …, onSuccess: … }→ "the same row withoutonSuccess… parses unchanged". After this PR, that row is still refused atactionType..changeset/pre/23332-page-action-row-operation.md).mainfirst carried an edit into thepre/copy through rename detection, and0c8c54fd8restores it byte-identical tomain.objectNameon a row. No page-level walk judges an update row on a page that binds no object and names noobjectName. Not measured as a runtime defect.nameand writes{ ...declared patch, ...params }, enforcing the declared param contract. So a page row's update writes only when itsnamenames a registeredoperation: 'update'action on the object. Read-only inference, NOT MEASURED against a running server.recordIdField. objectui'sresolveServerActionRecordIdreads it when it resolves the record an update writes. The contract refuses it beside the update, and objectui's inspector mirrors that refusal, so the read is incidental, not declared.undoableonaction:button. The rows carry none ofActionSchema'sundoablerule.propertiesis not parsed on the metadata save or load path". It is true atmainwhile feat(lint)!: the two page component rules run on the runtime publish door (ruling B) #23418 is open; whichever of the two lands later aligns it, as the spec(ui): a page action row withoperation: 'update'andonSuccessparses, whileActionSchemarefuses the same shape — the rows typeoperationasz.unknown()and never carried the rule #23332 report noted.Generated by Claude Code