Repository navigation
Commit de43524
fix(objectql): report a bound action named like one its target already lists, and registerAction replacing another owner's handler (#24526)
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 the
`domain:engine` seat 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
- **The listing face** (`packages/objectql/src/registry.ts`). The audit
sits beside `findUnboundActionTargets()` on `SchemaRegistry`, keyed on
`FIND_BOUND_ACTION_COLLISIONS`, a `unique symbol` declared in
`registry.ts` and re-exported from neither `index.ts` nor `core.ts`, so
the published face gains no nameable member (the shape of PR #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, so
`attachBoundActions` skips 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. `attachBoundActions` itself is unchanged.
- **The sameness test.** `sameActionDeclaration` compares two
declarations structurally. At the top level it leaves out the provenance
keys: every `_`-prefixed stamp (`_packageId`, `_provenance`,
`_packageVersion`, the `_lock` envelope), the `protection` block those
stamps translate, and `objectName`, 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.
- **The report** (`packages/objectql/src/plugin.ts`). At `kernel:ready`,
right after the unbound-target refusal seals, the boot logs one `warn`
per pair through `describeBoundActionCollision`. The boot always
continues. **Late artifacts are judged too.** An artifact registered
through the `manifest` service after `kernel:ready` (the
`boundActionTargetsSealed` path: 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.
- **The handler face** (`packages/objectql/src/engine.ts`,
`registerAction`). When a registration lands on an `OBJECT:ACTION` key
that another owner holds, it still replaces that owner's handler, so the
last registration runs as before. It now logs a `warn` naming 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:**
- These stay silent: the same owner registering again (a re-bind, a
reload), and the authored-action resync
(`removeActionsByPackage('metadata-service')` runs before it registers).
- A registration with no `packageName` is treated as its own owner.
- The handler face sees handlers, not declarations, so it cannot tell
"same declaration" apart from "different". When two packages carry one
declaration, the handler face warns and the listing face does not. That
is still a true statement: withdrawing the replacing package removes the
handler the other package's declaration relies on (pinned).
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 two `new
AppPlugin(...)` entries:
- `com.m.core` owns `crm_account`, with an embedded `script` action
`whoami` whose body returns `{ by: 'core' }`;
- `com.m.binder` binds a top-level `script` action `whoami`
(`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.
| case | served declaration | handler that ran | boot log before | boot
log after |
|---|---|---|---|---|
| different, core first | core's (`Who am I (core)`) | **binder's**
(`{"by":"binder"}`) | nothing | 1 listing warning (served `com.m.core`,
shadowed `com.m.binder`) + 1 handler warning (`app:com.m.binder`
replaces `app:com.m.core`) |
| different, binder first | core's | core's | nothing | 1 listing
warning (same pair) + 1 handler warning (`app:com.m.core` replaces
`app:com.m.binder`) |
| control, same declaration, core first | core's | core's body | nothing
| **0** listing warnings, 1 handler warning |
| control, same declaration, binder first | core's | core's | nothing |
**0** listing warnings, 1 handler warning |
- After the change, the served declaration and the running handler are
identical to before in every row.
- In the core-first row they disagree: core's declaration is served and
the binder's handler runs. That is pre-existing and is now reported, by
both faces.
- **Composed into one artifact instead:** `composeStacks` already
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_organization` with a
late artifact. Marketplace install-local takes an inline manifest
binding `change_slug` (`type: url`) to `sys_organization`.
- Before: nothing is logged, `change_slug` is served from
`com.objectstack.plugin-auth` (`type: api`), and the binder's
declaration is listed nowhere.
- After: one warning per arrival naming `com.objectstack.plugin-auth`
and `com.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-showcase` and `examples/app-todo` give 0 listing warnings
and 0 handler warnings each. Across the whole `@objectstack/objectql`
suite (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_ping` to `sys_organization`:
1. `GET /api/v1/meta/object/sys_organization` lists
`mrider_ping@com.m.rider`.
2. **Step 2 is refused.** `PUT /api/v1/meta/object/sys_organization`
with the served body answers `NOT_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.
3. **Step 3 is refused for a booted binder.** `DELETE
/api/v1/packages/com.m.rider` and `PATCH .../disable` both answer
`WRITABLE_PACKAGE_REQUIRED`.
4. **The one withdrawal door that exists**, marketplace install-local
`DELETE`, was measured with two late binders: `com.m.late1` binds only,
and `com.m.late2` binds and extends `sys_organization`. After both are
uninstalled, `GET /api/v1/meta/object/sys_organization` lists neither
action.
- The binding-only package never rewrote the boot-bridged
MetadataService copy, which gets the action attached at read time.
- The extending package's copy is restored by
`restoreExtendedObjectCopies` from 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 merging `origin/main` `f4755a68a`) unless
stated. Fix round 1 (`bb02d9415`) re-ran the pin file, the neighbour,
the ablations, the build, the typecheck, core `test:repo` and all 72
derived gates; the objectql full suite stands from `f679753aa`.
- **Pins:**
`packages/objectql/src/bound-action-collision-report.test.ts` has 15
tests (plus a face pin: the key is exported from neither entry, and
`SchemaRegistry.prototype` names no `findBoundActionCollisions`).
- Registry: both orders; same declaration across two packages; the
same-stack copy; a foreign-against-foreign pair; the narrowed set.
- Report: a boot warning carrying exact meta and naming its subjects;
the control boot; the late artifact; no re-report.
- Handler face: replace and warn; withdrawal leaves no handler; a
registration with no package; the silent same-owner and resync cases.
- The neighbouring `bound-action-foreign-object.test.ts` stays 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.
- **Pin sweep:** grepped the tests that pin `attachBoundActions`'
same-name skip and `registerAction`'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.
- **Ablations**, mutated on disk through `scripts/ablation-replace.mjs`,
each restored with `git diff HEAD` empty afterwards. The suite imports
`src` by relative path, so no `dist` rebuild was needed.
- Listing audit: the collision test is replaced by `continue`, and 7 of
15 tests go red.
- Handler report: the replacement guard is replaced by `false`, and 2 of
15 go red.
- Sameness: the top-level key set is replaced by a sentinel, and 3 of 15
go red (exactly the three "same declaration" controls). The first
attempt at this one was refused by the tool as a no-op, because the
replacement text contained the anchor. It was redone with a different
anchor.
- Face: `index.ts` re-exports the key, and 1 of 15 goes red (the face
pin).
- **Package checks:**
- `pnpm --filter @objectstack/objectql test` exits 0: 447 files, 8406
tests.
- `pnpm --filter @objectstack/objectql typecheck` exits 0, including
`check:test-typecheck`.
- `pnpm --filter @objectstack/core test:repo` exits 0: 6 files, 82 tests
(measured on `b848c4a36`).
- `pnpm --filter @objectstack/spec check:generated` exits 0 after the
merge, because spec moved on main's side.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, with no paths, prints 72 commands.
All 72 were run on `f679753aa` and again on `bb02d9415`, every one with
exit 0, and every exit code was captured before any pipe. Reconciling
with `--ran` gives "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN (a
DERIVED zero)". `check:dual-build-cjs-loads` first exited 3
(PREREQUISITE NOT MET: 8 packages had no `dist`), so the 8 were built
and the gate re-run, exit 0. The repo was deepened first
(`--shallow-since=2026-07-13`) for `check-engine-split-ratio --days 90`
and `check-plugin-teardown-shape`.
## Acceptance notes
- **Owner strings differ between the two faces.** The handler face names
the owner `registerAction` was given: `app:ID` from the runtime's
artifact binder, or `metadata-service`. The listing face names the
registry's package id, `ID`. Both are what each layer actually holds; no
mapping is invented.
- **Two copies of the comparison.** `sameActionDeclaration` is a local
structural comparator. `@objectstack/spec`'s `deepEqualAuthored` answers
the same "same declaration?" question, but it is `@internal` and 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.
- **Who becomes the replacing owner.** After a cross-package
replacement, `removeActionsByPackage(loser)` removes nothing and
`removeActionsByPackage(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.
- **Measurement fixtures.** The scratch compositions lived under
`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.com/claude-code)
https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT
---
_Generated by [Claude
Code](https://claude.ai/code/session_01PmE7dZP8v5iv3z1dgybAtT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ba4afcc commit de43524
5 files changed
Lines changed: 429 additions & 3 deletions
File tree
- .changeset
- packages/objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5563 | 5563 | | |
5564 | 5564 | | |
5565 | 5565 | | |
| 5566 | + | |
| 5567 | + | |
| 5568 | + | |
| 5569 | + | |
| 5570 | + | |
| 5571 | + | |
| 5572 | + | |
5566 | 5573 | | |
5567 | 5574 | | |
5568 | 5575 | | |
| 5576 | + | |
| 5577 | + | |
| 5578 | + | |
| 5579 | + | |
| 5580 | + | |
| 5581 | + | |
| 5582 | + | |
| 5583 | + | |
| 5584 | + | |
| 5585 | + | |
| 5586 | + | |
| 5587 | + | |
5569 | 5588 | | |
5570 | 5589 | | |
5571 | 5590 | | |
| |||
0 commit comments