Repository navigation
fix(objectql, metadata-protocol, cloud-connection)!: a declared .custom action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it - #30612
Conversation
…d action override's door and boot-order loose ends - install door: judge a declared action override against the base's code artifact, not the listing that serves the armed override - protocol door: a declared action override's base is keyed <objectName>:<name>, as os validate and the install door key it - registerAction: only the declared base's handler slots beneath an armed override; any other registration replaces it and is warned Claude-Session: https://claude.ai/code/session_01Utp6CXTiQqG1Y4zdXMck85 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXTiQqG1Y4zdXMck85 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 30 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 3bab68ea9259cb0a4ba90b67313ff27286e2d8f0 && git checkout 3bab68ea9259cb0a4ba90b67313ff27286e2d8f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 135c2fb82fcd2aeee208b6a8a372209c80655489 accb6240985b185dd97422ccb4504f535631aded && git checkout -B drift-repro 135c2fb82fcd2aeee208b6a8a372209c80655489 && git merge --no-ff accb6240985b185dd97422ccb4504f535631aded
node scripts/docs-audit/affected-docs.mjs --json 135c2fb82fcd2aeee208b6a8a372209c80655489
|
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver levelInconsistent with the ruling — must-fix M1. The dev declares ③ Boundary flags
④ Design judgment (narrowing face)Judged against the published spec on Must-fix: M1 (②). Should-fix: S1, S2 (③), both cards for the lane seat, not changes to this PR. Observations: ③ object-key spelling. Implemented-by: VERDICT: FAIL Must-fixes:
|
… none, patch The protocol did not narrow: os validate and the install door already refused a moved-object override; the save door caught up. Claude-Session: https://claude.ai/code/session_01Utp6CXTiQqG1Y4zdXMck85 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract review (delta)Served-tier: ① Derived judgments
② Semver level
③ Boundary flagsNone new. The scope sentence for #30505 is on that card (seat note). The should-fix about the refusal message is filed as #30623. Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC 维护者速读(席位终稿)
席位意见:同意落地。三道门从此给出同一个答案;唯一的收窄是保存门补上另两道门早有的拒绝。 席位核对ACCEPT · head
|
domain:server#3 · os-bill · session_01Utp6CXTiQqG1Y4zdXMck85
Requested by Bill · project thread
Fixes #30542
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: follow-up #30505
维护者速读
OBJECT:NAME认,和os validate、安装门一致;只有覆盖声明的那个基包的 handler 才垫在覆盖下面,别的注册(包括不带包名的)照常替换并告警。.custom行在真基包注册前绑上后会一直跑,而读已经不认它;不带包名的注册垫到覆盖下面时文档说告警、实际没有。NOT_OVERRIDABLE,规则declared_package_ships_no_such_item),这条形状os validate和安装门本来就拒。其余三处只改判断依据或日志。单一提供方的正常部署不受影响。回滚即 revert。engine-actions.ts的arrivesAsDeclaredBase(运行时谁能垫在覆盖下面);protocol-packaged-base-refusal.ts的onItsObject;marketplace-install-local-plugin.ts的artifactOf。pnpm --filter @objectstack/objectql exec vitest run src/plugin-authored-actions.test.ts src/protocol-flow-custom-override.test.ts,全绿(24 + 14)。What changes
Four loose ends PR #30497 left in the declared
.customaction override (ADR-0146 D9), from its contract reviews (round 0 ③-2, ③-3; delta ③-7, ③-8). Each was measured onmain(db04e5ce5) first, on the real engine, protocol and install door; all four reproduced.1. Install-door ratchet (
packages/cloud-connection/src/marketplace-install-local-plugin.ts)registry.getArtifactItem(type, name, base), kept only when it is that base's, at the sameOBJECT:NAMEidentity) asbaseItemOf, the item the protocol door'soverrideWriteVerdictjudges against. A registry that answers no artifact leaves the listed item, as before. A base delivered in the same install is still read off its body.2.
objectNamepinned at the protocol door (packages/metadata-protocol/src/protocol-packaged-base-refusal.ts)overrideWriteVerdictkeeps, for a declared action override, only the shipped artifacts on the override's ownobjectName(as shippers and asbaseItemOf). An override that moves the base's action to another object therefore finds no shipper and is refuseddeclared_package_ships_no_such_item: the ruleos validateand the install door already gave it, since both key an actionOBJECT:NAME(overrideWriteItemsOf). One private helper,overrideWriteActionObjectOf, now spells that object for both.actionrow inITEM_KEY_DISCRIMINATORS: that table also keys the registry's item collections and the protocol's list merge, so a row there would re-key every action read, far wider than this door. An undeclared item is judged exactly as before.3. Only the declared base's handler slots beneath an override (
packages/objectql/src/engine-actions.ts,engine.ts,plugin.ts)ObjectQL#registerActionOverridetakes an optional fifth argument,base: the package the override declares (overrides.package). The authored-action resync passes it, and the entry records it.registerActionunder a key such an override holds slots the arriving handler beneath it only when the registry's code artifact of the action (getArtifactItem('action', name)) is that package's, on this object. That is the judgement the resync itself binds the override by once the artifact is registered, and every path registers a package's items before it binds its handlers (AppPlugin.start, the marketplace rehydrate, the install). Any other registration replaces the override and is warned like every other handler collision.OBJECT:NAME, bound on its own declaration before the real base registered, is replaced by the real base's handler when it arrives: the run now agrees with the reads, which decline that row for the real base. The next resync judges the row against the registered artifact and does not bind it. Chosen overrehydrateannouncingmetadata:reloaded: that signal re-runs every consumer'skernel:readysweep (flows, security, translations) from inside the install plugin's ownkernel:ready, which is neither cheap nor safe at boot.basekeeps taking any package's handler beneath it.4. Log level: warn (
engine-actions.ts,engine.tsdocblock)registerAction's docblock promises for an unpackaged registration displacing a package's handler. Before, it slotted beneath atdebug. The docblock now names the exception precisely (the declared base's handler only).Declarations.
Clause-②: yes (narrowing), BREAKING: item 2 refuses at the save and publish doors a declared override the save door admitted before. Item 1 admits at the install door what the save door already admitted (a widening arm, outranked by the narrowing).Metadata-protocol: none: what an author can write did not narrow, becauseos validate(findOverrideDeclarationRefusals) and the managed install already keyed an action OBJECT:NAME onmainand refused a moved-object override; the save door caught up with them (the #30095 precedent). The changeset carries the ADR-0087 disposition (not-required (no-migration-prescription): no schema key moves; nothing at rest is rewritten). Items 3 and 4 change which handler runs and a log level, not what an author can write.patchon the three packages the diff moves.Tests
packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts, newdescribe"[objectql: a declared .custom action override has four door and boot-order loose ends left by PR #30497 #30542] the install door judges a declared action override against the base's code artifact": with the armed v1 served by the listing and the base's own action in the registry, a v2 back at the base's gates installs (200, saved), and a v2 that drops a gate the base carries is still refused 403declared_action_widens_access, nothing written. The harness's registry gains an optionalgetArtifactItem; the existing cases pass none and read as before.packages/objectql/src/protocol-flow-custom-override.test.ts, new case in the[#30065]describe(real protocol over a real engine): a moved-object override is refused at the save door (code+status),overrideWriteVerdictanswers ruledeclared_package_ships_no_such_item, nothing is stored and every read still serves the base; control: the same declaration on the base's object is admitted.packages/objectql/src/plugin-authored-actions.test.ts, two new cases on a real engine: the other-base row bound before the base registers runs until the real base binds, then the real base's body runs, one collision warning names both owners,isServedOverrideRowOfdeclines the row for the real base (and admits it for its own), and the next resync leaves the base bound; an unpackaged registration under an armed override replaces it with one warning naming both owners.Measurement on main (db04e5c), then on the fix
Throwaway probes (deleted) on the real engine, protocol and install door, the same harness the #30497 tests use:
mainV1undercom.example.showcaseevaluateOverrideWriteGate), same v2declared_action_widens_accessobjectNamemovedNOT_OVERRIDABLEdeclared_package_ships_no_such_itemos validate(findOverrideDeclarationRefusals)declared_package_ships_no_such_itemran:OTHER BASEran:BASE BODYisServedOverrideRowOf(row, realBase)(the reads' predicate)registerActionunder an armed overrideAblations, one per fix
Each through
scripts/ablation-replace.mjs(literal anchor, on-disk count, restore by trap), the pin run under the mutation:artifactOf(...) ?? held→held: exactly "a v2 that relaxes the v1 tightening back to the base's own gates installs" fails (1 failed, 11 passed). A first attempt with the bare replacementheldwas refused by the tool as a no-op (the count did not move), so it did not run; the second used a marked replacement.metadata-protocol, read fromdist/): the object comparison →!== 'ablate-30542-a2', rebuilt,ablation-dist-preflightfound the marker in 2 built files; exactly "an override that moves the action to another object is refused…" fails (1 failed, 13 passed). Restore leg: rebuilt,--absentexit 0, tree clean, suite 14/14. A first attempt (true ||) was folded away by the build (marker absent fromdist/), so its suite never ran.return true: exactly "another base's declared override of the same key is replaced…" fails (1 failed, 23 passed).packageName === undefined→ slot beneath: exactly "a registration with no package under an armed override replaces it…" fails (1 failed, 23 passed).Acceptance notes
undeclared_same_nameat the protocol door whileos validateand the install door admit it (they key itOBJECT:NAMEand find no collision). That is the pre-existing, stricter direction and outside this card; it narrows nothing new.Verification
All readings on head
76e59ff17(the fix merged withorigin/mainat739c97273) unless a line says otherwise. Headaccb62409changes only the changeset's declaration line and levels (no source), so the readings below cover its source; on itcheck-changeset-no-major.mjs --base origin/mainexit 0 andcheck-adr-0087-registration.mjs --base origin/mainexit 0.plugin-authored-actions+protocol-flow-custom-override38/38, cloud-connectionmarketplace-install-local-override-gate12/12, clioverride-declarations11/11, metadata-protocolprotocol.packaged-base-refusal91/91.da065aea3): metadata-protocol 22447 passed, objectql 11023 passed, cloud-connection 880 passed;typecheckclean on all three. The merge brought in no change to the files this PR touches.!):pnpm --filter @objectstack/dogfood testafter the@objectstack/dogfood^...build, lock VERDICT command-exit 0, Test Files 420 passed | 3 skipped (423), Tests 3496 passed | 36 skipped (3532), no failure.dispatch-gates on this head: 62 derived, 62 run, 0 NOT-MEASURED
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackover the PR's change set, each family run and recordedCOMMAND :: exit N;--ranreconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. Alsopnpm --filter @objectstack/spec check:livenessexit 0. On the first run (before the merge) two families did not measure and were re-run green:check-plugin-teardown-shape --self-testneeded its pinned fixture commit in this shallow clone (fetched), andcheck:dual-build-cjs-loadsneeded every package built (PREREQUISITE NOT MET until the full build).Human Minimum
packages/specholds no runtime logic. (packages/specnot touched.)pnpm lintis CI's.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code