Skip to content

feat(objectql)!: judge a packages[] stack's residual top-level items by the namespace name rule - #31767

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mjoky7
Nov 9, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mjoky7

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #31720

ADR-0150 child N follow-up (D2, ruled 6724570466; N's contract review 6728707576, finding N3). Claim 6729778749 (domain:server#1, card-thread session_01ADHkmQv69sQL5nfT8xuPWs).

Clause-②: no (narrowing) — measured on head c8f1e79 and, for base, by the ablations that remove each added call (below): the manifest service (every artifact or config registration, boot included) refuses a stack it accepted on base, one whose residual top-level item of the covered types bears a name an installed package of the stack's manifest.namespace holds (ablated = base: the stack registers, the residual item sits beside the other package's; head: NAMESPACE_CONFLICT 422, artifact put back); and the package door refuses a package it accepted on base, one holding such a name that a registered stack's residual of its namespace already holds (ablated = base: it installs; head: refused, naming the stack's id). Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — skills/** does not teach that a stack's top level may repeat a name another package of its namespace holds; the rule ships in the refusal text and in ADR-0150 D2.

No governed surface is touched. This is a narrowing, so the review of record runs at CONTRACT_REVIEW_TIER.

维护者速读

  • 改了什么:多包栈(packages[])顶层那些没有任何包体声明的「残余」条目,注册时不走安装门,feat(objectql)!: refuse a (type, name) held twice inside one namespace #31717 的命名空间同名规则看不到它们。现在 manifest 服务在所有包体装完、残余注册之前,用同一个谓词把残余当作栈命名空间里的一个持有者来判;冲突就 422 NAMESPACE_CONFLICT,点出所有持有者,整个制品回滚。残余注册成功后也记为持有者,之后再装的同名包会在安装门被拒,所以先后顺序不影响结果;卸载残余的属主 id 时一并释放这些名字。
  • 为什么改:feat(objectql)!: refuse a (type, name) held twice inside one namespace #31717 合同复审的 N3:残余登记在栈自己的 id 下,crm/home 这类地址可能同时指向两个条目。
  • 风险与回滚:只收窄。第一方实测 0 处(唯一的多包栈 examples/app-multi-package 没有残余)。已有这种冲突的部署升级后启动失败并点名双方。回滚即 revert。
  • 先看:namespace-co-owner-names.ts 的 residualNamespacePackageOf、registry.ts 的 refuseResidualNamespaceNameConflicts / recordResidualNamespaceHolder / 共用的 refuseNamespaceHolderConflicts、plugin.ts 的 manifest 服务里两处调用。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/manifest-service-residual-namespace-name.test.ts —— 预期 33 passed。
  • 需要你做的:无,席位复审后落地。CLI 没有改:一个制品里的残余不可能与本制品的包体同名(见下文测量),os validate / os build 又看不到已安装的包,这一点作为开放问题请席位确认。

What changes

  • packages/objectql/src/namespace-co-owner-names.ts — residualNamespacePackageOf(stack, residual): the residual, as unclaimedTopLevel (@objectstack/metadata) answers it, read as ONE holder: the residual's owner id, the stack's own manifest.namespace, its items of the covered types, named by the same declaredNamespaceUniqueNames. No second predicate: the conflicts and the sentence are namespaceNameConflicts / namespaceNameConflictMessage. ResidualItems is the structural shape of that answer, so the module takes no new import.
  • packages/objectql/src/registry.ts — N's walk moves into one private helper, refuseNamespaceHolderConflicts, that N's package door (refuseNamespaceNameConflicts, now one line) and the new refuseResidualNamespaceNameConflicts both call. recordResidualNamespaceHolder keeps each registered residual (by owner id; the same stack again replaces its own) and the helper reads them beside the installed package records, so a package or another stack's residual registering AFTER is judged against it too; uninstallPackage(id) forgets the holder beside forgetBareNameClaims(id), since the residual's items are registered under that id and withdraw with it. Same envelope, NamespaceNameConflictError (NAMESPACE_CONFLICT, 422, conflicts[]).
  • packages/objectql/src/plugin.ts — the manifest service: inside the one restore run (objectql: a refused later member of a multi-package artifact leaves the earlier members registered in the running kernel, unledgered (install-local inline and cloud) #31564), after every body has installed and before anything of the residual registers, refuseResidualNamespaceNameConflicts; a refusal puts the whole artifact back. After the run, beside recordResidualOwner and on the same terms (only an artifact that registered whole), recordResidualNamespaceHolder.
  • scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json — the invariant gains one sentence naming the residual call (contract review ③ 4).
  • packages/objectql/src/index.ts — exports residualNamespacePackageOf and the ResidualItems type.
  • .changeset/31720-residual-namespace-names.md — @objectstack/objectql minor, both declaration lines, BREAKING Why / Before / Now / Who moves / Unchanged, the census count, the one-line fix, adr-0087: not-required (no-migration-prescription).

Forced test edits: none. No existing assertion changed.

Step 0 (measured on main 0105a687)

  • Where the residual registers, and under which id. The decision is unclaimedTopLevel(stack) (packages/metadata/src/plugin.ts): the top-level items of a packages[] stack whose (type, name) slot no package body occupies (claimedArtifactSlots, per type, by name), owned by stack.manifest.id. Two doors register them: the metadata door (MetadataPlugin.registerUnclaimedTopLevel, artifact and config boots, into the metadata service) and the engine door, the manifest service (packages/objectql/src/plugin.ts), which both boot doors hand the stack to: it installs every body, then registers the residual's objects and other items under that id, with no package record and never through installPackage. @objectstack/metadata cannot import @objectstack/objectql (the dependency runs the other way), so the judge sits at the engine door, which every registration reaches.
  • The stack's namespace: stack.manifest.namespace (e.g. crm for examples/app-multi-package). The residual is addressed under it.
  • Which covered types can be residual: eleven. The residual rule walks the stack collections of ARTIFACT_FIELD_TO_TYPE, which has no jobs, so a top-level job is never residual (pinned).
  • os validate / os build today: findNamespaceNameRefusals walks the bodies only. And inside ONE artifact a residual can never share a covered (type, name) with one of its own bodies: a body holding the name claims the slot, so the item is not residual (pinned for all eleven types). The two ways N's reader names an item a body holds without a slot are both refused by the body parse (AssembledPackageBodySchema): a manifest-as-app (navigation is not a package-manifest key) and a nested plugins[]; an item without a name is refused for every covered type (measured: tools, pages, apps). So a residual clash involves a package installed by ANOTHER artifact, which os validate / os build cannot see. The CLI walk is therefore not changed: a residual check there could never answer anything but [], and no ablation could turn its pin red. See open question 1.
  • First-party census: examples/app-crm, app-showcase, app-todo carry no packages[] (no residual); examples/app-multi-package (stack crm, owner com.example.multi.core, 2 bodies) has 0 residual items. 0 stacks hold a residual item of the covered types; 0 clashes. HotCRM is not in this container: NOT MEASURED.

ADR-0150's four readings

  • (a) phase-1 one-holder tests stay green: bare-name-one-holder.ts is untouched; its test files run inside the objectql suite below (575 files / 11743 passed).
  • (b) bare-name behaviour unchanged for one-holder names: the CONTROL "a residual name no package of the namespace holds registers, under the residual owner id" reads the residual page back by its bare name; every CONTROL below registers.
  • (c) no migration: nothing stored changes shape; the residual holders are in-memory, rebuilt from each registration.
  • (d) Clause-②: no (narrowing) above and in the changeset; Metadata-protocol: narrowed; adr-0087: not-required (no-migration-prescription).

Verification

Head 9970d03 (review delta F1/F2, plus main c2bb879 merged in). Every heavy run under os-verify-lock:

  • New: packages/objectql/src/manifest-service-residual-namespace-name.test.ts 33 passed: per covered residual type (11), refused at the manifest service naming both holders with the artifact put back (the stack's body not installed, the name still answering the first holder's item), and a CONTROL in another namespace; the residual read as one holder; a top-level job is never residual; a package registered AFTER the stack refused at the package door naming the stack's id; uninstalling the residual's owner (a body id, the preserve shape) frees its names and a package holding the name then installs; CONTROLs: a residual name no package holds, a type outside the twelve and every other name rule (a doc), sys, the same stack registering again, a refused stack recording no holder, and inside one artifact the residual never sharing a covered name with a body (all eleven types). With N's file: 2 files / 89 passed.
  • Suites on 9970d03, each command-exit 0: objectql 575 files / 11756 passed; cli 616 (+2 skipped) / 7672 passed (10 skipped); metadata-protocol 328 (+4 skipped) / 22595 passed (26 skipped); runtime 563 / 9059 passed (19 skipped); dogfood battery 440 (+3 skipped) / 3653 passed (41 skipped). tsc objectql, the build closure and check:adr-anchors exit 0.
  • The other-namespace CONTROL for tool: since feat(objectql)!: a tool holds one name per package (bare-name one holder, after the AI family) #31661 a tool holds one name per deployment (phase 1's one-holder list), so the same tool name in another namespace is refused by phase 1 on its own terms; for tool that CONTROL pins the refusal as phase 1's (Bare-name conflict:), not the namespace rule's.
  • Lint: eslint packages/objectql --no-inline-config exit 0.
  • Head c8f1e79 merges main 5b7269f (R2 feat(core, metadata-core, objectql): the package-scoped resolution rule as a lean core, engine functions by package #31733's conflict in plugin.ts resolved keeping both: R2's recordResidualOwnerDependencies and this PR's recordResidualNamespaceHolder, each after the run returns; the refuse call stays inside the run after every body, before the residual registers). On c8f1e79: both namespace files 2 / 89 passed; objectql 579 files / 11849 passed; tsc objectql exit 0; 80 gates all exit 0, --ran 0 NOT-MEASURED, 0 UNRUN.
  • Gates on 9970d03: dispatch-gates --commands derived 80 families; all 80 ran, exit 0; --ran reconcile: 80 run, 0 NOT-MEASURED, 0 UNRUN.
  • CI on 9970d03: every check-run completed success or path-skipped (Test Core 8/8, Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate, Live E2E UI, Check Changeset, PR-body guards, Governed Surface Queue Guard, Check PR Size).
Ablations (on the committed code, each restored)
  1. ql.registry.refuseResidualNamespaceNameConflicts(residualHolder); replaced by void 0; in plugin.ts → 12 of 32 red: the 11 per-type refusals (expected [ undefined, undefined, undefined ] to deeply equal [ 'NAMESPACE_CONFLICT', 422, 422 ]: the stack registers) and the "refused stack records no holder" CONTROL (with nothing refused, the stack records its holder and the later package is refused). Restored.
  2. ql.registry.recordResidualNamespaceHolder(residualHolder); replaced by void 0; → 1 of 32 red: "a package registered AFTER the stack … is refused at the package door" (it installs). Restored.
  3. On f3d363a, this.residualNamespaceHolders.delete(id); replaced by void 0; in registry.ts → 1 of 33 red: "uninstalling the residual's owner … frees the residual's names" (the later package is refused naming a holder that holds nothing). Restored, git status clean.

Ablations 1 and 2 ran on 2faaebe (32 tests); the review delta touches neither call.

Acceptance notes

  • Open question 1 (seat's call): the CLI half. The delta asks for the same check in os validate / os build. Measured (Step 0): inside one artifact the residual cannot share a covered name with its own bodies, and those commands cannot see packages installed by other artifacts, so the check could only ever return []. Options: A, leave the CLI unchanged and state the bound (as built); B, add the call anyway as a guard against a future body shape that holds a name without a slot. Recommendation: A, because B ships code no test can turn red, and the bound is pinned in objectql ("inside one artifact …"), which fails first if a body shape ever changes that.
  • Open question 2 (seat's call): a residual whose stack has no namespace, or a shareable one. Not judged, as for a package (isNamespaceJudged). Recommendation: keep, it is N's rule unchanged.
  • Bound, stated: a residual holder is recorded per registration and kept in memory with the registry, like recordResidualOwner; uninstallPackage of its owner id withdraws the residual's items and forgets the holder with them (review F1).
  • Names (seat's call, house conventions): residualNamespacePackageOf, ResidualItems, SchemaRegistry.refuseResidualNamespaceNameConflicts, SchemaRegistry.recordResidualNamespaceHolder. No new error code.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs

…by the namespace name rule

Claude-Session: https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 12 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6be71abc34cdd7adcc6057d121479ba5a11724e3.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 21 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 6be71abc34cdd7adcc6057d121479ba5a11724e3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b79e9e914998edb459ee52b50eac3231377609f0 — the merge of head c8f1e79c94754e67169f969de12170f8125a23ed into base 6be71abc34cdd7adcc6057d121479ba5a11724e3, 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 b79e9e914998edb459ee52b50eac3231377609f0 && git checkout b79e9e914998edb459ee52b50eac3231377609f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6be71abc34cdd7adcc6057d121479ba5a11724e3 c8f1e79c94754e67169f969de12170f8125a23ed && git checkout -B drift-repro 6be71abc34cdd7adcc6057d121479ba5a11724e3 && git merge --no-ff c8f1e79c94754e67169f969de12170f8125a23ed

node scripts/docs-audit/affected-docs.mjs --json 6be71abc34cdd7adcc6057d121479ba5a11724e3

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract-review-claim: session_01ADHkmPF1skfu2kx89BEXZi · head 2faaebe · 2026-11-09T03:14Z

Narrowing face (Clause-②: no (narrowing)), CONTRACT_REVIEW_TIER review by an isolated reviewer on this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2faaebe929ab4463d78424b57819e3ded3ad9eb7
Local-runs: none
Scope: packages/objectql/src/namespace-co-owner-names.ts, packages/objectql/src/registry.ts, packages/objectql/src/plugin.ts, packages/objectql/src/index.ts, packages/objectql/src/manifest-service-residual-namespace-name.test.ts + packages/metadata/src/plugin.ts (unclaimedTopLevel, claimedArtifactSlots, ARTIFACT_FIELD_TO_TYPE, the artifact boot door), packages/core/src/artifact-packages.ts (resolveArtifactPackageEntries), packages/spec/src/stack.zod.ts (AssembledPackageBodySchema, assembledPackageBodyShape, composeStacks preserve), packages/spec/src/kernel/manifest.zod.ts, the eleven element schemas (app, page, dashboard, report, book, sharing, queue, assignment-rule, tool, hook, analytics cube), packages/objectql/src/registry.ts uninstallPackage / unregisterItemsByPackage / forgetBareNameClaims / restoringOnRefusal (live, outside the hunks), packages/objectql/src/registry-namespace-co-owner-names.test.ts, packages/cli/src/utils/namespace-co-owner-names.ts, packages/cli/src/commands/validate.ts, packages/cli/src/commands/compile.ts, docs/adr/0150-behavioural-metadata-addressed-by-package-objects-keep-the-literal-prefix.md (D2, child N row), scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json, examples/app-multi-package/objectstack.config.ts. The changeset .changeset/31720-residual-namespace-names.md is a prose face: read for ②, not reviewed. Code outside the diff was read to judge the narrowing, never reviewed.

Inputs: card #31720 (body and all four comments: triage 6728959174, unblock 6729750199, claim 6729778749, os-dev-report 6731329179), PR #31767 (body, six-file list, net diff against main at c2bb879f), the check-runs on the head. The dispatch order and the dispatching seat's conclusions were not read.

Check-runs on 2faaebe9 at the time of this record (03:21Z): Lint & Repo Gates success, TypeScript Type Check success, Build Core success, Governed Surface Queue Guard success, Live E2E UI success, Dogfood Regression Gate success, Test UI success, Check Changeset success, Check PR Size success, PR-body guards success; Lint UI, Type Check UI, Build & E2E UI skipped (no ui/ path in the diff). Test Core is still PENDING: shard 6/8 success, shards 1–5, 7 and 8 in_progress. The gate set is not yet whole-green; this record does not stand in for the pending shards.

① Derived judgments

Accept-set changes the diff implies, each judged against the live tree:

  1. The manifest service refuses a packages[] stack whose residual (the top-level items unclaimedTopLevel leaves to the stack's own id) declares a covered (type, name) that an installed package of stack.manifest.namespace holds; NAMESPACE_CONFLICT, 422, every holder named; the whole artifact put back by the one restoringOnRefusal run — right. The refuse call sits after every registerApp and before registerUnclaimedTopLevelObjects / registerUnclaimedTopLevelItems, so it reads the stack's own bodies' records too and nothing of the residual has registered when it throws. The predicate is N's (namespaceNameConflicts, namespaceNameConflictMessage); residualNamespacePackageOf rebuilds a body-shaped record keyed by singularToPlural(type) and hands it to declaredNamespaceUniqueNames, so the names are read the way N reads a package's — no second predicate.
  2. The package door refuses a package whose covered name a registered stack's residual of the same namespace already holds, naming the stack's owner id — right. refuseNamespaceHolderConflicts appends residualNamespaceHolders of the incoming namespace to sitting; namespaceNameConflicts dedups holder ids, so a residual whose owner id is also a body id (the preserve shape, see ④) is named once.
  3. The same stack registering again is not judged against its own residual (other.packageId === incoming.packageId skip) and replaces its own entry (Map.set by owner id) — right.
  4. A refused stack records no holder: recordResidualNamespaceHolder is called after the run returns, so a throw never reaches it — right (pinned by the "refused stack records no holder" CONTROL).
  5. A residual whose stack has no namespace or a shareable one (base, system, sys) is neither judged nor recorded (isNamespaceJudged at both calls) — right, N's rule unchanged and ADR-0150 D2's own exclusions (sys is the reserved kernel package; the environment package has no namespace).
  6. A residual item of a type outside the twelve is not judged (isNamespaceUniqueType filter) — right. A top-level job is never residual because ARTIFACT_FIELD_TO_TYPE carries no jobs key — read and confirmed (pinned).
  7. Order independence between a stack and a package arriving later — right by 1 and 2 together.
  8. Wrong: a recorded residual holder outlives its items on one real path. See ④ F1.
  9. Public surface: @objectstack/objectql gains the export residualNamespacePackageOf and the type ResidualItems, and SchemaRegistry gains two public methods, refuseResidualNamespaceNameConflicts and recordResidualNamespaceHolder — additive, correctly carried by a minor. ResidualItems is structurally identical to the two fields of @objectstack/metadata's UnclaimedTopLevel it reads (ownerId: string | undefined, items: {type, item}[]), so the engine's existing residualAnswer passes without an import from above the registry — right.
  10. os validate / os build are unchanged. Judged under ③ (1): right on this head, with the jsdoc that says otherwise graded under ④ F2.

② Semver level

.changeset/31720-residual-namespace-names.md: '@objectstack/objectql': minor; Clause-②: no (narrowing); Metadata-protocol: narrowed; a BREAKING banner with Why / Before / Now / Who moves / Unchanged, the one-line fix (rename one of the two items, or declare it in one of the stack's packages), the first-party census (0 stacks with a residual of the covered types; examples/app-multi-package has no residual), and exactly one ADR-0087 marker, adr-0087: not-required (no-migration-prescription), with its reason. The PR body carries the same two declaration lines. The diff publishes a narrowing of the accept set at the manifest service and at the package door, moves only @objectstack/objectql (cli untouched, so no cli bump is owed), and removes no key, export or field — so narrowed takes at least minor on the moved package, the breaking banner is owed and present, and no tombstone is owed. Check Changeset is green. Matches. Clause-②: no (narrowing) is the right arm: nothing on base that is now refused was ever accepted for a reason other than the residual escaping N's door, and nothing is widened.

③ Boundary flags

The os-dev-report's three open_questions and its one out_of_scope_findings entry, each answered:

  1. CLI half left unchanged (deviation from the card's "cli validate.ts / compile.ts" surface). Verified in the live tree, not from the PR's account: claimedArtifactSlots claims a body's slot by item.name; N's itemName reads name first and falls to id only when name is absent; the eleven covered element schemas all declare name required (SnakeCaseIdentifierSchema or a snake_case regex — app, page, dashboard, report, book, sharing rule, queue, assignment rule, tool, hook, cube), and every packages[] entry is gated whole by ArtifactPackageSchema.safeParse → AssembledPackageBodySchema = ManifestSchema.extend(assembledPackageBodyShape()), where ManifestSchema is closed and has no navigation key and plugins is an envelope key excluded from the body shape. So inside one artifact a body cannot hold a covered name without claiming its slot, and an item a body holds is by definition not residual (pinned for all eleven types). The only residual clash is with a package another artifact installed, which findNamespaceNameRefusals cannot see. Option A accepted: a CLI call could only ever answer [], and the card's "it counts only when the stack's namespace equals one of its packages' namespaces, or another installed package's" is served by the engine door alone. Two consequences for the record: the jsdoc on this head must not claim the CLI judges the residual (④ F2), and the CLI module's own bound sentence on main ("the residual … is not judged at either door") is now half-stale and should be corrected when that file is next touched (follow-up; outside this diff).
  2. A stack with no namespace or a shareable one is not judged. Option A accepted: identical to N's holder rule and to ADR-0150 D2 (sys is the kernel package, the environment package has no address); judging it would be a narrowing beyond D2, the maintainer's to rule.
  3. Names (residualNamespacePackageOf, ResidualItems, SchemaRegistry.refuseResidualNamespaceNameConflicts, recordResidualNamespaceHolder; no new error code). Option A accepted. Reusing NAMESPACE_CONFLICT is correct: the condition and the remedy are N's.
  4. Out of scope per dev: the anchor invariant (scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json) names installPackage and os validate / os build as the predicate's callers and not the manifest service's residual call. Read: check:adr-anchors is green on the head and the invariant's "never" list is not contradicted by the diff. Accepted as a one-sentence follow-up riding on the F1/F2 delta or a follow-up card; not a finding of its own.

No other dev flag or deviation is open. "HotCRM census NOT MEASURED" is stated in the PR body and the changeset; the first-party count is 0 and the maintainer's HotCRM measurement for D2 (0 of 297) is the ADR's. "Base side of Clause-② measured by ablation" is an acceptable measurement for a pure refusal insertion whose removal is the base.

④ Design judgment

The narrowing against the published spec on main (ADR-0150 D2, ruled 6724570466; #31717's namespace-co-owner-names rule): D2 says that within a namespace a behavioural (type, name) has at most one package holder so that NAMESPACE/NAME names one item. On main, a packages[] stack's residual registers under the stack's own id through registerItem(type, item, 'name', ownerId) with no package record and no pass through installPackage, so a residual crm/home beside a co-owner's crm/home was two items at one address — the spec's own refusal, not made real for this holder kind. This PR makes it real at the one door every stack registration reaches (the manifest service; @objectstack/metadata cannot import @objectstack/objectql, so the judge cannot sit in registerUnclaimedTopLevel) and makes the residual a holder for what arrives after it. It takes no declared capability away at the consumer: nothing in skills/**, the ADR or the stack schema declares that a stack's top level may repeat a name a co-owner holds, and the refusal text carries the rule and the remedy. The three epic red lines: resolution never loosens (only refusals are added); ambiguity is reported, never guessed (every holder named, conflicts[] structured); independently releasable (readings a–d stated and consistent with the tree). The twelve types only; no package-scoped resolution. Attempts to break it:

  • A body item carrying id but no name (N's fallback) sneaking past the slot claim: refused by the closed body parse — see ③ (1). Holds.
  • A manifest-as-app row on a body (name + navigation): navigation is not a ManifestSchema key. Holds.
  • A stack whose owner id equals one body's id (the preserve shape: composeStacks sets composed.manifest = selectManifest(stacks, 'last'), so examples/app-multi-package's owner is com.example.multi.core, a body): the predicate's same-id skip makes body and residual one holder, which is the changeset's own "Unchanged" line and D2-correct. Holds at install. Fails at uninstall — F1.
  • A refusal inside the run restoring the registry while residualNamespaceHolders is outside the restore point: the record call is after the run, so no entry is written on a refusal; the refuse call only reads. Holds.
  • The metadata door registering the residual into the metadata service before the engine door refuses (artifact boot: registerUnclaimedTopLevel in packages/metadata/src/plugin.ts, then the runtime's manifest.register): pre-existing shape shared with N's package door; on a boot the refusal fails the boot, so no divergent state survives. Read, not a finding of this PR.

Findings:

  • F1 — should-fix — registry.ts: a recorded residual holder survives the withdrawal of the items it stands for. uninstallPackage(id) calls unregisterItemsByPackage(id), which deletes every registry key prefixed ${id}: — and the residual's items are registered under exactly ownerId as their package id (registerUnclaimedTopLevelItems → registerItem(type, …, 'name', ownerId); registerUnclaimedTopLevelObjects likewise). When the owner id names one of the bodies — the normal preserve shape, measured on examples/app-multi-package — uninstalling that body withdraws the residual's items too, but residualNamespaceHolders keeps the entry: the next package of the namespace declaring that name is refused at the package door naming a holder that holds nothing. N pins the opposite for packages ("an uninstall takes its names with it", registry-namespace-co-owner-names.test.ts), and uninstallPackage already forgets every other per-id claim (forgetBareNameClaims, forgetSecurityCatalogClaims, forgetDefaultRecordPageClaims). The PR's stated bound ("its holder stays while its items stay registered") does not hold on this path. Not a must-fix: resolution is not loosened (an over-refusal, red line 1 intact), no stored shape is affected, and a restart rebuilds the map. Fix: drop residualNamespaceHolders.delete(id) into uninstallPackage beside forgetBareNameClaims(id) (downstream of the one refusing step), with a pin "uninstalling the residual's owner frees the residual's names". A delta review of that fix is owed on the new head.
  • F2 — should-fix — namespace-co-owner-names.ts header, the new "## The residual (objectql/cli: a packages[] stack's own top-level residual items are judged by the namespace co-owner rule too (ADR-0150 child N follow-up) #31720)" paragraph states that the same predicate judges the residual "in os validate / os build against the stack's bodies". On this head it does not (③ 1), and the CLI module's own header says the residual is judged at neither door. A module docblock the ADR anchor governs advertising a check the runtime does not run is the Prime Directive 10 shape (declared, not delivered). Fix: name the one door that judges it (the manifest service) and state the CLI bound (inside one artifact the residual cannot share a covered name with a body; other artifacts' packages are invisible there). Rides on F1's delta; while there, the anchor invariant may gain its one sentence (③ 4).
  • Nit — test title 'CONTROL — a residual item of a type outside the twelve is not judged (a dataset alpha also holds)': the assertion is not.toBe('NAMESPACE_CONFLICT'), which also passes if the dataset is refused for some other reason; a toBeUndefined() on err?.message would pin acceptance. No effect on the contract; accepted deviation.

No must-fix. Two should-fix findings: the round does not end here; the fix moves the head and this record no longer covers it — a delta review on the new head closes it.

Implemented-by: claude/project-thread-mjoky7
Reviewed-by: session_01ADHkmPF1skfu2kx89BEXZi

VERDICT: PASS

…amespace names

The residual's items are registered under their owner id, so
uninstallPackage(id) withdraws them; the recorded residual holder is now
forgotten beside the bare-name claims, and a package declaring the name
afterwards installs.

The module header names the manifest service as the one door that judges
the residual and states the os validate / os build bound; the ADR anchor
invariant names the residual call. The type-outside-the-twelve CONTROL now
asserts acceptance with a doc, a type no other name rule judges.

Claude-Session: https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs
Co-authored-by: Claude <noreply@anthropic.com>
…s refusal

Since a tool holds one name per deployment (phase 1's one-holder list),
the same tool name in a stack of another namespace is refused by phase 1
on its own terms; the CONTROL now pins that the namespace rule is not the
one refusing it.

Claude-Session: https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9970d032e898c196b3936d297f2108c042eee13b
Local-runs: none
Scope: packages/objectql/src/registry.ts, packages/objectql/src/namespace-co-owner-names.ts, packages/objectql/src/manifest-service-residual-namespace-name.test.ts, scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json + packages/objectql/src/index.ts, packages/objectql/src/plugin.ts (unchanged since 2faaebe, covered by record 6731477815)

Delta review of record 6731477815 (head 2faaebe, PASS with two should-fix). Read: 2faaebe..f3d363a (the fix) and e535403..9970d03 (the post-merge test). e535403 merges main at c2bb879 and adds no hunk of its own to the PR's paths.

① Derived judgments

② Semver level

Unchanged: @objectstack/objectql minor, Clause-②: no (narrowing), Metadata-protocol: narrowed. The fix only removes an over-refusal the round had found. It adds no export and no refusal.

③ Boundary flags

All flags from 6731477815 were answered there. The anchor-invariant sentence (③ 4 there) is now in: it names the manifest service's residual call, the holder record and the uninstall release. The dev reports check:adr-anchors exits 0.

④ Design judgment

  • F2 fixed: the module header names the manifest service as the one door and states the CLI bound, which matches the tree.
  • Nit fixed, and it was real: the old dataset CONTROL passed only through not.toBe. The dataset is phase 1's type and the shared literal carried alpha's _packageId stamp. The CONTROL now uses a doc, a type no name rule judges, with a fresh literal and toBeUndefined.

No new finding. Landing still waits for whole-green check-runs on this head.

Implemented-by: claude/project-thread-mjoky7
Reviewed-by: session_01ADHkmPF1skfu2kx89BEXZi

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf

Epic red-line review: N follow-up #31720, head 9970d03 · 2026-11-09T03:32Z

Verdict: PASS.

Red line Reading
1. Never loosens A packages[] stack's residual top-level items of the covered types now count as a holder under N's co-owner name rule, in both directions. The manifest service refuses a stack whose residual repeats a name another package of its namespace holds (NAMESPACE_CONFLICT 422, artifact put back). The package door refuses a package whose name a registered stack's residual already holds, naming the stack's id. Nothing is widened.
2. Ambiguity reported, never guessed Every refusal names the other holder. There is no pick between the residual and the package.
3. Independently releasable (a) bare-name-one-holder.ts is untouched and its suite is green. (b) The CONTROLs register, and a residual name no co-owner holds reads back by its bare name. (c) No migration: the residual holders live in memory, rebuilt on each registration. (d) Clause-②: no (narrowing), Metadata-protocol: narrowed, objectql minor. First-party census: 0 stacks hold a residual item of the covered types. HotCRM is not measured; the D10 upgrade report (S4 #31693) is where a third-party case surfaces.

Merge order: no dependency on R2 #31733 in either direction. Both touch registry.ts, so whichever lands second merges main (no rebase). This PR does not wait on R2.

Seat 1 accepts and lands this after green CI. It is not a governed surface.

…-mjoky7

# Conflicts:
#	packages/objectql/src/plugin.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c8f1e79c94754e67169f969de12170f8125a23ed
Local-runs: none
Scope: packages/objectql/src/plugin.ts, packages/objectql/src/registry.ts, packages/objectql/src/namespace-co-owner-names.ts, packages/objectql/src/index.ts, packages/objectql/src/manifest-service-residual-namespace-name.test.ts, scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json + none (unchanged since 9970d03 apart from the merge)

Delta review of record 6731564543 (head 9970d03, PASS). c8f1e79 is a merge commit of origin/main 5b7269f onto 9970d03, with no rebase.

① Derived judgments

One conflict hunk, in packages/objectql/src/plugin.ts after recordResidualOwner. R2 (#31733) put recordResidualOwnerDependencies in the same place this PR put recordResidualNamespaceHolder. The resolution keeps both, R2's call first, and both run after restoringOnRefusal returns, so a refusal still records nothing. refuseResidualNamespaceNameConflicts stays inside the run, after every body and before the residual's objects and items register. git diff origin/main...c8f1e79c -- plugin.ts shows only this PR's 17 lines, so right. No other path of the PR moved in the merge.

② Semver level

Unchanged: @objectstack/objectql minor, Clause-②: no (narrowing), Metadata-protocol: narrowed.

③ Boundary flags

None new.

④ Design judgment

The R2 owner-dependency record and the residual namespace-holder record are independent, and both key off the same post-run point. Their order does not matter. No new finding.

Implemented-by: claude/project-thread-mjoky7
Reviewed-by: session_01ADHkmPF1skfu2kx89BEXZi

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 9, 2026 06:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 9, 2026 06:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 69db709 Nov 9, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mjoky7 branch November 9, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql/cli: a packages[] stack's own top-level residual items are judged by the namespace co-owner rule too (ADR-0150 child N follow-up)

1 participant