From 0315605d867ba7107dc054936973866c23a761d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 02:18:31 +0000 Subject: [PATCH] docs(qa): classify the `manifest` capability in the platform-checklist coverage ratchet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:platform-checklist` was red on `main` with one problem: `coverage.json · manifest: UNCLASSIFIED`. The manifest liveness ledger (`packages/spec/liveness/manifest.json`) landed on 2026-08-23 and the checklist neither tested nor waived the capability it governs. Classified as ITEMS, not a waiver. Grounding against source shows the manifest is an authored, enforced, user-facing surface rather than an internal ledger: an incompatible `engines` range is refused before the registry write with a structured `OS_PROTOCOL_INCOMPATIBLE` diagnostic (metadata-protocol/protocol.ts calls assertProtocolCompat ahead of registry.installPackage), the ADR-0048 namespace gate refuses a package whose namespace another package owns, a namespace-less runtime package has one derived from its id, and `os plugin build` forks the artifact on `packaging` and refuses an invalid manifest with located errors. A waiver is a debt marker for a missing fixture (README) and no fixture is missing here. Two new items, authored verify-first against source: platform-core.manifest-install-contract (P1, api) — the install boundary: handshake refusal before the registry write, protocol-first precedence across all three range rungs, no-range/unparsed-range admitting with a warning, the three-legged namespace ownership gate, and namespace derivation. cli.plugin-manifest-build-contract (P2, cli) — the packaging boundary: located refusals with no artifact written, the two identity guards (schema-required id/version vs the empty-string post-parse guard), the `packaging` artifact fork, the compiled manifest read back out of the artifact, and the `protocol/missing-engines-range` lint nudge with its manifest scoping. `coverage.json` maps `manifest` to both, plus the existing `api-backend.package-rest-lifecycle`, which already drives manifest keys over the install door (its 409 "no silent manifest clobber" guard). Two open enforce-or-remove findings on this surface — the printed-but- unenforced `runtime` trust tier and the computed-but-unverified `integrity` map — are recorded in the items' `negative` blocks so a run cannot score either as working machinery. Gate script untouched: the fix is data, not code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx --- docs/qa/platform-checklist/areas/cli.json | 96 ++++++++++++++++ .../areas/platform-core.json | 104 ++++++++++++++++++ docs/qa/platform-checklist/coverage.json | 7 ++ 3 files changed, 207 insertions(+) diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 6ba3f44f08..4282e706e6 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -1415,6 +1415,102 @@ "history": [ { "revision": 1, "date": "2026-08-20", "change": "new — scoped scan-functionality sweep (扫描功能), authored VERIFY-FIRST as the brief required since the hunter had lower confidence here. Source confirmed the three-severity split and errors-only gating; two sketch claims were corrected against lint.ts before authoring: (1) the --json shape is lint's own {passed,total,errors,warnings,suggestions,issues,duration} (:543-553), not the os validate #3782 parity shape — the parity lint actually owes is the #4409 shared-registry agreement with os build (:407-436), encoded as its own clause; (2) the exit contract is 0/1 on both paths (process.exit(1) at :634, CliExitCode slot at :553), not build's human-path oclif-2 split. The i18n platform fold (:47-66) with its hiddenPlatform disclosure is covered as the third load-bearing surface", "ref": "claude/new-session-0pv25p" } ] + }, + { + "id": "cli.plugin-manifest-build-contract", + "title": "os plugin build enforces the manifest at the packaging boundary: an invalid manifest is refused with located errors, `packaging` forks the artifact, and the compiled manifest the artifact carries is readable back", + "since": "v15", + "status": "active", + "revision": 1, + "priority": "P2", + "surface": "cli", + "personas": [ + "plugin author (local shell, scratch plugin directory)" + ], + "fixtures": { + "app": "scaffold", + "requires": [ + "a scratch plugin directory OUTSIDE the repo tree holding objectstack.plugin.json (the filename os plugin build reads — NOT objectstack.config.ts), an entry at src/index.ts, and a package.json declaring at least one real runtime dependency that the entry actually imports. ⛔ Without a genuinely imported dependency the two `packaging` modes produce the same bundle and the fork clause is unscoreable — an equal-artifacts observation would then be a fixture defect reported as a product fail", + "esbuild resolvable from that directory — os plugin build refuses with its own named error when it is not, which is a fixture failure rather than a clause verdict", + "a second scratch directory for the refusal probes, so a bad manifest never overwrites the good one" + ], + "knownGaps": [ + "os plugin publish CANNOT complete locally: after extracting the artifact manifest it requires a cloud token and posts to /api/v1/cloud/packages. Only its PRE-NETWORK legs are scoreable here — artifact discovery in cwd, manifest extraction, the id/version refusal, and the unsigned-artifact notice. Record blocked(environment) for anything past the auth step; the cloud control plane is not part of this checklist's fixture set", + "the SERVER-side mirror of the identity refusal — POST /api/v1/packages/publish answering 400 PACKAGE_MANIFEST_INVALID when id or version is missing (packages/rest/src/package-routes.ts:532) — is deliberately not scored here: it needs a deployment composing the `package` service, and one without it answers 404 with the 'composes no `package` service' message, which is not a failure of anything. Score it from the api lane if a marketplace-capable deployment is available", + "the artifact is a gzipped ustar with mtime pinned to 0 and entries emitted in sorted path order, so identical inputs produce byte-identical archives — that is what makes the two packaging builds comparable, and it is also why a stale artifact from an earlier run is indistinguishable from a fresh one by name alone. Build each mode into its own --out path" + ] + }, + "steps": [ + "in the scratch plugin dir, run `os plugin build` on the GOOD manifest (id, version, name, an imported dependency in package.json, no `packaging` key) → capture the full output: the Loaded line, Artifact, Plugin, Files, Integrity entries, Size, sha256. Keep the artifact at its own --out path", + "re-run with `packaging: \"manifest-deps\"` in objectstack.plugin.json, into a DIFFERENT --out path → capture the same output block", + "compare the two artifacts: extract each (gunzip + tar) and diff the FILE LISTS and the bundled entry. Expect package.json (and pnpm-lock.yaml when present) inside the manifest-deps artifact and absent from the bundled one, and expect the declared dependency's import to survive as an external import in the manifest-deps dist/index.mjs while its code is inlined in the bundled one", + "read the compiled manifest out of each artifact (the objectstack.plugin.json entry inside the archive) and compare it to the authored one: expect the authored keys plus `main` and an `integrity` map", + "run `os plugin publish` against the good artifact with no token → capture the output up to and including the auth refusal; then run it against an artifact whose compiled manifest lacks a version → capture that refusal", + "REFUSAL probes, each in the second scratch dir, each followed by a check that NO artifact file was written: (a) objectstack.plugin.json that is not valid JSON; (b) a manifest missing `id`; (c) a manifest whose `version` is not semver; (d) a manifest with `id: \"\"` — which z.string() admits, so it reaches the explicit post-parse guard; (e) a manifest with no `main` and no src/index.* entry candidate", + "run `os lint` on a scaffold config whose manifest declares NO engines range → capture the finding (rule, severity, path, fix); then run it on a bare metadata fragment that declares no manifest at all → confirm the rule does NOT fire there", + "teardown: delete both scratch directories. ⚠️ Do not re-run the packaging comparison over the artifacts of a previous run — each mode writes its own --out path and a leftover file from an earlier run reads exactly like a fresh one" + ], + "acceptance": [ + { + "clause": "an invalid manifest is REFUSED at build with LOCATED errors and no artifact: an unparseable objectstack.plugin.json is refused by name ('Cannot read objectstack.plugin.json in '), and a manifest that parses as JSON but fails ManifestSchema is refused with the per-key Zod errors printed — each exits nonzero and leaves no .osplugin behind", + "oracle": "log", + "verify": "run probes 6a-6c and read stdout plus the exit code; then list the directory and confirm no artifact was written. ⛔ Score the exit code and the absence of the file together — a refusal that prints and still writes an artifact is the shape that ships a broken plugin", + "evidence": "each refusal's full output with its exit code, plus the directory listing taken after it" + }, + { + "clause": "identity is guarded in BOTH shapes, and the second guard is not redundant: `id` and `version` are required by the schema (an absent one fails safeParse with a located error, a non-semver version fails its regex), while `id: \"\"` satisfies z.string() and is caught only by the explicit post-parse refusal naming both keys — that guard is the one thing between an empty id and a nameless artifact", + "oracle": "log", + "verify": "probes 6b, 6c and 6d, read as three DIFFERENT refusals — 6b/6c located Zod errors, 6d the worded 'must declare both \"id\" and \"version\"' line. Collapsing them into one 'it refuses' observation cannot tell a working post-parse guard from a removed one", + "evidence": "the three refusals side by side, each with its exit code" + }, + { + "clause": "`packaging` FORKS the artifact rather than annotating it: the same source tree built as `bundled` (the default) and as `manifest-deps` produces two DIFFERENT artifacts — manifest-deps keeps every package.json dependency external in the bundle and packs package.json (plus pnpm-lock.yaml when present) into the archive; bundled inlines the dependency's code and packs neither", + "oracle": "build", + "verify": "diff the extracted file lists and the entry bundles of the two artifacts. Both differences must hold: the file-list difference alone could come from any archive change, and the external-import difference alone could come from an esbuild default", + "evidence": "the two file lists and the two entry-bundle excerpts showing the dependency inlined in one and imported in the other" + }, + { + "clause": "the artifact carries a COMPILED manifest — the authored manifest plus `main: \"dist/index.mjs\"` and an `integrity` map of archive-relative path → sha256 SRI digest, with the manifest itself and SIGNATURE excluded from the map — and os plugin publish reads that manifest back out of the artifact BEFORE any network call, refusing 'Artifact manifest is missing id or version.' when it cannot", + "oracle": "build", + "verify": "read the objectstack.plugin.json entry out of each archive and diff it against the authored file; then run publish against a good artifact (expect it to reach the auth refusal, proving the manifest was read) and against one whose manifest lacks a version (expect the artifact-manifest refusal, before any auth). ⚠️ The publish leg is scored on WHICH refusal arrives, not on success", + "evidence": "the compiled manifest of each artifact, and the two publish outputs distinguished by their refusal" + }, + { + "clause": "os lint closes the grandfathering loop and is SCOPED while doing it: a config whose manifest declares no compatibility range gets rule protocol/missing-engines-range at severity warning, on path manifest.engines.protocol, carrying an `engines: { protocol: '^' }` fix — and a config declaring no manifest at all is NOT flagged, because a bare metadata fragment has no package identity to hang a range on", + "oracle": "log", + "verify": "the two runs of step 7, read as a pair. ⛔ The negative half is the load-bearing one: an unscoped rule fires on every metadata fragment in a project and the warning becomes noise nobody reads, which is indistinguishable from the rule working", + "evidence": "the finding (rule, severity, path, fix) from the first run and the absence of it in the second" + } + ], + "negative": [ + "an artifact written from a manifest that failed the schema, or from one missing id/version, is the FAIL this item exists for — the refusal is worthless if the build still produces something publishable", + "the two packaging modes producing byte-identical artifacts is a FAIL of the fork clause — but check the fixture first: without a dependency the entry genuinely imports there is nothing to externalize, and that is a fixture defect, not a product one", + "⛔ the `integrity` map's PRESENCE is not evidence of an integrity CHECK. os plugin build computes it and writes it into the compiled manifest, and nothing re-verifies it at unpack — #11331 is open on exactly that. Scoring the printed 'Integrity entries' count as a working integrity guarantee is the false-compliance shape the enforce-or-remove ledger exists for, and this item asserts only that the map is PRODUCED", + "⛔ likewise the plugin trust tier: build and publish both print `runtime: `, and nothing in this repo dispatches on it — #11330 is open. A run that reads that printed line as isolation has scored a log line, not a capability", + "a lint warning that fires on a manifest-less metadata fragment is a FAIL of the scoping half even though the rule itself works" + ], + "traps": [ + "stale-dist", + "destructive-in-place" + ], + "source": [ + "packages/cli/src/commands/plugin/build.ts — manifest read + named refusal :101-107; ManifestSchema.safeParse + formatZodErrors :111-117; the explicit id/version guard :120-125; packaging default and the Loaded line :127-128; entry resolution :131-142; the manifest-deps externalization :155-166; the manifest-deps package.json / pnpm-lock.yaml packing :202-207; computeIntegrity + the compiled manifest :209-213; the artifact write and its printed block :219-230", + "packages/cli/src/utils/osplugin.ts — MANIFEST_FILENAME = 'objectstack.plugin.json' :71; OSPLUGIN_EXT :73; computeIntegrity :61-69 (manifest and SIGNATURE excluded, keys sorted); the ustar writer with mtime pinned to 0 :100-135; readOspluginManifest :163-167", + "packages/cli/src/commands/plugin/publish.ts — artifact discovery :67-77; readOspluginManifest before any network call :82-89; the id/version refusal :90-94; the unsigned-artifact notice :103-105; the auth refusal :107-116", + "packages/spec/src/kernel/manifest.zod.ts — ManifestSchema id :140 and version :202 (both required, version regex-pinned); PluginPackagingSchema :95-97 ('bundled' | 'manifest-deps', ADR-0025 §3.3); PluginRuntimeSchema :85-86 (the trust tier, ADR-0025 §3.6)", + "packages/cli/src/commands/lint.ts :375-400 — the protocol/missing-engines-range rule, its manifest scoping and its fix string", + "packages/rest/src/package-routes.ts :525-536 — the server mirror (400 PACKAGE_MANIFEST_INVALID), recorded in knownGaps rather than scored here", + "content/docs/releases/v15.mdx :514-523 (the release that dated objectstack lint's protocol/missing-engines-range) · ADR-0025 §3.3 / §3.6", + "sibling items: platform-core.manifest-install-contract (the same manifest at the install boundary) · cli.build-own-contract and cli.lint-severity-exit-contract (the os build / os lint exit-code contracts this item leans on but does not re-prove)" + ], + "history": [ + { + "revision": 1, + "date": "2026-08-24", + "change": "new — the packaging half of classifying the `manifest` capability for the coverage ratchet, which had flagged it UNCLASSIFIED since its liveness ledger landed. Authored VERIFY-FIRST against source, and two plausible sketch claims were corrected before they became clauses: (1) `id`/`version` are REQUIRED by ManifestSchema (:140, :202), so the explicit post-parse guard in build.ts is the empty-string arm rather than the only identity check — the clause now asserts both shapes as different refusals; (2) the manifest os plugin build reads is objectstack.plugin.json, not the objectstack.config.ts the manifest liveness ledger names first. `since` is v15 as a FLOOR, not a discovery: the protocol/missing-engines-range rule this item's last clause covers is dated to v15 by the release notes, and os plugin build already predates the earliest release in the CLI changelog reachable from this checkout. The two open enforce-or-remove findings on this surface (#11330 runtime tier printed-not-enforced, #11331 integrity computed-not-verified) are recorded in `negative` so a run cannot score either as working machinery", + "ref": "#11421" + } + ] } ] } diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 7c776ac866..bb2590c407 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1267,6 +1267,110 @@ "ref": "claude/new-session-0pv25p" } ] + }, + { + "id": "platform-core.manifest-install-contract", + "title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived", + "since": "v15", + "status": "active", + "revision": 1, + "priority": "P1", + "surface": "api", + "personas": [ + "seeded admin (admin@objectos.ai / admin123)" + ], + "fixtures": { + "app": "showcase", + "requires": [ + "a runtime that accepts package install over HTTP — os dev's dispatcher install route, POST /api/v1/packages → protocol.installPackage (the same door api-backend.package-rest-lifecycle drives for its 201/409/PATCH contract; this item drives the manifest GATES that run BEFORE the row is written)", + "the RUNNING runtime's protocol major, read from the server rather than assumed — every engines range below is derived from it as RT. A literal '^11' rots the moment the major turns, and a rotted range makes the compatible and incompatible legs swap places silently", + "scratch package ids only (qa_manifest_probe_*, com.example.leave, com.example.holiday) so no shipped package is mutated" + ], + "knownGaps": [ + "the DURABLE half of the handshake is NOT scored here: ADR-0087 also refuses an incompatible `sys_packages` row during boot-time rehydration, with boot CONTINUING (v15 release notes). Staging it needs an already-stored incompatible row — DB-level access, or a runtime whose major moved under a previously-installed package. Record blocked(fixture) if attempted. The install-path refusal this item does score is the same assertProtocolCompat call one boundary earlier, so a green here says nothing about the rehydration arm", + "OS_METADATA_COLLISION=warn downgrades the namespace refusal to a console warning (registry.ts installPackage) — the policy is read by the registry, not per request, so that leg needs a SECOND server booted with the env set, never a mid-run toggle. Score it only if that boot is cheap, and record which arm the run exercised", + "the install's durable leg is best-effort BY DESIGN (protocol.installPackage warns and continues when the `package` service cannot persist to sys_packages), so a probe that installs and does not survive a restart is that documented non-fatal path, not a failure of any clause here", + "NamespaceConflictError is a plain Error subclass with no `code` field (registry.ts:1080) — unlike the protocol refusal, its HTTP status/code mapping is decided by whatever handler catches it, not pinned by the class. The run RECORDS the status and code it observes rather than asserting a pre-agreed one; see `negative` for the direction that is a finding" + ] + }, + "steps": [ + "boot showcase isolated; sign in as admin. Read the runtime's protocol version from the running server (the boot banner / GET /api/v1/meta), take its leading major as RT, and derive every range below from RT — never from a literal", + "BASELINE: POST /api/v1/packages with { manifest: { id: 'qa_manifest_probe_a', name: 'Manifest Probe A', version: '1.0.0', scope: 'custom', type: 'app', namespace: 'qa_mfp', engines: { protocol: '^RT' } } }; capture status + body and GET it back — this is the compatible control every refusal below is judged against", + "INCOMPATIBLE: POST id 'qa_manifest_probe_old' with engines: { protocol: '^(RT-1)' }; capture the refusal IN FULL (status, code, message, rangeSource, targetMajor, migrateCommand). Then GET /api/v1/packages/qa_manifest_probe_old and GET /api/v1/packages and confirm NO row exists for that id", + "PRECEDENCE, both directions: POST 'qa_manifest_probe_prec_ok' with engines: { protocol: '^RT', platform: '^(RT-1)' } → expect install (protocol is consulted first, so the incompatible platform range is never reached); POST 'qa_manifest_probe_prec_no' with engines: { protocol: '^(RT-1)', platform: '^RT' } → expect refusal whose diagnostic names `engines.protocol` as the range source", + "LEGACY leg: POST 'qa_manifest_probe_legacy' with NO `engines` and engine: { objectstack: '^(RT-1)' } → expect refusal whose diagnostic names `engine.objectstack` as the source; repeat with '^RT' → expect install. This proves the third precedence rung is wired, not merely typed", + "GRANDFATHERED: POST 'qa_manifest_probe_norange' with no `engines` and no `engine` at all → expect install, plus the server's `[protocol] package '…' declares no engines.protocol range` warning in the log", + "UNPARSED: POST 'qa_manifest_probe_wsrange' with engines: { protocol: 'workspace:*' } → expect install, plus the `declares an unrecognized engines.protocol range` warning. This is the direction that would break every workspace-linked package if it inverted", + "NAMESPACE OWNERSHIP, three legs: (a) POST 'qa_manifest_probe_b' declaring namespace 'qa_mfp' — the one probe A already owns → expect refusal naming the namespace, the current owner and the incoming id; (b) re-POST probe A's OWN manifest unchanged (same id, same namespace) → expect NO conflict (reinstall/HMR is a normal path); (c) POST 'qa_manifest_probe_shared' declaring a shareable platform namespace (base / system / sys) → expect NO conflict (shareable namespaces are exempt)", + "NAMESPACE DERIVATION: POST id 'com.example.leave' with NO namespace declared → GET it back and read manifest.namespace; then POST 'com.example.holiday' WITH an explicit namespace 'timeoff' → GET it back and confirm the declared value survived untouched", + "teardown: DELETE every scratch package created above, or simply discard the isolated file DB — the cheaper path an isolated boot makes free. ⚠️ Several legs here are NOT re-runnable in place (step 8b depends on probe A still being installed, and 8a stops being a conflict once probe A is gone), so a re-run starts from a clean DB rather than from the tail of the previous one" + ], + "acceptance": [ + { + "clause": "an incompatible declared range is REFUSED, and refused BEFORE anything is written: POST /api/v1/packages carrying an `engines.protocol` range that excludes the runtime's major answers a refusal, and the follow-up GET /api/v1/packages/:id and GET /api/v1/packages show no row for that id — assertProtocolCompat runs ahead of registry.installPackage, so a rejected package leaves no half-installed remains", + "oracle": "api", + "verify": "capture the refusal, then the two post-refusal reads. ⛔ The refusal alone does not score this clause: a gate that throws AFTER the registry write looks identical from the response, and the two reads are the only thing that separates them", + "evidence": "the refusal body, plus the GET /packages/:id and GET /packages reads taken after it" + }, + { + "clause": "the refusal is a STRUCTURED diagnostic, not a bare failure: it carries code OS_PROTOCOL_INCOMPATIBLE, the declared `requiredRange`, the `rangeSource` it was read from, the runtime version it was judged against, and a `migrateCommand` of the form `objectstack migrate meta --from ` — an author refused at the boundary is told which key to fix and which command resolves it", + "oracle": "api", + "verify": "read the named fields off the captured refusal body; the migrateCommand's --from must be the major of the DECLARED range, not the runtime's", + "evidence": "the refusal body with every named field shown" + }, + { + "clause": "the declared range is resolved PROTOCOL-FIRST, and all three rungs are wired: `engines.protocol` is consulted before `engines.platform`, which is consulted before the legacy `engine.objectstack`. A manifest whose `engines.protocol` admits this runtime installs even when its `engines.platform` does not; the mirrored manifest is refused with `rangeSource: engines.protocol`; and a manifest carrying only the legacy `engine.objectstack` is judged on it in both directions", + "oracle": "api", + "verify": "the four probes of steps 4-5 — precedence-ok installs, precedence-no is refused naming engines.protocol, legacy-incompatible is refused naming engine.objectstack, legacy-compatible installs. ⚠️ A precedence that reads the LAST declared key instead of the first passes the two single-key probes and fails only the mixed pair, which is why the mixed pair is the load-bearing one", + "evidence": "the four responses, with the rangeSource of each refusal" + }, + { + "clause": "the two non-determinations ADMIT rather than refuse: a manifest declaring NO range installs (grandfathered), and a manifest whose range shape the parser does not recognize (`workspace:*`) installs too — each leaving a `[protocol]` warning naming the package. An unparsed range never becomes a false rejection, and grandfathering is warn-only", + "oracle": "api", + "verify": "both POSTs succeed and both packages read back; then find each warning in the server log. ⛔ A silent success is only half the clause — the warnings are what keep grandfathering visible instead of invisible, and `os lint`'s protocol/missing-engines-range nudge (covered by cli.plugin-manifest-build-contract) is the ratchet that closes it", + "evidence": "the two install responses and the two log lines" + }, + { + "clause": "the install-time namespace gate holds OWNERSHIP on all three legs: a second package declaring a namespace another installed package already owns is refused, and the refusal names the namespace, the current owner and the incoming id; the SAME package re-declaring its own namespace is NOT a conflict; and a shareable platform namespace (base / system / sys) is exempt. One leg does not prove the gate — an over-strict gate breaks reinstall, an under-strict one lets a package silently take another's object-name prefix", + "oracle": "api", + "verify": "the three probes of step 8; the conflict message must name all three identities (the ADR-0048 refusal text at registry.ts:1086-1095), not merely say 'conflict'", + "evidence": "the three responses, with the conflict message quoted in full" + }, + { + "clause": "a runtime-created package that declares NO namespace has one DERIVED from its id (`com.example.leave` → `leave`) and reads back carrying it, while an explicitly declared namespace is left untouched — the protocol's namespace-prefix rule is enforceable for Studio-authored packages, which never take the compile-time `defineStack` path that would have enforced it", + "oracle": "api", + "verify": "GET both packages back and read manifest.namespace off each. ⛔ Read the DERIVED value from the stored package, never from the POST echo — the derivation is set on the manifest object shared by the in-memory registry and the sys_packages row, and only a read-back proves both agree", + "evidence": "the two GET bodies with manifest.namespace shown" + } + ], + "negative": [ + "an incompatible package that installs — any 2xx, or a row present after the refusal — is the FAIL this item exists for: it means the mismatch will surface later as a deep schema/renderer crash instead of at the boundary that could name it", + "a compatible package REFUSED is the mirror fail and the more expensive one: no-range and unparsed-range must admit, so a false rejection here blocks every package that predates the range convention or is workspace-linked", + "a namespace conflict surfacing as a 5xx is the platform reporting its own fault for a manifest the caller authored — record it as a finding rather than scoring the clause on status alone; NamespaceConflictError carries no `code` (registry.ts:1080), so nothing pins its mapping today", + "a refusal whose message says only that something conflicted, without the namespace / current owner / incoming id, fails the ownership clause even when the refusal itself is correct — the ADR-0048 text exists so the author knows which of the two packages to rename", + "a derived namespace present in the POST echo but absent from the read-back (or vice versa) is a disagreement between the in-memory registry and sys_packages, not a pass" + ], + "traps": [ + "dispatcher-vs-hono-route", + "destructive-in-place", + "eventual-consistency" + ], + "source": [ + "packages/metadata-core/src/protocol-handshake.ts — resolveDeclaredRange :80-88 (protocol → platform → legacy engine.objectstack); rangeAdmitsMajor :106 (null = unrecognized, admit-with-warning); checkProtocolCompat :216-262 (the OS_PROTOCOL_INCOMPATIBLE diagnostic and its migrateCommand); assertProtocolCompat :276-301 (the four arms: ok silent, no-range warn, unparsed-range warn, incompatible throw)", + "packages/metadata-protocol/src/protocol.ts :19744-19756 — namespace derivation for namespace-less runtime packages, then assertProtocolCompat(manifest) BEFORE registry.installPackage", + "packages/objectql/src/registry.ts :3466-3490 — the ADR-0048 install-time namespace gate (shareable exemption, same-package exclusion, OS_METADATA_COLLISION=warn downgrade); NamespaceConflictError :1080-1100", + "ADR-0087 (metadata protocol upgrade contract) D1 · ADR-0048 (cross-package metadata collision) · ADR-0025 §3.10 #3 (protocol-first precedence)", + "content/docs/releases/v15.mdx :514-523 — 'ADR-0087 completed (#2972)': the handshake on boot-time durable rehydration and on code-defined AppPlugin loads, plus the objectstack lint nudge", + "sibling items: api-backend.package-rest-lifecycle (the same install door, judged on its 201/409/PATCH contract rather than on the manifest gates) · cli.plugin-manifest-build-contract (the same manifest at the packaging boundary) · platform-core.package-lifecycle-enable-disable (what happens to a package after it installs)" + ], + "history": [ + { + "revision": 1, + "date": "2026-08-24", + "change": "new — authored to classify the `manifest` capability for the coverage ratchet, which had flagged it UNCLASSIFIED since its liveness ledger landed. Grounding decided the shape: the manifest is an authored, enforced surface (namespace ownership refuses at install, an incompatible engines range refuses before the registry write, a namespace-less runtime package has one derived), not an internal ledger, so it earns items rather than a waiver. This item takes the INSTALL boundary; cli.plugin-manifest-build-contract takes the packaging boundary; api-backend.package-rest-lifecycle already covered the install door's 201/409/PATCH contract and is mapped alongside them. Every range in the steps is derived from the running runtime's major rather than written literally, so the compatible and incompatible legs cannot swap places when the major turns", + "ref": "#11421" + } + ] } ] } diff --git a/docs/qa/platform-checklist/coverage.json b/docs/qa/platform-checklist/coverage.json index e7a8678e53..d4893a759c 100644 --- a/docs/qa/platform-checklist/coverage.json +++ b/docs/qa/platform-checklist/coverage.json @@ -92,6 +92,13 @@ "integration-system.job-scheduled-run" ] }, + "manifest": { + "items": [ + "platform-core.manifest-install-contract", + "cli.plugin-manifest-build-contract", + "api-backend.package-rest-lifecycle" + ] + }, "mapping": { "items": [ "records-forms.named-import-mapping"