fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367) - #14476

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope
Sep 2, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on registerObject's cross-package ownership refusal (#14367)#14476
os-musk merged 2 commits into
mainfrom
claude/issue-14367-register-object-ownership-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14367

SchemaRegistry.registerObject's cross-package ownership refusal (ADR-0029 D3: a package claiming own on an object name a DIFFERENT package already owns) threw a bare Error — no code, no status — 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 throws ObjectOwnershipConflictError: code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422, plus objectName / existingPackageId / incomingPackageId — the same shape as the sibling ArtifactObjectNameConflictError (DUPLICATE_ARTIFACT_OBJECT_NAME, PR #14354) — with the message text byte-for-byte unchanged.

What changed

  • packages/objectql/src/registry.ts
  • packages/runtime/src/dispatcher-error-vocabulary.ts — one classification row beside DUPLICATE_ARTIFACT_OBJECT_NAME: shape: 'classfield', door: 'none', verdict: 'boot-refusal', with the measured call chain as its why (see Reachability).
  • Tests (audit of every current assertion whose SUBJECT is this refusal — Zone 2 Add Changesets and GitHub Actions automation #4):
    • registry-object-overlay-layer.test.ts:294 and :454 — upgraded from toThrow(/already owned by package .../) to toMatchObject({ code: 'OBJECT_OWNERSHIP_CONFLICT', status: 422 }) + the same message regex, through a local refusalOf helper.
    • metadata-facade.test.ts:205 — upgraded from rejects.toThrow(/.../) to rejects.toMatchObject({ code, status, message: expect.stringMatching(/.../) }).
    • new 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 (extend from the other package) is accepted.
    • Not subjects, left as they are: protocol-commit-history.test.ts:1008 (asserts a console.warn substring is ABSENT — a forwarding test, and the text it filters on is unchanged); protocol-object-overlay-layer.test.ts:505 and protocol-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.ts asserts 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). No BREAKING: nothing is narrowed or widened; no accept-set change.

Reachability (Zone 2 #3, measured on origin/main @ a98b61b3e and on this tree)

registerObject is the ONE spelling of this refusal (git grep 'cannot claim ownership' — one source hit; 'already owned by package' in registry.ts — two hits, the other is the sibling ArtifactObjectNameConflictError message). Every path to it either aborts boot or catches below any HTTP door:

  • ObjectQL.registerApp (engine.ts) propagates it to ManifestService.register() (objectql/src/plugin.ts), whose callers are the population the three ADR-0130 rows record: boot-time manifest.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 registered PLUGIN_REGISTER_FAILED at 422, message interpolated).
  • ObjectQL.registerPluginlogger.warn; ObjectQLPlugin reload ingest and subscribe('object') handler — logger.warn; metadata-protocolapplyObjectRegistryMutationconsole.warn; loadMetaFromDb — per-record errors count.
  • The two HTTP install sites — POST /packages (runtime/src/domains/packages.ts) and protocol.installPackage — call SchemaRegistry.installPackage, which records the package and never calls registerObject, 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 as DUPLICATE_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-review on the PR at creation. Code name chosen: OBJECT_OWNERSHIP_CONFLICT (the ledger's *_CONFLICT genre — METADATA_CONFLICT, MANIFEST_CONFLICT, FLOW_CONVERSION_CONFLICT; class name mirrors NamespaceConflictError / ArtifactObjectNameConflictError). Alternatives considered: DUPLICATE_OBJECT_OWNER (the DUPLICATE_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 implementation d932adbf3, then a one-string prose fix in the runtime vocabulary row for check:doc-authoring; git diff --stat d932adbf3 f8ff0a844 -- packages/objectql is EMPTY, so the objectql runs below on d932adbf3 measured the same objectql tree).

  • Build first: pnpm --filter '@objectstack/objectql^...' build (lock VERDICT command-exit 0), then pnpm --filter '@objectstack/runtime^...' build (objectql rebuilt with the new export; lock VERDICT command-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 VERDICT command-exit 0.
  • Targeted (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 with tsc --listFiles: registry-ownership-refusal-envelope.test.ts, registry-object-overlay-layer.test.ts, metadata-facade.test.ts are all three in the tsconfig.test.json program and 0 of 3 in the build program (tsconfig.json excludes **/*.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).
  • Runtime envelope/vocabulary suites (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 VERDICT command-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 new classfield site 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 its registry.ts:464 anchor; the page is untouched).
  • pnpm check:doc-authoring: RED once on d932adbf3 (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's why string; stripped in f8ff0a844, re-run EXIT 0.
  • pnpm lint (full repo, eslint . --no-inline-config, working tree of f8ff0a844): LINT_EXIT=0, no problems reported. (Targeted --format json over the 5 changed files earlier: 5 files, 0 errors, 0 warnings.)
  • Gate union re-derived on the final tree — 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 on d932adbf3 — 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 on f8ff0a844: 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 a turbo run test log 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/ … Run pnpm build first. ⛔ 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 Error back at the throw site (message kept) ⇒ exactly the envelope assertions go RED — the new file's case 1 (instance + code + status), the two upgraded registry-object-overlay-layer cases, the upgraded metadata-facade case — 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 TERM with git checkout HEAD -- ABSOLUTE_PATH):

  • mutation proven on disk: injected marker ABLATION-14367 count 0 → 1, removed text throw new ObjectOwnershipConflictError(fqn, existingOwner.packageId, packageId); count 1 → 0, git hash-object138e009e… ≠ HEAD blob 5a2abe9e….
  • no rebuild leg: the three suites import ./registry.js / ./registry (relative source specifiers, measured by reading the import lines) — no dist/ is on the path.
  • result: 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.
  • restore proven by bytes (not by the trap's exit code): git diff HEAD --stat empty, git status --porcelain empty, tree blob 5a2abe9e… == HEAD blob (non-empty), marker count 0.

Deviations / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx(via SchemaRegistry (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SchemaRegistry (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5edc25edf41ff398dd70ae69d0a7034be8c96609packageMentionDocs.

Which tree this was computed on

This run read content/docs from a58d7065324a9995e930b5d02a69b8870be07cd1 — the merge of head f8ff0a8442655448c0f28a45f0f24f80431476d4 into base 5edc25edf41ff398dd70ae69d0a7034be8c96609, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5edc25edf41ff398dd70ae69d0a7034be8c96609 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS + ACCEPT on the card: comment 5506895177 (#14367; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #14367 and on this PR at 08:45Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head f8ff0a844: every one of the 40 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:59:54Z; Check Changeset green on the objectql: patch + runtime: patch changeset); governed-surface test on the final 6-path file list: 0 of 6 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #14367, correct).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-musk@claude