Found by flight ⑨ of the published-skills factual sweep (#13658, member card #13840). Filed unassigned.
What was measured
os migrate meta --from N requires the operator to state the protocol major the metadata was authored against. The published upgrade skill told readers to read it from manifest.protocol — a phantom: manifest.protocol had zero occurrences in this repo outside that one skill line, and ManifestSchema (packages/spec/src/kernel/manifest.zod.ts) declares no such key. That falsehood is corrected in the sweep PR by pointing at manifest.engines.protocol, which is declared (PluginEnginesSchema) and genuinely checked at boot (assertProtocolCompat in packages/runtime/src/app-plugin.ts; assertArtifactProtocolCompatible in packages/runtime/src/artifact-reference.ts, raising OS_PROTOCOL_INCOMPATIBLE).
Chasing that surfaced a second, separate gap the sweep did not fix.
manifest.specVersion is read by three CLI commands and declared by nothing:
| read at | line |
|---|
packages/cli/src/commands/validate.ts | checkSpecVersionGap(config.manifest) |
packages/cli/src/commands/doctor.ts | checkSpecVersionGap(config.manifest) |
packages/cli/src/commands/compile.ts | checkSpecVersionGap((config as ...).manifest) |
packages/cli/src/utils/spec-version.ts describes exactly what it wants: "when an app's authored manifest.specVersion declares an OLDER major than the @objectstack/spec actually installed ... there is breaking-change guidance the author should read before proceeding." That is precisely the reading the upgrade flow needs.
But the key that call reads is the stack config'smanifest, and ManifestSchema has no specVersion member. The only specVersion in the spec is on a different surface — packages/spec/src/cloud/template-manifest.zod.ts (the marketplace template manifest). Consequences, in the order they bite:
ManifestSchema is not .strict(), so an authored specVersion is accepted and dropped. Nothing tells the author it went nowhere.- Being undeclared, it is invisible to
check:authorable-surface, carries no .describe(), and therefore never reaches the generated reference docs or Studio property forms — the same declared-versus-enforced asymmetry ADR-0049 exists for, running in the read-but-undeclared direction. - So the advisory is, in practice, dead for stack configs: it can only fire for a manifest that carries a key the schema does not offer.
Why this is not #9264
#9264 (closed) was the blank template declaring two protocol versions that disagreed — specVersion: "^6.0.0" in the manifest against engines.protocol: '^17' in the config — and fixed the sync between them. This is the layer under that: on the stack ManifestSchema there is no specVersion member to disagree with anything, while three commands read one.
The decision, not the patch
Two coherent shapes, and picking between them is a contract call rather than a bug fix:
Recommendation: B. Real business need favours it — engines.protocol has live readers on the boot path today while specVersion on a stack manifest has none; long-term soundness favours one declared version axis over two; it makes AI-authored metadata harder to get wrong, because the key an agent can write is the key the runtime reads; and it is the smaller surface, which is the startup-scope posture. A is the answer only if someone wants an app-declared spec range that is deliberately independent of the plugin compatibility range — a real need nobody has stated.
Refs: #13658 · #13840 · #9264 (closed, the adjacent template-side fix).
Generated by Claude Code
Found by flight ⑨ of the published-skills factual sweep (#13658, member card #13840). Filed unassigned.
What was measured
os migrate meta --from Nrequires the operator to state the protocol major the metadata was authored against. The published upgrade skill told readers to read it frommanifest.protocol— a phantom:manifest.protocolhad zero occurrences in this repo outside that one skill line, andManifestSchema(packages/spec/src/kernel/manifest.zod.ts) declares no such key. That falsehood is corrected in the sweep PR by pointing atmanifest.engines.protocol, which is declared (PluginEnginesSchema) and genuinely checked at boot (assertProtocolCompatinpackages/runtime/src/app-plugin.ts;assertArtifactProtocolCompatibleinpackages/runtime/src/artifact-reference.ts, raisingOS_PROTOCOL_INCOMPATIBLE).Chasing that surfaced a second, separate gap the sweep did not fix.
manifest.specVersionis read by three CLI commands and declared by nothing:packages/cli/src/commands/validate.tscheckSpecVersionGap(config.manifest)packages/cli/src/commands/doctor.tscheckSpecVersionGap(config.manifest)packages/cli/src/commands/compile.tscheckSpecVersionGap((config as ...).manifest)packages/cli/src/utils/spec-version.tsdescribes exactly what it wants: "when an app's authoredmanifest.specVersiondeclares an OLDER major than the@objectstack/specactually installed ... there is breaking-change guidance the author should read before proceeding." That is precisely the reading the upgrade flow needs.But the key that call reads is the stack config's
manifest, andManifestSchemahas nospecVersionmember. The onlyspecVersionin the spec is on a different surface —packages/spec/src/cloud/template-manifest.zod.ts(the marketplace template manifest). Consequences, in the order they bite:ManifestSchemais not.strict(), so an authoredspecVersionis accepted and dropped. Nothing tells the author it went nowhere.check:authorable-surface, carries no.describe(), and therefore never reaches the generated reference docs or Studio property forms — the same declared-versus-enforced asymmetry ADR-0049 exists for, running in the read-but-undeclared direction.Why this is not #9264
#9264 (closed) was the blank template declaring two protocol versions that disagreed —
specVersion: "^6.0.0"in the manifest againstengines.protocol: '^17'in the config — and fixed the sync between them. This is the layer under that: on the stackManifestSchemathere is nospecVersionmember to disagree with anything, while three commands read one.The decision, not the patch
Two coherent shapes, and picking between them is a contract call rather than a bug fix:
specVersiontoManifestSchemawith a.describe(), making the existing advisory reachable and authorable. Cost: a second version-shaped key besideengines.protocol, which is the ambiguity The blank template declares two protocol versions that disagree by eleven majors —specVersion: "^6.0.0"in the manifest,engines.protocol: '^17'in the config, and only the second is synced #9264 just paid down.checkSpecVersionGapatengines.protocol, which is already declared, already parsed and already enforced at boot, and deletespecVersionfrom the CLI's vocabulary for stack configs. Cost: template manifests keep their ownspecVersion, so the name means one thing in one place and nothing in the other — which is the status quo, stated honestly.Recommendation: B. Real business need favours it —
engines.protocolhas live readers on the boot path today whilespecVersionon a stack manifest has none; long-term soundness favours one declared version axis over two; it makes AI-authored metadata harder to get wrong, because the key an agent can write is the key the runtime reads; and it is the smaller surface, which is the startup-scope posture. A is the answer only if someone wants an app-declared spec range that is deliberately independent of the plugin compatibility range — a real need nobody has stated.Refs: #13658 · #13840 · #9264 (closed, the adjacent template-side fix).
Generated by Claude Code