feat(spec): make manifestId optional on TemplateManifestSchema + gate every shipped template manifest (#7319) - #7383
Conversation
…te every shipped template manifest (#7319) `TemplateManifestSchema` describes the on-disk `objectstack.manifest.json`, and the bundled blank template declares it as its `$schema` — but the file did not satisfy it. The schema inherited `manifestId` from `CreatePackageRequestSchema` as required, and no template has ever declared one. Measured on main, that was the only complaint the shipped file produced: manifestId: Invalid input: expected string, received undefined (invalid_type) Nothing broke because nothing parsed the file: `create-objectstack` reads and rewrites it as raw JSON and does not depend on the spec at all, and `objectstack package publish` also reads it raw, resolving the id as `--manifest-id ?? m.manifestId ?? deriveManifestId(artifact, path)`. That fallback is the measurement — on this file the id is a declarative default, not a requirement. Two changes: 1. `manifestId` is relaxed to optional on `TemplateManifestSchema`, declared LOCALLY: the inherited field is omitted and re-declared `.optional()`, reusing the publish field's value constraints — the same omit-then-extend split `namespace` uses (#6861), in the other direction. A malformed id is still rejected. `CreatePackageRequestSchema` is untouched: the publish request still requires `manifestId`, because a request with no package identity is not a request. 2. New gate `check:template-manifests` parses every `objectstack.manifest.json` under `packages/create-objectstack/src/templates/` against the schema, wired unfiltered into the required `TypeScript Type Check` job and classified in the `check:generated` ledger as a source audit. It is the check that would have caught both drifts this file has accumulated (#6861's stripped `namespace`, this one), so the `$schema` line stops being an unverified claim. It walks the tree rather than a file list, and fails rather than reporting success when it finds nothing to parse. Pins in `template-manifest-id.test.ts` hold both halves: optional-but-validated on the descriptor, still-required on the publish request, distinct field instances. Reference docs regenerated.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #7319.
Options 2 + 3 of the issue, combined. Option 1 (invent a template-level literal id) rejected per the dispatch: the scaffolder derives the identity per project, so a literal would name a package nobody publishes.
Premise — re-measured on merged
main(f188ed6)The blank template's manifest still fails to parse against the schema it declares as its
$schema, with exactly the complaint the issue recorded and nothing else:And the live path confirms the key is genuinely optional on this file:
objectstack package publishreads it as raw JSON and resolves--manifest-id ?? m.manifestId ?? deriveManifestId(artifact, artifactPath)(packages/cli/src/commands/package/publish.ts:317-320). A template tree that declares none publishes fine — the fallback deriveslocal.<slug>from the compiled artifact. Nothing on any live path requires a template-levelmanifestId, so the direction stands.Built on merged
main, which already carries #6861 / PR #7317's scaffold-onlynamespaceextra. Both intents are kept, and the new gate exercises both.(2) The widening is LOCAL — both schemas verified
packages/spec/src/cloud/template-manifest.zod.tsomits the inherited field and re-declares it optional, reusing the publish field's value constraints — the same omit-then-extend splitnamespaceuses, in the other direction:packages/spec/src/cloud/package.zod.tsis not in the diff at all — the publish request is untouched. Measured before and after, in one run:manifestIdinvalid_typeCreatePackageRequestwithoutmanifestIdparsesThe generated reference docs are the third witness: only
content/docs/references/cloud/template-manifest.mdxmoved (manifestIdrequired → optional). The create-request page is byte-identical.Optional is not unvalidated — a malformed id still fails at
['manifestId']withinvalid_format, and every other required key (displayName,name,specVersion) is still required. Pins in the newpackages/spec/src/cloud/template-manifest-id.test.tshold all of it, including that the two field instances are distinct and the publish field's owndescribeis unchanged.(3) The gate
pnpm --filter @objectstack/spec check:template-manifestsparses everyobjectstack.manifest.jsonunderpackages/create-objectstack/src/templates/againstTemplateManifestSchema.$schema: the gate's authority comes from the filename, so a contradictory claim means one of the two statements is wrong and a reader cannot tell which.--self-testproves detection against fixtures rather than against a one-file corpus, and deliberately exercises both intents this schema carries: green on a manifest with nomanifestId(The shipped blank template'sobjectstack.manifest.jsondoes not validate againstTemplateManifestSchema— it declares nomanifestId#7319) and on one with a well-formed id; red on a malformedmanifestId, a malformednamespace(objectstack.manifest.jsoncarries a livenamespacekey thatTemplateManifestSchemadoes not declare #6861), a missing required key, a contradictory$schema, and malformed JSON.Wired the way the repo's other source audits are: a
check:script inpackages/spec/package.json(self-test then run), classified in thecheck:generatedledger'sNO_GENERATORbucket (it readssrc/and writes nothing — a failure is a manifest to fix or a schema to fix, never agen:to run), and one step in lint.yml's required, unfilteredTypeScript Type Checkjob, in the pre-build group since it needs nodist.No edit to the blank manifest was needed for the
manifestIdgap — that was the acceptance condition, and it holds.Reverse verification (predicted in writing, then run)
Predicted: reverting only
template-manifest.zod.tstoorigin/mainmakes the new gate exit 1 on the blank template with exactly themanifestIdcomplaint, the self-test fails on its green fixture first, and restoring returns both to green.Observed, exactly:
template-manifest-id.test.tswent 3 failed / 4 passed under the same revert. Restored → gate exit 0, tests 7/7.Gates run locally
pnpm --filter @objectstack/spec buildthencheck:generated→ all 11 generated artifacts up to date; the four-step regen produced exactly one artifact diff (content/docs/references/cloud/template-manifest.mdx, reviewed above).check:api-surfacegreen — no export moved, so no dual-snapshot work.pnpm --filter @objectstack/spec typecheck(tsc +check:scripts-typecheck+check:test-typecheck) ✅pnpm --filter @objectstack/spec test→ 363 files / 9475 tests ✅check:template-manifests(self-test + run) ✅ ·check:published-files✅ ·check:workflow-status-functions✅ ·check:empty-changeset✅ ·check:adr-0087-registration✅ (no declared-breaking changeset) · ESLint on the changed files ✅Changeset
@objectstack/spec: minor— an acceptance-face widening on a cloud-facing schema plus a new gate, no removal or rename, so no ADR-0087 disposition marker is owed (check:adr-0087-registrationconfirms: 0 declared-breaking changesets).No
docs/adr/**and nocontent/docs/releases/files are touched.Generated by Claude Code