Repository navigation
fix(objectql): report a bound action named like one its target already lists, and registerAction replacing another owner's handler - #24526
Conversation
…ists with a different declaration, at kernel:ready and for each late artifact Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
…er the same key is replaced Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
…ion's replacement warning; changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
…ed registration Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 17 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 6ebf85042e05c375e76587d612c5db7a4b7ab157 && git checkout 6ebf85042e05c375e76587d612c5db7a4b7ab157
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04669a467eb6443abab008586ff1813b9a366200 bb02d94155ca3829199503f74b4fd4cd5410b79f && git checkout -B drift-repro 04669a467eb6443abab008586ff1813b9a366200 && git merge --no-ff bb02d94155ca3829199503f74b4fd4cd5410b79f
node scripts/docs-audit/affected-docs.mjs --json 04669a467eb6443abab008586ff1813b9a366200
|
Contract reviewServed-tier: What this record read:
① Derived judgmentsThe listing audit: right, and report-only.
The handler face: right.
The rider: measured and dropped. The card's step 2 answers The pins: 14 cases in ② Semver level — does not hold
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…symbol, so SchemaRegistry's published face gains no nameable member Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
Contract reviewServed-tier: What this record read:
① Derived judgmentsWhat moved, and what did not.
The audit is gone from every nameable surface.
The face pin is load-bearing.
② Semver level — holds
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body says Fixes #24472. 5 files, +429/−3. Not governed: check-governed-merges on the final file list matched 0 of 5 paths. |
| Scope | Listing: SchemaRegistry's audit, keyed on FIND_BOUND_ACTION_COLLISIONS, reports each bound action that its target does not list because the target already lists a different declaration under the same name. The plugin warns once per pair at kernel:ready, and for each late artifact. Handler: registerAction warns when another owner's OBJECT:ACTION key is replaced. Report only: nothing is refused, the router's precedence is unchanged, and there is no new error code. This is the shape of maintainer decision 6262054499. |
| Seat's own read | The fix commit re-keys one member and its two call sites; nothing else moves. The key is a unique symbol that neither index.ts nor core.ts re-exports, and no other package names it. The changeset is '@objectstack/objectql': patch with Clause-②: no. |
| Review | TIER_DEFAULT: record 6267837597 was FAIL on ② only (a public method on the exported SchemaRegistry). The delta 6268615812 is PASS on this head. |
| Declaration | Clause-②: no on the claim, the PR body and the changeset. The PR body was patched to this head (the symbol key, and the 15-pin and ablation counts). |
| Cross-lane | None. Every path is in packages/objectql, and the PR stays clear of the registry.ts extend refusal that #24341 holds. |
| Gates | Dev, on this head: 72 derived, 72 run, 72 exit 0. Core test:repo 82/82. The objectql full suite was run on f679753aa (447 files, 8406 tests); Test Core 1/6–6/6 run it again on this head. |
| CI | At this read on bb02d94155: ci-failure GREEN, all 40 check-runs completed, none failed. Mergeable, clean. |
Disposition:
open_questions: all accepted, per record 6267837597 ③:- the boot report is the listing (no
/metakey); - late artifacts are judged;
- the sameness key set is right.
- the boot report is the listing (no
deviations: all accepted (records 6267837597 ③ and 6268615812 ③). The one deviation this round, the objectql full suite not re-run, is accepted because CI re-ran it.out_of_scope_findings: none filed. The orphaned handler after the winner's withdrawal falls inside the maintainer's refusal decision; it is stated in the warning and pinned.- Downstream: platform-objects sys_organization: change_slug targets a cloud-only route, so on an open-source multi-organization composition the record page offers Change Slug and the click answers 404 #24318 reads this card's outcome; the seat claims it after landing.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24472 then closes the card.
Generated by Claude Code
Fixes #24472
Clause-②: no (a warning only: no accept set narrows, no door answer moves, and no published surface gains a key; owes the seat's review of record before the queue)
Dev session
session_01PmE7dZP8v5iv3z1dgybAtT, dispatched by thedomain:engineseat 1 (session_01JtSQQ3EWsbNPzyYMug1tXy) under claim 6264504693. It follows the maintainer decision 6262054499 ("18.x, report first"): the collision is reported, and nothing is refused.What changes
packages/objectql/src/registry.ts). The audit sits besidefindUnboundActionTargets()onSchemaRegistry, keyed onFIND_BOUND_ACTION_COLLISIONS, aunique symboldeclared inregistry.tsand re-exported from neitherindex.tsnorcore.ts, so the published face gains no nameable member (the shape of PR fix(objectql): a runAs:'user' hook sees its organization's members in sys_user, as the user's own request does #24518). The plugin calls it through that key. It returns each bound top-level action that its target object does not list, because the object already lists a different declaration under the same name. That declaration is either the object's own (rung 1 before rung 2, soattachBoundActionsskips the name) or another package's bound action that was attached first. Each pair names the object, the action, the package whose declaration is served, and the package whose declaration is not listed.attachBoundActionsitself is unchanged.sameActionDeclarationcompares two declarations structurally. At the top level it leaves out the provenance keys: every_-prefixed stamp (_packageId,_provenance,_packageVersion, the_lockenvelope), theprotectionblock those stamps translate, andobjectName, which an object's own action leaves to its position. A pair that is one declaration carried twice is not a collision. Two cases are pinned: a same-stack bound action merged onto its object and kept at the top level (in compiled-artifact shape, so two copies), and one declaration shipped by two packages.packages/objectql/src/plugin.ts). Atkernel:ready, right after the unbound-target refusal seals, the boot logs onewarnper pair throughdescribeBoundActionCollision. The boot always continues. Late artifacts are judged too. An artifact registered through themanifestservice afterkernel:ready(theboundActionTargetsSealedpath: a marketplace install, a ledger rehydrate) is judged after it registers, and only for pairs that name one of its packages on either side. A pair the boot already reported is not reported again. The judgment comes after registration because a warning has nothing to refuse beforehand.packages/objectql/src/engine.ts,registerAction). When a registration lands on anOBJECT:ACTIONkey that another owner holds, it still replaces that owner's handler, so the last registration runs as before. It now logs awarnnaming both owners and the consequence: the action door runs the new owner's handler whatever declaration the object lists, and withdrawing that owner leaves the action with no handler. The line drawn:removeActionsByPackage('metadata-service')runs before it registers).packageNameis treated as its own owner.Not built, by the ruling and the claim: no refusal, no new error code, no new key on the served object, and no change to the router's precedence.
Measurement M (booted composition, before and after)
Composition.
os dev --fresh, using the CLI built from this tree. The host config composes two independent packages as twonew AppPlugin(...)entries:com.m.coreownscrm_account, with an embeddedscriptactionwhoamiwhose body returns{ by: 'core' };com.m.binderbinds a top-levelscriptactionwhoami(objectName: 'crm_account', body{ by: 'binder' }) and extends the object with one field.Registration order is the plugin order. Each row is a fresh boot:
GET /api/v1/meta/object/crm_account,POST /api/v1/actions/crm_account/whoami, then a grep of the boot log.Who am I (core)){"by":"binder"})com.m.core, shadowedcom.m.binder) + 1 handler warning (app:com.m.binderreplacesapp:com.m.core)app:com.m.corereplacesapp:com.m.binder)composeStacksalready refuses the same pair at build time, identical declarations included ("cross-stack action key collision"). So the runtime collision exists only between separately installed packages, and that is the population measured here.The named producer's shape (#24318), live.
sys_organizationwith a late artifact. Marketplace install-local takes an inline manifest bindingchange_slug(type: url) tosys_organization.change_slugis served fromcom.objectstack.plugin-auth(type: api), and the binder's declaration is listed nowhere.com.objectstack.plugin-authandcom.m.late3. It fired once when the ledger rehydrated at boot, through the sealed door, and once more on re-install. The served object is unchanged.Volume on stock boots, after:
examples/app-crm,examples/app-showcaseandexamples/app-todogive 0 listing warnings and 0 handler warnings each. Across the whole@objectstack/objectqlsuite (447 files), the handler warning fires 3 times, all from this PR's own pins.Rider: stale attached copy after uninstall. Measured, not live, dropped
The card's steps, run on a booted composition with one package binding
mrider_pingtosys_organization:GET /api/v1/meta/object/sys_organizationlistsmrider_ping@com.m.rider.PUT /api/v1/meta/object/sys_organizationwith the served body answersNOT_OVERRIDABLE("provided by a managed package and is sealed"), so no stored overlay can be written back from a served read through the REST door.DELETE /api/v1/packages/com.m.riderandPATCH .../disableboth answerWRITABLE_PACKAGE_REQUIRED.DELETE, was measured with two late binders:com.m.late1binds only, andcom.m.late2binds and extendssys_organization. After both are uninstalled,GET /api/v1/meta/object/sys_organizationlists neither action.restoreExtendedObjectCopiesfrom the registry's current definition.The served answer is right on every reachable path, so nothing is built for the rider.
The optional
boundCodeActions()index rider was not taken. The boot audit walks the bound actions once, each against a cached fold, and this PR does not otherwise touch that function.Verification
All on head
f679753aa(after mergingorigin/mainf4755a68a) unless stated. Fix round 1 (bb02d9415) re-ran the pin file, the neighbour, the ablations, the build, the typecheck, coretest:repoand all 72 derived gates; the objectql full suite stands fromf679753aa.packages/objectql/src/bound-action-collision-report.test.tshas 15 tests (plus a face pin: the key is exported from neither entry, andSchemaRegistry.prototypenames nofindBoundActionCollisions).bound-action-foreign-object.test.tsstays green unmoved, 14 tests. Its pin "an action the object already declares under the same name keeps the object's declaration" still holds: the served answer did not move.attachBoundActions' same-name skip andregisterAction's replacement (bound-action-foreign-object,plugin-authored-actions,action-governance,engine-repo-execute-elevation,action-activation, and the runtime binder suites). None asserts that the replacement is silent, and none moves.scripts/ablation-replace.mjs, each restored withgit diff HEADempty afterwards. The suite importssrcby relative path, so nodistrebuild was needed.continue, and 7 of 15 tests go red.false, and 2 of 15 go red.index.tsre-exports the key, and 1 of 15 goes red (the face pin).pnpm --filter @objectstack/objectql testexits 0: 447 files, 8406 tests.pnpm --filter @objectstack/objectql typecheckexits 0, includingcheck:test-typecheck.pnpm --filter @objectstack/core test:repoexits 0: 6 files, 82 tests (measured onb848c4a36).pnpm --filter @objectstack/spec check:generatedexits 0 after the merge, because spec moved on main's side.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, prints 72 commands. All 72 were run onf679753aaand again onbb02d9415, every one with exit 0, and every exit code was captured before any pipe. Reconciling with--rangives "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: 8 packages had nodist), so the 8 were built and the gate re-run, exit 0. The repo was deepened first (--shallow-since=2026-07-13) forcheck-engine-split-ratio --days 90andcheck-plugin-teardown-shape.Acceptance notes
registerActionwas given:app:IDfrom the runtime's artifact binder, ormetadata-service. The listing face names the registry's package id,ID. Both are what each layer actually holds; no mapping is invented.sameActionDeclarationis a local structural comparator.@objectstack/spec'sdeepEqualAuthoredanswers the same "same declaration?" question, but it is@internaland not exported, and exporting it would widen spec's surface. This PR now holds a second copy of that comparison. It has a provenance-key filter at the top level, which spec's does not need.removeActionsByPackage(loser)removes nothing andremoveActionsByPackage(winner)removes the key. The handler warning states this consequence. Changing it would mean changing which handler runs, which is out of scope by the ruling.examples/_m24472*during the measurement and were deleted. The repository's tree is unchanged apart from the five files listed.🤖 Generated with Claude Code
https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
Generated by Claude Code