Skip to content

feat(install): a declared dependency co-owns its parent's namespace, and both boot hydrates sort by it (ADR-0141 D5) - #26054

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-78l2jg
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-78l2jg

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Part of #25792

Clause-②: yes (narrowing)
Metadata-protocol: none

Summary

ADR-0141 D5, card items 4 and 5, plus the two review notes from #25986.

Item 4: a declared dependency is a second co-owner source (packages/objectql/src/registry.ts). SchemaRegistry.installPackage builds one co-owner map, coOwnersOf, and both halves of the ADR-0130 D3 pair read it: the namespace gate and refuseCoOwnedObjectNameCollision. For a non-shareable namespace N used by the incoming package P, the map holds:

  • artifact: the artifact's other packages, exactly as before;
  • dependency: an installed X that P declares and that holds N, or a sitting owner of N that declares P. This is the same relation read from the other end, so an upgrade or reinstall of the app beside its modules, or a boot that hydrated a module first, is admitted;
  • sibling: a sitting owner S of N, when S and P each directly declare one installed package that holds N.

Only direct declarations count. The shared parent's namespace is read from its installed record, so two packages that declare one common package co-own nothing unless that package is installed and holds N.

ArtifactObjectNameConflictError keeps DUPLICATE_ARTIFACT_OBJECT_NAME / 422. Its message is now worded per source (same artifact, declared dependency, sibling), so a cross-artifact refusal never claims one artifact, and it carries coOwnership. NAMESPACE_CONFLICT now names the declaration as the fix.

Item 5: hydrate order (packages/core/src/hydration-order.ts, new). orderHydrationBatch sorts a batch with resolvePluginOrder, using declared ids as optional edges. This is the reading resolveArtifactPackageEntries already gives the key; there is no second sorter. A node lists every package id it carries. A cycle's internal edges are dropped before sorting, found with Tarjan's algorithm, so its members keep source order while still following their acyclic dependencies. The cycles are returned for the caller to log, and nothing throws. Both hydrates use it:

  • the sys_packages hydrate in packages/services/service-package/src/index.ts, where one row is one node;
  • the install-local kernel:ready rehydrate in packages/cloud-connection/src/marketplace-install-local-plugin.ts#inDependencyOrder, where one ledger entry is one node. A declared id resolves to the entry that carries that package, and an id declared between packages of one entry is no edge.

declaredDependencyIds (packages/core/src/artifact-packages.ts) is the one record-only reading of manifest.dependencies that the registry and both hydrates share.

Review notes from #25986:

  • An undecided union whose installed version is readable (<5 || latest against 7.0.0) now reports found: 'unreadable-range' instead of naming the version.
  • Install-local no longer counts the artifact wrapper's own id as delivered. DeclaredDependencyCheck.deliveredIds, which had no other caller, is removed.

Docs:

  • content/docs/deployment/app-dependencies.mdx now says that two modules of one app need not declare each other. It also explains namespace co-ownership by declaration and its object-name rule, the companion precondition, and the boot order with its cycle log.
  • The plugin-spec.mdx consumer callout now names five consumers.

Declarations

Clause-②: yes (narrowing). The widening: install acceptance (packages previously refused NAMESPACE_CONFLICT now install) and new @objectstack/core exports (orderHydrationBatch, declaredDependencyIds, HydrationNode, HydrationOrder). The narrowing: install-local refuses a module that declares only its artifact wrapper's id, and the deliveredIds option is removed. Both of those belong to the D2 check, which #25986 landed and which is still unreleased (its changeset is pending on main). The changeset is patch, with a BREAKING banner and an ADR-0087 not-required (no-migration-prescription) marker.

Metadata-protocol: none. No key, accepted value or metadata type changes in what an author can write.

Tests

Red first: each new suite failed on main before its wiring.

Suite Before the fix After the fix
packages/objectql/src/registry-artifact-co-ownership.test.ts, new describe extending the ADR-0130 D3 gate test, real load path through manifest.register(); the sixth case is a negative control 5 failed, 10 passed 15 passed
packages/services/service-package/src/hydration-dependency-order.test.ts, real SchemaRegistry 2 failed 2 passed
packages/cloud-connection/src/marketplace-install-local-rehydrate-order.test.ts 2 failed 2 passed
packages/core/src/hydration-order.test.ts new sorter 6 passed
the review-note cases in protocol-handshake-dependencies.test.ts and marketplace-install-local-dependency-missing.test.ts 1 failed each passed

