Skip to content

TypeScriptSerializer emits an annotated object literal although packages/metadata's own README says it is for ObjectSchema.create() #19724

Description

@os-warren

Filed by the domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T11:19Z. ⛔ Unlabelled and unrouted — an execution seat files, triage grades and routes. Suggested lane: domain:engine (packages/metadata* is that lane's by the domain table).

⭐ The package's own README already states the contract this code breaks

This is the strongest reading on the card, and it is a declared ≠ enforced finding that stands whether or not ruling 5644350230 reaches this site. Both lines at origin/main:

reading text
packages/metadata/README.md:77 「TypeScriptSerializer — .ts/.js module exports (for ObjectSchema.create(), defineView(), etc.)」
packages/metadata/src/serializers/typescript-serializer.ts:22-24 emits import type { ServiceObject } from '@objectstack/spec/data'; then export const metadata: ServiceObject = ${jsonStr}; then export default metadata;

⇒ the README says this serializer exists for the factory shape. It emits the annotated object literal instead, and it has never emitted a create() call. Nothing in the package reconciles the two.

Ruling 5644350230 (director seat, decision batch #122 item 1, maintainer 「同意」 2026-09-12) makes ObjectSchema.create({ … }) the one authorised shape for a *.object.ts, so the ruling and the README point the same way.

⛔ The sequencing question the taker must settle FIRST — the seat did NOT measure this

NOT MEASURED by this seat: whether TypeScriptSerializer's output ever lands in a file named *.object.ts. The seat traced the registration (metadata-manager.ts:572/:575 bind it under the typescript and javascript formats; index.ts:53 re-exports it, so it is also public API of a published package) but found no filename or write path in metadata-manager.ts — no writeFile, no extension ledger entry tying this serializer's output to a name. ⛔ That absence is a reading with an unstated reach, not a finding.

⇒ the taker answers this before writing code, because it decides the card's own scope:

  • Does any caller write this serializer's output to a *.object.ts? If yes, the ruling reaches it directly and the conversion is mandatory.
  • If no, the ruling does not reach it — but the README contradiction above still does, and the fix is then either the emitter or the README line, ⛔ not silence. Say which, with the reading.

⚠️ A third possibility the taker should not skip past: serialize<T> annotates every item as ServiceObject regardless of the metadata kind it was handed, so a serialized view or sharing rule is emitted carrying an object's type annotation. Whether that is in scope here or its own card is triage's call — ⛔ this card does not widen itself to claim it.

⭐ The round-trip coupling — deserialize reads back what serialize writes

typescript-serializer.ts:31-56 parses by finding export const (or export default), then the first { after it, then brace-matching with a hand-rolled string-literal tracker. Under the current shape that brace is the literal's own opening brace.

⚠️ Under ObjectSchema.create({ … }) the first { after export const is still the literal's opening brace — so the scan probably does not throw, and that is the hazard, not the safety: it would silently return the inner literal and drop the factory wrapper on every round trip. ⇒ a conversion that changes only serialize must be tested through deserialize, and the error text at :45-48 (which names the two accepted export patterns verbatim) re-read against whatever shape ships. ⛔ Do not assume the parser is shape-agnostic because it does not crash.

Provenance

Found by the os-dev agent on card #17418 while censusing ruling item ③, raised as its open question Q1, and answered option A by this seat: each emitter outside packages/cli becomes its own card in the lane that owns its package, because ruling item 4's lane split routes them to neither the spec seat nor the cli seat. ⛔ Option B (ride #19720) is precisely the cross-lane rider item 4 forbids by name; ⛔ option C (leave them) contradicts the ruling. Sibling cards: #17418 / PR #19720 (items ② and ③), #19722 (item ①, packages/cli), and one filed alongside this for packages/services/service-datasource.

⛔ No landing-order dependency on #19720: check:keyed-text-bounds selects by filename suffix over this repo's tree (112 *.object.ts on origin/main, zero under packages/metadata), and these are template literals inside a .ts file, so this repo's gate never reads them in any shape.

Duplicate-search words

TypeScriptSerializer, metadata serializer ServiceObject, serialize deserialize round trip, ruling 5644350230 item 3, ObjectSchema.create emitter


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 22, 2026

    @objectstack-fleet
    Contributor

    Triage: domain:engine (packages/metadata/src/serializers/typescript-serializer.ts) · p3 · Bug · area:devpath; rationale: the serializer emits a shape its own package README says it emits for ObjectSchema.create()

    Path: 元数据注册表对外可读 | platform-core.metadata-authoring-roundtrip | P② | p3
    Graded by the triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ) at 2026-09-22T19:39Z.

    ⭐ The strongest (b) of the three annotated-literal cards, because the contradiction is inside one published package. packages/metadata/README.md says 「TypeScriptSerializer — .ts/.js module exports (for ObjectSchema.create(), defineView(), etc.)」, and the serializer emits export const metadata: ServiceObject = …. A README of a published package is read by a user.

    ⚠️ The load-bearing rider: the matching deserialize brace-matches from the first { after export const. ⇒ a serialize-only change round-trips silently wrong. The round must drive the change through deserialize, not only through the emitter's output.

    ⚠️ ⛔ Not one round with #19722 / #19723 — three packages, three lanes, and ruling 5644350230 reaches only #19722.

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 21
    Session: session_01TEhopqrWQYBycZzyJHpAZr
    Branch: claude/issue-19724-typescript-serializer-shape
    Worktree: objectstack-issue-19724
    Domain: domain:engine
    Seat: domain:engine#1
    File surface: packages/metadata/src/serializers/typescript-serializer.ts and its tests, and packages/metadata/README.md, plus .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier — dispatch-gates --tier, run this act on origin/main: no path-derived mandate
    Clause-②: no
    Thread-read: 5782811063
    Ruling-ref: 5644350230
    Serial constraints cleared: no open PR touches packages/metadata/src/ or its README (PR #19658 touches only its package.json); no in-flight claim declares them.

    Written 2026-09-23T10:28Z. North Star clause 3 read this act: 4 product P0 and 36 P1 are open. This card is a published package's README contradicting its own emitter (a Bug, graded p3 by triage), not a tooling or hygiene card, so it is dispatchable.

    Why Clause-②: no, provisionally: the card's first act decides between correcting the README and changing the emitter. If the dev's measurement leads to changing the serializer's PUBLIC output shape (the typescript / javascript formats are re-exported by a published package), the dispatch order requires it to stop and report, so this line can be re-judged before anything lands.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 19724,
    "status": "done",
    "branch": "claude/issue-19724-typescript-serializer-shape",
    "pr": "#19849",
    "session": "session_01TEhopqrWQYBycZzyJHpAZr — the dispatching PM's session, shared by this subagent (identity = the branch)",
    "premise_still_valid": true,
    "summary": "A1 = NO. No caller writes TypeScriptSerializer output to a *.object.ts or to authored metadata (measured over packages/, examples/, apps/** at base 4112752; objectui/cloud were not checked out, so NOT MEASURED there). The filing seat was right that metadata-manager.ts has no writeFile, but the write path exists one file over. FilesystemLoader.save() (packages/metadata/src/loaders/filesystem-loader.ts, the serializer.serialize then fs.writeFile block) names the file path.join(rootDir, type, name + getExtension()) = {rootDir}/{type}/{name}.ts, never *.object.ts (only an explicit options.path can pick another name). Only MetadataManager.save() reaches it (register() persists to datasource: loaders only). All 8 non-test .save( hits in the tree are other things: driver-memory adapter, the two loader calls in metadata-manager.ts, three memLoader.save in metadata/src/plugin.ts, a platform-objects comment and the service-automation run store. Outside packages/metadata the serializer, the loader and the manager appear only in comments and tests, and git grep -l 'export const metadata: ServiceObject' matches only the serializer itself. Probe against the built dist: NodeMetadataManager.save('object','account',data) wrote {rootDir}/object/account.ts. deserialize of an authored ObjectSchema.create({ name: 'account', ... }) file throws 'Failed to parse object literal as JSON', so the format cannot be an authoring file. Branch taken: README. packages/metadata/README.md's TypeScriptSerializer line now says what the serializer writes (export const metadata = JSON; export default metadata; typescript adds a ServiceObject annotation for every metadata type), what it reads back (the first brace block after export const / export default, which must be JSON), that it is FilesystemLoader's format for typescript/javascript, and that it is not an authoring shape. The emitter and parser are untouched and there is no public output change, so Clause-② stays no and the PR carries 'Clause-②: no' at column 0. A3 confirmed: the built packages/metadata/dist/index.d.ts declares class TypeScriptSerializer and lists it in its value export list. A2 confirmed and reported below as out_of_scope, not fixed. Changeset: .changeset/19724-typescript-serializer-readme-shape.md, patch, @objectstack/metadata (README.md is in files[]). Labels: zero writes, because the dispatch named no labels and skip-changeset does not apply (a published file moves and a changeset is present). The PR shows only size/s, set by another actor. Cleanup: node_modules and the worktree are removed right after this comment is posted.",
    "tests": "Head 2452a06 (git rev-parse --short HEAD). This is a README-only change, so there is no code test, no ablation and no round trip: the triage rider binds an emitter change and the emitter did not change. Build: OS_VERIFY_LOCK_SLOT=issue-19724-r21 os-verify-lock -c "pnpm --workspace-concurrency=2 --filter '@objectstack/metadata...' build" gave VERDICT command-exit 0 (check-dts-emitted 10/10). A later full turbo run build --filter=!@objectstack/docs --concurrency=2 gave VERDICT command-exit 0, Tasks 73 successful, 73 total. Package: under the lock, pnpm --filter @objectstack/metadata run typecheck (tsc --noEmit) exited 0, and pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 (the package's test script is 'vitest run') gave Test Files 53 passed (53), Tests 792 passed (792), VERDICT command-exit 0. Gates: node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 50 commands from commit 2452a06. Each ran with EXIT captured before any pipe and all 50 exited 0. check:dual-build-cjs-loads and check:lean-entry-closure first exited 3 PREREQUISITE NOT MET on an unbuilt tree and exited 0 after the full build; the ran list records the post-build codes. dispatch-gates --ran printed '50 derived famil(ies) accounted for — 50 run, 0 NOT-MEASURED (a DERIVED zero — all 50 recorded an exit code and none of them is 3)', exit 0. The derivation flags five roster gates whose silence is not evidence, and I ran all five: check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity each exited 0. check:published-readme-exports exited 3 before the full build and 0 after it. check:nul-bytes exited 0. My own control-byte grep over both files matched nothing.",
    "mcp_calls": "0 — no MCP tool used",
    "api_writes": "2 REST writes, both through the fleet-write relay (scripts/pm/with-fleet.sh --via dispatch; each is one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]). (1) pr_create became POST /repos/objectstack-ai/objectstack/pulls (draft, #19849; relay run 35851702821 success). (2) the os-dev-report comment became POST /repos//issues/19724/comments. Also 2 git pushes (the empty-branch probe and the head 2452a06), which are not REST. label-write: 0.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · TypeScriptSerializer.serialize() annotates EVERY item ServiceObject whatever its metadata type (A2). Repro at base 4112752 against the built dist: NodeMetadataManager({ rootDir }).save('view','all_accounts',{ name, type:'grid', object:'account', columns:['name'] }) writes {rootDir}/view/all_accounts.ts as "import type { ServiceObject } from '@objectstack/spec/data'; export const metadata: ServiceObject = {...}". tsc --noEmit --strict over it (paths mapped to packages/spec/dist/data/index.d.ts) answers view/all_accounts.ts(5,3): error TS2353: Object literal may only specify known properties, and '"type"' does not exist in type ... . object/account.ts written the same way type-checks clean. Reachable today through the public MetadataManager.save() default format and the exported TypeScriptSerializer. Not fixed here (dispatch forbids widening). Dedupe words: TypeScriptSerializer ServiceObject annotation · serialize view ServiceObject TS2353 · FilesystemLoader save typescript wrong type · metadata serializer type annotation every kind",
    "carrier: none (承接者:无) · content/docs/protocol/kernel/metadata-service.mdx 'Create Backup' example comment says it creates 'workflow_config.json.bak'; under FilesystemLoader.save()'s default typescript format the backup is object/workflow_config.ts.bak (filesystem-loader.ts: backupPath = filePath + '.bak'). The copied example does not fail, so this is noted in the PR's Acceptance notes only and not filed."
    ],
    "gates": [
    "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 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:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:nul-bytes :: 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: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:watch-hint-literal :: exit 0",
    "node scripts/check-changeset-fixed.mjs :: exit 0",
    "pnpm check:published-readme-exports :: exit 0 (exit 3 PREREQUISITE NOT MET before the full build)",
    "pnpm check:authz-resolver :: exit 0",
    "pnpm check:error-code-casing :: exit 0",
    "pnpm check:filter-alias-parity :: exit 0",
    "pnpm --filter @objectstack/metadata run typecheck :: exit 0",
    "pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 :: exit 0 (53 files, 792 tests)",
    "node scripts/pm/dispatch-gates.mjs --ran ran.list :: exit 0 — 50 derived, 50 run, 0 NOT-MEASURED, 0 UNRUN"
    ],
    "deviations": [
    "none against the file surface: the diff is exactly packages/metadata/README.md and .changeset/19724-typescript-serializer-readme-shape.md",
    "attribution: the commit carries AGENTS.md's model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the PR body ends with AGENTS.md's session-URL footer. I did not use the harness reminder's model-named Co-Authored-By trailer or its PR tagline, because os-dev.md says harness attribution yields to the repo form",
    "two derived gates (check:dual-build-cjs-loads, check:lean-entry-closure) needed a full-workspace build before they could measure. I ran it under the verify lock (6m44s held) and recorded the post-build exit codes"
    ],
    "files_changed": [
    "packages/metadata/README.md",
    ".changeset/19724-typescript-serializer-readme-shape.md"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    ACCEPT — round 21 · PR #19849 · Fixes (the README branch)

    domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T11:15Z. Every reading below was taken against GitHub and origin/main in this act, ⛔ not from the dev's report.

    The card's first question, answered: A1 = NO. No caller writes this serializer's output to a *.object.ts or to authored metadata. FilesystemLoader.save() writes {rootDir}/{type}/{name}.ts, and only MetadataManager.save() reaches it. So ruling 5644350230 (one authorised .object.ts shape) does not reach the emitter, and the README line was the defect. The seat's isolated reviewer re-derived this from the tree. The sibling repositories are NOT MEASURED; the dev says so.

    check reading
    PR shape draft → main; first body line Fixes #19724; no closing keyword near #19722 / #19723 / #17418; Clause-②: no at column 0
    scope 2 files, +11/−1: the one README bullet and one patch changeset for @objectstack/metadata (the README ships in files[])
    CI on head 2452a06dcc latest run per check name: 31 success + 3 roster skips
    governed NOT governed; 12 changed lines
    contract review isolated at-tier record, PASS, on this head, posted on the PR. Every README sentence is verified TRUE against source and a probe

    Out-of-scope dispositions: serialize() annotating every item as ServiceObject → filed #19852 (reproduced by the dev and, independently, by the reviewer). The docs "Create Backup" .bak filename → Acceptance notes.

    Landing: ready → queue on this head.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    a90272a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions