Uh oh!
There was an error while loading. Please reload this page.
feat(spec): declare namespace on TemplateManifestSchema as a scaffold-only extra (#6861) - #7317
Conversation
…ld-only extra (#6861) `objectstack.manifest.json` carries a live `namespace` key that the schema describing that file did not declare: the blank template ships it, `create-objectstack` rewrites it at scaffold time, and `readTemplateNamespace` reads it back as the fallback source for the template's original namespace. The schema is a default strip-mode object, so parsing the manifest through it dropped the key and answered success, and a malformed value passed green. ADR-0049 enforce leg: declare it, do not remove it. The field reuses `CreatePackageRequestSchema.shape.namespace`'s constraints (one vocabulary, ADR-0048 addendum §A.7) but is re-declared rather than inherited, carrying a describe that says it is scaffold-only and NOT the publish namespace — publish still reads that off the compiled artifact's `manifest.namespace` (§A.2), so the second-authoring-surface footgun #6760 avoided stays avoided. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
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:
|
Uh oh!
There was an error while loading. Please reload this page.
Closes#6861
Executes the triage direction on #6861: the ENFORCE leg of ADR-0049 — declare the key, do not remove it.
Premise (verified on fresh
origin/main@3e8e669c0)All three legs of the issue still hold. Measured with a throwaway probe against the real schema before any edit:
TemplateManifestSchemadoes not declarenamespacemanifestId, displayName, description, visibility, category, tags, iconUrl, homepageUrl, license, publisher, isStarter, translations, name, specVersion, skills, preview, scaffold, readmePath"namespace": "blank"create-objectstackrewrites it;rewrite-identity.tsreads it backsrc/index.ts:281andsrc/rewrite-identity.ts:73The probe also pinned the consequence, which the issue predicted but did not measure: the schema is a default strip-mode object, so parsing a manifest through it dropped the key and answered success —
A malformed value was therefore accepted, not refused. That is the enforce-or-remove shape, and since the key is genuinely live, enforce is the right leg.
Corroboration found while verifying:
content/docs/getting-started/your-first-project.mdx:87has advertisedobjectstack.manifest.json # name, namespace, specVersionto users all along. The schema was the only surface denying the key existed — so no hand-written doc needed changing, only the generated reference.Changes
packages/spec/src/cloud/template-manifest.zod.ts— declaresnamespace, optional, reusingCreatePackageRequestSchema.shape.namespacefor its value constraints. The.omit()of the inherited create-request field stays, and the key is re-declared with its own describe; the comment that used to explain the omission now explains the split (omitted for publish, declared for scaffold). A TSDoc block on the field names both readers and states plainly that this is not the publish source.packages/spec/src/cloud/package-namespace.test.ts— the block that pinned the omission is replaced by five pins for the new semantics; the §A.7 vocabulary table gains the template field, andblankjoins the value table as the one in-repo instance of the scaffold surface.packages/spec/authorable-surface/cloud.json— one key added,cloud/TemplateManifest:namespace.content/docs/references/cloud/template-manifest.mdx— regenerated; adds thenamespacerow with the scaffold-only wording..changeset/template-manifest-scaffold-namespace.md—@objectstack/specminor.Why declare rather than remove
The removal leg would drop the
rewrite-identity.ts:73fallback and makeobjectstack.config.tsthe single source — but that fallback is what #4902 added for the remote-template shape, where the tree has no config to read. Triage ruled the key live and routed this as the enforce leg, and the measurement above agrees: the key is written, rewritten and consumed on a real shipped file.Reusing the constraints without inheriting the field is the load-bearing detail. Value constraints come from the publish field, so both surfaces judge every namespace identically (ADR-0048 addendum §A.7, "two gates, one vocabulary") — otherwise the scaffolder could stamp a namespace publish would later refuse. The meaning stays split: publish reads its namespace off the compiled artifact's
manifest.namespace(§A.2 Phase A1), never off this file. Collapsing the two into one inherited field is exactly the second-authoring-surface footgun #6760 avoided, and it stays avoided.Reverse verification — direction predicted first
Predicted before running: with the schema change removed, the no-strip pin, the declares-the-field pin, the rejection pin and the describe pin go red;
is OPTIONALstays green, because a manifest with no namespace parses either way.Measured (
git checkout origin/main -- template-manifest.zod.ts, then the pin file):One honest correction to the prediction.
is OPTIONALdid stay green as predicted, but six reds appeared where four were expected. Two of them —every field is optionaland the §A.7 table — go red by TypeError onundefined.safeParse, not by a verdict flip: both now touchTemplateManifestSchema.shape.namespace, which does not exist pre-change. Only four reds are genuine verdict flips (declares the field,SURVIVES the parse,REJECTS a malformed,says scaffold-only). Recording this because a red count is not by itself evidence — the reason each one went red is.Gates
pnpm --filter @objectstack/spec buildcheck:generatedcheck:docsstale only; fixed bygen:docs.check:api-surfaceandcheck:export-originswere green throughout — no public export added or removed, so the dual-snapshot rule is not triggered.authorable-surface.base.jsongen:authorable-surface-basedeliberately not run; this is an addition, not a re-anchoring.pnpm --filter @objectstack/spec testpnpm --filter create-objectstack testpnpm --filter @objectstack/spec typecheckpnpm --filter create-objectstack typechecknode scripts/check-nul-bytes.mjsOne detour worth recording. In the first full spec run — taken before the artifact regen —
scripts/build-schemas-check-mode.test.tsfailed 23 of 60. I ruled out an environmental cause by running the same file on a cleanorigin/mainworktree, where it passes 60/60; the real cause is that those sandboxed box tests read the in-treeauthorable-surface/cloud.json, which my change had made stale. The regen fixed it, and the full run above — 359/359 files including that one — is the post-regen confirmation. Recording it because "build a schema, then regenerate" is load-bearing here in a way the failure did not advertise: the red pointed at a box-test file with nothing to do withnamespace.Special inspection items
git checkout origin/main -- ...while the work was still uncommitted, sogit checkout branch -- ...could not bring it back — the branch tip wasorigin/main. I re-applied the three edits from context and committed before doing anything else. The zero-diffgen:schemare-run is evidence the restore is faithful, but the source diff deserves a real read rather than a skim..describe()sits on theZodOptionalwrapper, not the inner string. It renders correctly in the generated docs row (verified in the mdx diff), but it is the reason the describe pin reads.descriptionoff the field rather than off.unwrap().manifestId. That is a separate pre-existing drift, untouched here and filed as The shipped blank template'sobjectstack.manifest.jsondoes not validate againstTemplateManifestSchema— it declares nomanifestId#7319.Out of scope
Filed as #7319 (
finding, unassigned): the shipped blank template'sobjectstack.manifest.jsondoes not validate againstTemplateManifestSchema, because the schema inheritsmanifestIdas required and the file declares none. A second declared-vs-live drift on the same file, unrelated tonamespace, with its own fork to settle — so it is filed rather than fixed here.Generated by Claude Code