The new objectql cases cover:

  • the cross-artifact pair: the dependent is admitted, and a dependent defining crm_account is refused;
  • the sibling pair: the sibling is admitted, and a sibling defining the other sibling's name is refused;
  • the parent arriving second;
  • the upgrade case: the parent is admitted, and a parent newly defining the module's name is refused naming the module;
  • an undeclared sharer, which is still refused;
  • siblings whose parent is absent or holds another namespace, which are refused.

Every refusal asserts code and status.

Package suites on this branch after a full build (119/119 tasks), all typecheck exit 0:

Package Test files Tests
core 120 2774
objectql 470 8748
service-package 10 127
cloud-connection 58 732
metadata-core 20 818

Ablations (scripts/ablation-replace.mjs, each restore checked against HEAD):

Mutation Failures
parent arm removed 3
reverse arm disabled 2 (either end, upgrade)
sibling arm removed 1
sibling arm reading any shared declared id instead of an installed parent holding N 1
sys_packages hydrate in list order 1
install-local rehydrate in listed order 2
cycle edges kept 1
found-label fix reverted 1

Gates: node scripts/pm/dispatch-gates.mjs --ran accounts for all 106 derived families, all exit 0. One found a real miss: check-registry-log-declared flagged the service-package suite that now builds a SchemaRegistry, and vitest.config.ts now declares OS_REGISTRY_LOG. Scoped eslint on the touched files exits 0, and check:nul-bytes exits 0.

Full dogfood on 968c670094: 2522 passed, 18 skipped, 1 failed (VERDICT command-exit 1). The head 10c01a4181 adds only the service-package vitest.config.ts env line. The failure is authz-probe-blind-spot.test.ts, the packages/runtime/src/domains/i18n.ts row (handleI18nRequest expected 3, got 4). That is main's red since #25956, reported on #25986 too; this PR touches neither file.

Acceptance notes


🤖 Generated with Claude Code

https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA

Generated by Claude Code

…pace, and both hydrates sort by it (ADR-0141 D5)

The install gate's co-owner set gains a second source beside the artifact:
a package declaring an installed X that holds its namespace, a sitting owner
declaring the incoming package, and a sibling under one declared parent. The
object-name check reads the same set and words its refusal per source. The
sys_packages hydrate and the install-local rehydrate order their batch by
manifest.dependencies through resolvePluginOrder; a cycle keeps source order
for its members and is logged. Docs and changeset included.

Also carries the D2 review notes: an undecided union with a readable installed
version reports unreadable-range, and install-local no longer counts the
artifact wrapper's id as delivered.

Claude-Session: https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA
Co-authored-by: Claude <noreply@anthropic.com>
…e builds a SchemaRegistry

Claude-Session: https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cloud-connection, @objectstack/core, @objectstack/metadata-core, @objectstack/objectql, @objectstack/service-package, touching 22 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/services/service-package/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/app-dependencies.mdx (via installPackage (symbol, a method of class SchemaRegistry))
  • content/docs/deployment/cli.mdx (via MarketplaceInstallLocalPlugin (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/contracts/metadata-service.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/protocol/kernel/plugin-spec.mdx (via SchemaRegistry (symbol, a top-level class), orderHydrationBatch (symbol, a top-level function))

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

  • content/docs/releases/v12.mdx (via installPackage (symbol, a method of class SchemaRegistry))
  • content/docs/releases/v17/17-0.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via SchemaRegistry (symbol, a top-level class), /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))

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

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/services/service-package/vitest.config.ts) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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 — 40 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 431f065177de0cfc9d682b3b18692132390e0c57 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 431f065177de0cfc9d682b3b18692132390e0c57

⚠️ 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 431f065177de0cfc9d682b3b18692132390e0c57 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 10c01a418105fedab936a4b17a343fecf621fe04
Local-runs: none
Scope: packages/cloud-connection/src/marketplace-install-local-dependency-missing.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-rehydrate-order.test.ts, packages/core/src/artifact-packages.ts, packages/core/src/hydration-order.test.ts, packages/core/src/hydration-order.ts, packages/core/src/index.ts, packages/metadata-core/src/protocol-handshake-dependencies.test.ts, packages/metadata-core/src/protocol-handshake.ts, packages/objectql/src/registry-artifact-co-ownership.test.ts, packages/objectql/src/registry.ts, packages/services/service-package/src/hydration-dependency-order.test.ts, packages/services/service-package/src/index.ts, packages/services/service-package/vitest.config.ts + packages/core/src/plugin-order.ts (resolvePluginOrder), packages/cloud-connection/src/local-manifest-source.ts (InstalledManifestEntry), packages/objectql/src/index.ts (what objectql re-exports from core), packages/objectql/src/registry.ts at origin/main (the ArtifactObjectNameConflictError message before the change), .changeset/25792-pre-install-dependency-check.md on origin/main (the still-pending D2 changeset), packages/{core,objectql,metadata-core,cloud-connection,services/service-package}/package.json and CHANGELOG.md on origin/main (versions and release state), scripts/check-registry-log-declared.mjs, scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, docs/adr/0141-inter-app-dependency-declared-checked-before-install.md (D5, Open items 2, merge questions 3, 4 and 12), the GitHub releases and tags listing. The prose faces (.changeset/25792-dependency-declared-co-ownership.md, content/docs/deployment/app-dependencies.mdx, content/docs/protocol/kernel/plugin-spec.mdx) were read for the claims they make about the code above.

① Derived judgments

CI state at review time (2026-10-26T10:21Z, 47 check-runs on the head). 25 concluded success, 7 skipped, 0 failure, 15 still in progress: Test Core shards 1 to 8, Type Check · workspace, Type Check · debt ledger, Type Check · consumer gates, Type Check UI, Build Core, Build & E2E UI. Concluded green and load-bearing here: Lint · CI, release & engine gates (runs check:changeset-no-major and check:adr-0087-registration), Check Changeset, Lint · docs & code guards, Lint · repo & tooling gates, Lint · ESLint, Lint · PM dispatch-gates, Type Check · source gates, Build Docs, Spec property liveness, Governed Surface Queue Guard, Part-of PR must not also close its card, The card this PR closes must claim this branch, Check PR Size, Check Documentation Links. The Dogfood Regression Gate aggregator is success over skipped shards, as on every PR event. The judgments below are rendered on the diff and what has concluded; the test shards, the workspace typecheck and Build Core are the gates still owed, and nothing in the diff was found that predicts them red. This record does not vouch for them.

A. The co-owner map and its three sources — right. SchemaRegistry.installPackage now computes coOwnersOf(manifest, selfId, scope) once, a Map from package id to CoOwnershipSource, and both halves of the ADR-0130 D3 pair read it: the namespace gate's .find(owner is not manifest.id and not coOwners.has(owner)) is byte-unchanged and reads the map's keys, and refuseCoOwnedObjectNameCollision takes the map and passes coOwners.get(ownerId) into the error. So the relaxation and the refusal cannot disagree about who counts, which is the D3 invariant the card restates. Source by source, against ADR-0141 D5:

  • artifact: scope.packageIds minus selfId, exactly the set the old code built. Precedence: an artifact co-owner is set first and never overwritten, so a package that is both co-delivered and declared is reported as artifact, which is the stronger proof. Right.
  • dependency, from P's side: declaredDependencyIds(manifest) filtered to ids that are installed (this.getPackage(id)) and whose installed manifest.namespace === N. A declared but absent parent, or a parent under another namespace, is no co-owner, which is D5's "N is X's namespace" condition and merge question 12's refusal to admit without the parent present. Right.
  • dependency, from X's side (the reverse arm): every sitting owner of N whose own installed manifest.dependencies names selfId is a co-owner. A sitting owner is by definition installed and holds N, so no further check is owed. This is D5's "read from either end" (merge question 3), and the upgrade test pins both consequences: a re-registered parent is admitted, and a parent newly defining a name its module owns is refused DUPLICATE_ARTIFACT_OBJECT_NAME naming the module (existingPackageId = the module, coOwnership: 'dependency'). Right.
  • sibling: a sitting owner S of N whose declarations intersect parents, where parents is P's own declared-installed-holding-N set. So S and P each directly declare one installed Q holding N, which is D5's sibling arm verbatim (merge question 4), direct declarations only, no closure walk. The two negative cases the ADR names are pinned: siblings whose parent is absent, and siblings sharing a utility under another namespace, are both still NAMESPACE_CONFLICT. Right.
  • Guards: a shareable namespace, no namespace, or no selfId return the artifact set alone. isShareableNamespace is the same predicate the namespace gate uses, so the dependency sources cannot admit into base/system/sys. Right.

The relation is one-way in who declares (X declares nothing) and symmetric in who is recognised, as the ADR states. Nothing is persisted; the map is rebuilt from the registry's current rows on every install, so a PATCH that deletes a declaration removes the proof at the next related registration, which is D3's stated disposition. The ADR-0130 D1 path is untouched: with no namespace sources the map equals the old set, and refuseCoOwnedObjectNameCollision's coOwners.size === 0 early return still holds for the single-package case (its comment "No artifact scope, or an artifact carrying one package" is now narrower than the condition it guards, since a declaration can fill the map without a scope; the condition is right, the comment is stale by one clause, not blocking).

Wording per source — right for all three. CO_OWNERSHIP_WORDING is a closed record over CoOwnershipSource. The artifact entry reproduces the pre-change message byte for byte ("Object name conflict inside one release artifact: ... — delivered by the same artifact — ... Sharing a namespace across packages in one artifact is allowed (ADR-0130 D1); sharing an object NAME is not (ADR-0130 D3)."), so the existing ADR-0130 D3 tests and any consumer matching that text are unchanged, and the constructor's coOwnership = 'artifact' default keeps every pre-existing call site's message. The dependency entry says "between packages that share a namespace through a declared dependency ... because one of the two declares the other in manifest.dependencies ... (ADR-0141 D5)", which is direction-neutral and therefore correct for both ends of the relation (the refusing test asserts it names "declared dependency" and never "same artifact" or "one release artifact"). The sibling entry says "between sibling packages that share a namespace under one declared parent ... because both declare the same installed package". One gap, not blocking: the sibling message does not name the shared parent Q, only the two siblings; the test's toContain('com.acme.crm') is satisfied by the sibling ids' prefix and does not pin Q. The card asks for "message worded per co-ownership source", which holds; naming Q would be a kindness for the Console's text. NAMESPACE_CONFLICT now carries one added sentence naming manifest.dependencies as the fix (pinned), the rest of its text unchanged, code and status unchanged. The new coOwnership field on ArtifactObjectNameConflictError is additive.

B. orderHydrationBatch and the cycle degrade — right, and the degrade is the ruling's own. The sorter is resolvePluginOrder with declared ids as optionalDependencies, keyed by node position (String(index)), so a second row naming an already-seen id is still a node (carrierOf is first-wins for edge targets; the duplicate-row test pins that every row reaches the caller). A declared id resolves to the node carrying it (packageIds may hold several, the install-local case), a self-edge is dropped (carrier !== index), and an id no node carries is not an edge, which is the resolveArtifactPackageEntries reading the ADR says to reuse. Insertion order is preserved where no edge decides (pinned). Strongly connected components are found with an iterative Tarjan; every edge whose two ends lie in one component of size above 1 is removed before resolvePluginOrder runs, so the sorter can no longer throw on this input, the cycle's members keep source order among themselves, and their edges to acyclic nodes still hold (pinned: parent before x, x before y). cycles returns each component's labels in source order for the caller to log.

Is the degrade acceptable under the card's rulings? Yes, it is what they prescribe, not a departure from them. Card item 5: "A cycle degrades to source order with a logged refusal and never throws out of the hydrate." ADR-0141 D5: "The cyclic members degrade to source order for that batch, with the cycle logged as a refusal naming them; the acyclic rest keeps its sorted order, and nothing throws out of the hydrate." Merge question 12: "The sorter's cycle throw is not inherited: cyclic members of a batch fall back to source order with a logged refusal, and the hydrate never throws — the per-entry tolerance both paths have today stands." Both callers log at error naming the members and the remedy (remove the closing declaration) and then register every entry: packages/services/service-package/src/index.ts (one row = one node, label the row's manifest.id) and MarketplaceInstallLocalPlugin.inDependencyOrder (one ledger entry = one node, label the manifestId, packageIds from packages[].manifest or the manifest itself, which is the same split the D2 check at step 1c' and resolveArtifactPackageEntries make). The refinement over the ADR's words (a cycle member still follows its acyclic edges instead of falling to raw source order) is strictly more ordered and never less, so no registration that would have succeeded under raw source order can fail under it. The intra-artifact sort in resolveArtifactPackageEntries keeps its throw, as the ADR says it should. ⛔ No second sorter: hydration-order.ts sorts only through resolvePluginOrder; Tarjan is edge pruning, not ordering. D2 is still not run at either hydrate (the diff touches neither hydrate's registration call, and the cloud-connection D2 suite's "ledger entry whose dependency is absent at boot still re-registers" case is unchanged).

C. The two #25986 review notes — right. (i) found label: when rangeAdmitsVersion returns null after the 0.0.0 probe showed some alternative reads, the installed version is now re-read with readExactVersion; readable means the range is the undecided side, so unreadable-range; unreadable means unreadable-version. The test pins the union lt-5 || latest (written with the less-than operator) against 7.0.0 as unreadable-range with installedVersion: '7.0.0', and ^6.0.0 against 6.1.0-beta.1 as unreadable-version. The accept set is unchanged (both are refused as before); only the fact row's side is now right. (ii) The wrapper id: deliveredIds is removed from DeclaredDependencyCheck and the delivered set is built from delivered[] ids alone; install-local no longer passes [manifestId]. The new test pins a module declaring the wrapper id com.example.multi (which names no package) refused absent, with nothing written. A wrapper whose id is also one of its packages' ids (composeStacks(..., { manifest: 'preserve' })) is unaffected, since that package is in delivered. At the head no reader of deliveredIds remains anywhere under packages/ (the three on origin/main are all in this diff).

D. New public surface — right. @objectstack/core gains orderHydrationBatch, declaredDependencyIds and the types HydrationNode, HydrationOrder through export *; no name collides with an existing core export (none of the four appears elsewhere under packages/core/src). @objectstack/objectql re-exports only resolveArtifactPackageOrder and ArtifactPackageError from core, so its published surface gains only CoOwnershipSource (a type) and the additive coOwnership field. @objectstack/metadata-core loses the optional deliveredIds input (see ② for why that is legal). Core has no api-surface file to regenerate; packages/spec is untouched. The changeset names the two value exports; the types are their inputs and outputs.

E. The vitest env line — right and machine-required. scripts/check-registry-log-declared.mjs demands env: { OS_REGISTRY_LOG: LEVEL } in the package-root vitest config of every package whose suite builds a SchemaRegistry; hydration-dependency-order.test.ts now does. warn is the level the other harnesses use. Lint · repo & tooling gates is green on the head.

F. Claims outside these inputs. The HotCRM precondition paragraph (every crm companion declares app.objectstack.hotcrm at d91c7b0) is outside this brief's inputs; what the code pins is that if it holds, the sibling arm admits each companion beside the app and beside each other, and the intra-artifact declaration is judged for presence only by D2.

② Semver level

Levels. The changeset introduces patch on @objectstack/core, @objectstack/objectql, @objectstack/service-package, @objectstack/cloud-connection and @objectstack/metadata-core, which are exactly the five published (private: false, all at 18.1.0) packages the diff moves; docs and the changeset publish nothing. Under AGENTS.md Post-Task Checklist item 3 the level follows the Metadata-protocol kind alone, and none takes patch. Check Changeset and Lint · CI, release & engine gates (check:changeset-no-major, check:adr-0087-registration) are green on the head.

Clause-②: yes (narrowing) — right, and it is the line the PR body and the changeset both carry. The widening: install acceptance (a module declaring its app, or a sibling under one declared parent, used to be refused NAMESPACE_CONFLICT and now installs), plus the new @objectstack/core exports. The narrowing: install-local refuses a module that declares only its artifact wrapper's id, and DeclaredDependencyCheck.deliveredIds is removed. Per scripts/pm/clause2-line.mjs, yes (narrowing) is "a diff that widens one surface and narrows another; both facts are true and both are read". It is.

The narrowing is of an unreleased surface — confirmed from the in-repo release state, so it is legal under the arm with no migration owed. deliveredIds, checkDeclaredDependencies and DEPENDENCY_MISSING all landed with #25986 (merge 3446d43d40, 2026-10-26T08:58Z). Every moved package is at 18.1.0 on origin/main; the latest GitHub release is @objectstack/*@18.1.0, published 2026-10-24T09:13Z from e649c2d91e, two days before that merge; no tag contains 3446d43d40; the 18.1.0 entries of packages/metadata-core/CHANGELOG.md and packages/cloud-connection/CHANGELOG.md contain no DEPENDENCY_MISSING, deliveredIds or rangeAdmitsVersion; and .changeset/25792-pre-install-dependency-check.md (the D2 changeset) is still pending in .changeset/ on origin/main, which is the state changeset version leaves until the next release consumes it. So no published tarball exposes deliveredIds, and no consumer can have called it. AGENTS.md item 3's "removing anything an author can write ... puts the FROM → TO mapping ... in the body" has no FROM to map, and the ADR-0087 not-required (no-migration-prescription) marker is the right disposition. The dev's choice to still carry the (narrowing) arm and the **BREAKING** banner is the conservative reading (the two changesets will publish in one release, and the D5 entry explicitly says the D2 check is "not-yet-released"), and check-adr-0087-registration accepts it with the one marker it carries. One consequence to know, not blocking: the CHANGELOG a reader greps at the next release will carry both entries under one version, the D2 entry describing presence-only delivery and the D5 entry correcting the wrapper-id arm; the D5 entry's own sentence makes that readable.

Metadata-protocol: none stands. ManifestSchema.dependencies keeps z.record(z.string(), z.string()); nothing an author writes changes shape, accepted value or meaning. What changes is what the install act and the boot do with the declaration already defined (co-ownership and order). No packages/spec edit. The (narrowing) arm beside none and patch is the combination check-changeset-no-major.mjs admits (a non-protocol narrowing ships patch and keeps its banner and disposition).

Carriers. The changeset carries the **BREAKING** banner, exactly one ADR-0087 marker, both declaration lines and a "What changes for you" section; the PR body's Declarations section repeats both lines with the same values.

③ Boundary flags

open_questions: none in the report. Confirmed: the only decision the D2 report carried (items 6a/6b) is now #26000 (domain:spec, pm:queue, an issue not a PR), and this diff makes no spec change that would pre-empt it: declaredDependencyIds reads the record spelling only and returns [] for the array shape (pinned), so neither PackageDependencySchema nor AppManifestSchema.dependencies gains a reader here. D8 item 2 (optional never read) holds by construction.

