Skip to content

feat(install): check a package's declared dependencies before install — 422 DEPENDENCY_MISSING at the three install acts (ADR-0141 D2) - #25986

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

objectstack-fleet[bot] merged 7 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 D2: installing a package now checks its own declared manifest.dependencies first. Every entry must name a package installed at a manifest.version inside its range, or one the same artifact delivers (presence only). Otherwise the whole install is refused 422 DEPENDENCY_MISSING before anything is registered, written or synced, with one AppCompatibilityCheck issue (category: 'dependency_missing') and one unmet fact per entry, all in one answer.

  • One range reader, beside the handshake (packages/metadata-core/src/protocol-handshake.ts). rangeAdmitsVersion(range, version) reads npm-style ranges at exact-version granularity (^ ~ >= > <= < =, exact, x/*, hyphen, space/comma intersection, ||). It shares the comparator peel, hyphen split and length cap with the D1 major-only reader (rangeAdmitsMajor), whose verdicts are unchanged. Unlike D1, D2 fails closed: an unreadable range (latest included) or a non-MAJOR.MINOR.PATCH installed version never satisfies.
  • The check and its answer (same file): checkDeclaredDependencies, assertDeclaredDependencies, DependencyMissingError (code + status 422, branded like the protocol-incompatible error) and dependencyMissingAnswer, so every door answers the same envelope.
  • Exactly the three install acts:
    • the protocol's installPackage (packages/metadata-protocol/src/protocol-package-install.ts), which is also the composed arm of POST /api/v1/packages;
    • the no-protocol fallback arm of POST /api/v1/packages (packages/runtime/src/domains/packages.ts);
    • POST /api/v1/marketplace/install-local, inline-manifest and cloud-snapshot branches (packages/cloud-connection/src/marketplace-install-local-plugin.ts). A multi-package artifact judges each packages[].manifest, with the artifact's own ids counted as delivered.
  • Not checked: boot / kernel:ready rehydrate re-registration, PATCH /api/v1/packages/:id, and uninstall or upgrade of a dependency.
  • Ledger: DEPENDENCY_MISSING is registered under @objectstack/metadata-core in packages/spec/src/api/error-code-ledger.zod.ts (the only packages/spec/** edit); the reference pages are regenerated.
  • Docs: new content/docs/deployment/app-dependencies.mdx covers how to declare a dependency, the grammar, what install checks, the refusal and its found values, and what is not checked. The plugin-spec.mdx "Version Constraints" callout is corrected to name the install check as a consumer.

Declarations

Clause-②: yes (narrowing). The diff adds public surface (a ledger code and new @objectstack/metadata-core exports) and narrows install acceptance (a package with an unmet dependency used to install). The changeset is patch, with a BREAKING banner, the "What changes for you" section 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. manifest.dependencies still parses every value it parsed before. A value outside the grammar, such as latest, is now refused at install, which is a run-time refusal and not a parse change (precedent #25954, #25964). This line is borderline, so the reviewer may want to confirm it.

HotCRM

At hotcrm d91c7b0, service, marketing and revenue each declare dependencies: { 'app.objectstack.hotcrm': '^6.0.0' }, and the app is 6.0.0. They are composed into one artifact, so the check treats the app as delivered (presence only) and they pass unchanged. A module shipped standalone also passes, because ^6.0.0 admits 6.0.0.

Tests

Red first: the new metadata-protocol suite failed on main (2 failures) and passes after wiring. The new suites:

Package Suite Tests
metadata-core protocol-handshake-dependencies.test.ts 17
metadata-protocol protocol-package-install-dependencies.test.ts 4
runtime packages-install-dependency-missing.test.ts (composed and fallback arms) 6
cloud-connection marketplace-install-local-dependency-missing.test.ts (inline, cloud snapshot, multi-package, rehydrate not checked) 7

Package suites on the merged head: metadata-core, metadata-protocol, cloud-connection and spec typecheck + test exit 0. Runtime typecheck exits 0, and its tests pass (448 files, 7188 passed, 19 skipped).

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

Removed Failures
the fallback-arm call in packages.ts 2
the install-local check 4
the presence-only arm for delivered ids 1
the shared comparator peel 9 (5 in the new suite, 4 in the D1 handshake suite)

Gates: node scripts/pm/dispatch-gates.mjs --ran accounts for all 127 derived families, all exit 0. Three needed a prerequisite (unshallow, full build) and one found a real miss, the doc roster entry, which is now fixed. Scoped eslint on the touched packages exits 0, and check:nul-bytes exits 0. The gates ran at b1ed3d7420, before the last merge of main. Package typecheck and tests, the full build, and the full dogfood ran on the head 12daab9542.

Full dogfood on 12daab9542: 2522 passed, 18 skipped, 1 failed. The one failure is authz-probe-blind-spot.test.ts, the packages/runtime/src/domains/i18n.ts row: handleI18nRequest count expected 3, got 4. It is red on main too. Neither that file nor the census differs from origin/main, and the count moved with #25956. This PR does not touch it.

Acceptance notes


🤖 Generated with Claude Code

https://claude.ai/code/session_01NndsH9Qm7k6AS2i2fWbjNA

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cloud-connection, @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, touching 51 documentable anchor(s).

21 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 d1663ba1826c338861c3b92c117346322a6af8fa.

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

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 11 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 — 153 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 d1663ba1826c338861c3b92c117346322a6af8fa → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d1663ba1826c338861c3b92c117346322a6af8fa → 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: 12daab9542fa8ea76bdcce4d3650b7b6da5edcc2
Local-runs: none
Scope: packages/spec/src/api/error-code-ledger.zod.ts (the reviewFace hit, schema face), packages/metadata-core/src/protocol-handshake.ts, packages/metadata-core/src/protocol-handshake-dependencies.test.ts, packages/metadata-protocol/src/protocol-package-install.ts, packages/metadata-protocol/src/protocol-package-install-dependencies.test.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-install-dependency-missing.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-dependency-missing.test.ts, scripts/adr-anchors/packages__metadata-core__src__protocol-handshake.ts.json, scripts/docs-audit/handwritten-docs.json + packages/spec/src/kernel/manifest.zod.ts (ManifestSchema.dependencies, version), packages/spec/src/system/app-install.zod.ts (AppCompatibilityCheckSchema.issues[].category, AppManifestSchema), packages/spec/src/marketplace/package-version.zod.ts (PackageDependencySchema), packages/spec/src/kernel/package-dependency-dual-source.test.ts, packages/metadata-core/src/errors.ts (MetadataError), packages/metadata-core/src/index.ts (export * of the handshake module), packages/metadata-protocol/src/protocol.ts (the two in-process installPackage callers), packages/core/src/artifact-packages.ts (resolveArtifactPackageEntries), scripts/check-adr-anchors.mjs, scripts/docs-audit/check-audit-scope.mjs, scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, packages/qa/dogfood/test/authz-probe-blind-spot.test.ts, packages/runtime/src/domains/i18n.ts, .github/workflows/ci.yml (dogfood job conditions). The prose face (.changeset/25792-pre-install-dependency-check.md, content/docs/deployment/app-dependencies.mdx, content/docs/protocol/kernel/plugin-spec.mdx, the three regenerated reference pages, content/docs/deployment/meta.json) was read for the claims it makes about the code above.

① Derived judgments

CI state at review time (2026-10-26T08:18Z, 46 check-runs on the head). 32 concluded success, 0 failure. Still in progress: Test Core shards 1, 3, 4, 5 and 7 of 8, Type Check · workspace, Type Check UI. Concluded green and load-bearing here: Build Core, Build Docs, Test Core shards 2, 6, 8, Type Check · source gates, Type Check · consumer gates, Lint · ESLint, Lint · docs & code guards (runs check:docs-audit-scope and check:error-status-conformance), Lint · repo & tooling gates (runs check:adr-anchors), Lint · PM dispatch-gates, Spec property liveness, Check Changeset, Governed Surface Queue Guard, Part-of PR must not also close its card, Check PR Size. The judgments below are rendered on what has concluded; the five open Test Core shards and the workspace typecheck are the only gates still owed, and nothing in the diff was found that predicts them red.

A. The accept set at the three install acts — right. The one predicate checkDeclaredDependencies (protocol-handshake.ts) is called from exactly three places and nowhere else in the diff: installPackage in protocol-package-install.ts through assertDeclaredDependencies, placed after assertProtocolCompat and before the prior read, the registry write and the persist, so the composed arm of POST /api/v1/packages and the two in-process callers in protocol.ts (duplicatePackage at the dupManifest call and the base-package plan) inherit it; the no-protocol fallback arm of POST /api/v1/packages (packages.ts) directly after its own assertProtocolCompat and before registry.installPackage; and POST /api/v1/marketplace/install-local at step 1c', after the id gate and the handshake and before the install-mode gate and the manifest service, on both branches (the inline manifest and the cloud snapshot converge at manifest before 1b). Not touched: the kernel:ready rehydrate in the install-local plugin, packages/services/service-package (the sys_packages rehydrate), PATCH /api/v1/packages/:id (updatePackage). The cloud-connection test pins that a ledger entry whose dependency is absent at boot still re-registers.

Refused 422 DEPENDENCY_MISSING after this PR: a delivered package whose own manifest.dependencies record carries an entry whose id is neither delivered by the same act nor installed (absent); installed but with a manifest.version outside the range (out-of-range); whose range is outside the stated grammar, whether or not the dependency is installed (unreadable-range, latest included); whose installed version is not MAJOR.MINOR.PATCH with optional leading v and +build (unreadable-version, a prerelease included); or whose dependencies is present but not a record (unreadable-declaration, pinned against the marketplace array shape). Every unmet entry of every delivered package is reported in one answer, in declaration order. Still passing: a package with no dependencies or an empty record (pinned in the protocol suite as "exactly as it installed before"); every entry installed inside its range; every entry delivered by the same artifact. Enablement is not judged, as ADR-0141 D2 says. The refusal envelope on the three HTTP doors is the one shape (code, httpStatus 422, details closed to compatible, issues, unmet); the runtime and cloud-connection suites assert the key set exactly, and the runtime suite parses the error with ApiErrorSchema, which the regenerated ledger makes pass.

Two consequences of fail-closed worth stating, both correct under the ADR and both documented on the new docs page, neither blocking: (i) ManifestSchema.version admits SemVer 2.0.0 prereleases, so an app installed at 6.1.0-beta.1 can never satisfy a dependent until a release version is installed; (ii) the found label has one mislabelled corner: a union whose first alternative admits 0.0.0 but not the installed version while another alternative is unreadable (e.g. lt-5 || latest against installed 7.0.0) passes the rangeReadable probe and is then reported as unreadable-version naming a perfectly readable 7.0.0. The install is still refused, so the accept set is right; only the fact row and its message misname the side. Worth a one-line fix in the D5 PR (probe the range with each alternative, or report unreadable-range when admits is null and the version read).

B. Presence-only satisfaction by a composed app — right. delivered ids and deliveredIds are collected into one set before any entry is judged, and if (delivered.has(dependency)) continue runs before the range is read, so an in-artifact entry is never judged for range (metadata-core test: orders declaring ^1.0.0 against a co-delivered core at 2.0.0 passes; the control without core is refused). Install-local judges packages[].manifest when packages is an array and the manifest itself otherwise, which is the same split resolveArtifactPackageEntries makes, so what is judged is what will register. One note: deliveredIds: [manifestId] also counts the artifact wrapper's id as delivered. In a composeStacks(..., { manifest: 'preserve' }) artifact the wrapper id is one of the package ids, so the arm is redundant there; where a wrapper id names no package, a module declaring it passes at install but would be absent on a later standalone install. ADR-0141 D2 arm 2 speaks of "a package delivered by the same artifact", and the install-local ledger does record the artifact under manifestId, so the reading is defensible; noted, not a defect.

C. D1 verdicts unchanged by the shared peel — right, by construction. The three extracted helpers are byte-equivalent to the code they replace: hyphenRangeEnds keeps split(/\s+-\s+/) with the length === 2 test; peelComparatorOperator keeps the same startsWith ladder and slices and the same null on no operator; COMPARATOR_SEPARATOR is the same /[\s,]+/; MAX_RANGE_LENGTH is the same 128. alternativeAdmitsMajor still reads latest as true and comparatorAdmitsMajor still returns null on an unparsed bound, so assertProtocolCompat's admit-with-warning on no-range / unparsed-range and ADR-0087 D1's never-false-rejection contract are untouched. protocol-handshake.test.ts is not in the diff; the new suite cross-asserts rangeAdmitsMajor('^6.2.0', 6) true beside rangeAdmitsVersion('^6.2.0', '6.1.0') false, which is the one case the card exists for. The two readers share parsing and not policy: rangeAdmitsVersion returns null and each caller owns what null means.

D. Fail-closed on an out-of-grammar range — right. readPartialVersion returns null on anything that is not up to three dot-joined numeric or x/X/* parts, so latest, next, ~gt6.0, ^6.0.0.1, an empty alternative and a 200-char string all read null (pinned). checkDeclaredDependencies probes the range before looking at the registry, so an unreadable range is reported as such even when the dependency is absent (the author's fix is the range, not an install). The desugaring matches npm at exact-version granularity in every case I traced: caret on zero-major narrows to the first non-zero part (^0.2.3, ^0.0.3, ^0.0, ^0), tilde pins the minor when written and the major otherwise, gt on a partial becomes gte the ceiling, lte on a partial becomes lt the ceiling, hyphen with a partial high end becomes lt its ceiling, */x alone admits everything, a union admits when any alternative admits and is null only when none admits and one is unreadable (the same combination rule as rangeAdmitsMajor). The grammar the refusal prints (DEPENDENCY_RANGE_GRAMMAR) is the one the reader implements and the docs table repeats.

E. The ledger row and the new metadata-core exports — right. DEPENDENCY_MISSING sits under @objectstack/metadata-core, alphabetically first in that list, with no duplicate anywhere in the ledger at the head; its producer DependencyMissingError lives in that package, redeclares code as a literal and declares status and statusCode 422, the pattern ProtocolIncompatibleError in the same file already uses over MetadataError(code, message), which is what check:error-status-conformance reads (green). The three reference pages regenerated from the ledger carry the new member (+344 more, the bullet in three lists); packages/spec/api-surface/api.json records that ERROR_CODE_LEDGER exists and not its members, so no api-surface regeneration is owed, and no other generated carrier of ledger codes exists (a sibling code appears at the head only in docs, changelogs, a release note and a hand-written QA checklist). The new public surface of @objectstack/metadata-core, through the existing export * from './protocol-handshake.js': values rangeAdmitsVersion, readExactVersion, DEPENDENCY_RANGE_GRAMMAR, checkDeclaredDependencies, assertDeclaredDependencies, DependencyMissingError, isDependencyMissingError, dependencyMissingAnswer; types ExactVersion, DependencyDeclaringManifest, InstalledDependencyRecord, UnmetDependency, DeclaredDependencyCheck, DependencyMissingIssue, DependencyMissingAnswer. The changeset names the eight values; the types are the inputs and outputs of those values and need no separate line. isDependencyMissingError recognises by a Symbol.for brand, so the runtime door recognises an error thrown inside @objectstack/metadata-protocol (pinned by the lookalike test).

F. The two out-of-surface files. scripts/docs-audit/handwritten-docs.json: machine-required and nothing more. check-audit-scope.mjs derives the roster from the filesystem and exits 1 on any drift, so a new page under content/docs/ cannot land without the entry; the one added line is the new page's path in sorted position. scripts/adr-anchors/packages__metadata-core__src__protocol-handshake.ts.json: right, and nothing more than an anchor, but "machine-required" overstates it. check-adr-anchors.mjs verifies the entries that exist (file present, ADR ids real, ids cited in the file); it does not demand an anchor for a newly governed file. Adding one is the convention its header states ("add one when an accepted ADR's decision is realized in code that would look arbitrary to someone reading the file alone"), which this file now is: fail-closed beside an admit-with-warning handshake is exactly the shape a reasonable engineer would "fix". The entry's shape matches its siblings (file, adrs, invariant), both ids it lists (ADR-0141, ADR-0087) appear in the file, and its invariant text restates D2 correctly. check:adr-anchors is green on the head.

G. Claims I could not verify from the inputs. The PR body's HotCRM paragraph (three modules declaring 'app.objectstack.hotcrm': '^6.0.0' at d91c7b0) is outside this brief's inputs. What the code pins is that the claim follows if the manifests read as stated: a co-delivered app is presence-only, and ^6.0.0 admits 6.0.0.

② Semver level

Levels. The changeset introduces patch on @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/cloud-connection and @objectstack/spec, which are exactly the five published packages the diff moves (docs and scripts/ publish nothing). Under AGENTS.md Post-Task Checklist item 3 the level follows the Metadata-protocol kind alone: none takes patch, and "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". The levels match. Check Changeset is green.

The declaration line. The dev's Clause-②: yes (narrowing) is the one AGENTS.md requires here, and the claim's no (narrowing ...) does not survive the diff. AGENTS.md Post-Task Checklist item 3 fixes the syntax (yes|no plus at most one arm from the closed pair, (narrowing) is BREAKING, no (widening) malformed); the meaning is held by the one reader AGENTS.md's rule points at, scripts/pm/clause2-line.mjs: the value answers 「本卡放宽接受集或扩大公开面吗」, and yes (narrowing) is "a diff that widens one surface and narrows another; both facts are true and both are read", while no (narrowing) is "NOT a widening, but breaking". check-changeset-no-major.mjs (ruling A) restates it: "Clause-② keeps its MEANING (does this diff widen or narrow a published accept set or public surface)". This diff adds a member to the closed ApiErrorSchema.code set through the ledger and eight value exports to @objectstack/metadata-core's public surface, which is a widening of a published surface, and it narrows install acceptance, which is the (narrowing) arm. So yes (narrowing). The claim line was written before the diff existed and reads the card's intent; the dev measured the diff, and the measurement governs. The PR body and the changeset both carry yes (narrowing) and agree. Served-tier is unaffected: the ledger file is a published schema, so the schema face already puts this review at CONTRACT_REVIEW_TIER whatever the line reads.

Metadata-protocol: none stands. AGENTS.md item 3 defines the kind as "the change to what a metadata author can write": a new key, accepted value or type is added; "a key removed or tombstoned, a value refused or a requirement tightened is narrowed". ManifestSchema.dependencies is unchanged at z.record(z.string(), z.string()), its JSDoc already reads "Map of package IDs to version requirements" with a ^2.0.0 example, and every value it parsed before still parses, builds, validates and is stored by PATCH. What changes is the answer the install act gives against the environment, which ADR-0141 D2 places at the install doors and D8 fences off from the spec ("No spec shape changes in this record"). The borderline the dev names is real: an out-of-grammar range such as latest is refused at install whatever the environment holds, which reads like "a value refused". I rule it none because the refusal is at the install act and not at the authoring protocol, the grammar enforced is the one the key has always documented (a version requirement, not a dist-tag), and ADR-0141 merge question 6 pairs this backstop with the author-side os lint rule on the sibling card, where the authoring surface is stated. If the maintainer reads the latest refusal the other way, the correction is Metadata-protocol: narrowed with minor on at least one moved package, and nothing else in the record changes. Precedents #25954 and #25964 are cited by the dev and are outside this brief's inputs; this ruling rests on the AGENTS.md text alone.

Carriers. The changeset carries the **BREAKING** banner (signal 2 of check-adr-0087-registration.mjs), the (narrowing) arm (signal 4) and exactly one ADR-0087 marker, not-required (no-migration-prescription), with a "What changes for you" section; the summary line carries no ! (signal 3), which the gate does not require when signals 2 and 4 are present. The disposition is right: no metadata key is added, removed, renamed or re-shaped, so objectstack migrate meta has nothing to rewrite and no tombstone is owed. The PR body's Declarations section repeats both lines with the same values.

③ Boundary flags

Open question 6a (PackageDependencySchema with its optional arm) and 6b (AppManifestSchema.dependencies). Neither is in this diff, and the PR is sound without them. D2 reads ManifestSchema.dependencies only; an array-shaped declaration is reported unreadable-declaration (pinned), so the marketplace shape cannot pass as "no dependencies"; and ADR-0141 D8 already fixes that optional has no record spelling and D2 never reads it, which this PR honours (every record entry required, no optional arm, D8 item 2 satisfied by construction). The dev's zero-reader measurement holds at the head: at 12daab9542, PackageDependencySchema, AppManifestSchema and versionRange have no hit under packages/, examples/ or apps/ outside packages/spec/, and inside spec the hits are the two declaring files, their two tests, and package-dependency-dual-source.test.ts, which pins the #4741 rename and would need updating by a retirement PR (there is no ui/ tree in this repo; the console is objectui and outside these inputs). Recommendation A for both (retire now, one spec PR with the retirement kit) is the contract-first answer and matches D8 item 3's "decided on a measured reader count". Escalated to the maintainer as the card asks; not a condition on this PR.

Open question 3 (the declaration line). Ruled above: keep yes (narrowing). No edit owed.

Dogfood failure authz-probe-blind-spot. Verified: packages/qa/dogfood/test/authz-probe-blind-spot.test.ts (the dev's test/... path is relative to packages/qa/dogfood) and packages/runtime/src/domains/i18n.ts are byte-identical between origin/main (05f7758f05) and the head; handleI18nRequest occurs 4 times in i18n.ts on both. The head's CI dogfood check-runs are skipped, not red: Dogfood Regression Gate (shard/3) and Dogfood Verify CLI carry if: github.event_name != 'pull_request' && != 'merge_group' in ci.yml, so they never run on a PR event; the aggregator Dogfood Regression Gate runs if: always() and concluded success over skipped shards. The dev's full local dogfood is therefore the only measurement of that row, and its "red on main since #25956" attribution is outside these inputs; what this record can say is that this PR does not touch either file. Carrier stays as the dev filed it (noted, not filed).

Deviations from the claim. (1) The declaration line, ruled above. (2) Two files outside the claim's listed surface: the roster entry is machine-required; the anchor is convention-required and correct (① F). Neither is a ui/** edit or a spec key change, so neither is the stop-on-breach case the claim names.

Out-of-scope findings and the split. Items 4 and 5 (D5 co-owner source and rehydrate order) and First readings 2 and 6 are deferred to the next PR on #25792; the docs page says D5 is not delivered, and the Sharing-a-namespace section states today's NAMESPACE_CONFLICT behaviour correctly. The PR is Part of, the card stays open, and Part-of PR must not also close its card is green. Item 3 has no ui/** edit (verified: 18 files, none under ui/). Item 6 made no spec key change (the only packages/spec/** edit is the ledger row).

Two non-blocking notes for the D5 PR, from ① A and ① B: the found mislabel on a union with one unreadable alternative, and the artifact-wrapper id in deliveredIds.

CI still owed at review time. Test Core shards 1, 3, 4, 5, 7 and Type Check · workspace were in progress; every concluded run is green. Landing waits on them as it would for any PR; this record does not vouch for them.

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

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-26T08:23Z

Head 12daab9542, report 6411207761 (on #25792), claim 6409455733, contract review 6411460417 (PASS, narrowing face).

item reading
Shape draft, base main, Part of #25792 (items 1, 2, 7: ADR-0141 D2), Clause-②: yes (narrowing): the dev's measurement overrides the claim's no (narrowing …), as the review confirms (a new ledger code and new metadata-core exports widen one surface while install acceptance narrows), Metadata-protocol: none, patch on the moved packages with the BREAKING banner.
Files (18, +1500/−42) the D2 check at the three install acts (protocol installPackage, the fallback arm of POST /api/v1/packages, install-local on both branches), one fail-closed range reader sharing the D1 comparator peel (D1 verdicts unchanged), the ledger row (the only packages/spec/** edit), docs, an ADR anchor and the docs-audit roster entry. Not governed (check-governed-merges --pr 25986).
Fix an install with an unmet manifest.dependencies entry answers 422 DEPENDENCY_MISSING, one issue and one unmet fact per entry; a composed app counts as delivered (presence only); HotCRM passes unchanged.
Evidence red first, four ablations, 127/127 derived gates reconciled (report). The dogfood authz-probe-blind-spot red is main's own since #25956 (census file and i18n.ts byte-identical to main).
Carried items 4 and 5 (D5) are the next PR on #25792, with the review's two notes (the found label on a union with one unreadable alternative; the artifact-wrapper id in deliveredIds). Items 6a/6b (retiring PackageDependencySchema / optional and AppManifestSchema.dependencies) are not conditions of this PR and go to the maintainer.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready + auto-merge through the relay; auto-merge waits for the last Test Core shards. The card stays open (Part of).

Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants