Skip to content

fix(metadata): TypeScriptSerializer annotates each item with its own metadata type's spec type, never ServiceObject for a non-object - #19865

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-19852-ts-serializer-per-kind-annotation
Sep 23, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-19852-ts-serializer-per-kind-annotation

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #19852
Clause-②: no

What changed

TypeScriptSerializer.serialize() annotated every typescript-format item ServiceObject, so a saved view (or any other non-object item) was a .ts file that tsc refused with TS2353. Files that FilesystemLoader.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 carry any, unknown or 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() calls serializeTypeScriptForMetadataType(item, type, options) only when the serializer's serialize is this package's own, un-overridden TypeScriptSerializer.prototype.serialize and its format is typescript.
    • Every other serializer is called through its own serialize() exactly as before: a custom one, a subclass that overrides serialize(), or a TypeScriptSerializer class copy from the package's other entry bundle.
    • The predicate tests the method, not the class. The earlier instanceof also matched an overriding subclass and bypassed its override, which the contract review measured.
    • The function is module-level in serializers/typescript-serializer.ts, and no exports entry re-exports it (., ./node, ./migrations, ./errors, ./view-container). A test pins that.
    • SerializeOptions and serializer-interface.ts are byte-identical to the base.
    • The public TypeScriptSerializer.serialize(item, options) keeps its exact signature and writes no annotation. It cannot know the item's metadata type, and the ServiceObject it 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.
    • The define* helpers are value factories, so emitting one would turn the annotation into a runtime parse.
    • The CLI's generate templates 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 the z.input type of the schema getMetadataTypeSchema() binds for that metadata type. That rule is now pinned in the in-test tsc:

    • One strict type-identity assertion per row, with a Book / BookSchema control line that must be the one error. Book is assignable both ways to z.input of BookSchema, so mutual assignability alone would not refuse it; identity is stronger.
    • A run-time pin that each row's schema export on the annotation's subpath is the object getMetadataTypeSchema() returns.
  • Deliberately unannotated.

    • view: ViewMetadataSchema is a z.preprocess, so ViewMetadata is unknown. It type-checks anything, including the card's own repro body, which ViewMetadataSchema.safeParse rejects.
    • book: the hand-written Book lacks the _packageId / _provenance keys BookSchema accepts. A stamped book fails tsc against it (TS2353) even though it parses.
    • external_catalog, a plugin's own type, and a plural spelling also get no annotation.
  • The object file written by the built-in serializer the package wires in is byte-identical to the base (A4). An exact-string test pins it.

  • deserialize is unchanged. A legacy file (a view annotated ServiceObject) still reads back, pinned.

  • The annotation is a static claim, not a runtime verdict. tsc now checks each annotated file against its own type instead of ServiceObject. It is stricter than a strip-mode schema at run time: DeclarativeConnectorEntrySchema drops an undeclared key that tsc refuses as TS2353. Among the 28 representative bodies with one undeclared key added, only connector parses at run time (the key is dropped).

Premise: no declaration reachable from an exports entry changes

I built @objectstack/metadata on the base and on the head, with the deps from turbo run build --filter='@objectstack/metadata^...' and then pnpm --filter @objectstack/metadata build. I copied every emitted *.d.ts / *.d.cts under dist/ (the same 10-file set on both sides: index, node, errors, view-container and migrations/index, ESM and CJS) and ran diff -r:

  • merge base 2bbb462335 vs head ee7faa27ca: 8/10 files byte-identical, and 10/10 identical once comments are stripped. The only difference is one added JSDoc block, 4 lines, on TypeScriptSerializer.serialize in index.d.ts and index.d.cts.
  • These are the same hunks as at 8b51f96280. The predicate change is implementation only.
  • A control holds: an injected metadataType?: string; in SerializeOptions is detected as a difference.

Routing, measured on the built bundles