Dev flags and out-of-scope findings.

  • Dogfood authz-probe-blind-spot, the i18n.ts row. Same row as on feat(install): check a package's declared dependencies before install — 422 DEPENDENCY_MISSING at the three install acts (ADR-0141 D2) #25986; the dev states both files are unchanged from origin/main 3518664cb6, and this diff touches neither packages/runtime/src/domains/i18n.ts nor the census test (17 files, none of them). The head's dogfood shard check-runs are skipped on a PR event, as before. Carrier stays as filed.
  • First reading 2 (rehydrate order across paths). Read from the code, not instrumented: each hydrate now sorts its own batch; the kernel:ready seals and the ledger rehydrate run on the same hook in registration order with no declared edge between the install-local plugin and ObjectQLPlugin; the residual case (two sys_packages siblings whose parent is only in the install-local ledger) is the one ADR-0141 Open items 2 names as the card's residual reading, and the reverse arm means the later-hydrated parent is still admitted beside the first sibling. Reported, not solved; the ADR places it as a reading, not a build item. Accepted as filed.
  • First reading 6 (the standalone module's artifact type). Not measured here; the dev routes it to the HotCRM lane with reading 7. Accepted as filed; nothing in this diff depends on it (coOwnersOf and both sorters read namespace and dependencies, never type).
  • First readings 3 and 4. Reading 3 (a declared id naming no installable package) was answered on feat(install): check a package's declared dependencies before install — 422 DEPENDENCY_MISSING at the three install acts (ADR-0141 D2) #25986 by the HotCRM measurement (the only declared id is app.objectstack.hotcrm); reading 4 (enabled: false on X) has not been reported by either PR, and D2 by design does not judge enablement (ADR-0141 D2, Open items 4). It remains a reading on the card, not a build item; escalated to the seat as an open reading, not a condition on this PR.
  • Fixes #25792 vs Part of. The body says Part of because items 3 and 6 are not closed by this PR; Part-of PR must not also close its card is green. Right as written; see the item ledger below.
  • Deviations from the claim. None found: no ui/** edit (17 files, none under ui/), no packages/spec/** edit, every touched path inside the claim's file surface (packages/core/src/plugin-order.ts callers includes the new hydration-order.ts), the declaration line matches the measurement the seat already ruled on feat(install): check a package's declared dependencies before install — 422 DEPENDENCY_MISSING at the three install acts (ADR-0141 D2) #25986.

Card #25792 item ledger, with this slice plus #25986 (merged 3446d43d40).

Card item Closed by Remaining
1. D2 pre-install check at the three install acts, 422 DEPENDENCY_MISSING #25986 (merged); #26054 corrects two D2 facts (the found side, the wrapper id) none
2. The range reader (rangeAdmitsVersion, shared parsing, stated grammar) #25986 (merged) none
3. The Console refusal text (「请先安装 HotCRM 6.x」) neither PR; triage 6398501495 routed it to objectui's own card if the copy needs a new string, and the claim scoped it out (⛔ no ui/**) open on this card as an outside-repo item unless the seat reads the triage routing as closing it here; no objectui card number is in these inputs
4. D5 dependency-declared co-owners, both halves, cross-artifact and sibling tests, the upgrade case #26054 none (sibling message does not name Q, non-blocking)
5. D5 rehydrate order through resolvePluginOrder, cycle degrades and is logged #26054 none; Open items 2's cross-path residual is a reading, stated
6. D8 items 2 and 3 (optional, AppManifestSchema.dependencies) #26000 (open, domain:spec, pm:queue), the spec retirement #26000
7. Docs: declare, grammar, refusal meaning, two modules need not declare each other, os validate blind spot #25986 (the page: declare, grammar, refusal, os validate) + #26054 (the "need not declare each other" sentence, the co-ownership section replacing "not delivered yet", the boot-order paragraph) none

So this slice plus #25986 closes items 1, 2, 4, 5 and 7. Item 6 is #26000's. Item 3 is the one item neither PR closes and #26000 does not carry: it lives outside this repository by the triage's routing. First readings 2 and 6 are stated as residual readings, and reading 4 is unreported.

Implemented-by: claude/project-thread-78l2jg
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T10:25Z

Head 10c01a4181, report 6413551415 (on #25792), contract review 6413730489 (PASS, narrowing face, by server seat 2). D5 slice of the card.

item reading
Shape draft, base main, Part of #25792 (the card keeps items 3 and 6 open, see below), Clause-②: yes (narrowing) for the unreleased D2 install-local narrowing, Metadata-protocol: none; 17 files across core, objectql, metadata-core, cloud-connection and service-package; no packages/spec/** path.
Item 4 (D5 co-owners) SchemaRegistry.installPackage computes one co-owner map (artifact, dependency read from either end, sibling) and both halves of the ADR-0130 D3 pair read it, so the relaxation and the refusal cannot disagree about who counts; the refusal is worded per source.
Item 5 (D5 order) orderHydrationBatch in core orders both the sys_packages hydrate and the install-local rehydrate through resolvePluginOrder; a cycle degrades and is logged.
#25986 notes the unreadable-range case on an undecided union is found; the wrapper id no longer counts as delivered (deliveredIds removed, unreleased).
Evidence red first on the three new suites, 8 ablations red; 106/106 derived gates exit 0 (check-registry-log-declared found a real miss, fixed); five packages green after a full build. Auto-merge holds for the head's Test Core / Type Check / Build Core runs.
Card after this slice items 1, 2, 4, 5, 7 closed (with #25986). Item 6 is #26000 (domain:spec). Item 3, the Console refusal text, was routed to objectui by triage 6398501495; objectui now lives in this repository under ui/, so the seat carries it on this card as the remaining server-side-to-UI slice.
Carried the dogfood authz-probe-blind-spot red was main's own, fixed on main by #26002.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 10:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 10:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit be78977 Oct 26, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-78l2jg branch October 26, 2026 11:37
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants