Repository navigation
fix(metadata-protocol)!: installPackage refuses a manifest the install declaration refuses (type, scope, unknown keys) - #24203
Conversation
…he install door's declaration Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
…l; state the hydration tolerance Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
…install-refusal pins Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
…stall-primitive-parses-manifest
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 546d7d5deb69185f23e333807a2b6a78d54410d4 && git checkout 546d7d5deb69185f23e333807a2b6a78d54410d4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8a88901be1c317e2e078e6d29f21efcae5b9485e a263c0885f1376f32d866f2c106b88bf90187608 && git checkout -B drift-repro 8a88901be1c317e2e078e6d29f21efcae5b9485e && git merge --no-ff a263c0885f1376f32d866f2c106b88bf90187608
node scripts/docs-audit/affected-docs.mjs --json 8a88901be1c317e2e078e6d29f21efcae5b9485e
|
… and reported Claude-Session: https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flagsReport 6237660624 — deviations:
Report 6237660624 — open questions:
Addendum 6237799160 — deviations:
Cross-lane notes:
Residual, named and not a defect of this diff: the primitive still completes a versionless manifest (#2532) where the door refuses one; pre-existing, documented in the primitive and the changeset's Unchanged bullet, left as is. Gate verdicts on this head (newest run per name, read at the time of this record): 28 success — Lint & Repo Gates and every Lint · job (ESLint, PM dispatch-gates, docs & code guards, repo & tooling, CI/release/engine, the three scoped lanes), Type Check · workspace / source / consumer / debt ledger and TypeScript Type Check, Build Core, Check Changeset, Check Documentation Links, Flag docs affected by code changes, Governed Surface Queue Guard, Dogfood Regression Gate, filter, the four claim/branch guards (card claims this branch; no other open PR claims the issue; no other open PR claims the single-writer path; part-of PR must not close its card), Test Core 1/6, 5/6 and 6/6; 8 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Dogfood Regression Gate shards, Dogfood Verify CLI, Packed-tarball smoke opt-in, Temporal Conformance) — none a failure; 3 in_progress: Test Core (2/6), Test Core (3/6), Test Core (4/6). No check on this head has concluded failure. The PASS below is the verdict on the diff; the three running shards remain the seat's to re-read green before any landing step. Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24086, its only closing keyword. objectstack-ai/cloud#2948 and its PR objectstack-ai/cloud#2978 are named without one. The seat made the two body edits the dev's addendum 6237799160 asked for; the relay read back identical. |
| Scope | 22 files (+430 / −54): installPackage in packages/metadata-protocol/src/protocol.ts, new pins in metadata-protocol and service-package, a fixture triage across 15 test files (each adds the declaration; none relaxes the rule), one comment at service-package's hydration seam, and one test alias. The service-package files are declared to domain:services on #6021. Not governed (check-governed-merges.mjs --pr 24203); 484 lines. |
| Premise and ruling | The primitive parses the install door's own declaration (PackageInstallManifestSchema), as dispatched. The seat checked the record's central claims on this head before adopting it. The parse at protocol.ts:31429 is PackageInstallManifestSchema.safeParse(manifest). Its parsed copy is never written back (no declaredManifest.data read). duplicatePackage rethrows the refusal. The door hands the primitive the manifest as sent (packages.ts:1228, :1423). |
| Contract review | CONTRACT_REVIEW_TIER, an isolated reviewer: record 6238198640, PASS on this head (narrowing face, every non-prose path). |
| Prose faces | Changeset, sentence by sentence. FROM and TO match the pins (400, VALIDATION_ERROR at the boundary, the key paths, no writes). "Unchanged" matches the code: the version default 0.1.0 and the namespace derivation run as before, and the parse defaults are not stored. "Rows already stored … keep hydrating", and a duplicatePackage of such a base is refused: both are pinned on the real stack. |
| Pins and ablation | Refusals assert status 400 + code VALIDATION_ERROR through resolveThrownHttpError, and the absence of both writes. One ablation of the parse gate turns the refusal pins red in both packages, and the hydration half stays green. The restore is clean. |
Clause-② |
no (narrowing), at a line start in the PR body and the changeset. @objectstack/metadata-protocol minor with a bang (BREAKING under the launch-window convention), and the ADR-0087 marker not-required (no-migration-prescription). |
| Gates | Dev union: 69 derived, 69 run, all exit 0. CI on this head: 47 check-runs, 39 success, 8 skipped, 0 failed. Every skip is rostered in EXPECTED_SKIPS: Auto Label, Check PR Size, Temporal Conformance, Console Pin Gate, Dogfood Verify CLI, the Dogfood matrix template, Build Docs, Packed-tarball smoke. |
Disposition:
open_questions:- [0] A: refuse a typeless manifest. The declaration has no default, and the door refuses one too.
- [1] A: the refusal stays at the write. Repairing stored rows belongs to the producer half: objectstack-ai/cloud#2948, its PR objectstack-ai/cloud#2978, and the cloud maintainer's decision box named in note 6236742886.
- [2] A: tracked at the cloud pin bump. The cloud pin that carries this refusal must carry objectstack-ai/cloud#2978's merge; the
repo:cloudseat stated it on this card (6236742886).
out_of_scope_findings: none.deviations: accepted (the fixture radius; zero label writes under the new os-dev budget; the{ id, name, type: 'app' }control; the file's own refusal prefix, sinceinstallBodyRefusalis private to@objectstack/runtime, which depends on this package; the gate-runner reruns).- Residual, pre-existing and not this card's: the primitive still completes a versionless manifest that the door refuses. It is documented in the primitive and in the changeset's "Unchanged" line.
Landing: ordinary queue (pr_ready + automerge_enable through the relay), in this stroke.
Generated by Claude Code
Fixes #24086
Clause-②: no (narrowing)
What changed
ObjectStackProtocolImplementation.installPackage(packages/metadata-protocol/src/protocol.ts) parsed onlymanifest.id, so an in-process caller stored manifests thatPOST /api/v1/packagesanswers400 VALIDATION_ERRORfor. cloud's AI authoring (ensureNamedBasePackage,ensureAppPackage) storedtype: 'application'/scope: 'environment'this way.version: '0.1.0'default, and before the namespace default and any write, the manifest is parsed throughPackageInstallManifestSchema(@objectstack/spec/api). That is the declaration the door parses,ManifestSchemawithscopenarrowed toproject. It is notManifestSchema, which still admitscloud/system(thedomain:specseat's note 6231970586). Nothing in this file decides which values are allowed: no enum check, and no copy of the door's rule.{ statusCode: 400 }, the same spelling as the id leg. An HTTP boundary (resolveThrownHttpError) answers it400 VALIDATION_ERROR. The message isInvalid package manifest — manifest.KEYPATH: ISSUE_MESSAGE; …(ISSUE_MESSAGE is the declaration's own wording), so a platform scope carriespackageInstallScopeRefusal's sentence verbatim. The door'sinstallBodyRefusalis module-private in@objectstack/runtime, which depends on this package, so it cannot be reused here. This file surfaces the zod path and message instead.scope,defaultDatasource) are not copied onto the stored manifest.PackageServicePlugin.startin@objectstack/service-package, which hydratessys_packagesinto the registry. It never judgedtype/scope. That tolerance is now stated in a comment at the seam and pinned on the real stack. It applies to the read only, and a new write of the same manifest is refused.File surface
All edits stay inside the claim's surface:
protocol.ts'sinstallPackageregion, the hydration read-back (packages/services/service-package/src/index.ts, comment only), pins, and the changeset. The pin sweep also touched fixtures outsidemetadata-protocolthat feed this rule (listed below). No source edit was made inpackages/specorpackages/runtime.Pins (the flipped answer is load-bearing)
packages/metadata-protocol/src/protocol.install-manifest-declaration.test.ts(new). Each refused manifest is checked forresolveThrownHttpError(err)={ status: 400, code: 'VALIDATION_ERROR' }, for the key path it names, and for neither writer having run. The refused manifests aretype: 'application',scope: 'environment',scope: 'cloud',scope: 'system', a missingtype, and an unknown key (pages). Thecloudcase also asserts the declaration's sentence is surfaced verbatim. The id leg still answers first withmanifestIdRefusal. Lit controls: a versionless{ id, name, type }installs with0.1.0and a derived namespace, and the stored manifest gets no parse defaults.packages/services/service-package/src/install-primitive-declaration.test.ts(new) runs on the real stack:ObjectKernel,ObjectQLPlugin,SqlDriveron better-sqlite3,PackageServicePluginand the real protocol.application,environmentandcloudare each refused with 400 /VALIDATION_ERROR, and afterwardsregistry.getPackageis undefined and thesys_packagesrow isnull. The versionless control survives a restart. The H3 pin publishes atype: 'application', scope: 'environment'row through the service's ownpublish, the write the primitive used to make. After a restart that row is hydrated as stored (source: 'authored', values intact, no error line), and a fresh install of the same manifest is refused.hydration-convert-forward.test.ts› "the install primitive is authoring-side and stays strict". The assembled HotCRM body (withpages) used to reach the registry, which refused it422 MisplacedDefaultPageError. The install declaration's strict close now refuses it first, so the pin asserts400/VALIDATION_ERRORnamingpages, and still that nothing is written. Its docblock was updated to match.Fixture triage (each fixture got its declaration added, not the rule relaxed)
The rule's consumer radius covers every caller of
protocol.installPackageand everyduplicatePackagesource.metadata-protocoltest files installed{ id, name }or{ id, version }with notype. Each now writestype: 'app', plusnamewhere it was absent. The declaration requires both, and the door has always refused a manifest without them.objectqlprotocol test files usedtype: 'application', the off-spec value this card is about, as didprotocol.flow-canonicalizer.test.tsand theapp.iojnduplicate source inprotocol-package-lifecycle.test.ts. Each was respelledtype: 'app'. Thescope: 'environment'kept on that duplicate source is stripped byduplicatePackageitself.runtime/src/app-plugin.disabled-seed.test.ts(code boot), andservice-packagehydration.test.ts/mysql2-tuple.test.ts(stored rows and the service's own write).Mechanism hypotheses, measured
PackageInstallManifestSchemarequiresid,name,typeandversion, andnamespaceis optional. Placing the parse after the version default keeps the DB-created base packages vanish from GET /packages after server restart #2532 versionless install working.typehas no default anywhere: the declaration requires it and the door refuses its absence. So a manifest withouttypeis refused (settled by the four axes, seeopen_questionsin the report). The in-repo callers that reach the primitive outside the door areduplicatePackage(it copies the source'stype) and the unmanaged install's base (it copiestypeas an identity key). The door parses the same declaration first.protocol.installPackageare the runtime door (packages/runtime/src/domains/packages.ts),duplicatePackage(it deletesscope), andimportUnmanagedPackage(itsscopekey is abasedisposition, not copied). None installs a platform scope. Platform packages reach the registry throughObjectQL.registerApp, which does not go through this primitive.PackageServicePlugin.start→registry.installPackage(convertForward(rec.manifest), 'authored')judges only the protocol handshake and the registry's own rules. The tolerance is stated there and pinned.PackageInstallManifestSchemacomes from@objectstack/spec/api, whichprotocol.tsalready imports.check-undeclared-dep-importsexit 0.deps.error(msg, 400)→VALIDATION_ERROR(standardErrorCodeForHttpStatus(400)). The primitive throwsstatusCode: 400, whichresolveThrownHttpErrorresolves to the same{ 400, VALIDATION_ERROR }(pinned).Serial constraint with the producer
From triage 6228198471, verbatim: "⛔ Serial with the producer. The producer half is objectstack-ai/cloud#2948 (open,
pm:blocked, assigned). The cloud pin that carries this refusal must also carry cloud#2948's change; otherwise cloud's AI authoring is refused on the first new base." objectstack-ai/cloud#2948 remains open as the producer half; this PR leaves it untouched.The producer half's change is objectstack-ai/cloud#2978, the cloud PR for objectstack-ai/cloud#2948 (named by the
repo:cloudseat 2 note 6236742886 on #24086). The cloud pin that carries this refusal must carry that merge. (Seat edit: the dev's addendum 6237799160 asked for this line, because the dev writes the body only once.)Verification
All readings are on head
e9ac2429aa(origin/main7d6ec91d2fmerged in, clean, with no overlap with the touched packages), in a cold cloud container. Builds and tests ran throughscripts/pm/os-verify-lock.sh.pnpm --filter '@objectstack/metadata-protocol...' build,pnpm --filter '@objectstack/service-package...' build, and a rebuild of@objectstack/specand@objectstack/metadata-protocolafter the merge: VERDICT command-exit 0.@objectstack/metadata-protocol: 244 files passed, 3 skipped; 20638 tests passed, 19 skipped.@objectstack/service-package: 9 files / 124 tests passed.@objectstack/objectqlsrc/protocol-*: 54 files / 715 tests passed.metadata-protocolhad 34 failures across 7 files. Every one was a fixture manifest without a declaredtype(47manifest.typeissues, 4manifest.name). That is the consumer radius the triage covered.metadata-protocol,service-packageandobjectql(tsc --noEmitpluscheck:test-typecheck, debt held) all pass. The firstservice-packagerun failed withTS2307 @objectstack/objectqlbecauseobjectqlhad nodistin this cold container. The pre-existinghydration-convert-forward.test.tsfailed the same way. It was green after building the closure.scripts/ablation-replace.mjs, run againstsrc). Both suites resolvemetadata-protocolfrom source:./protocol.js, andservice-package's vitest alias. The mutation changedif (!declaredManifest.success) {toif (false && ...). Anchor 1 to 0, blob115a51f6to0f44c5c3.protocol.install-manifest-declaration.test.ts: 7 failed, 3 passed. The 6 refusals and the surfaced-sentence pin went red. The id-order pin and both lit controls stayed green.service-package(install-primitive-declarationandhydration-convert-forward): 5 failed, 5 passed. The real stack registered and stored the off-spec manifests, which reproduces the defect. The hydration half of the H3 pin stayed green and only its fresh-write assertion went red, as designed.115a51f6), andgit diff HEADis empty.pnpm check:adr-0087-registration: exit 0. It reads[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).node scripts/check-changeset-no-major.mjs: exit 0, "This diff introduces nomajorbump".node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) printed 69 commands one9ac2429aa. All 69 ran with exit codes captured before any pipe, and all 69 exited 0. The--ranreconciliation reads: "69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)".check:test-source-alias: the newservice-packagepins imported@objectstack/spec/apiwithout an alias. I fixed it with one anchored alias inpackages/services/service-package/vitest.config.ts, as the gate prescribes. Two gates refused on the shallow clone (check-engine-split-ratio --days 90,check-plugin-teardown-shape --self-test) and passed aftergit fetch --unshallow.Acceptance notes
duplicatePackageis a re-entry path for stored manifests. Duplicating a pre-change stored base or app (type: 'application',scope: 'environment', anicon) is refused400 VALIDATION_ERRORnamingmanifest.typeandicon. The refusal is reported to the caller, and nothing is written to the registry or tosys_packages; this is pinned inpackages/services/service-package/src/install-primitive-declaration.test.tsat heada263c0885f. The stored rows themselves keep hydrating. Repairing them belongs with the producer change or a migration, as recorded in the report'sopen_questions. (Seat edit, from the dev's addendum 6237799160.)🤖 Generated with Claude Code
https://claude.ai/code/session_012HLZ1LzgJX5mNE9C4DqDYE
Generated by Claude Code