Skip to content

feat(objectql)!: refuse a (type, name) held twice inside one namespace - #31717

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 8, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #31670

ADR-0150 child N (D2, ruled 6724570466): inside one namespace, a (type, name) of the twelve bucket 2/3 types phase 1 does not cover has one package holder. Claim 6727391505 (domain:server#1, card-thread session_01ADHkmQv69sQL5nfT8xuPWs).

Clause-②: no (narrowing) — measured on head 97b94ed7 and, for base, by the ablations that remove the one added call at each door (the predicate module is otherwise unread): the package door (SchemaRegistry.installPackage, every install, boot included) refuses a package registration it accepted on base, one whose item of these twelve types bears a name another installed package of its namespace holds (ablated = base: the second co-owner installs; head: NAMESPACE_CONFLICT 422); os validate / os build refuse the artifact that carries it (ablated = base: os validate --json exits 0; head: exits 1). Riding along at those two commands, because the walk reads packages[] through @objectstack/core's resolveArtifactPackageOrder (the boot's own resolver): an artifact declaring one package id twice (DUPLICATE_ARTIFACT_PACKAGE, 422) and an artifact whose packages' dependencies form a cycle ([Kernel] Circular dependency detected, no code) now exit 1 through the commands' catch-all instead of exiting 0; neither artifact ever booted, so the two doors now agree with the boot on them. Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — skills/** does not teach that two packages of one namespace may share one of these names; the rule ships in the refusal text and in ADR-0150 D2, which already records it.

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

维护者速读

  • 改了什么:同一个命名空间里的几个包(同一制品里的包、或靠依赖共用命名空间的包),它们的共享规则、队列、分配规则、hook、job、应用、页面、仪表盘、报表、文档书、分析立方、工具这 12 类元数据不能重名。安装时拒绝(422 NAMESPACE_CONFLICT,点出所有持有包),os validate 和 os build 用同一句话提前拒绝,os build 不产出制品。
  • 为什么改:ADR-0150 以后按「命名空间/名字」寻址这些元数据,两个同命名空间的包同名时,地址就指向两个条目、读者只能静默取第一个。ADR-0150 D2 已裁定:同一命名空间内一个名字只有一个包持有,就像对象名早已如此(ADR-0130 D3)。
  • 风险与回滚:只收窄接受集,不改已存数据、不迁移。实测第一方 0 处冲突(启动的 showcase 29 个包 60 个名字、4 个示例应用)。已经存在这种冲突的部署升级后会启动失败并点名两个包——这一点作为开放问题交席位裁定(另一选项:像一期规则那样冷启动只告警)。回滚即 revert。
  • 先看:packages/objectql/src/namespace-co-owner-names.ts(唯一判定)、registry.ts 里 installPackage 紧跟 D3 对象检查的那一行和 refuseNamespaceNameConflicts、packages/cli/src/utils/namespace-co-owner-names.ts(按启动安装顺序走制品)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-namespace-co-owner-names.test.ts —— 预期 56 passed。
  • 需要你做的:无,席位落地(收窄,席位按契约档复审)。

What changes

  • packages/objectql/src/namespace-co-owner-names.ts (new) — the ONE predicate. NAMESPACE_UNIQUE_TYPES (the twelve, in ADR-0150's child-N order); declaredNamespaceUniqueNames reads the names the engine registers (each collection by singularToPlural(type), nested plugins[] one level deep, the manifest-as-app and plugin-as-app rows, name else id); namespaceNameConflicts(incoming, sitting) returns, per (type, name) of the incoming package, EVERY other package of its namespace that holds it; namespaceNameConflictMessage is the one sentence. Not judged: a package with no namespace, the shareable platform namespaces (base, system, sys, the namespace gate's exemption), the environment catalog, the same package again.
  • packages/objectql/src/registry.ts — installPackage asks it right after the ADR-0130 D3 object check (refuseNamespaceNameConflicts), ahead of every mutation, at every install; the sitting holders are the installed package records of the incoming namespace, so an uninstall takes its names with it and nothing new is stored. New NamespaceNameConflictError: code: 'NAMESPACE_CONFLICT' (the namespace gate's, as the bare-name rule reuses it; no code minted), status/httpStatus 422, conflicts[], every holder named. No collisionPolicy downgrade. ⛔ Not the feat(objectql)!: a tool holds one name per package (bare-name one holder, after the AI family) #31661 hunks (its registerItem comment line and bare-name-one-holder.ts) — untouched.
  • packages/objectql/src/index.ts — exports the predicate, the sentence and the type list for the CLI (the class stays unexported, as the registry's other refusals).
  • packages/cli/src/utils/namespace-co-owner-names.ts (new) — the walk: an artifact's packages[] bodies in resolveArtifactPackageOrder's order (the order the boot installs them, the same seam os validate already drives for the union stack), each against the bodies before it; every refused body reported, each naming every holder. A stack without packages[] is one package and is never refused here.
  • packages/cli/src/commands/validate.ts (step 2c-bis) and compile.ts (step 3a-ter) — the same call right after the parse, beside the view-container refusal and with its faces (--json errors rows; text bullet list); os build writes no artifact. test/validate-build-gate-parity.test.ts — findNamespaceNameRefusals joins the shared-gate roster (forced: the roster is closed, every gate either command calls must be classified).
  • scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json — anchors the module to ADR-0150 D2 / ADR-0130.
  • .changeset/31670-namespace-co-owner-names.md — @objectstack/objectql and @objectstack/cli minor, both declaration lines, BREAKING Why / Before / Now / Who moves / Unchanged with the one-line fix (rename the item in one of the two packages), adr-0087: not-required (no-migration-prescription).

Forced test edits: two rows, no existing assertion changed. The parity roster row above, and one row in packages/objectql/src/federated-injected-column-readers.test.ts's closed ledger: namespace-co-owner-names.ts#<module> :: queue as not-a-read (its scanner matches the 'queue' literal of the twelve-type list as the ADR-0142 injected queue column; it names a metadata type and reads no row, the same disposition as assignment-rules.ts#<module> :: queue).

Why (measured, Step 0 on 5d1382a5)

  • ADR-0130 D3's object check lives at ONE door, not two. SchemaRegistry.installPackage → refuseCoOwnedObjectNameCollision (registry.ts). os validate (packages/cli/src/commands/validate.ts) has no D3 check: git grep for DUPLICATE_ARTIFACT_OBJECT_NAME / ArtifactObjectNameConflictError / ADR-0130 D3 under packages/cli/src and packages/lint/src is 0 hits. So this card builds the validate door for its own rule; it does not sit on an existing one there.
  • How D3 decides "same namespace": it does not compare namespaces. It reads coOwnersOf — artifact (every other package of the same release artifact, whatever its namespace), dependency and sibling (ADR-0141 D5, only for a non-shareable namespace) — and leaves a stranger to the namespace gate. Objects are global names, so that is enough for D3. For these twelve types two packages of one artifact in two namespaces hold two addresses, so this rule compares manifest.namespace itself (and so also covers two strangers the warn collision policy lets share a namespace).
  • The twelve types' collection keys (PLURAL_TO_SINGULAR, the engine's METADATA_ARRAY_KEYS): sharingRules, queues, assignmentRules, hooks, jobs, apps (top level only; plus manifest-as-app and plugin-as-app), pages, dashboards, reports, books, analyticsCubes, tools. The namespace field is the package's manifest.namespace for every one of them: no item of these types carries its own.
  • First-party census. Booted showcase (examples/app-showcase, pnpm dev --fresh, GET /api/v1/packages): 29 packages (22 in sys, 4 with none), 60 declared names of the twelve types, 0 (namespace, type, name) held by two packages — sys included, though it is not judged. Example stacks (each objectstack.config.ts, packages[] resolved as the boot does): app-crm 4 names, app-todo 8, app-multi-package 2 packages in crm, 1 name, app-showcase 50 — 0 held twice. os validate --json on all four: exit 0, 0 errors. HotCRM is not in this container: NOT MEASURED (the card's 0 of 297 is its claim).

ADR-0150's four readings

  • (a) phase-1 one-holder tests stay green: registry-bare-name-one-holder*.test.ts run inside the objectql suite below — green in the suite, and run alone: 4 files (registry-bare-name-one-holder.test.ts and its ai/flow/request family files) / 154 passed.
  • (b) bare-name behaviour unchanged for one-holder names: the CONTROL "co-owners with distinct names both install, and the bare name keeps serving its one holder" (real load path, getItem('page', NAME) answers each package's own page), and every per-type CONTROL (two namespaces, two types, the same package again) installs; first-party census 0, so no shipped name changes answer. For an uncovered type's name with two holders inside one namespace, this card's declared narrowing stands in its place.
  • (c) no migration: nothing stored changes shape and nothing is rewritten; the refusal reads the installed package records at install time and keeps no state of its own.
  • (d) Clause-②: no (narrowing) above and in the changeset; Metadata-protocol: narrowed; the changeset carries adr-0087: not-required (no-migration-prescription).

How the two checks compose at installPackage (with #31661)

Order in installPackage: namespace gate → ADR-0130 D3 object check → this rule → security catalog → default record page → phase 1's bare-name rule. So:

Verification

Head 97b94ed7 (merged origin/main at a2d0f737 once, no conflict). Every heavy run under os-verify-lock:

  • New: packages/objectql/src/registry-namespace-co-owner-names.test.ts 56 passed; packages/cli/src/utils/namespace-co-owner-names.test.ts + parity 2 files / 63 passed; packages/cli/test/validate-namespace-name.test.ts (spawns os validate / os build; asserts the row's message EQUAL to the package door's thrown message) 4 passed.
  • Suites on the final head: @objectstack/objectql 573 files / 11672 passed; @objectstack/cli unit project 406 / 5754 passed (+ the two new/edited integration files 2 / 38 passed); @objectstack/metadata-protocol 328 passed, 4 skipped / 22595 passed, 26 skipped; @objectstack/rest 395 / 7627 passed, 332 skipped; @objectstack/runtime 563 / 9033 passed, 19 skipped. Typecheck objectql and cli exit 0.
  • Dogfood battery (narrowing arm): 97b94ed7 · os-verify-lock: VERDICT command-exit 0 · Test Files 440 passed, 3 skipped (443) · Tests 3652 passed, 41 skipped (3693) · no failure.
  • Lint: eslint packages/objectql --no-inline-config exit 0; eslint packages/cli --no-inline-config exit 0.
  • Gates: dispatch-gates --ran on 97b94ed7: 84 derived, 84 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0. Two first ran exit 3 for a missing prerequisite and were re-run green: check-plugin-teardown-shape --self-test (its pinned fixture commit fetched first), check:dual-build-cjs-loads (after building the five packages without a dist/).
Ablations (each committed first, `ablation-replace.mjs`, restored, `git diff HEAD` empty)
  1. Package door — this.refuseNamespaceNameConflicts(manifest); replaced by a comment (ABLATION-31670) in registry.ts → registry-namespace-co-owner-names.test.ts 17 of 56 red: the 12 per-type refusals ("expected undefined to be an instance of NamespaceNameConflictError"), the real-load-path per-family refusal, the multi-name refusal, the dependency co-owner, the warn-policy refusal, the after-seal tool. The 39 green are the vocabulary cells and every CONTROL. Restored: blob 3b698a04 == HEAD, git diff HEAD 0 bytes. (objectql's tests import src, so no dist leg.)
  2. os validate door — if (namespaceNameRefusals.length > 0) { → < 0 in validate.ts → validate-namespace-name.test.ts 1 of 4 red: "os validate --json exits 1 …" (expected +0 to be 1): with the branch gone os validate exits 0, so no other gate catches the clash. The os build cell stayed green (its own call). Restored: blob f997cced == HEAD, diff 0 bytes. (run-dev.js runs the CLI from src.)
  3. The walk — if (conflicts.length > 0) { → < 0 in the CLI util → namespace-co-owner-names.test.ts 14 of 29 red (12 per-type rows, the three-holder walk, the dependency-order walk); the 15 CONTROLs green. Restored: blob fc052169 == HEAD, diff 0 bytes.

Acceptance notes

  • Open question 1 (seat's call): the cold boot. This rule refuses at every install, the boot included, as ADR-0130 D3's object check does. A deployment whose installed packages of one namespace already share one of these names (measured first-party: 0; HotCRM per the card: 0 of 297) no longer boots after upgrading, and the refusal names both packages. The alternative is phase 1's posture: judge only after kernel:bootstrapped seals the registry and warn at the cold boot. Recommendation: keep install-time, because D2 is ruled as an install refusal "on the ADR-0130 D3 object check", and the measured population that would stop booting is 0.
  • Open question 2 (seat's call): sys. ADR-0150 D2 names sys/NAME as an address too, but this rule does not judge the shareable namespaces (base, system, sys), mirroring the namespace gate's exemption and D3's co-owner map. Measured first-party: the 22 sys packages of a booted showcase hold 0 duplicate names, so judging sys would refuse nothing here today; cloud's sys packages are NOT MEASURED. Recommendation: leave sys out in this child; a later child can widen it once cloud's sys packages are measured.
  • Name (seat's call, house conventions): NamespaceNameConflictError; module namespace-co-owner-names.ts; exported NAMESPACE_UNIQUE_TYPES, namespacePackageOf, namespaceNameConflicts, namespaceNameConflictMessage. No new error code.
  • Not judged at either door: the residual (top-level items of a packages[] stack no body declares), which the boot registers without an install. os validate cannot see packages already installed in a deployment; that half is the package door's alone.
  • Bounds, stated (contract review N1, N2): the sitting holders are read from the stored package record and the incoming one from the live manifest, so a nested plugins[] entry that is a plugin class instance counts on the incoming side only; and on a running registry, an upgraded artifact that moves a covered name from co-owner A to co-owner B is refused when B re-registers before A (A's stale record still holds it), the same disposition ADR-0130 D3's object check has; the cold boot starts empty and is unaffected, and os validate accepts that artifact.
  • Integration tier: test/validate-namespace-name.test.ts spawns the CLI, so it falls in the cli integration project (declared to CI); it was also run locally above.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs

Inside one namespace, an item of the twelve bucket 2/3 types phase 1's
one-holder rule does not cover (sharing rule, queue, assignment rule,
hook, job, app, page, dashboard, report, book, analytics cube, tool) has
one package holder (ADR-0150 D2). The package door refuses the install
beside the ADR-0130 D3 object check, and os validate / os build refuse
the artifact, through one predicate and one sentence.

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

Claude-Session: https://claude.ai/code/session_01ADHkmQv69sQL5nfT8xuPWs
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl 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 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/objectql, touching 40 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.

42 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 b126c13065db29c80996efc7e0527a40f651c923.

⛔ 14 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
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 87 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 49 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 b126c13065db29c80996efc7e0527a40f651c923 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json b126c13065db29c80996efc7e0527a40f651c923

⚠️ 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 b126c13065db29c80996efc7e0527a40f651c923 → 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 97b94ed · 2026-11-08T23:17Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Epic red-line review — N #31670 · head 97b94ed · 2026-11-08T23:19Z

Verdict: PASS on the epic's three red lines. The lane seat (domain:server#1) keeps its own contract review, CI and landing. This record is the epic seat's part (ruling 6723571682).

Read: the diff against main (13 files, +1235): namespace-co-owner-names.ts in objectql and cli, registry.ts installPackage, os validate / os build, and the tests. Checked against ADR-0150 D2 (record 6724570466) and the plan's N row.

  • Red line 1, never loosens. The diff only adds a refusal. No resolution path changes: phase 1's one-holder rule, getItem and R's resolver have no diff. The same package re-installing is skipped (packageId equality), so an upgrade is not refused against itself.
  • Red line 2, ambiguity named. NamespaceNameConflictError carries every conflict and every existing holder. The message names them all. It reuses NAMESPACE_CONFLICT, an existing code, at 422.
  • Red line 3, releasable. The narrowing is the one the N row declares (D2: a (type, name) held twice inside one namespace, for the 12 uncovered types), measured first-party 0 of 297. Clause-②: no (narrowing) is the right declaration. One predicate serves installPackage, os validate and os build, so the doors cannot disagree.

The thread's three open questions, ruled:

  1. Refuse at cold start, not warn: keep the refusal. D2 as ruled says "refused at install and os validate", and boot installs each package. The first-party count is 0. A third-party deployment that holds such a pair learns at upgrade, loudly and with every holder named, which is the outcome red line 2 asks for. (This differs from E6's N2 on purpose: that was a reference the ADR declared refused at the read; this is an install door the ADR names.)
  2. base, system and sys not judged: accepted for this child. It neither narrows nor loosens anything today. G1's qualified address must answer 409 with the candidates for a name two packages of one of these namespaces hold, and never pick, so red line 2 holds there. G1 rest: by-name doors take an optional namespace segment and the _env segment, and answer 409 with the candidates on ambiguity (ADR-0150 child G1) #31691 carries that pin. The PR states the measured count of such duplicates on first-party trees. Child A (docs/adr: ADR-0048 addendum (§3.4 and §5 lint text) and ADR-0129 cross-reference for ADR-0150 (ADR-0150 child A) #31685) records the exemption in ADR-0150 D2.
  3. New public names (NAMESPACE_UNIQUE_TYPES, namespaceNameConflicts, NamespaceNameConflictError and the rest of the objectql index exports): the seat's call, accepted as named. They follow R's export pattern.

Serial: K #31681 and G1 #31691 follow this one on registry.ts installPackage, on the same seat.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

席位分诊 · Test Core (7/8) 红 = 已知 flake

运行 42534270683 的唯一红:plugin-security 的 delegated-admin-own-name-set.posture.test.ts:878,"Test timed out in 5000ms",是超时,不是断言失败(249/250 文件,6024 passed)。本 PR 不碰 plugin-security,也不碰该测试。这就是 #31663 记录的 main 上的 flake(main push 529e535 同文件同行超时;#31665 的队列运行 42530245440 同样被它踢出),修复在 PR #31707,已入队。

处置:经 relay 对失败 job 重跑一次;再红按真红处理。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 97b94ed710055aa56eb2b6db2b1feaa02f381383
Local-runs: none
Scope: packages/objectql/src/namespace-co-owner-names.ts, packages/objectql/src/registry.ts, packages/objectql/src/index.ts, packages/cli/src/utils/namespace-co-owner-names.ts, packages/cli/src/commands/validate.ts, packages/cli/src/commands/compile.ts, packages/objectql/src/registry-namespace-co-owner-names.test.ts, packages/cli/src/utils/namespace-co-owner-names.test.ts, packages/cli/test/validate-namespace-name.test.ts, packages/cli/test/validate-build-gate-parity.test.ts, packages/objectql/src/federated-injected-column-readers.test.ts, scripts/adr-anchors/packages__objectql__src__namespace-co-owner-names.ts.json + packages/objectql/src/engine-registration.ts, packages/objectql/src/engine-package-registration.ts, packages/objectql/src/engine-top-level.ts, packages/objectql/src/bare-name-one-holder.ts, packages/core/src/artifact-packages.ts, packages/core/src/plugin-order.ts, packages/spec/src/meta-spelling/manifest-collection-spelling.ts, packages/cli/src/utils/view-container-names.ts, packages/cli/src/utils/artifact-packages.ts (all read on main, never reviewed)

Inputs: card #31670 (body and both comments: the claim 6727391505 and the os-dev-report 6728539138), the epic's three red lines in the card body, ADR-0150 on main (D2, D9, execution plan row N), rulings 6723462584 / 6723499113 / 6723571682 / 6724570466, PR #31717 body, file list and git diff origin/main...97b94ed7 (13 files, +1235/-0), and the check-runs on the head. Check-runs at the time of this record: every run that has concluded is success — Lint & Repo Gates and all eight Lint shards, Type Check · source gates / debt ledger / consumer gates, Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, Dogfood Regression Gate, Test UI, Live E2E UI, PR-body guards; Test Core (1/8 … 8/8) and Type Check · workspace are still in_progress. Lint UI, Type Check UI and Build & E2E UI are skipped (no ui/ path in the diff). An in_progress run is not a pass: the seat lands only once those nine conclude success.

① Derived judgments

Accept-set changes the diff implies, each judged against the code it cites:

  1. The package door (SchemaRegistry.installPackage) refuses a package whose item of one of twelve types bears a name another installed package of the same manifest.namespace holds. Right. NAMESPACE_UNIQUE_TYPES is exactly sharing_rule, queue, assignment_rule, hook, job, app, page, dashboard, report, book, analytics_cube, tool — ADR-0150's execution plan row N verbatim (rag_pipeline retired, as the row says) — and the vocabulary test pins the list and that it shares only tool with BARE_NAME_ONE_HOLDER_TYPES. No thirteenth type is reachable: declaredNamespaceUniqueNames iterates that frozen list only. Nothing beyond the declared twelve, so nothing goes to the maintainer.
  2. The declared-name readers match the engine's registration seams. Right, checked against engine-registration.ts (registerPackageBody, registerMetadataCollections), engine-package-registration.ts (registerPlugin) and engine-top-level.ts (METADATA_ARRAY_KEYS, resolveMetadataItemName, registersManifestAsApp): each collection is read by singularToPlural(type) and all twelve keys are own entries of the spec's map (sharingRules, queues, assignmentRules, hooks, jobs, apps, pages, dashboards, reports, books, analyticsCubes, tools); the item key is name else id, as resolveMetadataItemName reads it for every non-view key; the manifest-as-app row is name && navigation && !apps?.length, the engine's registersManifestAsApp; a nested plugins[] entry is read one level deep (the engine recurses no deeper), its apps skipped (not in METADATA_ARRAY_KEYS, and registerPlugin passes apps: false), and its plugin-as-app row is plugin.name && plugin.navigation, the engine's condition. A nested plugin's items are stamped under the parent id, so assigning its names to the parent's namespace is what the engine does.
  3. Not judged: a package with no namespace; base, system, sys; the environment catalog; the same package again; one package declaring a name twice; every other type. Right. The shareable set equals isShareableNamespace on main (RESERVED_NAMESPACES + sys) and the test holds them equal; the sitting set is the installed package records only, so a registerItem with no package (the environment catalog, sys_metadata hydration) is never a holder; other.packageId === incoming.packageId is skipped, so a reload, re-install or upgrade of the same id passes (pinned), and uninstallPackage deletes the record, so an uninstall frees its names (pinned).
  4. No collisionPolicy downgrade. Right — the same disposition as the security catalog, the default record page and the bare-name rules; pinned with fresh('warn').
  5. Placement: after the ADR-0130 D3 object check, ahead of every mutation, at every install including the boot. Right. The call sits between refuseCoOwnedObjectNameCollision and refuseSecurityCatalogNameConflicts, before registerNamespace and the record write; the test pins that a refused install leaves no record and no namespace ownership, and that an install refused by both D3 and this rule answers DUPLICATE_ARTIFACT_OBJECT_NAME.
  6. os validate / os build refuse the artifact in the same sentence, exit 1, errors rows (packageId, code, httpStatus, message, conflicts[]), and os build writes no artifact. Right. The CLI helper owns only the walk — resolveArtifactPackageOrder(parsed), the boot's dependency-topological order — and calls namespacePackageOf, namespaceNameConflicts and namespaceNameConflictMessage from @objectstack/objectql; no second copy of the predicate or the sentence exists (the spawn test asserts the row message EQUAL to the door's thrown message). One undeclared side-effect rides along — see ② and ④ S1.
  7. Public surface. @objectstack/objectql gains four values (NAMESPACE_UNIQUE_TYPES, namespacePackageOf, namespaceNameConflicts, namespaceNameConflictMessage) and three types (NamespaceNameConflict, NamespacePackage, NamespaceUniqueType); NamespaceNameConflictError stays unexported, as BareNameConflictError, ArtifactObjectNameConflictError and NamespaceConflictError are on main; no error code is minted (NAMESPACE_CONFLICT_CODE was already exported). Right and consistent. @objectstack/cli gains an internal util only.
  8. Forced test rows. The parity roster row belongs in SHARED_NON_REGISTRY_GATES (both commands call it, cross-package by construction) — right. The injected-column ledger row keys the module-level site of the queue literal: the module spells 'queue' twice, in the NamespaceUniqueType alias and the frozen list, both at module level, so one row under the module-level key is the correct count — right.
  9. ADR anchor. Names ADR-0150 and ADR-0130; the module header spells both ids. Right.

② Semver level

.changeset/31670-namespace-co-owner-names.md: @objectstack/objectql: minor, @objectstack/cli: minor. Matches what the diff publishes: both packages move (objectql's door, exports and module; cli's two commands and util), no other package's source changes (core, spec, metadata-protocol, runtime are read, not edited). The (narrowing) arm is BREAKING under AGENTS.md's level table and the title carries !; Metadata-protocol: narrowed takes at least minor on a moved package — satisfied; minor is also what the four phase-1 bare-name changesets (#30836, #31006, #31093, #31375) took for the same class of refusal. Clause-②: no (narrowing) is on the PR body and in the changeset with the same sentence; Skills-docs: none — REASON present; adr-0087: not-required (no-migration-prescription) present and correct (no stored shape changes, no key removed). Check Changeset is green. One wording gap: the Clause-② sentence names the two refusals of the twelve-type rule and says "Nothing is widened" — true — but does not name the two boot refusals the new walk lets through to os validate / os build (④ S1).

③ Boundary flags

  1. Cold boot refuses rather than warns (dev: keep refuse). Agree — keep A. D2 is ruled as a refusal "at install and os validate, on the ADR-0130 D3 object check" (ADR-0150 row N; ruling 6724570466), and D3 refuses at every install, the boot included; phase 1's warn-at-boot posture belongs to an environment-wide backstop that ADR-0150 D9 retires per type, not to this rule. The population that would stop booting is measured 0 first-party and 0 of 297 on HotCRM (the card's reading; the dev marks HotCRM NOT MEASURED in this container, honestly). The changeset's "Who moves" names the upgrade consequence and the one-line fix. No escalation.
  2. sys / base / system not judged (dev: leave out). Agree for this child — A. Mirrors isShareableNamespace and D3's co-owner map, measured 0 duplicates on showcase's 22 sys packages; cloud's sys packages NOT MEASURED, stated. ADR-0150 D2 does spell sys/NAME as an address, so the exemption is a bound on D2's guarantee, not a reading of it: the seat should carry it forward as a follow-up card (widen once cloud's sys packages are measured) rather than leave it in acceptance notes only. Not a blocker: the module, the anchor and the changeset all state the exemption.
  3. New public names. Accept A. NamespaceNameConflictError follows the sibling classes' spelling and envelope; NAMESPACE_UNIQUE_TYPES / namespacePackageOf / namespaceNameConflicts / namespaceNameConflictMessage follow BARE_NAME_ONE_HOLDER_TYPES / declaredBareNames / bareNameReason. Two nits only (④ N4): the class differs from the existing NamespaceConflictError by one word, and the module file is namespace-co-owner-names.ts while its exports say namespaceName*. No new code — right: the condition is the one NAMESPACE_CONFLICT registers for.
  4. Not raised by the dev, answered here: the residual (④ N3), the hot re-registration order (④ N2), the record projection asymmetry (④ N1), and the core resolver's refusals surfacing at the CLI doors (④ S1).

④ Design judgment

Scenario accepted against: ADR-0150 D2 (one package holder per (type, name) inside a namespace, so NAMESPACE/NAME names one item), on ADR-0130 D3's object check in installPackage, under the epic's red lines.

Red line 1 — resolution never loosens. Holds. No reader is touched; getItem's bare-slot-first answer and every first-registered fallback on main stay exactly as they are (a tool, a page with two holders in two namespaces still answers the first — phase 1 / children R and G, declared out of scope). The diff only removes configurations from the accept set; the ablation record and the CONTROLs (two namespaces, two types, same package, sys, no namespace, environment catalog, a view/flow/permission set) pin what stays accepted.

Red line 2 — ambiguity is reported, never guessed. Holds. namespaceNameConflicts collects every other holder per (type, name) (existingPackageIds, never empty), the message lists every conflict and every holder, the structured conflicts[] rides both the thrown error and the CLI row; the CLI walk reports every refused body and keeps a refused body as a holder for the bodies after it (pinned: the three-holder walk). The test "names EVERY other holder of the namespace, once per (type, name)" pins the multi-holder case.

Red line 3 — independently releasable, readings (a)–(d). (a) phase-1 one-holder tests untouched and reported green; bare-name-one-holder.ts is not in the diff. (b) bare-name behaviour unchanged for one-holder names: the CONTROL on the real load path reads both co-owners' pages back by bare name. (c) no migration, nothing stored changes shape, the rule keeps no state (reads the package records at install time). (d) Clause-②: no (narrowing) on body and changeset, Metadata-protocol: narrowed, adr-0087: not-required. Holds.

Per file.

  • packages/objectql/src/namespace-co-owner-names.ts — holds against the engine seams it cites (① 2). namespaceNameConflicts dedupes holders per key and conflicts per incoming (type, name); isNamespaceJudged equals the namespace gate's exemption; artifactPackageId is the one id derivation (id || name, so a package installed under its name is excluded from its own sitting set correctly). The \u0000 key separator cannot collide with a name (SnakeCase grammars). Message and line are the one spelling both doors use.
  • packages/objectql/src/registry.ts — refuseNamespaceNameConflicts reads this.metadata.get('package') records' manifest.namespace and projects each through namespacePackageOf; placed as ① 5. NamespaceNameConflictError carries code, status, httpStatus 422 like NamespaceConflictError and BareNameConflictError on main, so the REST passthrough (status) and the CLI envelope (httpStatus) both read it — the envelope is consistent with the namespace gate. Composition with phase 1's tool: this rule runs before refuseBareNameConflicts, carries the same code and status, and the test pins that after sealBareNameHolders() a same-namespace tool is refused by THIS class; a cross-namespace tool stays the bare-name rule's (after feat(objectql)!: a tool holds one name per package (bare-name one holder, after the AI family) #31661) — the two never disagree about a refusal, only about the door's timing, as the module doc says. feat(objectql)!: a tool holds one name per package (bare-name one holder, after the AI family) #31661's hunks (registerItem comment, bare-name-one-holder.ts) are not touched.
  • packages/objectql/src/index.ts — exports exactly what the CLI consumes; the class stays private as its siblings do.
  • packages/cli/src/utils/namespace-co-owner-names.ts — the walk is resolveArtifactPackageOrder, the order AppPlugin → manifest.register → registerApp(body) installs in (ADR-0130 D4/D5), each body against the bodies before it; a stack without packages[] returns [] (one package, nothing to hold beside). Same predicate, same sentence, no second copy. Stated bounds (installed packages of the deployment invisible; residual unjudged) are true.
  • packages/cli/src/commands/validate.ts (2c-bis) and compile.ts (3a-ter) — the call sits right after the parse, beside 2c/3a, with 2c/3a's faces (--json errors rows carrying warningsSoFar() and conversionNotices; text printError + printBulletList with JSON_FULL_LIST_REMEDY), exit 1 before any artifact write. The parity roster row classifies it where both commands call it.
  • Tests — the twelve-type describe.each at both doors, the real-load-path refusal per family, the spawn test asserting message equality across doors, the ablations in the PR body (17/56, 1/4, 14/29 red) show each door's call is load-bearing. The two forced rows are correctly keyed (① 8).

Findings.

  • S1 (should-fix) — an undeclared accept-set change at os validate / os build, through the walk's resolver. findNamespaceNameRefusals is the first call either command makes to @objectstack/core's resolveArtifactPackageOrder (the sibling walks — view-container-names.ts, picklist-references, override-declarations, collect-docs — read artifactPackages, a plain map, and collect-docs.ts states "this collector has no duplicate-id refusal of its own"). The resolver throws DUPLICATE_ARTIFACT_PACKAGE (422) for an artifact declaring one package id twice and resolvePluginOrder's bare [Kernel] Circular dependency detected (no code) for a dependency cycle inside the artifact. The stack schema at step 2 parses packages: z.array(ArtifactPackageSchema), so a malformed entry is already refused there, but neither of those two was refused by os validate on base: they now exit 1 through the commands' catch-all (error + errorCodeFields in --json, a printed line in text), not as an errors row, and the cycle carries no code. Nothing is loosened and both artifacts never booted — stack-collections.ts records the house position that a named 422 beats that silence — so this is not a narrowing beyond the twelve types and not a maintainer question. But the Clause-② sentence ("Nothing is widened"; two refusals named) does not name it, and the cycle refusal reaches the author without an envelope. Fix: name the two pass-through refusals in the PR body's Clause-② sentence (a body edit, no new head) and, when the branch next moves, in the changeset; optionally catch the resolver's refusal in the walk and emit it as a row with its code. Not a must-fix.
  • N1 (nit) — sitting holders are read from the projected record, the incoming from the live manifest. toRecordManifest drops class instances, so a plugins[] entry that is a plugin instance carrying a covered collection counts on the incoming side and is absent on the sitting side — an order-dependent gap for a shape GET /packages cannot serve either. State the bound in the module doc beside the residual bound, or run namespacePackageOf over toRecordManifest(manifest) on the incoming side too.
  • N2 (nit) — hot re-registration order. On a running registry, moving a covered name from co-owner A to co-owner B in one upgraded artifact is refused when B re-registers before A (A's stale record still holds the name); the cold boot starts from an empty registry so it is unaffected, and refuseCoOwnedObjectNameCollision has the identical disposition for an object (it reads the sitting owner), so this is inherited from the ruled sibling. os validate walks only the new bodies and accepts that artifact, so the two doors can disagree on this one hot path. State it as a bound.
  • N3 (nit) — the residual. Top-level items of a packages[] stack that no body declares are registered under the stack's own id without an install (MetadataPlugin.registerUnclaimedTopLevel) and are judged at neither door, as the PR body and module say; the stack manifest can carry the same namespace as its packages. Already in the acceptance notes; it should become a filed follow-up card (Prime Directive 10: a known gap in enforcement is filed, not only noted).
  • N4 (nit) — names. NamespaceNameConflictError vs the existing NamespaceConflictError; module namespace-co-owner-names.ts vs exports namespaceName*; no rename suggestion carried as BareNameConflict.rename is (the sentence says rename generically). No change required.
  • N5 (nit) — itemName requires a non-empty string where resolveMetadataItemName takes any truthy name/id. Unreachable for a spec-valid item (names are strings); no change required.

No must-fix. S1 is a declaration gap on an accept-set change that is the boot's own; the seat decides whether to take it as a body edit on this head or with the next push.

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

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

席位 ACCEPT · head 97b94ed710055aa56eb2b6db2b1feaa02f381383

Adopts the contract-review record 6728707576 (CONTRACT_REVIEW_TIER, narrowing face, PASS) on this head.

  • S1 (should-fix): taken as a body-only fix, so the head does not move. The Clause-② sentence in the PR body now names the two boot refusals that reach os validate / os build first through resolveArtifactPackageOrder: DUPLICATE_ARTIFACT_PACKAGE 422 and the dependency cycle. Neither loosens anything, and neither narrows beyond the twelve types. The changeset wording follows only if the branch moves again.
  • N1, N2: stated as bounds in the PR body. N2 has the same disposition as ADR-0130 D3's object check.
  • N3 (residual top-level items of a packages[] stack, judged at neither door), plus the sys/base/system exemption: handed to the epic seat. Its 23:19Z ruling already places sys with G1 (409 with candidates) and child A (an ADR note). N3 goes to it as a possible child.
  • N4, N5: accepted deviations, no change.
  • Open questions, ruled by the epic seat and agreed by this seat: the cold boot keeps refusing; base/system/sys are not judged in this child; the names are accepted as they are.
  • Epic seat red-line review: PASS, no objection to landing.
  • check-governed-merges: NOT governed.

Landing waits for CI. Test Core (7/8)'s red is the #31663 flake (triage comment above). The seat re-runs it once when run 42534270683 completes, then pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 8, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 8, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit d655caf Nov 9, 2026
82 of 86 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mjoky7 branch November 9, 2026 00:58
rajeev-kumar-se pushed a commit to rajeev-kumar-se/objectstack that referenced this pull request Nov 10, 2026
…0129 and ADR-0130 point at it (ADR-0150 child A) (objectstack-ai#31841)

<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbRSEASPFQojyg3PhVYDG2Jp)_

Fixes objectstack-ai#31685

Clause-②: no
Metadata-protocol: none

## 维护者速读

- 改了什么:只改文档(四份 ADR 和一处代码注释,不改代码)。ADR-0150
补记子卡上已裁定、或由专题席位记录的七条:闭包外的包被拒绝(含同一发布包里没声明依赖的兄弟包)、阶段一之前就重名的部署被拒绝、同对象同名动作被拒绝(落在动作收尾卡)、`base`/`system`/`sys`
不受同命名空间唯一规则约束、扩展目标和 everyone/guest 锚点按无包调用方解析、无归属的引擎函数算平台持有者。ADR-0048 §5
的 lint 条目改成现状;ADR-0129、ADR-0130 加带日期的交叉引用。
- 为什么改:这些裁决都已在子卡上定下并有记录,但 ADR-0150
正文还写着旧说法(比如「阶段一覆盖的类型不会有两个持有者」「动作已被阶段一兜底」);P 合入后 ADR-0048 §5 还说 lint
对所有裸名告警、阶段三会改成拒绝。不改的话,后续子卡会照着过时的正文去做。
- 风险与回滚:只改文字,无运行时风险,不发版(`skip-changeset`)。回滚即撤销这一个提交。
- 先看:ADR-0150 D3 新增的 (c) 段和「Covered and uncovered types」段;ADR-0048 §5
第一个条目;ADR-0130 D1 下的注记。
- 两分钟验证:运行 `pnpm check:adr-index && node
scripts/check-adr-links.mjs`,两条都应退出 0。
- 需要你做的:契约复审通过后,审阅并合并(Tier H,`docs/adr/**`)。

## What changes

Docs only: four ADRs and one code comment. No code, schema, lint or
skills change.

- **ADR-0150** (status line gains an **Amended** entry). Each sentence
cites the record it writes in:
- **D3 (c)**: a package caller never resolves to a package outside its
declared closure by bare name. This is step 3 of the ruled rule
(6723462584), recorded as such by the epic seat (6728487836). A sibling
package of the same release artifact is no exception unless declared
(N1, ruled A, 6729820123), with the severity that record gives.
- **D3, caller-less references**: a picklist extension's target
(epic-seat ruling X, 6728613177) and the audience anchors `everyone` and
`guest` (an epic-seat reading recorded in its E3 review, 6732998040 on
draft PR objectstack-ai#31815) resolve as a caller with no package.
- **D2**: the shareable namespaces `base`, `system` and `sys` are not
judged by child N's co-owner rule (epic red-line review of N, 6728571147
on PR objectstack-ai#31717); a qualified address there answers `409` with the
candidates, and G1 pins it.
- **D4**: an owner-less engine function registration is the platform's
holder, reachable from every closure like `sys` (R2 review 6729484996
item 2, refined by re-review 6730444942 item 1).
- **Execution plan, covered types**: the object-less action is what
phase 1 covers. Two cases keep two package holders even so: a clash
sealed before phase 1 (objectstack-ai#31677 Q1 ruled A, 6729831159) and a same-object
same-name action pair (objectstack-ai#31697 ruled A, 6729825528). The sentence "no two
package holders of a name exist while its refusal stands" is narrowed
accordingly.
- **Execution plan, releasability**: the "except D3 (a)" sentence now
also names D3 (c).
- **TL;DR 7**: the three ruled refusals above join the list; "Measured
first-party, none of them refuses anything" becomes "Where measured
first-party, ...".
- **ADR-0048**:
- §5's `[done]` authoring-lint bullet is rewritten in place, as d5106ae
did for the same bullet. It now describes what `naming/namespace-prefix`
reports since child P (objectstack-ai#31765): the duplicate finding
(`DUPLICATE_KEYED_TYPES`), and the bare-name finding for capabilities
and positions only (`PREFIX_JUDGED_COLLECTIONS`). Phase 3 of ruling C is
withdrawn (record 6721941146 on objectstack-ai#30806).
- A dated note under §3.4 says its lint sentence still holds (see
Acceptance notes).
  - The status line gains an **Amended** entry.
- **ADR-0129**: a cross-reference to ADR-0150 on the status line, and a
dated note under D4. ADR-0028 is re-opened for the behavioural tier
only, objects stay under this record, and ruling C's planned amendment
of D2 is withdrawn (6721941146).
- **ADR-0130**: one dated note under D1's first paragraph (N1,
6729820123). Co-ownership does not decide how a reference between two
packages of one artifact resolves; the sibling allowance narrows to
declared dependencies (ADR-0150 D3). The ADR-0130 text itself is not
rewritten.
- **`packages/cli/src/utils/picklist-references.ts`**: the module
comment at line 42 gains a dated note pointing at ADR-0150 D3 and the
ADR-0130 note. Comment only.

## Why

Child A of epic objectstack-ai#31527. The epic seat carried seven items into A's
ADR-0150 text (comment 6729937614 on objectstack-ai#31685), on top of the card's
ADR-0048 and ADR-0129 items. Each item was checked against the record it
cites before its sentence was written.

## Tests

None: a docs-only change (`skip-changeset`). The one code edit is a
comment, so there is no tested behaviour to ablate.

## Acceptance notes

- **Deviation: ADR-0048 §3.4 is noted, not amended.** The card called
its lint sentence stale. Measured on `origin/main` 732def5, it is not:
the sentence cites the rule for "an author shipping two `page/home` in
one package". That duplicate finding still ships
(`DUPLICATE_KEYED_TYPES` in `packages/cli/src/commands/lint.ts`), and
ADR-0150 D9 keeps it. So §3.4 gets a dated note saying the sentence
holds and pointing at §5, and only §5's bullet is rewritten. The seat
agreed to this reading.
- **Deviation: the ADR-0130 note sits under D1.** ADR-0130 has no
sentence about resolving picklists or references. The CLI comment at
`picklist-references.ts:42` leans on D1's co-ownership claim, so that is
the sentence N1 means. The seat agreed to this placement.
- **Status-line pointers:** ADR-0028's and ADR-0048's pointers to
ADR-0150 landed with objectstack-ai#31586, and they are not duplicated here.
ADR-0150's own status line still reads "Proposed (2026-11-08) ...
becomes Accepted by the maintainer's merge", and ADR-0048's pointer
still says "takes effect when ADR-0150 is accepted". Both are left as
written; they are noted here for the seat.
- **`tool`, corrected per contract review F1** (record 6734267031):
`tool` joined phase 1's list (objectstack-ai#31414; ADR-0048 P1.10), and
`BARE_NAME_ONE_HOLDER_TYPES`
(`packages/core/src/package-scoped-resolution.ts`) lists 13 entries with
`tool` last. ADR-0150 now counts 14 covered types (the constant's 13
plus permission sets), 11 uncovered types without `tool`, and 14 B
children. The same fix commit (0a18e76) drops the letter from the N1
citation (F2), dates the ADR-0130 note's install-judge reading to E6
objectstack-ai#31731 (F3), and scopes the sealed-clash count to E5's measurement (F5).
- **The E3 reading** is cited as an epic-seat reading recorded on draft
PR objectstack-ai#31815 (6732998040), not as a ruling.

## Verification

<details><summary>Readings</summary>

Measured on commit bfc1d99 (base `origin/main` 732def5); after merging
`origin/main` at ca23ce3, none of the five files changed, and
`check:adr-index`, `check-adr-links` and `check-adr-anchors` were re-run
there: exit 0 each.

- `dispatch-gates`: 56 gates derived for this diff, 56 run, all exit 0.
`--ran` reconcile: 0 NOT-MEASURED, 0 UNRUN.
- Prerequisite builds under the verify lock: spec + formula + lint
prebuild, VERDICT 0; full `turbo run build --filter=./packages/**`,
VERDICT 0 (81/81 tasks).
- First pass: 6 gates exited 3 (prerequisites missing, not a
measurement): the teardown self-test (pinned fixture commit 621a487 not
fetched in a shallow clone), and dual-build-cjs-loads, i18n,
i18n-coverage, i18n-walk-parity and lean-entry-closure (needed built
packages). After fetching the commit and running the full build, all six
exit 0.
- Body gates with this body as the event payload:
check-closing-keyword-parity, check-changeset-no-major,
check:adr-0087-registration and check-partof-closing-keyword, exit 0
each.
- Diff: 5 files, +106 / -26. The review-fix commit 0a18e76 touches only
ADR-0150 and ADR-0130; `check:adr-index`, `check-adr-links` and
`check-adr-anchors` were re-run on it: exit 0 each.

</details>

## Human Minimum

- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema touched.)
- [x] 2. Every name I added uses the casing for its kind. (No name
added.)
- [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran. The
repo-wide `pnpm lint` scan is CI's run; the only source edit is a
comment.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset. (Publishes
nothing: `skip-changeset`.)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01QsR2jUkMujHycV1pMLHL8h

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jUkMujHycV1pMLHL8h)_

<!-- signed: domain:spec#2 · zhuangjianguo ·
session_01QsR2jUkMujHycV1pMLHL8h -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/xl 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: refuse a (type, name) held twice inside one namespace for the uncovered bucket 2/3 types (ADR-0150 child N)

1 participant