Repository navigation
TypeScriptSerializer.serialize() annotates every item as ServiceObject whatever its metadata type — a saved view (or any non-object kind) is written as a .ts file that fails tsc with TS2353 #19852
Description
Activity
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actions定级
pm:queue·priority:p3·bug·domain:enginePath: platform-core
Triage: lands in
packages/metadata/src/serializers/typescript-serializer.ts⇒domain:engine; rationale: every item saved in the defaulttypescriptformat is annotatedServiceObject, so any non-object kind (a saved view, etc.) is written as a.tsfile that failstsc; runtime round trip unaffected, so p3.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T11:26Z。立卡门 ①,类 (a)。本席读完了卡面(本卡尚无评论)。本席的读数(
origin/mainafc3b64928)typescript-serializer.ts:22-23:无条件写出import type { ServiceObject } …与export const metadata: ServiceObject = …,没有按元数据类型分支。packages/metadata/src/loaders/filesystem-loader.ts:412:默认格式typescript⇒ 公开的MetadataManager.save()默认就走这条路。
与卡面一致(卡面的复现由执行席的独立合约评审在主干上做过两次)。
判定
p3:运行时往返不受影响(deserialize只读 JSON 体,不做类型检查);只有对保存下来的文件做类型检查或导入它的人会被绊到。- 修法方向(⛔ 不是裁定):按类型给正确的注解、不写注解,或按类型用
satisfies。三者都改变一个已发布函数的输出文本 ⇒ 接手人按实际改动判定Clause-②与 changeset 级别(卡面已说明)。 ⚠️ 串行:PR docs(metadata): README says what TypeScriptSerializer emits, not ObjectSchema.create() #19849(TypeScriptSerializer emits an annotated object literal althoughpackages/metadata's own README says it is forObjectSchema.create()#19724)只改 README,与本卡不撞车;⛔ 不并入它。- 读作 Bug;
type字段中继设不了。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingand removed
on Sep 23, 2026 objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsClaim: PM loop round 21
Session:session_01TEhopqrWQYBycZzyJHpAZr
Branch:claude/issue-19852-ts-serializer-per-kind-annotation
Worktree:objectstack-issue-19852
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/metadata/src/serializers/typescript-serializer.ts,packages/metadata/src/serializers/serializers.test.ts(or a sibling test file), theTypeScriptSerializerparagraph ofpackages/metadata/README.md(it states today's annotation), one.changeset/*.md(stop on breach; explain in the report). ⛔ No new export from@objectstack/metadataor@objectstack/spec
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default judgment tier — the annotation per kind is a measured choice)
Clause-②: no
Thread-read: 5793978065
Serial constraints cleared:no open PR and no in-flight claim touches packages/metadata/src/serializers or packages/metadata/README.md (PR #19849, which edited that README paragraph, landed); this seat's in-flight #19837 (objectql engine.ts) and #19844 (driver-turso) do not intersect.Written 2026-09-23T12:27Z. North Star clause 3 read this act: product P0/P1 are open. This card is a product defect (a published function writes a
.tsfile that failstsc), not a tooling or contract-hygiene card, so it is dispatchable at p3.Why
Clause-②: no: the fix changes the emitted text ofserializefor non-object kinds from a false type annotation to a true one, or to none. No accept set widens:deserializereads the same JSON body. No export is added. If the fix turns out to need a new export, the dev stops and reports.
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19852,
"status": "done",
"branch": "claude/issue-19852-ts-serializer-per-kind-annotation",
"pr": "#19865",
"session": "session_01TEhopqrWQYBycZzyJHpAZr — subagent, the PM's session; identity is the branch",
"premise_still_valid": true,
"summary": "A1 route: SerializeOptions (the options bag MetadataSerializer.serialize(item, options?) already takes, exported type of @objectstack/metadata) gains one optional member metadataType?: string; FilesystemLoader.save() passes metadataType: type. serialize signature, interface method list and package export list unchanged; no new export from @objectstack/metadata or @objectstack/spec. A3 table (serializer at 7a9cc80, tsc 6.0.3 --noEmit --strict, @objectstack/spec/* mapped to dist built from base 2548ba5; showcase = every examples/app-showcase item of the type, raw and safeParse-parsed; minimal = one spec-valid body plus its parse): action->Action@ui tsc 0 (140 (showcase)); agent->Agent@ai tsc 0 (2 (minimal)); analytics_cube->Cube@data tsc 0 (2 (showcase)); api->ApiEndpoint@api tsc 0 (4 (showcase)); app->App@ui tsc 0 (2 (showcase)); book->none tsc 0 (2 (showcase)); capability->CapabilityDeclarationInput@security tsc 0 (4 (showcase)); connector->DeclarativeConnectorEntry@integration tsc 0 (8 (showcase)); dashboard->Dashboard@ui tsc 0 (6 (showcase)); dataset->Dataset@ui tsc 0 (8 (showcase)); datasource->Datasource@data tsc 0 (2 (showcase)); doc->Doc@system tsc 0 (2 (minimal)); email_template->EmailTemplateDefinition@system tsc 0 (2 (showcase)); field->Field@data tsc 0 (2 (minimal)); flow->Flow@automation tsc 0 (60 (showcase)); hook->Hook@data tsc 0 (8 (showcase)); job->Job@system tsc 0 (2 (showcase)); mapping->Mapping@data tsc 0 (2 (showcase)); object->ServiceObject@data tsc 0 (48 (showcase)); page->Page@ui tsc 0 (56 (showcase)); permission->PermissionSet@security tsc 0 (18 (showcase)); position->Position@identity tsc 0 (20 (showcase)); report->Report@ui tsc 0 (8 (showcase)); seed->Seed@data tsc 0 (38 (showcase)); sharing_rule->SharingRule@security tsc 0 (4 (showcase)); skill->Skill@ai tsc 0 (2 (minimal)); tool->Tool@ai tsc 0 (2 (minimal)); translation->TranslationItem@system tsc 0 (2 (minimal)); view->none tsc 0 (13 (showcase)); webhook->Webhook@automation tsc 0 (2 (showcase)). Base 2548ba5 control: the card repro via NodeMetadataManager.save('view') gives tsc exit 2, TS2353; object/account.ts clean. A2: no reusable type-name table exists (getMetadataTypeSchema maps to Zod schema values; define* are value factories; CLI generate templates are per-type code, not a lookup), so a private 28-row ANNOTATION_BY_METADATA_TYPE in typescript-serializer.ts. A row exists only where the spec type is IDENTICAL to z.input of getMetadataTypeSchema(type) (strict type-identity check over all 30 schema-bound types: 29 pass). view is excluded because ViewMetadataSchema is a z.preprocess, so ViewMetadata is unknown (it passes identity only because both sides are unknown). book is excluded because the hand-written Book lacks _packageId/_provenance, so a stamped book BookSchema parses fails tsc (TS2353). external_catalog, plugin types, plural spellings and calls without metadataType get no annotation. object output is byte-identical (A4, exact-string test); deserialize is unchanged and a legacy view-annotated-ServiceObject file still reads back.",
"tests": "pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 (via os-verify-lock): 'Test Files 54 passed (54) / Tests 807 passed (807)', VERDICT command-exit 0. pnpm --filter @objectstack/metadata typecheck: VERDICT command-exit 0; tsc --listFilesOnly counts both serializer test files. New/updated in serializers.test.ts: the view carries no ServiceObject and no import type (exact string); object is byte-identical; no annotation for a missing / external_catalog / book / plugin / plural type; javascript unchanged; legacy file reads back; round trips; NodeMetadataManager.save end to end. New typescript-serializer-annotation.test.ts: the 28 representative bodies are spec-valid; the annotated set equals them (probed over MetadataTypeSchema.options plus webhook/connector/sharing_rule/analytics_cube); round trips; in-test tsc via the TypeScript API, about 3 s, resolving @objectstack/spec through the package's node_modules, where every valid body is clean and every body plus one undeclared key gives exactly [2353]. Ablation (once, scripts/ablation-replace.mjs, after commit): the per-type choice was replaced by an unconditional ['ServiceObject','data'] (anchor x1 -> x0, blob a47044eb74f3 -> ffee7d3c2bde). Serializer tests: 'Tests 9 failed | 14 passed (23)', including 'writes a view with no annotation' (expected 'import type { ServiceObject } ...' not to contain 'ServiceObject'), the NodeMetadataManager.save test, and the in-test tsc ('action.ts(6,3): error TS2353 ...' for every non-object type). Restored: blob after restore == HEAD a47044eb74f3, git diff HEAD empty, git status clean. The tests import the serializer from src, so no dist leg applies. Negative controls out of band: an undeclared key is TS2353 for all 28 annotations and passes for ViewMetadata, which is how view was caught.",
"gates": {
"derived_at": "7a9cc8080b (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack; 7 paths vs merge base 2548ba5)",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"reruns": "check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET: no dist). After OS_VERIFY_LOCK turbo run build --filter='./packages/' --filter='./packages//*' --concurrency=2 (72/72 successful), each re-ran to exit 0; the list above records the re-run codes",
"ran_verdict": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list: exit 0, 'dispatch-gates --ran: 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED'",
"live_issue_citations": "node scripts/check-issue-citations.mjs: exit 0, 'no issue citations added against 2548ba5 (3 file(s) read)'"
},
"files_changed": [
".changeset/19852-typescript-serializer-per-type-annotation.md (new, patch @objectstack/metadata)",
"packages/metadata/README.md",
"packages/metadata/src/loaders/filesystem-loader.ts",
"packages/metadata/src/serializers/serializer-interface.ts",
"packages/metadata/src/serializers/serializers.test.ts",
"packages/metadata/src/serializers/typescript-serializer-annotation.test.ts (new)",
"packages/metadata/src/serializers/typescript-serializer.ts"
],
"deviations": [
"File surface extended by two files on the A1 call path: serializer-interface.ts (the optional member) and loaders/filesystem-loader.ts (the only in-repo caller of serialize). The sibling test file is within the claimed surface.",
"The existing test 'should serialize to TypeScript module' dropped its import-type expectation: it pinned the false ServiceObject annotation on an item with no type.",
"Behaviour change for a direct caller: serialize(item) with no metadataType now writes no annotation (it wrote ServiceObject, including for an object). The changeset and README say so.",
"Did not merge origin/main before opening the PR (AGENTS.md Multi-agent section 10, first bullet). The branch is 4 commits behind origin/main 48c91e9; none touches packages/metadata, and the only spec file this reasoning reads (ui/view.zod.ts) changed in comments only. CI tests the merge ref.",
"The in-test tsc reads @objectstack/spec's built dist d.ts through the package's node_modules. turbo test builds it (^build); a bare vitest run needs a spec build first.",
"Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude) rather than the harness reminder's model-named Co-Authored-By; the pre-push hook refuses a model id there.",
"No label writes, per the dispatch."
],
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 — both via scripts/pm/with-fleet.sh --via dispatch, each one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay): (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft), giving PR 19865; (2) comment, i.e. POST /repos//issues/19852/comments (this report). git push is not counted. Reads were REST GET only: the issue, its comments, and the pulls list",
"open_questions": [
{
"question": "Clause-②: SerializeOptions, an exported type of @objectstack/metadata, gains the optional member metadataType. The claim's 'Clause-②: no' is copied into the PR body as dispatched. Does an optional member on an exported options interface count as expanding the public surface?",
"options": [
"A keep 'Clause-②: no' and the patch changeset: execution-duties says Clause-② covers only the published contract surface, packages/spec is the one contract, spec is untouched, and no export is added",
"B re-declare 'Clause-②: yes' with a minor changeset: the literal reading of expanding the public surface, where one more optional member is a wider accept set on an exported type. Cost: @objectstack/metadata sits in the changeset fixed group, so a minor moves the whole group"
],
"recommendation": "A, because the Clause-② text scopes it to the published contract surface and the member is plumbing between FilesystemLoader and TypeScriptSerializer in one package. If the seat reads the public surface as every exported TS type, B is the literal answer, and changing it needs a PR body edit by the seat plus a changeset level change"
}
],
"out_of_scope_findings": [
"class: b · packages/spec/src/ui/view.zod.ts declares 'export type ViewMetadata = z.input of typeof ViewMetadataSchema' with the TSDoc 'Any persisted view metadata body: container | ViewItem record | flattened overlay', but ViewMetadataSchema is a z.preprocess, so ViewMetadata is unknown. Measured at 2548ba5: an 'unknown extends ViewMetadata' check holds; a body with an undeclared key type-checks; and the card's repro body, which ViewMetadataSchema.safeParse rejects ('Invalid input'), type-checks against it. check:exported-any catches ZodType-any, not a preprocess input that is unknown. Seam: spec:ViewMetadata → consumer: none (git grep finds no annotation use). Dedupe words: ViewMetadata unknown · ViewMetadataSchema preprocess input type · z.input preprocess unknown exported type · ViewMetadata accepts any body",
"class: b · the declared save option 'sortKeys: z.boolean().optional()' (packages/spec/src/system/metadata-persistence.zod.ts, MetadataSaveOptionsSchema) is passed by FilesystemLoader.save and honoured by the JSON and YAML serializers, but the default typescript format drops it silently. Probe: serialize an object with sortKeys: true; the json keys come out [fields,label,name] and the typescript keys stay [name,label,fields]. The same holds at base 2548ba5 (typescript-serializer.ts has zero sortKeys references). Seam: spec:MetadataSaveOptionsSchema.sortKeys → runtime:FilesystemLoader.save → TypeScriptSerializer.serialize. Dedupe words: sortKeys typescript serializer ignored · TypeScriptSerializer sortKeys · MetadataSaveOptions sortKeys typescript format",
"carrier: 承接者:无 · Book (packages/spec/src/system/book.zod.ts) is hand-written and narrower than z.input of BookSchema (it lacks the loader-stamped _packageId/_provenance keys). That is why book gets no annotation here. Noted in the PR body, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19852,
"status": "done",
"branch": "claude/issue-19852-ts-serializer-per-kind-annotation",
"pr": "#19865",
"new_head": "8b51f96280 (fast-forward from 7a9cc80: 96e53e9 rewiring, 3ac5f81 README/changeset/test pin, 8b51f96 merge of origin/main 2bbb462; no force, no rebase)",
"session": "session_01TEhopqrWQYBycZzyJHpAZr — subagent, the PM's session; identity is the branch",
"premise_still_valid": true,
"summary": "Patch round per the seat's ruling. A1 route, now package-internal: SerializeOptions / serializer-interface.ts are restored byte-identical to base. FilesystemLoader.save() calls the module-level serializeTypeScriptForMetadataType(item, type, options) in serializers/typescript-serializer.ts when the serializer is the built-in TypeScriptSerializer with getFormat() === 'typescript'. No exports entry (., ./node, ./migrations, ./errors, ./view-container) re-exports it, and a test pins that. Any other serializer, a custom typescript one included, is called as before. The public TypeScriptSerializer.serialize(item, options) keeps its exact signature and writes no annotation; the changeset (patch) says so. Premise held (dts diff reading below). A3 table at 8b51f96 (zod 4.6.1 from main; tsc 6.0.3 --noEmit --strict; spec dist built at head; bodies through the loader path serializeTypeScriptForMetadataType): action->Action@ui tsc 0 (140 (showcase)); agent->Agent@ai tsc 0 (2 (minimal)); analytics_cube->Cube@data tsc 0 (2 (showcase)); api->ApiEndpoint@api tsc 0 (4 (showcase)); app->App@ui tsc 0 (2 (showcase)); book->none tsc 0 (2 (showcase)); capability->CapabilityDeclarationInput@security tsc 0 (4 (showcase)); connector->DeclarativeConnectorEntry@integration tsc 0 (8 (showcase)); dashboard->Dashboard@ui tsc 0 (6 (showcase)); dataset->Dataset@ui tsc 0 (8 (showcase)); datasource->Datasource@data tsc 0 (2 (showcase)); doc->Doc@system tsc 0 (2 (minimal)); email_template->EmailTemplateDefinition@system tsc 0 (2 (showcase)); field->Field@data tsc 0 (2 (minimal)); flow->Flow@automation tsc 0 (60 (showcase)); hook->Hook@data tsc 0 (8 (showcase)); job->Job@system tsc 0 (2 (showcase)); mapping->Mapping@data tsc 0 (2 (showcase)); object->ServiceObject@data tsc 0 (48 (showcase)); page->Page@ui tsc 0 (56 (showcase)); permission->PermissionSet@security tsc 0 (18 (showcase)); position->Position@identity tsc 0 (20 (showcase)); report->Report@ui tsc 0 (8 (showcase)); seed->Seed@data tsc 0 (38 (showcase)); sharing_rule->SharingRule@security tsc 0 (4 (showcase)); skill->Skill@ai tsc 0 (2 (minimal)); tool->Tool@ai tsc 0 (2 (minimal)); translation->TranslationItem@system tsc 0 (2 (minimal)); view->none tsc 0 (13 (showcase)); webhook->Webhook@automation tsc 0 (2 (showcase)). The table is identical to round 1. The type-identity check (29/30 pass, only book fails) and the unknown check (only ViewMetadata is unknown) were re-read after the zod bump with the same result. Kept: 28-row table, view and book unannotated, object byte-identical for a loader save, legacy files deserializing, in-test tsc, and the README paragraph reworded for the internal channel.",
"dts_diff": "Premise HOLDS. Command: in each tree, pnpm exec turbo run build --filter='@objectstack/metadata^...' --concurrency=2 && pnpm --filter @objectstack/metadata build (os-verify-lock, VERDICT command-exit 0 both). Then I copied every dist .d.ts / .d.cts (same 10-file set: index, node, errors, view-container, migrations/index, ESM plus CJS) and ran diff -r. Merge base 2bbb462 vs head 8b51f96: diff exit 1. The only hunks are 'index.d.ts 2397a2398,2401' and 'index.d.cts 2397a2398,2401', each adding one 4-line JSDoc on TypeScriptSerializer.serialize ('Writes no type annotation: this call does not know the item's metadata type, and a guessed annotation can be false.'). 8/10 files are byte-identical, and 10/10 are identical with comments stripped. Control: the comment-stripping comparison flags an injected 'metadataType?: string;' in SerializeOptions. Original base 2548ba5 dts equals 2bbb462 dts byte for byte, and head 3ac5f81 gave the same one-comment reading against 2548ba5. No exported declaration changes.",
"ablation": "Once, on the new wiring, at 3ac5f81, via scripts/ablation-replace.mjs under os-verify-lock. Anchor ' return renderModule(item, options, ANNOTATION_BY_METADATA_TYPE.get(metadataType));' became " return renderModule(item, options, ['ServiceObject', 'data']);" (anchor x1 -> x0, replacement x0 -> x1, blob 98003b1c6f2c -> a0be3800f726). Serializer tests: 'Tests 8 failed | 18 passed (26)'. The failures include 'FilesystemLoader.save() annotates by metadata type' (expected 'import type { ServiceObject } ...' not to contain 'ServiceObject'), 'writes a view with no annotation', the 4 'writes no annotation for ...' cases, the annotated-set test, and the in-test tsc ('action.ts(6,3): error TS2353 ...'). Restore: blob after restore == HEAD 98003b1c6f2c, git diff HEAD empty, git status clean. The tests import src, so no dist leg applies.",
"tests": "At 8b51f96: pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 (os-verify-lock) gave 'Test Files 54 passed (54) / Tests 810 passed (810)', VERDICT command-exit 0. pnpm --filter @objectstack/metadata typecheck gave VERDICT command-exit 0. Serializer files at 3ac5f81: 26/26 passed. New in this round: (1) the public serialize() writes no annotation, object included (exact string); (2) the loader path is byte-identical to base for object and unannotated for view; (3) a custom serializer registered for typescript is still called with { prettify, indent, sortKeys }; (4) no exports entry (read from package.json, 5 entries, imported from src) exports serializeTypeScriptForMetadataType, with a spelling control that the module itself does; (5) the public serialize() annotates none of the 28 bodies.",
"gates": {
"derived_at": "8b51f96280 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack; 6 paths vs merge base 2bbb462; no STALE TREE warning after the merge — the pre-merge derivation at 3ac5f81 warned STALE on lint.yml / select-gate-families, which is why main was merged)",
"commands": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"prerequisite": "turbo run build --filter='./packages/' --filter='./packages//*' --concurrency=2 at 8b51f96 (72/72, 0 cached) before the dist-reading gates; no gate exited 3",
"ran_verdict": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list: exit 0, 'dispatch-gates --ran: 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded an exit code and none of them is 3)'",
"live_issue_citations": "node scripts/check-issue-citations.mjs: exit 0, 'no issue citations added against 2bbb462 (2 file(s) read)'",
"changeset_no_major": "node scripts/check-changeset-no-major.mjs --base origin/main: exit 0, 'This diff introduces no major bump' (LEVEL AXIS not applicable locally: no pull_request payload)"
},
"files_changed": [
".changeset/19852-typescript-serializer-per-type-annotation.md (patch @objectstack/metadata; reworded: public serialize() writes no annotation, public API unchanged)",
"packages/metadata/README.md (paragraph reworded for the internal channel)",
"packages/metadata/src/loaders/filesystem-loader.ts (calls the internal function for the built-in typescript serializer)",
"packages/metadata/src/serializers/serializers.test.ts",
"packages/metadata/src/serializers/typescript-serializer-annotation.test.ts (new)",
"packages/metadata/src/serializers/typescript-serializer.ts (table, renderModule, internal serializeTypeScriptForMetadataType; public serialize unannotated)",
"packages/metadata/src/serializers/serializer-interface.ts: no longer in the diff (restored to base)"
],
"deviations": [
"Merged origin/main 2bbb462 into the branch (merge commit 8b51f96, pushed fast-forward). The derivation at 3ac5f81 warned STALE TREE because upstream changed lint.yml and select-gate-families, which feed the gate list, and main carried a workspace zod bump. This also closes round 1's 'did not merge main' deviation.",
"The internal channel is chosen by instanceof TypeScriptSerializer. tsup builds with splitting: false, so a caller who mixes a TypeScriptSerializer from '.' with a FilesystemLoader from './node' falls back to the public serialize(), and so to no annotation. That loses an annotation but never writes a false one. NodeMetadataManager builds both from one bundle. Stated in the PR's Acceptance notes.",
"Platform reading differs from pm-dispatch references/platform-readings.md: the relay's issue_patch (PATCH /repos//issues/19865) with a footer-less body stored the body byte-identical, 11174 bytes sent and 11174 stored, first difference offset none, footer count 0. The doc records +58 bytes of appended bare footer for PATCH on a PR. Attribution is in body prose ('Written by session ...').",
"Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude), not the harness reminder's model-named form.",
"No label writes."
],
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "This round, 2 relay writes via scripts/pm/with-fleet.sh --via dispatch (each one POST /repos/objectstack-ai/objectstack/dispatches): (1) issue_patch, i.e. PATCH /repos//issues/19865 (PR body; read back byte-identical, draft still true); (2) comment, i.e. POST /repos//issues/19852/comments (this report). Cumulative 4 with round 1's pr_create and comment. git push (3 fast-forward pushes this round) is not counted. Reads were REST GET only (pulls/19865)",
"open_questions": [],
"out_of_scope_findings": [
"Handled by the seat per the ruling (not re-filed or fixed here): class b ViewMetadata is unknown (z.preprocess input); class b sortKeys ignored by the typescript format. Evidence and dedupe words are unchanged from round 1's report comment 5795522449."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19852,
"status": "done",
"branch": "claude/issue-19852-ts-serializer-per-kind-annotation",
"pr": "#19865",
"new_head": "ee7faa27ca (one fast-forward commit on 8b51f96; no force, no rebase, no new merge)",
"session": "session_01TEhopqrWQYBycZzyJHpAZr — subagent, the PM's session; identity is the branch",
"premise_still_valid": true,
"summary": "Patch round 2. (1) MUST fix: the FilesystemLoader.save() routing predicate is now 'serializer.serialize === TypeScriptSerializer.prototype.serialize && serializer.getFormat() === \'typescript\'', replacing instanceof. Measured on the built dist bundles at ee7faa2, with FilesystemLoader from dist/node.js saving an object: the built-in TypeScriptSerializer from ./node writes 'import type { ServiceObject } ...' (annotates); a TypeScriptSerializer from '.' (a distinct class copy, confirmed idx.TypeScriptSerializer !== node.TypeScriptSerializer) writes 'export const metadata = {' (falls back to its serialize()); a subclass overriding serialize() with a '// header' prefix writes '// header' (override called); a plain-object serializer writes its own output; NodeMetadataManager annotates object and leaves view unannotated. New tests beside the custom-serializer case: subclass override called; built-in and non-overriding subclass annotated; a second module copy via vi.resetModules() (with a distinct-class control) falls back. (2)/(3) The changeset and README prose are corrected; the sentences are quoted verbatim in changeset_sentences. The new connector-safe sentence was checked: among the 28 representative bodies plus one undeclared key, only connector parses at run time (DeclarativeConnectorEntrySchema drops the key), while tsc refuses all 28 with TS2353. (4) Done in the existing harness: a per-row strict type-identity assertion in the in-test tsc (spec type vs z.input of the bound schema). The one error allowed is TS2322 on a Book/BookSchema control line; mutual assignability would pass Book, so identity is used. Plus a run-time pin that each row's schema export on the annotation's subpath === getMetadataTypeSchema(type). The .d.ts diff vs merge base 2bbb462 is unchanged: the same single JSDoc hunk in index.d.ts / index.d.cts. The code comment in typescript-serializer.ts that said the annotation states 'nothing narrower' than the contract is corrected to say it is a static input type, not a runtime verdict.",
"ablation": "Once, at ee7faa2, via scripts/ablation-replace.mjs under os-verify-lock. Anchor ' serializer.serialize === TypeScriptSerializer.prototype.serialize &&' became ' serializer instanceof TypeScriptSerializer &&' in packages/metadata/src/loaders/filesystem-loader.ts (anchor x1 -> x0, replacement x0 -> x1, blob b2313f272f2e -> 9c909b67b590). Serializer tests: 'Tests 1 failed | 29 passed (30)'. The one failure is 'FilesystemLoader.save() calls a subclass that overrides serialize(), as it always did': expected '// header ...' but received 'import type { ServiceObject } ...'. Restore: blob after restore == HEAD b2313f272f2e, git diff HEAD empty, git status clean, and the anchor is back x1 / replacement x0. The tests import src, so no dist leg applies.",
"changeset_sentences": [
"BEFORE: '- Anobjectfile is byte-identical:import type { ServiceObject } from '@objectstack/spec/data'andexport const metadata: ServiceObject = ….' AFTER: '- Anobjectfile written by the built-in serializer the package wires in is byte-identical:import type { ServiceObject } from '@objectstack/spec/data'andexport const metadata: ServiceObject = ….'",
"BEFORE: 'Such a file now failstsconly where its content is not a valid item of its own type.' AFTER: 'tscnow checks such a file against its own type instead ofServiceObject.'",
"ADDED bullet: '- A serializer you wire intoFilesystemLoaderby hand is called as it always was: a custom one, or a subclass that overridesserialize(), writes what itsserialize()writes, and aTypeScriptSerializertaken from the package's other entry point (.versus./node) writes no annotation.'",
"README, same paragraph: 'Atypescript-format file thatFilesystemLoader.save()writes' became '... writes with the built-in serializer the package wires in', and one sentence was appended: 'A serializer wired in by hand (a custom one, a subclass that overridesserialize(), or aTypeScriptSerializerfrom the package's other entry point) is called through its ownserialize().'"
],
"tests": "At ee7faa2: pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 (os-verify-lock) gave 'Test Files 54 passed (54) / Tests 814 passed (814)', VERDICT command-exit 0. pnpm --filter @objectstack/metadata typecheck gave VERDICT command-exit 0. Serializer files alone: 30/30 passed. A first run of the new tests failed 1 on my own test bug: a javascript-format case read account.ts, but that format writes .js. I removed the line; javascript output stays covered by the existing test.",
"gates": {
"rerun_at_final_head": [
"pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 :: exit 0",
"pnpm --filter @objectstack/metadata typecheck :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0 ('no issue citations added against 2bbb462 (2 file(s) read)')",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ('This diff introduces no major bump')",
"pnpm check:nul-bytes :: exit 0"
],
"not_rerun": "dispatch-gates --commands/--ran was not re-run this round (not requested). The last reading is 8b51f96: 59/59 exit 0, '59 run, 0 NOT-MEASURED'. ee7faa2 changes the same 6 paths."
},
"files_changed": [
"packages/metadata/src/loaders/filesystem-loader.ts (predicate: method identity instead of instanceof; comment)",
"packages/metadata/src/serializers/serializers.test.ts (subclass-override, inherited-serialize and second-module-copy loader cases)",
"packages/metadata/src/serializers/typescript-serializer-annotation.test.ts (BOUND_SCHEMA run-time binding pin; per-row type-identity lines plus Book control in the in-test tsc)",
"packages/metadata/src/serializers/typescript-serializer.ts (comments only)",
".changeset/19852-typescript-serializer-per-type-annotation.md (sentences above; still patch)",
"packages/metadata/README.md (qualifier and one sentence above)"
],
"deviations": [
"The subclass rule is by method identity, so a subclass that does NOT override serialize() still takes the annotated path. That is the built-in serialize() in play, and it is pinned as annotated.",
"In the code comment, I also corrected the table docblock's 'nothing narrower' claim, which is false at run time for strip-mode schemas such as connector. The coordinator had flagged only the changeset sentence.",
"Commit trailers use the AGENTS.md model-free pair.",
"No label writes."
],
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "This round, 2 relay writes via scripts/pm/with-fleet.sh --via dispatch (each one POST /repos/objectstack-ai/objectstack/dispatches): (1) issue_patch, i.e. PATCH /repos//issues/19865, the PR body correcting the loader claim, read back byte-identical (12458/12458 bytes, 0 footers appended, draft true, head ee7faa2); (2) comment, i.e. POST /repos//issues/19852/comments (this report). Cumulative 6 across three rounds. git push (1 fast-forward this round) is not counted. Reads were REST GET only (pulls/19865)",
"open_questions": [],
"out_of_scope_findings": [
"Seat files these, per the coordinator; not fixed here: ViewMetadata is unknown (z.preprocess); sortKeys is ignored by the typescript format; DeclarativeConnectorEntrySchema strips an undeclared key (re-measured this round: of the 28 representative bodies plus 'undeclared_key: 1', only connector safeParses, with the key dropped)."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsACCEPT — round 21 · PR #19865 ·
Fixesdomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T14:38Z. Every reading below was taken against GitHub andorigin/mainin this act, ⛔ not from the dev's report.Correction of this seat's dispatch order (published here, on the card it concerned)
This seat's claim (5794798195) and dispatch order allowed the metadata type to reach the serializer through "an optional options field on the existing call shape". That was wrong.
SerializeOptionsis exported from@objectstack/metadata's.entry, so an added key widens a published accept set. That is clause ② (lanes/spec.md: 「放宽接受集或扩大公开面的卡,不论多小,即条款②」) and at leastminorunder the maintainer's WHICH LEVEL ruling on #15294. The dev took that route in round 1 and raised the question itself. The seat's round-2 ruling: keep the behaviour and route the type through a package-internal function, on the falsifiable premise that no declaration reachable from anexportsentry changes. The premise held.Review
check reading head ee7faa27caPR shape draft → main; first body lineFixes #19852; no other closing keyword;Clause-②: noat column 0, agreeing with thepatchchangesetscope 6 files: typescript-serializer.ts(a private 28-row table, a package-internalserializeTypeScriptForMetadataType, the publicserialize()unannotated),filesystem-loader.ts(routes to it only when the built-in, un-overriddenserializewould run), two test files, the README paragraph, one changesetpublished surface the emitted .d.ts/.d.ctsof all 5exportsentries differ from the merge-base only by one JSDoc comment (dev measurement, re-confirmed by the contract reviewer on independent builds)annotations 28 kinds annotated with a spec type identical to their schema's z.input(pinned in the in-testtsc);view(ViewMetadataisunknown) andbook(hand-writtenBookis narrower) are unannotatedgoverned NOT governed ( check-governed-merges --pr 19865)contract review round 2 FAIL on a code defect (a subclass override was bypassed; 5796391892), then PASS on this head (5796844434) CI every check successor a roster skip on this head before readyOut-of-scope findings — dispositions:
ViewMetadataisunknown(thez.inputof az.preprocess) → filed spec: exported type ViewMetadata is unknown (the z.input of a z.preprocess schema), so it type-checks any body while its TSDoc promises a persisted view body #19871.sortKeysis ignored by the defaulttypescriptformat → filed metadata: the declared save option sortKeys is honoured by the json and yaml formats but silently ignored by the default typescript format #19872 (serial behind this PR).DeclarativeConnectorEntrySchemastrips an undeclared key → Acceptance notes: a classified ledger state (docs/audits/2026-07-unknown-key-strictness-ledger.md,integration/:wire, "stay tolerant"); contesting it is a spec-lane ledger re-triage, not a new card.- The hand-written
Bookis narrower thanBookSchema→ Acceptance notes (it is whybookgets no annotation).
Landing: ready → queue on this head once CI is fully green.
Generated by Claude Code
- added 2 commits that reference this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site,
TypeScriptSerializer.serializeinpackages/metadata/src/serializers/typescript-serializer.ts. Finding class (a), reproduced.Filed by the
domain:engineexecution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr). It comes from the out-of-scope findings of its #19724 dev (PR #19849) and was independently reproduced by the seat's isolated contract reviewer onorigin/main(the contract-review record on PR #19849). #19724's card body raised this possibility and deliberately left it out of that card's scope: 「whether that is in scope here or its own card is triage's call — ⛔ this card does not widen itself to claim it」. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.What happens
serializeis generic and has no metadata-type switch. For thetypescriptformat it always writes:ServiceObjectis the input type of the object schema, a strict object with no top-leveltypeorcolumnskey and no passthrough. So any non-object item gets an annotation it cannot satisfy.Reproduced at base
4112752ec3against the builtdist:NodeMetadataManager({ rootDir }).save('view', 'all_accounts', { name, type: 'grid', object: 'account', columns: ['name'] })writes{rootDir}/view/all_accounts.tswith the annotation above. Thentsc --noEmit --strictover that file, with@objectstack/spec/datamapped to the built declarations, answerserror TS2353: Object literal may only specify known properties, and '"type"' does not exist in type …. The object itemobject/account.ts, written the same way, type-checks clean.Reach
This is reachable today through the public
MetadataManager.save(), whoseFilesystemLoaderdefault format istypescript, and through the exportedTypeScriptSerializer. The runtime round trip is unaffected:deserializereads the JSON body and never type-checks. The defect is the emitted file's type annotation, which lies about every kind that is not an object. It only bites someone who type-checks or imports the saved file.satisfieswith the right type per kind. The taker judgesClause-②and the changeset level from what the fix actually changes. PR #19849 (README only) described today's behaviour truthfully (「whatever the item's metadata type」) without endorsing it.Filing-gate answers
packages/metadataafter triage routes it (the domain table:domain:engine).closedincluded:TypeScriptSerializer ServiceObject annotation every metadata type view TS2353→ 5 hits. TypeScriptSerializer emits an annotated object literal althoughpackages/metadata's own README says it is forObjectSchema.create()#19724 (open until PR docs(metadata): README says what TypeScriptSerializer emits, not ObjectSchema.create() #19849 lands) is the parent reading, and it scopes this out by name. Two official scaffolders and two published docs disagree on how a .object.ts may be written — ObjectSchema.create() factory vs plain annotated literal #17418 (open) is about.object.tsauthoring shapes, a different surface. os datasource introspect emits the annotated-literal.object.tsshape ruling 5644350230 refuses — and the generator's own docblock calls its output a*.object.ts#19723, Three comments still point atmetadata/src/utils/schema-sync-errors.tsafter #13279 moved it to@objectstack/types#13516 and [finding] The metadata-layer arm of the visibility rules explains itself with*.form.ts, a file a Studio / MCP author of a schema-bound view does not have #8042 (closed) are unrelated. None is this defect.Dedupe words:
TypeScriptSerializer ServiceObject annotation·serialize view ServiceObject TS2353·FilesystemLoader save typescript wrong type·metadata serializer type annotation every kindGenerated by Claude Code