At ee7faa27ca, with FilesystemLoader from dist/node.js saving an object, the first line of each file is:

  • built-in TypeScriptSerializer from ./node: import type { ServiceObject } …
  • a TypeScriptSerializer from . (a distinct class copy; splitting: false): export const metadata = { (no annotation, never a false one)
  • a subclass overriding serialize() to prepend // header: // header
  • a plain-object serializer: its own output
  • NodeMetadataManager: object is annotated ServiceObject, and view is unannotated

A3: annotation and tsc exit per metadata type

  • Setup: tsc --noEmit --strict (typescript 6.0.3), with @objectstack/spec/* mapped to the built declarations. Head 8b51f96280 includes main's zod 4.6.1.
  • Bodies: each body went through the loader's path, serializeTypeScriptForMetadataType(item, type), which ee7faa27ca leaves unchanged.
    • "showcase" means every examples/app-showcase item of that type, both raw and after getMetadataTypeSchema(type).safeParse.
    • "minimal" means one spec-valid body plus its parsed form.
  • Card repro: the view row includes it (all_accounts).
metadata type annotation files tsc exit
action Action (@objectstack/spec/ui) 140 (showcase) 0
agent Agent (@objectstack/spec/ai) 2 (minimal) 0
analytics_cube Cube (@objectstack/spec/data) 2 (showcase) 0
api ApiEndpoint (@objectstack/spec/api) 4 (showcase) 0
app App (@objectstack/spec/ui) 2 (showcase) 0
book none 2 (showcase) 0
capability CapabilityDeclarationInput (@objectstack/spec/security) 4 (showcase) 0
connector DeclarativeConnectorEntry (@objectstack/spec/integration) 8 (showcase) 0
dashboard Dashboard (@objectstack/spec/ui) 6 (showcase) 0
dataset Dataset (@objectstack/spec/ui) 8 (showcase) 0
datasource Datasource (@objectstack/spec/data) 2 (showcase) 0
doc Doc (@objectstack/spec/system) 2 (minimal) 0
email_template EmailTemplateDefinition (@objectstack/spec/system) 2 (showcase) 0
field Field (@objectstack/spec/data) 2 (minimal) 0
flow Flow (@objectstack/spec/automation) 60 (showcase) 0
hook Hook (@objectstack/spec/data) 8 (showcase) 0
job Job (@objectstack/spec/system) 2 (showcase) 0
mapping Mapping (@objectstack/spec/data) 2 (showcase) 0
object ServiceObject (@objectstack/spec/data) 48 (showcase) 0
page Page (@objectstack/spec/ui) 56 (showcase) 0
permission PermissionSet (@objectstack/spec/security) 18 (showcase) 0
position Position (@objectstack/spec/identity) 20 (showcase) 0
report Report (@objectstack/spec/ui) 8 (showcase) 0
seed Seed (@objectstack/spec/data) 38 (showcase) 0
sharing_rule SharingRule (@objectstack/spec/security) 4 (showcase) 0
skill Skill (@objectstack/spec/ai) 2 (minimal) 0
tool Tool (@objectstack/spec/ai) 2 (minimal) 0
translation TranslationItem (@objectstack/spec/system) 2 (minimal) 0
view none 13 (showcase) 0
webhook Webhook (@objectstack/spec/automation) 2 (showcase) 0
  • Before the fix, at base 2548ba57de: the card's repro through NodeMetadataManager.save('view', …) makes tsc exit 2 with TS2353. object/account.ts is clean.
  • Negative control, every annotation (in-test): the same valid body plus one undeclared key fails with exactly TS2353 for all 28 annotated types.

Tests

  • serializers.test.ts covers these cases:
    • The public serialize() writes no annotation, an object included.
    • The loader path writes a view with no ServiceObject and no import type.
    • The object output is byte-identical to the base.
    • No annotation for external_catalog / book / plugin / plural types.
    • The javascript format never annotates.
    • The legacy file still reads back.
    • Round trips.
    • NodeMetadataManager.save() end to end.
    • A FilesystemLoader wired by hand with each of these:
      • a plain-object custom serializer, called with { prettify, indent, sortKeys }
      • a subclass overriding serialize(), called: its // header survives
      • the built-in serializer, and a subclass that keeps the inherited serialize(): both annotated
      • a TypeScriptSerializer from a second module copy, obtained with vi.resetModules(), with the control that it is a distinct class: called through its own serialize(), so no annotation
  • typescript-serializer-annotation.test.ts covers these cases:
    • Each of the 28 representative bodies is spec-valid.
    • The annotated set is exactly those 28.
    • The public serialize() annotates none of them.
    • No exports entry re-exports the internal function, with a spelling control.
    • Run-time binding pin: each row's schema export on the annotation's subpath === getMetadataTypeSchema(type).
    • Round trips.
    • In-test tsc: per-row strict type identity (spec type vs z.input of the bound schema), with exactly one error allowed, TS2322 on the Book control line. Every valid body type-checks clean, and every undeclared-key body fails with exactly [2353].
  • Package suite, at ee7faa27ca: pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 gives 54 files / 814 tests passed, and pnpm --filter @objectstack/metadata typecheck exits 0.
  • Ablation, routing predicate, at ee7faa27ca. Through scripts/ablation-replace.mjs, serializer.serialize === TypeScriptSerializer.prototype.serialize && was changed back to serializer instanceof TypeScriptSerializer && (anchor x1 -> x0, blob b2313f272f2e -> 9c909b67b590).
    • Result: 1 failed / 29 passed. The failure is FilesystemLoader.save() calls a subclass that overrides serialize(), as it always did, received import type { ServiceObject } … instead of // header ….
    • Restored: blob == HEAD b2313f272f2e, and git diff HEAD is empty.
  • Earlier ablation, annotation choice, at 3ac5f81003. serializeTypeScriptForMetadataType was forced to an unconditional ['ServiceObject', 'data']: 8 failed / 18 passed, including the loader-wired view test and the in-test tsc. Restored to HEAD.

Gates

  • Derived at 8b51f96280: node scripts/pm/dispatch-gates.mjs --commands gave 59 commands, all exit 0, and --ran reported 59 run, 0 NOT-MEASURED. ee7faa27ca touches the same 6 paths.
  • Re-run at ee7faa27ca:
    • metadata tests, exit 0
    • metadata typecheck, exit 0
    • node scripts/check-issue-citations.mjs, exit 0
    • node scripts/check-changeset-no-major.mjs --base origin/main, exit 0
    • pnpm check:nul-bytes, exit 0

Acceptance notes

  • main merged at 2bbb462335 as a fast-forward merge commit, with no rebase and no force. It brings zod 4.6.1. The type-identity and unknown checks and the A3 table were re-read after it.
  • Clause-②: no stands, per the seat's ruling. No exported declaration changes (see Premise).
  • Findings handed to the seat, not fixed here:
    • ViewMetadata is unknown (z.preprocess).
    • The typescript format ignores the declared sortKeys save option.
    • DeclarativeConnectorEntrySchema strips an undeclared key.

Written by session session_01TEhopqrWQYBycZzyJHpAZr, the dev for PM seat domain:engine#1, round 21, patch round 2.

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/email-templates.mdx (via email_template (literal, a string literal in ANNOTATION_BY_METADATA_TYPE))
  • content/docs/concepts/metadata-lifecycle.mdx (via email_template (literal, a string literal in ANNOTATION_BY_METADATA_TYPE))
  • content/docs/deployment/validating-metadata.mdx (via sharing_rule (literal, a string literal in ANNOTATION_BY_METADATA_TYPE))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-2.mdx (via analytics_cube (literal, a string literal in ANNOTATION_BY_METADATA_TYPE))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/metadata/README.md) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b940f32a568e6b07b97b1b08572c632ecfb1660f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e56b9e262be4571c2f18f2e10a89f0ebfcc3796f — the merge of head ee7faa27ca5395f2ce9ed5129c4b1514bfb8c7d3 into base b940f32a568e6b07b97b1b08572c632ecfb1660f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b940f32a568e6b07b97b1b08572c632ecfb1660f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
…-kind-annotation

Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8b51f9628092d47457cc6a532f853663c3c7186d

Reviewed head = branch tip = PR head; merge-base 2bbb462335; the 6 named files. Check runs at the head, latest per name: 33 success, 7 skipped, 0 failure. Measurements were made in a throwaway worktree of the head (offline install, turbo run build --filter='@objectstack/metadata...', typescript 6.0.3), removed afterwards; no gate was re-run.

① Derived judgments

  • (a) "An object file is byte-identical": TRUE for the wiring the package builds (NodeMetadataManager → FilesystemLoader with MetadataManager's own TypeScriptSerializer), pinned by an exact-string test and an end-to-end save. Not true for a hand wiring mixing the . and ./node entries: each bundle carries its own TypeScriptSerializer, so instanceof fails and the annotation is lost (never a false one). The changeset states the claim unqualified.
  • (b) TRUE: exactly 28 rows, object included; every type name exists and is exported from the named @objectstack/spec subpath (all 28 compiled in a tsc probe against the built dist); each row names the type of the schema getMetadataTypeSchema() binds.
  • (c) TRUE at head but unpinned: the tests check that the import resolves, the body is assignable and one excess key is TS2353. The identity criterion was applied out of band; re-measured with a mutual-assignability test: 29 of 30 bindings identical, book the only failure, view identical only because both sides are unknown.
  • (d) FALSE in one corner: DeclarativeConnectorEntrySchema accepts and strips an undeclared key at runtime while tsc refuses it with TS2353, so "fails tsc only where its content is not a valid item of its own type" does not hold for connector.
  • (e) TRUE: ViewMetadata is exactly unknown (z.input of a z.preprocess); the hand-written Book lacks the protection fields BookSchema spreads.
  • (f) TRUE: the public serialize() writes no annotation, pinned.
  • (g) CONFIRMED: the merge-base and head .d.ts / .d.cts of every exports entry differ only by the 4-line JSDoc on TypeScriptSerializer.serialize; SerializeOptions is byte-identical; serializeTypeScriptForMetadataType appears in no declaration file and no ESM or CJS export map.
  • (h), (i) TRUE: deserialize is untouched and a legacy annotated view reads back; the javascript format is unchanged.
  • (j) No in-repo wiring hits the cross-bundle fallback.
  • DEFECT (introduced by this PR): the routing predicate serializer instanceof TypeScriptSerializer && serializer.getFormat() === 'typescript' also matches a subclass of the exported TypeScriptSerializer. A registered subclass that overrides serialize() is silently bypassed. Measured: an override returning a header plus super.serialize(...), saved through FilesystemLoader.save('object', …), wrote the built-in annotated module without the header. At the merge-base the override was called. This contradicts the loader comment, the PR body and the custom-serializer test name.

② Semver level

@objectstack/metadata: patch with Clause-②: no is correct. The emitted declarations differ only by one comment; no symbol is added to any exports entry; SerializeOptions is unchanged; deserialize reads the same body. The direct-caller change (public serialize() writes no annotation) corrects an emitted artefact and removes nothing an author can write.

③ Boundary flags

  • Subclass override bypassed by the loader: MUST FIX BEFORE LANDING. It is a silent behaviour change on a public extension point. The fix is one predicate (route to the internal function only when the built-in, un-overridden serialize is in play) plus a subclass-override test; no declaration moves.
  • ViewMetadata is unknown via z.preprocess: REAL; file separately (spec surface).
  • sortKeys ignored by the typescript format: REAL, pre-existing; file separately.
  • DeclarativeConnectorEntrySchema strips undeclared keys where every other bound schema is strict: file separately (spec strictness).
  • Cross-entry instanceof fallback: acceptable (never a false annotation; no in-repo wiring); optionally qualify "byte-identical".
  • Identity criterion unpinned: acceptable; a cheap mutual-assignability pin is possible.

Implemented-by: claude/issue-19852-ts-serializer-per-kind-annotation
Reviewed-by: session_01TEhopqrWQYBycZzyJHpAZr

VERDICT: FAIL: FilesystemLoader.save() silently bypasses a registered subclass's serialize() override. A patch round is in flight; it also corrects the (d) and (a) sentences in the same push.


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>
@github-actions github-actions Bot added size/l and removed size/m labels Sep 23, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ee7faa27ca5395f2ce9ed5129c4b1514bfb8c7d3

Reviewed head = branch tip = PR head; merge-base 2bbb462335. Delta from the FAILED head 8b51f96280 (record 5796391892): one commit, 6 files (+139/−16). Record 5796391892's judgments (b), (e), (f), (h), (i), (j) and the 28-row table are carried for unchanged bytes; (a), (c), (d), (g) and the defect were re-measured in throwaway worktrees of the head and the merge-base (offline install, a forced tsup rebuild on both sides, typescript 6.0.3, zod 4.6.1), removed afterwards.

① Derived judgments

  • The defect of record 5796391892 is FIXED. The predicate is now serializer.serialize === TypeScriptSerializer.prototype.serialize && serializer.getFormat() === 'typescript'. Measured on the built bundles (loader from dist/node.js, saving object, view, flow):
    • the built-in serializer from the same bundle annotates per kind (ServiceObject / none / Flow), and so does NodeMetadataManager end to end, ESM and CJS;
    • a subclass overriding serialize() has its override called, exactly as at the merge-base;
    • a subclass keeping the inherited serialize() is annotated per kind (the built-in method is what would have run; at the merge-base it wrote a false ServiceObject);
    • every cross-bundle mix (. versus ./node, ESM with CJS) writes no annotation, never a false one;
    • own-property wrappers, .bind(), and class-field arrows are called; a Proxy around the built-in is annotated;
    • a javascript-format instance is unchanged; a plain-object serializer gets { prettify, indent, sortKeys } as before.
  • (g) RE-CONFIRMED: the 10 emitted declaration files of the merge-base and the head differ only by the 4-line JSDoc on TypeScriptSerializer.serialize; comment-stripped, all 10 are byte-identical. SerializeOptions is unchanged. serializeTypeScriptForMetadataType appears in no declaration file and in the runtime export key set of no entry.
  • (b), (c): getMetadataTypeSchema() binds 30 types; the table is those minus view and book. A reviewer tsc probe of the 28 identity rows: 0 errors; Book against z.input of BookSchema is mutually assignable but not identical, so only identity refuses it; a deliberately wrong pair errors, so the in-test assertion discriminates.
  • (a) "An object file written by the built-in serializer the package wires in is byte-identical": TRUE (pinned by exact string); the cross-bundle exception is named in the same changeset.
  • (d) "tsc now checks such a file against its own type instead of ServiceObject": TRUE for all 28, connector included.
  • Every other new or changed sentence (the loader comment, the internal function's docblock, the table docblock, the test-file header) holds against the code; the changeset read end to end is consistent. The new tests assert what their names say.
  • Check runs at the head, latest per name, all complete: 29 success, 5 skipped, 0 failure.

② Semver level

@objectstack/metadata: patch with Clause-②: no, CONFIRMED. No declaration reachable from an exports entry changes except a comment; no runtime export is added or removed; nothing an author can write is removed or renamed. The public serialize() output change corrects an emitted artefact.

③ Boundary flags

Implemented-by: claude/issue-19852-ts-serializer-per-kind-annotation
Reviewed-by: session_01TEhopqrWQYBycZzyJHpAZr

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 23, 2026 14:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit e9eb224 Sep 23, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19852-ts-serializer-per-kind-annotation branch September 23, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant