fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

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 the install-time namespace conflict refusal (#14474) - #14738

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope
Sep 3, 2026
Merged

fix(objectql): carry an ADR-0112 envelope on the install-time namespace conflict refusal (#14474)#14738
os-musk merged 2 commits into
mainfrom
claude/issue-14474-namespace-conflict-envelope

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14474

NamespaceConflictError — the ADR-0048 install-time namespace gate's refusal, raised by SchemaRegistry.installPackage when a package's manifest.namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1) — carried namespace / existingPackageId / incomingPackageId but no code and no status. It now carries code: 'NAMESPACE_CONFLICT' and status: 422: the same three-field envelope shape as its sibling ArtifactObjectNameConflictError, 107 lines below it in the same file, same install-time refusal family. The message text is byte-for-byte unchanged — the prose was already correct and specific, so this adds fields rather than rewriting a sentence.

The measurement came first, and the premise held

The card said its 500 was a reading — a two-hop inference across packages/runtime/src/domains/packages.ts and packages/types/src/thrown-http-error.ts — not an observation, and triage made measuring it binding: a card whose premise is a reading should not be closed on the same reading. So the door was observed before the class was touched, on a booted stack (@objectstack/dogfood, real HTTP): install com.probe.alpha claiming namespace probe, then POST /api/v1/packages again with com.probe.beta claiming the same namespace.

Seed install, both runs: SEED-STATUS 201.

Before — measured against the unmodified class:

MEASURED-STATUS 500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Namespace conflict: namespace \"probe\" is already owned by package \"com.probe.alpha\" ...","httpStatus":500}}

and the door logged it as server breakage:

ERROR [5xx] 500 POST /api/v1/packages — Namespace conflict: namespace "probe" is already owned ...
{"status":500,"code":"INTERNAL_ERROR","method":"POST","path":"/api/v1/packages","requestId":"req_25dc5fff4c524ba3a15ba10aa4f3ce99"}

After — same request, same probe, envelope in place:

MEASURED-STATUS 422
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Namespace conflict: namespace \"probe\" ...","httpStatus":422,"declaredCode":"NAMESPACE_CONFLICT"}}

So the premise held: the refusal really was answering as 500 INTERNAL_ERROR, telling an operator the server had broken when the truth is that their package's namespace is already taken — which invites a retry instead of a rename. The [5xx] log line is gone with it, so this also stops a client-side conflict from counting against the door's server-error signal.

The vocabulary row carries the measured verdict, not the expected one

door: 'dispatcher', verdict: 'pending-registration'. Reachability is what separates this row from the three ADR-0130 install-time rows whose door: 'none' turns on needing an artifact install scope that no HTTP caller builds: this gate needs no scope, so an ordinary one-package install reaches it — and the measurement above is the proof rather than the call graph.

pending-registration is exactly what the after-body shows: NAMESPACE_CONFLICT is not an ErrorCode member, so the door's narrowing puts the closed member VALIDATION_ERROR on error.code and the semantic spelling rides the open declaredCode sibling. The body parses; what the producer loses is its semantic code on error.code until a ledger row lands. Registering the code is the packages/spec lane's call and is not made here — this row is that batch's input, and registering it is what ratchets the row back out.

The tests assert the envelope, and I proved they can fail

The existing tests are instance-typed (toBeInstanceOf(NamespaceConflictError)), which is why they never caught this: they were true of a class carrying no code and no status at all. Both files now assert the envelope itself.

Reverse-verified by ablation on the committed implementation — envelope fields removed, removal confirmed on disk (the code line 1 to 0, readonly status = 422 occurrences 3 to 2, so the sibling's own field was left intact), tests re-run, then restored with git checkout HEAD -- and the restore proven byte-identical (git diff HEAD empty, working blob hash equal to the HEAD blob 6f4c2b2f0832efd9bc1934d7b15901fef67f1955):

Test Files 2 failed (2)
Tests 2 failed | 15 passed (17)
AssertionError: expected undefined to be 'NAMESPACE_CONFLICT'

Exactly the two new assertions went red, one in each file, while the other 15 stayed green — the instance-typed blindness demonstrated rather than asserted.

Changeset level

@objectstack/objectql: patch + @objectstack/runtime: patch, matching the door-family precedent set by the nearest sibling #14367 (OBJECT_OWNERSHIP_CONFLICT — the same envelope-plus-vocabulary-row shape), which shipped exactly that pair. The neighbouring #14309 fix to this same door shipped patch as well.

Contract review

Clause ② is yes: this puts a new status and a new registered code on a published door's wire. needs:contract-review is hung on this PR as well as the card. The review is not performed in this seat.

Verification

Union re-derived on the final tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (its provenance line names this repo at 966dd3897) and run at that commit — 34 of 36 exit 0.

  • pnpm check:dispatcher-error-vocabularyOK — 60 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry
  • pnpm check:error-code-casingno unlisted lowercase error codes in 5278 scanned file(s)
  • pnpm check:doc-authoring, check:nul-bytes, check:type-check-debt, check:dual-build-cjs-loads, and the rest of the derived family — green
  • pnpm --filter @objectstack/objectql --filter @objectstack/runtime run typecheck — green, and objectql's check:test-typecheck compiles the test layer, so the edited test files are actually covered rather than excluded
  • pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-install-gate.test.ts src/registry-artifact-co-ownership.test.tsTest Files 2 passed (2) · Tests 17 passed (17)

Two gates report exit 3, which is each script's own NOT MEASURED branch and not a finding: check-test-completeness needs a piped test-run log CI tees and a local family run cannot hand it, and scripts/pm/check-half-states.mjs needs a trigger-file index this container does not have. Both print that verdict themselves.

One correction found by the gates and fixed in the second commit: the vocabulary row's why string quoted an internal tracker id, which check:doc-authoring reds because a runtime string reaches operators who cannot resolve one. The anchor moved to the adjacent // comment, and the same id came out of the changeset prose, which compiles into release notes.

Recovery note

This branch was recovered from a container restart that killed the seat mid-flight with the work uncommitted. The first commit is therefore a verbatim wip: snapshot of what was on disk, pushed before anything was reviewed; the history was not reshaped afterwards because that would need a force-push. The predecessor's door measurements survived as run logs and are quoted above rather than re-asserted, and every gate, test and the ablation above were re-run in this seat on the final tree.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… string prose
check:doc-authoring reds on an internal issue id inside sibling-package string
prose: a runtime string reaches operators who cannot resolve one. The anchor
moves to the adjacent // comment, where the reader who CAN resolve it looks.
Same edit in the changeset, which compiles into release notes.
Co-Authored-By: Claude Opus 5 <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 4 documentable anchor(s).

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

  • content/docs/getting-started/examples.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/permissions/permission-sets.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/protocol/kernel/http-protocol.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))
  • content/docs/ui/apps.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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

  • content/docs/releases/v17.mdx(via /api/v1/packages (route, a path literal in why; a path literal on a changed line))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 dbf115284295b1989d4648dbfbd7e5f3f96357dcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc — the merge of head 966dd3897eef76d5f3cf911bc94b4fa0dd50470d into base dbf115284295b1989d4648dbfbd7e5f3f96357dc, 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 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc && git checkout 4ac821b8c9fb7ceaed09dfc2830590860c95c4dc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf115284295b1989d4648dbfbd7e5f3f96357dc 966dd3897eef76d5f3cf911bc94b4fa0dd50470d && git checkout -B drift-repro dbf115284295b1989d4648dbfbd7e5f3f96357dc && git merge --no-ff 966dd3897eef76d5f3cf911bc94b4fa0dd50470d
node scripts/docs-audit/affected-docs.mjs --json dbf115284295b1989d4648dbfbd7e5f3f96357dc

⚠️ 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 dbf115284295b1989d4648dbfbd7e5f3f96357dc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-muskos-musk added needs:contract-review and removed documentation Improvements or additions to documentation size/m tests tooling labels Sep 2, 2026 — with Claude
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim

Clause ② is yes, and the review derived that from the final diff rather than from the PR's own claim: a new (unregistered) code and a new status on a published door's wire — packages/objectql/src/registry.ts:1271-1272 adds readonly code = 'NAMESPACE_CONFLICT' / readonly status = 422, and resolveThrownHttpError in packages/types/src/thrown-http-error.ts reads exactly .status / .code off the throw, so POST /api/v1/packages moves from 500 INTERNAL_ERROR to 422 VALIDATION_ERROR + declaredCode: NAMESPACE_CONFLICT for a real caller.

Performed by an isolated reviewer at the configured tier, in its own worktree, with no access to this seat's reasoning. The verdict is adopted as written; this seat did not review its own dispatch.

What the reviewer checked rather than accepted

Three advisory findings, none of which downgrades the PASS

  1. packages/runtime/src/dispatcher-error-vocabulary.ts:591-594 — the why string generalises "the three ADR-0130 install-time rows below, whose door: none turns on needing an artifact install SCOPE" to rows that are boot refusals for a different reason (packages/core/src/artifact-packages.ts rows run inside plugin init). The operative claim — this gate needs no scope, the door reaches it, measured — is correct.
  2. dispatcher-error-vocabulary.ts:587-588 — the door: 'dispatcher' / pending-registration reachability has no permanent pin; the only thing that ever drove the door to this 422 was the predecessor's temp probe, which is not in the PR. Not required by the row type (pin is mandatory only for runtime-pinned) nor by the triage scope.
  3. .changeset/registry-namespace-conflict-refusal-envelope.md:17 — "Not narrowed, not widened" is scoped to accept-set/exports/ledger and the bullets above it disclose the wire change, but it would be sharper as "no accept-set, export or schema change; the door's answer to this refusal changes 500→422".

⇒ These are being filed as one finding card rather than held against this PR, so nothing is lost and the p2 refusal-envelope fix is not stalled behind three text corrections. ⛔ This seat does not grade that card.

needs:contract-review is cleared from this PR and from #14474 in one stroke.


Generated by Claude Code

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at 966dd3897. What was checked, in the order it was checked, so the next reader does not have to reconstruct it.

1. Contract review — PASS. Clause ② is yes, derived by an isolated reviewer at the configured tier from the final diff rather than from this PR's claim. Adopted verbatim in the comment above; three advisory findings recorded there and filed as #14745 rather than held against this PR. ⛔ Not reviewed in this seat.

2. needs:contract-review cleared from both carriers, read-modify-write with a compare read-back on each: gone from this PR (its label set is now empty) and from #14474 (bug, priority:p2, pm:dispatched, domain:engine remain). A gate label being silently stripped is indistinguishable from one that was never hung, which is why both sides were read back rather than assumed.

3. Governed-surface predicate, re-run on the FINAL five-file list — ⛔ not recalled from earlier in the session:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/registry-namespace-conflict-refusal-envelope.md \
packages/objectql/src/registry-artifact-co-ownership.test.ts \
packages/objectql/src/registry-namespace-install-gate.test.ts \
packages/objectql/src/registry.ts \
packages/runtime/src/dispatcher-error-vocabulary.ts
→ 0 of 5 path(s) hit the register (5 surfaces) — NOT governed; ordinary queue landing applies.

4. Every check run green or skipped — 52 check runs, read at perPage: 50, each by its own completed + conclusion, ⛔ not by an aggregate rollup. The last to finish were Lint & Repo Gates (23:22:40Z) and the Test Core rollup (23:27:30Z); the only commit status is Vercel's "Canceled by Ignored Build Step".

5. Flipped draft: false, read back draft: false, then armed auto-merge SQUASH.⚠️ The API echo reads method: MERGE regardless of what is requested — a known platform artifact; the merge-queue build ref is the authoritative reading of what actually lands.

⛔ No approval was given from this seat, and none is claimed. Landing is the merge queue's; if the queue ejects this PR it re-queues itself, so an ejection notice is not by itself a failure.


Generated by Claude Code

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

Labels

None yet

Projects

None yet

2 participants

@os-musk@claude