Uh oh!
There was an error while loading. Please reload this page.
fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476
Conversation
…ackage ownership refusal The ADR-0029 D3 refusal (a package claiming `own` on an object name another package already owns) threw a bare `Error`. It is now `ObjectOwnershipConflictError` — `code: 'OBJECT_OWNERSHIP_CONFLICT'`, `status: 422`, the two package ids and the object name as fields — with the message text byte-for-byte unchanged. The dispatcher error-code vocabulary gains its classification row (boot-refusal, door none, measured), the existing subject tests assert the envelope instead of a bare throw, and a new pin covers the class, the message fence and the D9 §6.1 late-install branch that must stay a non-refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… (check:doc-authoring) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 33 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 a58d7065324a9995e930b5d02a69b8870be07cd1 && git checkout a58d7065324a9995e930b5d02a69b8870be07cd1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5edc25edf41ff398dd70ae69d0a7034be8c96609 f8ff0a8442655448c0f28a45f0f24f80431476d4 && git checkout -B drift-repro 5edc25edf41ff398dd70ae69d0a7034be8c96609 && git merge --no-ff f8ff0a8442655448c0f28a45f0f24f80431476d4
node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609
|
os-musk
commented
Sep 2, 2026
Landing provenance — engine execution seat (session Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read Flip pre-checks on head Action: Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14367
SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claimingownon an object name a DIFFERENT package already owns) threw a bareError— nocode, nostatus— so no rejection test on that path could assert an ADR-0112 envelope, and a throw-shaped assertion one layer up stayed green through an ablation of the check it named (the card's measurement). It now throwsObjectOwnershipConflictError:code: 'OBJECT_OWNERSHIP_CONFLICT',status: 422, plusobjectName/existingPackageId/incomingPackageId— the same shape as the siblingArtifactObjectNameConflictError(DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.What changed
packages/objectql/src/registry.tsObjectOwnershipConflictErrorbesideArtifactObjectNameConflictError(class shape matched:readonly code,readonly status = 422, the three identity fields,this.nameset). The two package-id fields are typedstring | undefinedrather thanstringbecause that is what the site holds (ObjectContributor.packageIdisstring | undefinedsince finding(objectql):SchemaRegistry.registerObjectrequirespackageIdwhile its siblingregisterItemdeclares it optional — 14 ledgered TS2554 in packages/rest and 68 identical call sites in packages no tsc program reads #12623, andregisterObject's ownpackageIdis optional), which is also what keeps the message identical on a package-less call.ownership: 'own'/existingOwner.packageId !== packageIdbranch throws the class instead ofnew Error(...); message template unchanged.registerObjectdocblock's@throws Errorline now names the class (one line in the method's own contract; declared as a deviation below).packages/runtime/src/dispatcher-error-vocabulary.ts— one classification row besideDUPLICATE_ARTIFACT_OBJECT_NAME:shape: 'classfield',door: 'none',verdict: 'boot-refusal', with the measured call chain as itswhy(see Reachability).registry-object-overlay-layer.test.ts:294and:454— upgraded fromtoThrow(/already owned by package .../)totoMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 })+ the same message regex, through a localrefusalOfhelper.metadata-facade.test.ts:205— upgraded fromrejects.toThrow(/.../)torejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).registry-ownership-refusal-envelope.test.ts(5 cases): instance + envelope + fields; message byte-identical to the legacy text (full-string equality, not substring); the class constructible on its own with the same text; the ADR-0029 D9 §6.1 late-install branch (tenant-authored sitting owner) throws nothing; the message's own remedy (extendfrom the other package) is accepted.protocol-commit-history.test.ts:1008(asserts aconsole.warnsubstring is ABSENT — a forwarding test, and the text it filters on is unchanged);protocol-object-overlay-layer.test.ts:505andprotocol-boot-object-package-binding.test.ts:31,:287(comments narrating the pre-D9 history, no assertion on this refusal);protocol-writepath-object-ownership.test.ts:16(header prose);activation-ledger-registration.test.ts:18/packaged-activation-ledger-reach.dogfood.test.ts:41(comments quoting the message as the reason a MOVE was the only shape).registry-artifact-co-ownership.test.tsasserts the sibling code, not this one; re-run green..changeset/registry-object-ownership-refusal-envelope.md—@objectstack/objectql: patch,@objectstack/runtime: patch(one file, both packages, the PR feat(objectql): admit same-artifact co-owners at the install gate, and refuse two of them defining one object name (ADR-0130 D1+D3) #14354 shape). NoBREAKING: nothing is narrowed or widened; no accept-set change.Reachability (Zone 2 #3, measured on
origin/main@a98b61b3eand on this tree)registerObjectis the ONE spelling of this refusal (git grep 'cannot claim ownership'— one source hit;'already owned by package'inregistry.ts— two hits, the other is the siblingArtifactObjectNameConflictErrormessage). Every path to it either aborts boot or catches below any HTTP door:ObjectQL.registerApp(engine.ts) propagates it toManifestService.register()(objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-timemanifest.register()inside plugin init (runtime/src/app-plugin.ts, the platform app plugins, the service plugins) where a throw aborts boot; the marketplace rehydrate loop (per-entry catch + log); the marketplace import route (catch → its own registeredPLUGIN_REGISTER_FAILEDat 422, message interpolated).ObjectQL.registerPlugin—logger.warn;ObjectQLPluginreload ingest andsubscribe('object')handler —logger.warn;metadata-protocolapplyObjectRegistryMutation—console.warn;loadMetaFromDb— per-recorderrorscount.POST /packages(runtime/src/domains/packages.ts) andprotocol.installPackage— callSchemaRegistry.installPackage, which records the package and never callsregisterObject, so neither can raise it.MetadataFacade.register('object')would propagate it and has no production instantiation (new MetadataFacade(— zero non-test hits).⇒
door: 'none',verdict: 'boot-refusal'— the same reading asDUPLICATE_ARTIFACT_OBJECT_NAME; no door test is owed because there is no door.Clause-② self-reading: yes
A registered refusal code joins the dispatcher error vocabulary and the refusal's error shape gains
code+status; message unchanged.needs:contract-reviewon the PR at creation. Code name chosen:OBJECT_OWNERSHIP_CONFLICT(the ledger's*_CONFLICTgenre —METADATA_CONFLICT,MANIFEST_CONFLICT,FLOW_CONVERSION_CONFLICT; class name mirrorsNamespaceConflictError/ArtifactObjectNameConflictError). Alternatives considered:DUPLICATE_OBJECT_OWNER(theDUPLICATE_ARTIFACT_*genre, but nothing is duplicated — the second claim is refused),OBJECT_ALREADY_OWNED.Verification (union re-derived on the final tree, head
f8ff0a844)All exit codes captured after a redirect (never through a pipe); verdict lines quoted from the gate's own output. Final head
f8ff0a844(two commits: the implementationd932adbf3, then a one-string prose fix in the runtime vocabulary row forcheck:doc-authoring;git diff --stat d932adbf3 f8ff0a844 -- packages/objectqlis EMPTY, so the objectql runs below ond932adbf3measured the same objectql tree).pnpm --filter '@objectstack/objectql^...' build(lock VERDICTcommand-exit 0), thenpnpm --filter '@objectstack/runtime^...' build(objectql rebuilt with the new export; lock VERDICTcommand-exit 0).pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2(full package suite,d932adbf3):Test Files 258 passed (258)·Tests 4462 passed (4462)· lock VERDICTcommand-exit 0.src/registry-ownership-refusal-envelope.test.ts src/registry-object-overlay-layer.test.ts src/metadata-facade.test.ts src/registry-artifact-co-ownership.test.ts):Test Files 4 passed (4)·Tests 68 passed (68).pnpm --filter @objectstack/objectql typecheck(tsc --noEmit+tsconfig.scripts.json+check:test-typecheck): EXIT 0;check:test-typecheck: OK — @objectstack/objectql's test layer compiles under packages/objectql/tsconfig.test.json; 44 file(s) / 242 error(s) / 69 pinned signature(s) held. Program membership measured withtsc --listFiles:registry-ownership-refusal-envelope.test.ts,registry-object-overlay-layer.test.ts,metadata-facade.test.tsare all three in thetsconfig.test.jsonprogram and 0 of 3 in the build program (tsconfig.jsonexcludes**/*.test.ts) — so the test-layer gate is the one that measured them.pnpm --filter @objectstack/runtime typecheck: EXIT 0 (on the rebuilt objectql.d.ts).error-envelope.conformance,package-door-error-parity,http-dispatcher.error-leak,domains/actions-validation-envelope;f8ff0a844):Test Files 4 passed (4)·Tests 69 passed (69)· lock VERDICTcommand-exit 0.pnpm check:dispatcher-error-vocabulary(f8ff0a844):check-dispatcher-error-vocabulary: OK — 57 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).(56 before this PR; the newclassfieldsite is the 57th.)pnpm check:error-status-conformance:✓ every derivable runtime status is documented, and every documented status is reachable.·pnpm check:error-code-casing: EXIT 0 ·pnpm check:nul-bytes:check-nul-bytes: OK (scanned 7904 text file(s) …)·pnpm check:system-context-census:OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve(no line shift reached itsregistry.ts:464anchor; the page is untouched).pnpm check:doc-authoring: RED once ond932adbf3(NEW internal issue-id reference(s) in sibling-package string prose: packages/runtime/src/dispatcher-error-vocabulary.ts #14367) — the card id had been written INSIDE the row'swhystring; stripped inf8ff0a844, re-run EXIT 0.pnpm lint(full repo,eslint . --no-inline-config, working tree off8ff0a844):LINT_EXIT=0, no problems reported. (Targeted--format jsonover the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths; the script took the change set from the merge-base itself): 37 commands, identical to the derivation ond932adbf3— the dispatch's 61-command seed was over the intended surface, the real change set derives 37 (the spec/docs/lint-package families are not touched). Run serially onf8ff0a844: 33 exit 0, 0 red, 4 exit 3 — NOT MEASURED in the gate's own words:node scripts/check-test-completeness.mjs("Nothing was measured: this gate exited before parsing a single summary line" — it grades aturbo run testlog CI supplies),node scripts/pm/check-half-states.mjs("Treat this exit as an unread instrument" — it needs the GitHub board),pnpm check:dual-build-cjs-loads("PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ … Runpnpm buildfirst. ⛔ This is NOT a pass"),pnpm check:type-check-debt("Build the closure first … ⛔ This is NOT a pass and NOT a finding"). All four are CI-owned full-repo runs; none is red, none measured.Ablation (direction predicted before the run)
Predicted BEFORE the run: put the bare
Errorback at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance +code+status), the two upgradedregistry-object-overlay-layercases, the upgradedmetadata-facadecase — and every message-substring assertion stays GREEN (including the new file's byte-identical-message case, which is the demonstration that a message assertion cannot see this mutation).Observed (
ablation.sh, run from the committed state;trap restore EXIT INT TERMwithgit checkout HEAD -- ABSOLUTE_PATH):ABLATION-14367count 0 → 1, removed textthrow new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId);count 1 → 0,git hash-object138e009e…≠ HEAD blob5a2abe9e…../registry.js/./registry(relative source specifiers, measured by reading the import lines) — nodist/is on the path.Tests 4 failed | 55 passed (59), the four being exactly the predicted envelope cases (refuses to claim an object another package owns, and writes nothing;a second cross-package OWNER is still refused at registration;does NOT re-classify a packaged owner — a second code package is still refused;refuses a second code package with ObjectOwnershipConflictError: code + status 422 …). Direction matched the prediction; no reversal.git diff HEAD --statempty,git status --porcelainempty, tree blob5a2abe9e…== HEAD blob (non-empty), marker count 0.Deviations / notes
registerObject's own@throwsline (registry.ts:1739) now names the class — the method's declared contract, left sayingErrorit would be a declared-vs-actual lie.GET /repos/.../issues/14367→GitHub access is not enabled for this session), so label writes went through MCPissue_write(read current set → union → write → read back) as the documented fallback.check-test-completeness.mjs,pm/check-half-states.mjs,check:dual-build-cjs-loads,check:type-check-debt(each quoted above; all CI-owned full-repo runs).NamespaceConflictError(the ADR-0048 install-time namespace gate's refusal) carries no ADR-0112 envelope — nocode, nostatus— andPOST /packageswould answer it as 500 by reading #14474 (unassigned, no labels) —NamespaceConflictError, the ADR-0048 install-time namespace gate's refusal, carries nocode/statuseither, and unlike this one it sits onPOST /packages, so by reading it answers 500; not fixed here because it lives inside the region GET /api/v1/packages and /meta/package/<showcase> answer 500 "Converting circular structure to JSON" — the registry stores the live defineStack manifest, plugin instances included #14309 is editing and the code name is its own contract decision.🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Generated by Claude Code