Repository navigation
fix(metadata): TypeScriptSerializer annotates each item with its own metadata type's spec type, never ServiceObject for a non-object - #19865
Conversation
…a type's spec type TypeScriptSerializer annotated every item ServiceObject, so a saved view (or any non-object type) was a .ts file that failed tsc with TS2353. FilesystemLoader.save() now passes the metadata type in the existing SerializeOptions bag; a type with no spec type gets no annotation. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
Drops view from the annotation table: ViewMetadata is the input type of a z.preprocess schema, so it is unknown and would check nothing. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
…notates per metadata type Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 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 e56b9e262be4571c2f18f2e10a89f0ebfcc3796f && git checkout e56b9e262be4571c2f18f2e10a89f0ebfcc3796f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b940f32a568e6b07b97b1b08572c632ecfb1660f ee7faa27ca5395f2ce9ed5129c4b1514bfb8c7d3 && git checkout -B drift-repro b940f32a568e6b07b97b1b08572c632ecfb1660f && git merge --no-ff ee7faa27ca5395f2ce9ed5129c4b1514bfb8c7d3
node scripts/docs-audit/affected-docs.mjs --json b940f32a568e6b07b97b1b08572c632ecfb1660f
|
…ckage-internal function SerializeOptions is back to its published shape: metadataType on it would widen @objectstack/metadata's public surface for a caller that lives in the package. FilesystemLoader.save() now calls the module-internal serializeTypeScriptForMetadataType for the built-in typescript serializer; the public TypeScriptSerializer.serialize() writes no annotation. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
…notation path Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
…-kind-annotation Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed head = branch tip = PR head; merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL: Generated by Claude Code |
…ialize() would run The instanceof predicate also matched a subclass that overrides serialize(), so FilesystemLoader.save() bypassed the override. The test is now the method itself (TypeScriptSerializer.prototype.serialize), which a subclass override and a class copy from the other entry bundle both fail. Also pins each table row's spec type as identical to z.input of its bound schema inside the in-test tsc, and corrects the changeset and README sentences the contract review flagged. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed head = branch tip = PR head; merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19852
Clause-②: no
What changed
TypeScriptSerializer.serialize()annotated everytypescript-format itemServiceObject, so a saved view (or any other non-object item) was a.tsfile thattscrefused with TS2353. Files thatFilesystemLoader.save()writes with the built-in serializer the package wires in are now annotated with the spec type of the item's own metadata type, or not annotated at all. They never carryany,unknownor another type's shape.How the metadata type reaches the annotation (A1, per the seat's ruling on this PR). It goes through a package-internal channel, and the public surface does not change.
FilesystemLoader.save()callsserializeTypeScriptForMetadataType(item, type, options)only when the serializer'sserializeis this package's own, un-overriddenTypeScriptSerializer.prototype.serializeand its format istypescript.serialize()exactly as before: a custom one, a subclass that overridesserialize(), or aTypeScriptSerializerclass copy from the package's other entry bundle.instanceofalso matched an overriding subclass and bypassed its override, which the contract review measured.serializers/typescript-serializer.ts, and noexportsentry re-exports it (.,./node,./migrations,./errors,./view-container). A test pins that.SerializeOptionsandserializer-interface.tsare byte-identical to the base.TypeScriptSerializer.serialize(item, options)keeps its exact signature and writes no annotation. It cannot know the item's metadata type, and theServiceObjectit used to write was false for every non-object. This output change is the correction, not a new surface.Which type (A2). No existing table maps a metadata type to a type name:
getMetadataTypeSchema()maps a metadata type to a Zod schema value.define*helpers are value factories, so emitting one would turn the annotation into a runtime parse.generatetemplates are per-type code templates, not a lookup.So the table is private:
ANNOTATION_BY_METADATA_TYPE, 28 rows. A row exists only when the spec exports a type identical to thez.inputtype of the schemagetMetadataTypeSchema()binds for that metadata type. That rule is now pinned in the in-testtsc:Book/BookSchemacontrol line that must be the one error.Bookis assignable both ways toz.inputofBookSchema, so mutual assignability alone would not refuse it; identity is stronger.getMetadataTypeSchema()returns.Deliberately unannotated.
view:ViewMetadataSchemais az.preprocess, soViewMetadataisunknown. It type-checks anything, including the card's own repro body, whichViewMetadataSchema.safeParserejects.book: the hand-writtenBooklacks the_packageId/_provenancekeysBookSchemaaccepts. A stamped book failstscagainst it (TS2353) even though it parses.external_catalog, a plugin's own type, and a plural spelling also get no annotation.The
objectfile written by the built-in serializer the package wires in is byte-identical to the base (A4). An exact-string test pins it.deserializeis unchanged. A legacy file (a view annotatedServiceObject) still reads back, pinned.The annotation is a static claim, not a runtime verdict.
tscnow checks each annotated file against its own type instead ofServiceObject. It is stricter than a strip-mode schema at run time:DeclarativeConnectorEntrySchemadrops an undeclared key thattscrefuses as TS2353. Among the 28 representative bodies with one undeclared key added, onlyconnectorparses at run time (the key is dropped).Premise: no declaration reachable from an
exportsentry changesI built
@objectstack/metadataon the base and on the head, with the deps fromturbo run build --filter='@objectstack/metadata^...'and thenpnpm --filter @objectstack/metadata build. I copied every emitted*.d.ts/*.d.ctsunderdist/(the same 10-file set on both sides:index,node,errors,view-containerandmigrations/index, ESM and CJS) and randiff -r:2bbb462335vs headee7faa27ca: 8/10 files byte-identical, and 10/10 identical once comments are stripped. The only difference is one added JSDoc block, 4 lines, onTypeScriptSerializer.serializeinindex.d.tsandindex.d.cts.8b51f96280. The predicate change is implementation only.metadataType?: string;inSerializeOptionsis detected as a difference.Routing, measured on the built bundles
At
ee7faa27ca, withFilesystemLoaderfromdist/node.jssaving anobject, the first line of each file is:TypeScriptSerializerfrom./node:import type { ServiceObject } …TypeScriptSerializerfrom.(a distinct class copy;splitting: false):export const metadata = {(no annotation, never a false one)serialize()to prepend// header:// headerNodeMetadataManager:objectis annotatedServiceObject, andviewis unannotatedA3: annotation and
tscexit per metadata typetsc --noEmit --strict(typescript 6.0.3), with@objectstack/spec/*mapped to the built declarations. Head8b51f96280includesmain's zod 4.6.1.serializeTypeScriptForMetadataType(item, type), whichee7faa27caleaves unchanged.examples/app-showcaseitem of that type, both raw and aftergetMetadataTypeSchema(type).safeParse.viewrow includes it (all_accounts).actionagentanalytics_cubeapiappbookcapabilityconnectordashboarddatasetdatasourcedocemail_templatefieldflowhookjobmappingobjectpagepermissionpositionreportseedsharing_ruleskilltooltranslationviewwebhook2548ba57de: the card's repro throughNodeMetadataManager.save('view', …)makestscexit 2 with TS2353.object/account.tsis clean.Tests
serializers.test.tscovers these cases:serialize()writes no annotation, an object included.viewwith noServiceObjectand noimport type.objectoutput is byte-identical to the base.external_catalog/book/ plugin / plural types.javascriptformat never annotates.NodeMetadataManager.save()end to end.FilesystemLoaderwired by hand with each of these:{ prettify, indent, sortKeys }serialize(), called: its// headersurvivesserialize(): both annotatedTypeScriptSerializerfrom a second module copy, obtained withvi.resetModules(), with the control that it is a distinct class: called through its ownserialize(), so no annotationtypescript-serializer-annotation.test.tscovers these cases:serialize()annotates none of them.exportsentry re-exports the internal function, with a spelling control.===getMetadataTypeSchema(type).tsc: per-row strict type identity (spec type vsz.inputof the bound schema), with exactly one error allowed, TS2322 on theBookcontrol line. Every valid body type-checks clean, and every undeclared-key body fails with exactly[2353].ee7faa27ca:pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2gives 54 files / 814 tests passed, andpnpm --filter @objectstack/metadata typecheckexits 0.ee7faa27ca. Throughscripts/ablation-replace.mjs,serializer.serialize === TypeScriptSerializer.prototype.serialize &&was changed back toserializer instanceof TypeScriptSerializer &&(anchor x1 -> x0, blobb2313f272f2e->9c909b67b590).FilesystemLoader.save() calls a subclass that overrides serialize(), as it always did, receivedimport type { ServiceObject } …instead of// header ….b2313f272f2e, andgit diff HEADis empty.3ac5f81003.serializeTypeScriptForMetadataTypewas forced to an unconditional['ServiceObject', 'data']: 8 failed / 18 passed, including the loader-wired view test and the in-testtsc. Restored to HEAD.Gates
8b51f96280:node scripts/pm/dispatch-gates.mjs --commandsgave 59 commands, all exit 0, and--ranreported 59 run, 0 NOT-MEASURED.ee7faa27catouches the same 6 paths.ee7faa27ca:node scripts/check-issue-citations.mjs, exit 0node scripts/check-changeset-no-major.mjs --base origin/main, exit 0pnpm check:nul-bytes, exit 0Acceptance notes
mainmerged at2bbb462335as a fast-forward merge commit, with no rebase and no force. It brings zod 4.6.1. The type-identity andunknownchecks and the A3 table were re-read after it.Clause-②: nostands, per the seat's ruling. No exported declaration changes (see Premise).ViewMetadataisunknown(z.preprocess).typescriptformat ignores the declaredsortKeyssave option.DeclarativeConnectorEntrySchemastrips an undeclared key.Written by session
session_01TEhopqrWQYBycZzyJHpAZr, the dev for PM seatdomain:engine#1, round 21, patch round 2.