Skip to content

fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown - #19919

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19871-view-metadata-type
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19871-view-metadata-type

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #19871

Clause-②: no (narrowing)

What changed

  • packages/spec/src/ui/view.zod.ts, the type export region only: ViewMetadata was z.input of typeof ViewMetadataSchema, which is exactly unknown. It is now z.input over (typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]: the union of the input types of the four members the schema's union runs. The TSDoc says why the type cannot be derived from the schema itself, and what it still does not express.
  • ⛔ No runtime change. ViewMetadataSchema, its members and its z.preprocess are untouched, and a 44-body parse probe answers byte-identically before and after (below).
  • New pin: packages/spec/src/ui/view-metadata-type.test.ts.
  • packages/metadata/src/serializers/typescript-serializer.ts: the comment that documented the unknown now says why view stays unannotated. The reason is the table's own rule (the spec type must equal the bound schema's z.input), and it holds for a real reason: the door accepts bodies ViewMetadata refuses. One word in the sibling test's docblock ("is" to "once was") keeps it true.
  • Changeset .changeset/view-metadata-type-not-unknown.md: @objectstack/spec minor, BREAKING banner, Clause-②: no (narrowing), ADR-0087 disposition not-required (no-migration-prescription).

Why this type: the reading

  • The TSDoc promises "any persisted view metadata body: container | ViewItem record | flattened overlay". Under the ADR-0122 naming, the bare name is the input shape and ViewMetadataParsed is the output shape. So the fix is the members' INPUT union.
  • It cannot be read off the schema. The z.preprocess input type is unknown, and where the union is built its members are cast to z.ZodTypeAny, so both z.input and z.infer of ViewMetadataSchema are unknown. VIEW_METADATA_MEMBERS is the union's member list by construction, so it is read there instead.
  • Consumers: git grep -w ViewMetadata finds no annotation consumer in this repo. objectui has zero hits at the pinned sha 62597c5880 and at its origin/main 544ecba. The control grep for ViewMetadataSchema / VIEW_METADATA_MEMBERS at the pin answers exit 0 with 9 files, so the negative reading is valid.

Measurements: before 8490127962 (origin/main), after 2f378ec5cf

Type probe, compiled against src AND against the BUILT dist/ui/index.d.mts, with the same results on both:

reading before after
unknown extends ViewMetadata holds does not hold (TS2322)
a value typed unknown, assigned to ViewMetadata compiles TS2322
list overlay plus undeclared key notAViewKey compiles TS2353
42 compiles TS2322
the #19852 repro body (name, type: 'grid', object, columns), which the parse rejects compiles still compiles, see Residual
ViewMetadataParsed is unknown yes yes, not changed here

Probe liveness control: at base, a ListView literal with an undeclared key reports TS2353 in both programs. --listFiles shows 11 spec dist files and 36 spec src files in the two programs.

Runtime acceptance is byte-identical. The probe runs 44 bodies through ViewMetadataSchema.safeParse: the view-union-diagnostics battery plus the undeclared-key body, the repro and a list overlay with console row ids. It records the full result (verdict, parse output, issue code/path/message). On src and on dist, before and after, it gives 20 accepted and 24 refused, report sha256 f652fb343387bf490eda6fd7efdde6330d6103db811f246c8234ded9ee4993d7 all four times.

Type against door over the same 44 bodies. Before, all 44 compile (unknown). After:

door accepts door refuses
type-checks 17 8
type error 3 16

Residual: the #19852 repro body still type-checks

TypeScript checks an object literal's keys against the union AS A WHOLE. The container member (ViewSchema) has only optional keys. So once type and columns are found on another member, name + type + object + columns is assignable to the container member. Measured: the same union without the container member refuses the body (TS2741, viewKind missing). Each member alone refuses it too.

An exclusive union would add, to each member, every other member's keys as optional-never. That refuses the repro and container.withAux. But it also refuses four bodies the door accepts and the platform writes itself: put.isPinned, put.sortOrder, put.pinAndOrder and overlay.badOperator. Both forms are wrong somewhere, and the dispatch named the plain members' input union. So this PR ships the plain union and documents the gap in the TSDoc.

Firing control for the pin

The fix was committed first. scripts/ablation-replace.mjs then put the declaration back to today's spelling (anchor hit 1 to 0, blob bb6384c31aeb to a74e16786768). pnpm --filter @objectstack/spec check:test-typecheck then goes red with "src/ui/view-metadata-type.test.ts: 3 type error(s) in a file the ledger does not cover". tsc names them TS2578 Unused '@ts-expect-error' directive at lines 61, 63 and 65. Restored: blob equals HEAD, and git diff HEAD is empty. On the fixed tree the same gate is green: 53 files, 255 errors and 142 signatures, held exactly by the unchanged test-typecheck-debt.json.

Generated artifacts, and what check:api-surface sees

gen:api-surface, gen:export-origins and gen:declaration-map all ran on the rebuilt dist and wrote zero diff ("0 shard(s) rewritten"). check:api-surface answers "public API surface + factory signatures unchanged". The snapshot records that an export EXISTS and its kind, not what a type alias resolves to (see the build-api-surface.ts header), so this type change is invisible to it. The change is stated here instead: ViewMetadata narrows from unknown to the union of the four members' input types. The dispatch expected check:api-surface to report it. Measured, it does not.

Clause-② reading

  • Value no. The runtime accept set does not move (the probe above), and no export is added or removed.
  • Arm (narrowing). A published TYPE narrows from unknown: code that assigns a non-view value to a ViewMetadata slot stops compiling. ADR-0087's 2026-08-30 addendum reads a published type-surface narrowing as BREAKING, truthfully.
  • Level minor. This follows the launch-window convention in the check-changeset-no-major.mjs header: breaking-ness is carried by the banner and the disposition, not by the level.
  • Disposition not-required (no-migration-prescription). type-surface-only is closed to this diff by its predicate 2 (it touches packages/spec/**) and predicate 3 (a *.zod.ts moved). Nothing authorable is removed or renamed, and no stored row moves. check-adr-0087-registration answers "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". check-changeset-no-major answers "no major bump".
  • The claim's provisional Clause-②: no is superseded by no (narrowing).

Local verification at 2f378ec5cf

  • pnpm --filter @objectstack/spec test: 528 files, 15508 passed, 1 todo. The new file: 5 passed, one per member plus the coverage check.
  • pnpm --filter @objectstack/spec typecheck (tsc, scripts program, test layer): exit 0.
  • pnpm --filter @objectstack/metadata typecheck and test: exit 0, 54 files, 821 passed.
  • pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date. It ran against a dist built from aa5156d9de; the only later commit adds the changeset, so no spec source differs.
  • dispatch-gates --ran: 83 families derived, 81 run with exit 0, 0 unrun, and 2 NOT MEASURED with exit 3, PREREQUISITE NOT MET: check:dual-build-cjs-loads and check:type-check-debt. Both need the whole workspace built. Neither reads the changed symbol: it has no in-repo importer. The four type-check lanes the tool lists as CI-only are CI's.
  • origin/main moved 2 commits since the base (beac798026, a34c27cbe5). Neither touches packages/spec or packages/metadata, so they are not merged here.

Acceptance notes

Sibling census: every exported type alias in packages/spec/src that resolves to unknown, compiled with the package's own tsconfig at 2f378ec5cf. 2359 aliases were scanned. ⛔ None of these is changed here; they are listed for the seat.

  • InlineAction (src/ui/action.zod.ts:2161) is z.input of a lazySchema wrapping a z.preprocess, so it is unknown. It has the same shape as this card's type, in another file. (InlineActionParsed resolves to a concrete type.)
  • ViewMetadataParsed (src/ui/view.zod.ts:6329) is unknown: its z.infer reads the union whose members are cast to z.ZodTypeAny. Narrowing it the same way is not a one-line change. diagnoseViewMetadata returns data: parsed.data, and parsed.data is unknown, which is not assignable to the members' output union (TS2322, measured). So the fix also needs an edit in that function, outside this region.
  • AssembledViewArtifact and AssembledViewArtifactParsed (src/ui/assembled-views.zod.ts:102/104) are unknown. The cause is a union of z.ZodTypeAny-cast members (the same VIEW_METADATA_MEMBERS values), not a preprocess. The TSDoc says "One assembled viewItems: entry (input shape)".
  • JoinedReportBlock (src/ui/report.zod.ts:407) is unknown because JoinedReportBlockSchema is annotated z.ZodTypeAny.
  • AssignmentValue(Parsed) and GetPublishedMetaItemResponse(Parsed) are unknown by design: their schemas are z.unknown().
  • Nested, not top-level: the viewItem member's config is typed unknown, because viewItemArmShape takes config: z.ZodTypeAny. So at the type level ViewItem, ViewItemWire and this union's viewItem arm accept any config.
  • check:exported-any catches a ZodType of any, not an exported alias that resolves to unknown. No gate sees this class today. The census is about 60 lines of TypeScript compiler API and could become one. Not in scope here.

Patch round (head 3cb0a8f6a), after the at-tier FAIL 5808373817

Written by the domain:spec seat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), which took the card over on the maintainer's 「你接手派补丁轮」. Two fast-forward commits; no code moved.

  • packages/metadata/README.md:77 (7ca39fe366): the published parenthetical said ViewMetadata is unknown, which this PR makes false. It now reads: ViewMetadataSchema is a z.preprocess, whose z.input type is unknown; ViewMetadata is declared instead as the union of the input types of the members that schema's union runs, so it is not that schema's z.input type. That is the TSDoc's own wording, and it is consistent with the serializer comment and the annotation-test docblock.
  • .changeset/view-metadata-type-not-unknown.md (3cb0a8f6a): one paragraph naming the still-pending .changeset/19852-*.md sentence (ViewMetadata is unknown) as superseded. That file is not edited, because editing it is the deliberate-correction class.
  • No second changeset for @objectstack/metadata. The README does publish (files[]), but the two packages sit in one fixed group, so the corrected README ships in the same release, and the false clause never shipped. At-tier re-review 5815765984: PASS.

Generated by Claude Code

…pes, not unknown

ViewMetadata was z.input of ViewMetadataSchema, a z.preprocess whose input
type is unknown, so the published name type-checked any value. It is now
read off VIEW_METADATA_MEMBERS, the members the schema's union runs. The
runtime schema is unchanged.

A type-level pin (view-metadata-type.test.ts) refuses unknown, a key no
member declares and a non-object, and types one body per member that the
door accepts through that member. The metadata serializer's comment that
documented the unknown now states why view stays unannotated.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
A minor, BREAKING-bannered changeset for @objectstack/spec: the published
type narrows while the runtime accept set does not move. Its ADR-0087
disposition is no-migration-prescription.

Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/metadata/README.md, packages/metadata/src/serializers/typescript-serializer.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/metadata/README.md, packages/metadata/src/serializers/typescript-serializer.ts) — pages documenting those are invisible to this run
  • 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 — 139 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 2c1011b01bc071c545f72f2761647b8d9ab56375 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5763b14fd6729d60510836b8a039d1684bd77a6d — the merge of head 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c into base 2c1011b01bc071c545f72f2761647b8d9ab56375, 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 5763b14fd6729d60510836b8a039d1684bd77a6d && git checkout 5763b14fd6729d60510836b8a039d1684bd77a6d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c

node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 61/61 CONTRACT_REVIEW_TIER
Head-sha: 2f378ec5cfda272f20cdc961c084f27d660b37ce

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session on the maintainer's instruction in that session (「帮我处理」, with the landing route chosen there); every one of its 61 transcript turns served at the tier the constant names. Adopted by the seat 2026-09-24T05:39Z. The record below is the reviewer's, unedited except the two header lines. The reviewer's own Served-tier: line declared a lower tier than the transcript shows; the seat replaced it with the transcript measurement.

① Derived judgments

  1. The change, verified at the ref. git diff --stat 8490127962834a125239ae85d9e6a88c0386896c refs/review/pr-19919 (merge-base to head): 5 files, +143/−6, identical to pulls/19919/files. The one declaration moved is packages/spec/src/ui/view.zod.ts:6327 at refs/review/pr-19919: export type ViewMetadata = z.input over (typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch] (origin/main :6310 had z.input over typeof ViewMetadataSchema). ViewMetadataParsed (:6329) is a context line, unchanged. ViewMetadataSchema (:6020-6037), VIEW_METADATA_MEMBERS (:5615-5624) and the preprocess are untouched, so the runtime accept set cannot have moved. RIGHT against the card, which asked for exactly "the input of the post-preprocess union (the members the union runs)".

  2. Derivable and honest. VIEW_METADATA_MEMBERS is as const satisfies Record of ViewMetadataBranch to z.ZodTypeAny (:5624), so its values keep concrete types; lazySchema is typed T extends z.ZodTypeAny, (factory: () to T): T (packages/spec/src/shared/lazy-schema.ts:21), so z.input over the indexed union distributes to the four members' inputs. The schema is built from the same record (:6026 maps VIEW_METADATA_BRANCHES over it), so member list and type cannot drift. The card's premise (preprocess input is unknown; members cast to z.ZodTypeAny at :6035) holds at both refs.

  3. Exactly the runtime accept set? No, and no TS type can be, and the PR says so in the TSDoc (:6320-6325) and the changeset. Wider than the door: refinements (containerHasAView on the container member :5538 region, assertViewIdentity in the preprocess :6029, checkListViewCalendarVisualization :5573) and mixed-member literals (container member all-optional; TS judges an object literal's keys against the union as a whole). Narrower than the door: stripViewConsoleDecorations removes row ids before any member judges, and strip-mode members drop undeclared keys, so those bodies parse but their literals are TS2353. The PR measured the exclusive-union alternative and rejected it with a reason (four platform-written PUT bodies would be refused). Not a defect against the card. The 17/3/8/16 matrix is the author's measurement; I did not re-run it (no local gate re-runs), but each class's direction follows from the schema text above.

  4. Pin is weight-bearing and CI-judged. packages/spec/src/ui/view-metadata-type.test.ts:61-66 carries three @ts-expect-error directives (unknown, undeclared key, scalar), one body per member typed through the published name (:33-49), and a coverage assertion against VIEW_METADATA_BRANCHES (:71-73). The file sits inside tsconfig.test.json include: ["src/**/*"] and has no entry in packages/spec/test-typecheck-debt.json (grep for view-metadata-type exits 1; ledger byte-identical between origin/main and the PR ref), so an unused directive (TS2578) reds the gate. CI at head: Type Check · workspace job 107341254326 (head_sha 2f378ec…), step "Type check workspace packages" success; log shows @objectstack/spec:typecheck running tsc --noEmit && pnpm check:scripts-typecheck && pnpm check:test-typecheck, then check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json; 53 file(s) / 255 error(s) / 142 pinned signature(s) held. With the new file in that program, that green line is CI-side proof the narrowing fired; the PR's ablation control (revert the line, 3x TS2578 at 61/63/65) is consistent.

  5. Consumers. git grep -n -w ViewMetadata refs/review/pr-19919 -- ':!packages/spec/src/ui/view.zod.ts': no annotation consumer outside the new test — only comments (packages/metadata/src/serializers/typescript-serializer.ts:31,34), the test docblock, two changesets, packages/metadata/README.md:77, and the baseline rows api-surface/ui.json:428, export-origins/ui.json:414. objectui at the pin (.objectui-sha at origin/main = 62597c588072636e9c30ea35b3d89b1e46fd765d): git grep -n -w ViewMetadata exits 1, ViewMetadataParsed exits 1; control grep ViewMetadataSchema|VIEW_METADATA_MEMBERS|diagnoseViewMetadata hits 11 files, so the negative reading is valid. All four Type Check lanes at head are success with head_sha 2f378ec… (source gates 107341254519, workspace 107341254326, debt ledger 107341254648, consumer gates 107341254656). Console Pin Gate was skipped at head (ci.yml console path filter: .objectui-sha, scripts/build-console.sh, …; none touched), so objectui was not built against this head in CI; with zero references at the pin nothing there can break.

  6. Does the api-surface baseline record it? No, by design. packages/spec/api-surface/ui.json:428 "ViewMetadata (type)" identical at both refs; packages/spec/scripts/build-api-surface.ts header: one name (kind) row per declared kind, and the signature hash "prints a type REFERENCE and does not expand it"; api-surface-signatures.json covers only defineX factories, none of which names ViewMetadata. AGENTS.md :750: the snapshot "records that an export exists, never what it resolves to". Consumer-gates step "Check @objectstack/spec public API surface" success. The changeset is therefore the only record of the narrowing, which is what the PR states.

  7. Collision with [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920: none. [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 names ViewMetadataParsed (two lines below), InlineAction, AssembledViewArtifact, JoinedReportBlock; this diff changes none (view.zod.ts hunk is :6309-6327 only). The changeset states ViewMetadataParsed is unchanged and still unknown. [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 is pm:blocked Blocked-by 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 with triage saying do not fold it into fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919; honoured.

② Semver level

  • .changeset/view-metadata-type-not-unknown.md: '@objectstack/spec': minor; line-leading Clause-②: no (narrowing); a BREAKING banner naming the affected party (TypeScript code that annotates with ViewMetadata); one adr-0087 HTML-comment marker not-required (no-migration-prescription) with its why. The PR body carries the same line-leading Clause-②: no (narrowing); the claim's provisional Clause-②: no is declared superseded.
  • AGENTS.md :1084-1085: declaration is yes|no plus at most one arm; (narrowing) is BREAKING; no (widening) malformed. no (narrowing) is legal and honest here: no new key on a published payload (runtime accept set unchanged), a published TYPE narrows. Level: scripts/check-changeset-no-major.mjs header (launch window: "a breaking change ships as minor … the mandatory information carriers are the BREAKING banner and the ADR-0087 disposition") makes minor correct; patch would misgrade a breaking change, major is refused.
  • Disposition: type-surface-only (ADR-0087 addendum 2026-08-30, docs/adr/0087-…md:717-871) is closed by predicate 2 (diff touches packages/spec/**) and predicate 3 (a *.zod.ts moved); the addendum's closing paragraph itself says a spec-bumping case "is not a member of this class". registered would put a prescription in a ledger objectstack migrate meta cannot project. no-migration-prescription is the remaining honest category, and the marker says why. Nothing authorable is removed or renamed, so no FROM to TO block is owed.
  • CI at head, Check Changeset job 107341251773 log: check-adr-0087-registration: 1 declared-breaking changeset(s) … [BREAKING+clause-②-narrowing] not-required (no-migration-prescription); check-changeset-no-major: "This diff introduces no major bump" and LEVEL AXIS: this PR declares clause-② no (narrowing) … direction arm: narrowing — a BREAKING change; during the launch window it ships minor; check-empty-changeset: no merge-base changeset modified or deleted.
  • Judgment: correctly declared at every layer.

③ Boundary flags

BLOCKING

  • Published prose made false and not corrected: packages/metadata/README.md:77 at refs/review/pr-19919 still reads "view (its ViewMetadata type is unknown)". packages/metadata/package.json files is ["dist","README.md","CHANGELOG.md"], so this sentence ships in the @objectstack/metadata tarball; after this PR the parenthetical is false, and it steers a consumer toward exactly the assumption the changeset's BREAKING banner warns against. The PR corrected the same sentence's two siblings (typescript-serializer.ts:27-36 comment, typescript-serializer-annotation.test.ts:15 docblock) but not the published one; the docs-drift bot on the PR warned that pages documenting typescript-serializer.ts were NOT COVERED by its run. Remedy is one clause (the reason view stays unannotated is that ViewMetadata is no longer the bound schema's z.input, as the serializer comment now says); comment-only for @objectstack/metadata, no second changeset needed.

Non-blocking

  • Files outside the claim's named surface: packages/metadata/src/serializers/typescript-serializer.ts (comment only) and its -annotation.test.ts (one docblock word). Declared as deviations in the os-dev report; the claim anticipated the serializer comment "becomes a reader of the fix". No runtime change; metadata typecheck and tests green.
  • .changeset/19852-typescript-serializer-per-type-annotation.md:11 (pending on origin/main, compiles into the same CHANGELOG release) still says "ViewMetadata is unknown". Editing another PR's pending changeset is the route-0 "deliberate correction" class in the Check Changeset step (written confirmation, check stays red), so leaving it is defensible; the two entries will contradict each other in one release note unless the maintainer rules otherwise. A decision, not a defect.
  • The card's second half ("add the case to whatever gate is meant to catch exported types that collapse to unknown") and triage's same direction have no carrier: the PR's acceptance notes say "carrier: none", [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 only mentions the census as a possible gate. check:exported-any reads ZodType-of-any annotations only (AGENTS.md :750-753; consumer-gates step "Check no exported spec type resolves to any" green while the four [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 sites still resolve to unknown). Worth its own card.
  • The dispatch expected check:api-surface to record the change; measured, it cannot (①.6). The PR states this rather than hand-editing a baseline. Correct handling.
  • Console Pin Gate skipped at head by path filter, so "pinned sibling still compiles" rests on grep evidence, not a build (①.5).
  • Declared process deviations: the empty-branch probe push was a bare git push, not via write-pace; origin/main had moved 2 commits at open and was not merged (now 10+, none touching packages/spec or packages/metadata; PR reads mergeable: true, clean). Neither affects the diff.
  • Commit trailers on aa5156d9de… and 2f378ec5cf… carry Claude-Session: and the model-free Co-authored-by: Claude pair; no model identifier in title, body, changeset or comments. PR is draft: true, no auto-merge, no dev-written labels: consistent with the governed-surface stop rule.
  • Nit: the PR body cites objectui origin/main as 544ecba; my fetched objectui origin/main is 62597c58… (the pin). Zero references either way; not weight-bearing.

Implemented-by: claude/issue-19871-view-metadata-type
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: FAIL — one blocking item: packages/metadata/README.md:77 ships a sentence this PR makes false (ViewMetadata is no longer unknown). Fix that clause; every other judgment above is PASS-grade.


Generated by Claude Code

The TypeScriptSerializer paragraph gave "its ViewMetadata type is unknown"
as the reason a saved view is written with no annotation. This branch
narrows ViewMetadata to the union of the input types of the members
ViewMetadataSchema's union runs, so that parenthetical is false, and
README.md ships in the @objectstack/metadata tarball. The clause now says
what the serializer comment says: the schema's z.input is unknown, and
ViewMetadata is declared as that member union instead, so it is not the
bound schema's z.input type.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
…n as superseded

The still-pending @objectstack/metadata changeset for the per-type
annotation says a view file is unannotated because ViewMetadata is
unknown. That file is another change's release note and is not edited
here; one sentence in this changeset says its reason is superseded and
its outcome stands, now because ViewMetadata is no longer the z.input
type of the schema getMetadataTypeSchema('view') binds.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 67/67 CONTRACT_REVIEW_TIER
Head-sha: 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c

Isolated at-tier reviewer subagent, run by the domain:spec seat-4 session; every one of its 67 transcript turns served at the tier the constant names. Re-review after the takeover patch round (the maintainer's 「你接手派补丁轮」); it supersedes the FAIL record 5808373817. Adopted by the seat 2026-09-24T14:11Z. The record below is the reviewer's, unedited except the two header lines.

① Derived judgments

  1. The diff, verified at the ref. git diff --stat 8490127962 refs/review/pr-19919 (merge-base to head): 6 files, +149/−7, identical to pulls/19919/files. The patch round 2f378ec5cf..3cb0a8f6a2 is two fast-forward commits (7ca39fe366 README, 3cb0a8f6a2 changeset) touching only packages/metadata/README.md (+1/−1) and .changeset/view-metadata-type-not-unknown.md (+5/−0): exactly the takeover claim's surface (5814360296), no code moved. The one declaration is unchanged from the prior head: packages/spec/src/ui/view.zod.ts:6327 export type ViewMetadata = z.input over (typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]; ViewMetadataParsed (:6329) is a context line; ViewMetadataSchema (:6020-6037), VIEW_METADATA_MEMBERS (:5615-5624) and the preprocess (:6029) are untouched. RIGHT against the card, which asked for "the input of the post-preprocess union (the members the union runs)".

  2. Derivable and honest. VIEW_METADATA_MEMBERS is as const satisfies Record of ViewMetadataBranch to z.ZodTypeAny (:5624), so satisfies keeps each value's concrete type; lazySchema is typed T extends z.ZodTypeAny, factory returning T, returns T (packages/spec/src/shared/lazy-schema.ts:21); the schema maps the same record (:6026), so the member list and the type cannot drift. The members at head: viewItem = ViewItemWireSchema, a discriminated union of two .strip() objects (:5089-5100); container = ViewSchema.refine(containerHasAView) (:5539) over a strictObject (:4695); listOverlay = ListViewShapeSchema.extend(...).strip().superRefine(...) (:5573-5574); formOverlay = FormViewSchema.extend(...).strip() (:5589). So the TSDoc's and changeset's "three members strip undeclared top-level keys" is TRUE (three strip, the container is strict), and the serializer comment's "the preprocess removes the console's row ids before any member judges the body" is TRUE (:6029; the code comment at :5171-5177 says the nested filter[].id / sort[].id are removed by stripViewConsoleDecorations BEFORE the union runs).

  3. The new README sentence is TRUE and consistent. packages/metadata/README.md:77 at head: "view (ViewMetadataSchema is a z.preprocess, whose z.input type is unknown; ViewMetadata is declared instead as the union of the input types of the members that schema's union runs, so it is not that schema's z.input type)". Clause by clause: ViewMetadataSchema = lazySchema(() => z.preprocess(...)) (:6020-6028) and lazySchema returns T, so its z.input is the preprocess's, unknown; ViewMetadata reads VIEW_METADATA_MEMBERS (:6327), so it is not that schema's z.input. It fits the paragraph's own rule ("Each is exactly the z.input type of the schema getMetadataTypeSchema() resolves... A metadata type with no such spec type is written with no annotation"): getMetadataTypeSchema binds view: ViewMetadataSchema (packages/spec/src/kernel/metadata-type-schemas.ts:110). It says the same thing in the same words as the TSDoc (view.zod.ts:6310-6326: "the union of the INPUT types of the members ViewMetadataSchema's union runs... Deliberately NOT z.input of ViewMetadataSchema. That schema is a z.preprocess, whose input type is unknown") and the serializer comment (typescript-serializer.ts:30-36, under the table's rule at :18-22: listed only when the spec type IS the bound schema's input type). The README makes no claim about the runtime accept set; the serializer comment does, and that claim holds (item 2). The prior blocking item is cleared.

  4. Pin is weight-bearing and CI-judged at this head. packages/spec/src/ui/view-metadata-type.test.ts:61/:63/:65 carry the three @ts-expect-error directives; tsconfig.test.json:45 include: ["src/**/*"]; the ledger packages/spec/test-typecheck-debt.json has no view-metadata-type entry (grep exit 1) and is byte-identical between origin/main and head; scripts/check-test-typecheck.mts:425 reds on "type error(s) in a file the ledger does not cover", so an unused directive (TS2578) fails the gate. Chain: Type Check · workspace runs turbo run typecheck --filter='./packages/*' ... (.github/workflows/lint.yml:6029-6030); spec's typecheck is tsc --noEmit && pnpm check:scripts-typecheck && pnpm check:test-typecheck (packages/spec/package.json). Check-run 107640315088 at head: success. The job log was not readable through this session's proxy (CONNECT 403 on the log host), so the reading rests on workflow text + script text + the check-run conclusion; the ablation control (3x TS2578 on revert) is the author's and is consistent with that chain.

  5. Consumers. git grep -n -w ViewMetadata refs/review/pr-19919 outside view.zod.ts: the new test, the serializer comment (:31,:34), the annotation test docblock (:15, "once was"), the two changesets, README.md:77, and the baseline rows api-surface/ui.json:428, export-origins/ui.json:414. No annotation consumer. objectui at the pin (.objectui-sha = 62597c588072636e9c30ea35b3d89b1e46fd765d, identical at origin/main and head): git grep -w ViewMetadata exit 1, ViewMetadataParsed exit 1; control grep ViewMetadataSchema|VIEW_METADATA_MEMBERS|diagnoseViewMetadata hits 11 files, so the negative is valid. objectui origin/main is that same sha in my fetch. Console Pin Gate skipped at head (path filter); nothing there can break.

  6. Generated baselines: api-surface/ui.json:428 "ViewMetadata (type)" identical at both refs; git diff --stat origin/main refs/review/pr-19919 -- api-surface export-origins declaration-map test-typecheck-debt.json is empty. AGENTS.md :750: the snapshot records that an export exists, never what it resolves to. Type Check · consumer gates (107640314963) success. The changeset remains the only record of the narrowing, as the PR states.

  7. [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 collision: none. The view.zod.ts hunk is :6309-6327 only. origin/main (2c1011b01) is 24 commits past the merge-base; the one that touches view.zod.ts (f5a7250b7, fix(spec): title the view.columns, view.tabs and view.sort repeater row properties #19917) has hunks at :1015, :1664, :2706. git merge-tree --write-tree origin/main refs/review/pr-19919 exits 0 (tree 22675b15a7) with the new declaration at :6335 of the merged file; PR reads mergeable: true, clean. CI at head ran on the merge commit 5763b14fd6 (docs-drift bot: merge of head into base 2c1011b01), so the 35 green check-runs cover the merged tree.

② Semver level

  • .changeset/view-metadata-type-not-unknown.md at head: '@objectstack/spec': minor; line-leading Clause-②: no (narrowing); BREAKING banner naming the affected party; exactly one adr-0087 HTML-comment marker not-required (no-migration-prescription) with its why. Level, arm, banner and marker are byte-unchanged from the prior head; the patch added one paragraph (:35-38). The PR body carries the same line-leading Clause-②: no (narrowing).
  • AGENTS.md :1084-1085: (narrowing) is BREAKING, no (widening) malformed; no (narrowing) is legal and honest here (runtime accept set unchanged, a published TYPE narrows). scripts/check-changeset-no-major.mjs:47-51: during the launch window a breaking change ships as minor, major is refused. ADR-0087 docs/adr/0087-metadata-protocol-upgrade-contract.md:784-790: type-surface-only predicate 2 (diff must not touch packages/spec/**) and predicate 3 (no *.zod.ts moved) both close it; no-migration-prescription is the honest remaining category and nothing authorable is removed or renamed, so no FROM to TO block is owed.
  • CI at head: Check Changeset (107640314037) success; its annotation on the changeset file reads "ADR-0087 exemption (no-migration-prescription): Nothing authorable is removed, renamed or re-typed at runtime...". pr-automation.yml:899-900, 939-940, 1075 run check-empty-changeset, check-adr-0087-registration and check-changeset-no-major against the merge base.
  • No second changeset for @objectstack/metadata: confirmed against the rule text, with the prior record's reason corrected. AGENTS.md :1081-1084 ("Add a changeset for anything that publishes. Feature, functional improvement or fix... A bug fix in a released package takes a patch changeset"); .claude/agents/os-dev.md:298 (published = what each package's files[] ships), :299 (comments do not publish); .claude/skills/pm-dispatch/references/review-checklist.md:20 (docs-only takes the skip-changeset label in this repo). Measured: packages/metadata/package.json files = ["dist","README.md","CHANGELOG.md"], so the README edit PUBLISHES; the prior record's "comment-only" was wrong and the dev's refutation is right. Even so, no second changeset is owed: (a) the PR carries a changeset and .changeset/config.json puts @objectstack/spec and @objectstack/metadata in the same fixed array, so metadata releases at the same version and the corrected README ships in that release; (b) the false clause never shipped: it came in with e9eb2244d (fix(metadata): TypeScriptSerializer annotates each item with its own metadata type's spec type, never ServiceObject for a non-object #19865, 2026-09-23), whose metadata patch changeset 19852-typescript-serializer-per-type-annotation.md is still pending on origin/main, and the published @objectstack/metadata@17.4.0 README (npm view, my own read) has 0 ViewMetadata hits with 1 TypeScriptSerializer hit as control; (c) the edit is neither a feature nor a bug fix in metadata: it corrects prose that this PR's own spec change falsifies, and that change is what this PR's changeset records. Judgment: correctly declared at every layer.

③ Boundary flags

Non-blocking

  • PR body not updated for the patch round: updated_at 2026-09-24T12:53:29Z precedes the dev report (13:31:44Z); the body names neither README.md, nor the head 3cb0a8f6a, nor a patch-round section; "What changed" lists 4 bullets for 6 files and "Local verification" is at 2f378ec5cf. Every claim in it stays true (no code moved), the takeover claim says "PR body edits are the seat's", and the dev report supplies the text (A)(B)(C). No AGENTS.md rule makes a stale body a gate. The seat's step before landing.
  • .changeset/19852-typescript-serializer-per-type-annotation.md:11 on origin/main still says "ViewMetadata is unknown" and is correctly NOT edited here: editing it is the DELIBERATE CORRECTION class (scripts/check-empty-changeset.mjs:126-129; pr-automation.yml:718-727: written confirmation, the check stays red). The patch instead added a supersession sentence to this PR's spec changeset (:35-38); its claims verify (the 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 note gives that reason; getMetadataTypeSchema('view') binds ViewMetadataSchema). AGENTS.md :697's "never an erratum in a later entry" concerns released entries and 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 is unreleased, so this is a cross-reference, not an erratum. The metadata CHANGELOG will still carry the false sentence in the release that consumes both notes unless the maintainer rules to correct the pending note (or to add a metadata line). Same decision item as before; not a defect.
  • Card's second half (a gate for exported types that collapse to unknown): still no carrier; the dev report says "carrier: the domain:spec seat, noted, not filed"; [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #19920 only mentions the census. Status unchanged; worth its own card.
  • check:api-surface cannot record the narrowing (①.6): unchanged, correctly handled by stating it rather than hand-editing a baseline.
  • Console Pin Gate skipped at head by path filter; the sibling reading rests on grep evidence (①.5). Unchanged.
  • Files outside the original claim's named surface (serializer comment, test docblock, README): each declared, the README one inside the takeover claim's surface; the patch round changed only the two files that claim names.
  • Process: the patch pushes were fast-forward (2f378ec5c..3cb0a8f6a in the fetch), through write-pace per the report; no force-push; origin/main not merged (24 commits, merge clean, ①.7).
  • Hygiene: both patch commits carry Claude-Session: and the model-free Co-authored-by: Claude trailer; grep for model names over the changeset, README, test, serializer, PR title and body is empty. PR draft: true, no auto-merge; labels are Auto Label's (documentation, size/m, tests, tooling, protocol:ui).
  • Docs drift at head: the bot says README.md and typescript-serializer.ts yield no anchor. Hand re-read: content/docs, skills and the other package READMEs have no TypeScriptSerializer, "no annotation and no import" or ViewMetadata prose (grep empty), so no other page restates the clause.
  • Prior nit on the objectui origin/main sha: my fetched objectui origin/main equals the pin 62597c58; the PR body's 544ecba stands as the author's reading. Zero references either way; not weight-bearing.
  • CI job logs were not fetchable through this session's proxy; check-run conclusions, annotations and the workflow/script text were used instead (①.4, ②).

Implemented-by: claude/issue-19871-view-metadata-type
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 14:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 119a02b Sep 24, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19871-view-metadata-type branch September 24, 2026 14:44
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
… like #19919

The seat aligned this changeset with the #19919 precedent for the same
defect class: level minor, `Clause-②: no (narrowing)`, the BREAKING
banner for TypeScript code annotating the four aliases, and the ADR-0087
`not-required (no-migration-prescription)` disposition. The runtime accept
set is unchanged; only TypeScript annotations narrow.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…(Parsed) name their shapes, not unknown (objectstack-ai#19920) (objectstack-ai#20260)

Part of objectstack-ai#19920

Clause-②: no

Three of the four sites objectstack-ai#19920 names now resolve to the shape their
TSDoc promises. `JoinedReportBlock` is the remainder: its region of
`report.zod.ts` is held by the open PR objectstack-ai#20238 (objectstack-ai#20161), and the dispatch
put this site after that PR merges. objectstack-ai#19920 remains open for it. The
measured options for it are under "The remainder" below.

## What changed

Types only. No schema, no parse, no export and no declared type of any
schema moves. Each alias was derived from a schema whose own static type
erases to `unknown`, so any value type-checked against it. Each is now
derived from the member schema the parse actually runs, the way PR
objectstack-ai#19919 re-derived `ViewMetadata`.

| name | FROM (all `unknown` at `e0f17a37`) | TO |
|:--|:--|:--|
| `InlineAction` (`ui/action.zod.ts`) | `z.input` of `typeof
InlineActionSchema`. The schema is a `z.preprocess`, whose input type is
the preprocess function's `unknown` parameter. | `z.input` of `(typeof
InlineActionSchema)['out']`: the pipe's `out` member, the `.pick()`ed
action object. |
| `ViewMetadataParsed` (`ui/view.zod.ts`) | `z.infer` of `typeof
ViewMetadataSchema`. The union's members are cast to `z.ZodTypeAny`
where it is built. | `z.infer` over `(typeof
VIEW_METADATA_MEMBERS)[ViewMetadataBranch]`: the members' OUTPUT union,
the same record `ViewMetadata` reads its input types from. |
| `AssembledViewArtifact` (`ui/assembled-views.zod.ts`) | `z.input` of
`typeof AssembledViewArtifactSchema`. Same cast. | `z.input` over the
`VIEW_METADATA_MEMBERS` entries minus `container`: the three members
that schema's union is mapped from. |
| `AssembledViewArtifactParsed` | `z.infer` of the same schema. Same
cast. | `z.infer` over the same three members. |

- `diagnoseViewMetadata`: the one edit the new type forces. Its success
branch returned `data: parsed.data`, and `parsed.data` is `unknown` for
the same member cast. Without an edit that line is TS2322 (measured
below). It now asserts `parsed.data` to `ViewMetadataParsed`, with a
comment saying why that holds. At runtime the union's output IS the
accepting member's output, since the union's `.check()` transforms
nothing. No value changes. A new test asserts `diagnosis.data`
deep-equals the member's own parse output for every member.
- Every changed TSDoc states what the type does NOT express. The schema
is still the only judge: the preprocess folds and strips, and
refinements are not types. For `InlineAction`, the legacy `type:
'navigation'` and `to` spellings are refused by the type while the door
still folds them. That is pinned in both directions.
- The `JoinedReportBlockSchema` `z.ZodTypeAny` annotation and the member
casts inside `ViewMetadataSchema` / `AssembledViewArtifactSchema` are
all untouched.
- Changeset `.changeset/19920-exported-types-not-unknown.md`: `minor` on
`@objectstack/spec`, `Clause-②: no (narrowing)` with the BREAKING banner
(a narrowing of published TYPES; the runtime accept set does not move)
and the ADR-0087 `not-required (no-migration-prescription)` disposition,
matching the objectstack-ai#19919 precedent; FROM and TO per name, plus the "if your
code stops compiling" instruction.
- `.changeset/view-metadata-type-not-unknown.md` is the unreleased
objectstack-ai#19919 entry. It said "`ViewMetadataParsed` is not changed by this
release: it is still `unknown`", which this branch makes false if both
entries ship in one release. It now reads "`ViewMetadataParsed` is not
changed by this change. It is re-derived from the same members, as their
output types, by its own entry (objectstack-ai#19920)." That holds whichever release
carries either entry. My own entry's `JoinedReportBlock` sentence is
worded the same way.

## Confirmation needed: a pending release note is corrected on purpose
(`Check Changeset` stays red)

`check-empty-changeset` refuses this PR because it changes
`.changeset/view-metadata-type-not-unknown.md`, a changeset it did not
add. This is the gate's DELIBERATE CORRECTION class, not a filename
collision:
- **Note:** the pending (unreleased) objectstack-ai#19919 entry for `ViewMetadata`.
- **What changed under it:** this PR re-derives `ViewMetadataParsed`, so
the note's sentence "`ViewMetadataParsed` is not changed by this
release: it is still `unknown`" would ship false in any release that
carries both entries.
- **The rewrite:** that one sentence now reads "`ViewMetadataParsed` is
not changed by this change. It is re-derived from the same members, as
their output types, by its own entry (objectstack-ai#19920)." That holds whichever
release carries either entry. Nothing else in the note moves.

Per the gate's own prescription, the file is ⛔ not restored from the
base, which would put the false sentence back. `Check Changeset` stays
red until the correction is confirmed here in writing. It is not a
required context. If a release consumes the objectstack-ai#19919 entry before this PR
lands, the correction becomes moot: the merge resolves by keeping
`main`'s deletion.

## Measurements

All readings are TypeScript compiler-API reads of the package's own
`tsconfig.json` / `tsconfig.test.json`, unless named otherwise.

1. **The premise holds.** At base `e0f17a37`: `ViewMetadataParsed`,
`InlineAction`, `AssembledViewArtifact`, `AssembledViewArtifactParsed`
and `JoinedReportBlock` all have type flag `Unknown`. The control
`ViewMetadata` (re-derived in PR objectstack-ai#19919) is NOT unknown, and
`InlineActionParsed` was never unknown. At head, the four changed names
are not unknown, and `JoinedReportBlock` still is.
2. **Declaration cost.** No cast is removed; each alias is emitted
verbatim. `pnpm --filter @objectstack/spec build` was run on both trees
in one lock turn:
- total `.d.ts` bytes: 30,830,702 at `e0f17a37` → 30,836,475 at
`61b382d9` (+5,773, +0.019%, TSDoc and alias text);
- affected chunks: `view.zod` 499,735 → 500,541, `action.zod` 76,621 →
77,606, `page.zod` 304,750 → 305,845;
   - `TS7056` occurrences in the build log: 0 on both.

The casts are real declaration-size dodges, which is why this PR derives
from the members and leaves the casts alone. In-memory declaration emit,
replacing each union's `z.ZodTypeAny` tuple with the real member tuple:
- `view.zod.d.ts`: 500,881 → 663,301 bytes (+32%);
`ViewMetadataSchema`'s own declaration grows 323 → 162,743 bytes (3,995
lines);
- `assembled-views.zod.d.ts`: 8,824 → 64,776 bytes (×7.3); the schema's
declaration grows 306 → 56,258 bytes.

Neither emits TS7056. The same root fix would also have removed the
`diagnoseViewMetadata` assertion, so that assertion is the cheaper of
the two ways to type `data`.
3. **What the type change forces.** Census, whole tree, `git grep -w`
excluding `.md`/`.mdx`: outside `packages/spec`, nothing names the four
types or `diagnoseViewMetadata`. Inside, only `view.zod.ts` itself and
six test files do. An in-memory ablation removing the assertion yields
exactly one diagnostic: `view.zod.ts` TS2322 "Type 'unknown' is not
assignable to type 'ViewMetadataParsed'". The six census test files
carry 8 diagnostics under `tsconfig.test.json`, identical on base and
head apart from line numbers, all in the ledgered `view.test.ts` debt.
4. **Consumer compile.**
- objectui at the pinned `f8a9d0fb` names none of the three changed
types. It names `JoinedReportBlock`, which this PR leaves alone.
- cloud (local checkout `48d7066`) names none of the four. The control
leg (`defineStack`) hits 18 files.
- No consumer package in this repo imports them, so there is no consumer
suite to run beyond `@objectstack/spec`'s own.

## Reverse verification

In-memory ablation: each alias reverted to its base spelling through a
compiler-host override, with the anchor matched exactly once and nothing
written to disk. The pin file is then compiled under
`tsconfig.test.json`, and every `@ts-expect-error` pin turns red:

| alias reverted | pin file | result |
|:--|:--|:--|
| `InlineAction` | `inline-action-type.test.ts` | 4 × TS2578 (unused
directive) |
| `ViewMetadataParsed` | `view-metadata-type.test.ts` | 3 × TS2578 |
| `AssembledViewArtifact` | `assembled-view-artifact-type.test.ts` | 3 ×
TS2578 |
| `AssembledViewArtifactParsed` | `assembled-view-artifact-type.test.ts`
| 1 × TS2578 |

With the fix in place the three pin files compile with 0 diagnostics.
`tsc -p tsconfig.test.json --listFilesOnly` lists all three, among 523
test files.

## Tests

Final head `f8792c93`. Its code is identical to `61b382d9`; `cf123662`
and `f8792c93` touch only `.changeset/`. Round 2 (`f8792c93`, the
changeset level / arm / banner / marker only) re-ran the 17 derived
families that read `.changeset` plus `check:spec-changes`:
`check-adr-0087-registration` exit 0 (`[BREAKING+clause-②-narrowing]
not-required (no-migration-prescription)`), `check-changeset-no-major`
exit 0 (including the level axis driven with this PR's payload), and
`check-empty-changeset` exit 1 on the deliberate correction only. The
other 66 stand at their `cf123662` reading. Heavy runs went through
`scripts/pm/os-verify-lock.sh`, and each exit code was written to disk
before it was read.

- **Build, typecheck and tests** at `61b382d9` (lock turn 1):
  - `pnpm --filter @objectstack/spec build`: exit 0, TS7056 ×0;
- `pnpm --filter @objectstack/spec typecheck`: exit 0,
"check:test-typecheck: OK — @objectstack/spec's test layer compiles
under packages/spec/tsconfig.test.json; 53 file(s) / 255 error(s) / 142
pinned signature(s) held in test-typecheck-debt.json";
- `pnpm --filter @objectstack/spec test`: "Test Files 550 passed (550)",
"Tests 16120 passed | 2 todo (16122)".
- **Generated artifacts and pins** at `cf123662` (lock turn 2):
- `pnpm --filter @objectstack/spec check:generated`: exit 0, "All 15
generated artifacts are up to date";
- the three pin files by name, `vitest run --project local
--maxWorkers=2`: "Test Files 3 passed (3)", "Tests 17 passed (17)".
- **Derived gate union** at `cf123662`: `node
scripts/pm/dispatch-gates.mjs --commands`, 83 commands, all run.
  - 80 exit 0. Among them:
- `check:api-surface`: "public API surface + factory signatures
unchanged";
- `check:exported-any`: "no exported type resolves to `any`: 2376 types
+ 1452 schemas";
- `check:export-origins`: "5213 exports across 18 entry points resolve
exactly as recorded";
    - `check:docs`: "226 generated files in sync";
- `check:dual-source-exports`, `check:entry-nameability`,
`check:liveness`, `check:spec-parsed-alias`, `check:test-source-alias`,
`check:issue-citations`, `check:nul-bytes`;
- `check:lean-entry-closure` and `check:doc-formula-expressions`,
measured after building their closures.
- `check-empty-changeset --base origin/main`: exit 1, the deliberate
correction above, red on purpose.
- `check:dual-build-cjs-loads` and `check:type-check-debt`: exit 3,
PREREQUISITE NOT MET. NOT MEASURED: both need the whole `./packages/*`
build closure, which CI builds.
- `dispatch-gates.mjs --ran` over the exit-coded record: "83 derived, 81
run, 2 NOT-MEASURED, 0 UNRUN".
- **Lint, narrowed and proven.** Repo-wide `pnpm lint` is CI's. I ran
`eslint --no-inline-config --format json` over the six changed `.ts`
files:
  - the JSON counts 6 files, 0 errors, 0 warnings;
- the population is `eslint.config.mjs`'s own TS/JS globs, so the two
`.changeset/*.md` files are outside it;
- that config "never enables type-aware linting (no
`parserOptions.project`, no typed `@typescript-eslint` rules) for ANY
file" (`eslint.config.mjs:327`), so this diff cannot move the verdict on
any untouched file.
- **Consumer suites:** none owed. No package outside `@objectstack/spec`
imports the four types or `diagnoseViewMetadata` (census above).
- **NOT MEASURED, left to CI:**
  - the Type Check workspace and consumer lanes;
  - Test Core shards;
  - Dogfood;
  - Build Core;
  - the two gates above.

## The remainder: `JoinedReportBlock`

- **Why it is not here.** PR objectstack-ai#20238 (objectstack-ai#20161, +77/−14 in `report.zod.ts`)
edits the joined-report block schema this type is derived from, and it
is still open (draft). The dispatch fixed the order: this site lands on
the merged `main`.
- **Measured fork, for whoever takes it.** `JoinedReportBlockSchema` is
annotated `z.ZodTypeAny`. I did an in-memory declaration emit of
`report.zod.ts` on current `main`, with the annotation removed. It
produced no TS7056 and no other diagnostic. `report.zod.d.ts` grows
18,608 → 34,390 bytes (×1.85): the block schema's declaration grows to
7,582 bytes, and `ReportSchema` doubles (8,677 → 16,937) because
`blocks:` now inlines the block type. So the annotation buys declaration
size, not an escape from TS7056. The two options are:
  - remove the annotation and derive the type from the schema;
- keep the annotation and derive the type from an explicitly named
shape.

  Both need re-measuring on the post-objectstack-ai#20238 tree.
- **objectui tripwire.** objectui at the pin holds an inverted pin,
`true satisfies IsUnknown` of the spec's `JoinedReportBlock`, in
`packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`.
Its docblock says the day the spec types this, the pin stops compiling,
and "the failure is the instruction: re-run the triage and burn it
down". The Console Pin Gate only builds objectui (the `types` build
config excludes `__tests__/`), so that pin reds objectui's own
`type-check` on its next spec bump, not this repo's CI. Measured for the
follow-up; this PR does not move it.

## Acceptance notes

- `check:spec-parsed-alias` recognises a bare alias only in the spelling
`z.input` of `typeof` the schema. Its population drops 1443 → 1441 bare
aliases (paired 657 → 655), because `InlineAction` and
`AssembledViewArtifact` now use member derivations, as `ViewMetadata`
has since objectstack-ai#19919. Both `…Parsed` siblings still exist; the gate just no
longer sees the pairs. Noted, not filed. Carrier: none.
- Same family, nested: `viewItemArmShape(viewKind, config:
z.ZodTypeAny)` makes `config` `unknown` on both members of `ViewItem`
and `ViewItemWire` (measured). So it is `unknown` on the `viewItem`
member of every union above too. Reported to the dispatching seat to
fold into this family's closing card; not changed here.
- Zone 3's "a value missing a required key is a type error" pin cannot
be expressed for `InlineAction`: every key of its input is optional
(`type` has a default; `name` and `label` are `.partial()`). Its pins
are `unknown`, the two legacy spellings, and a scalar.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…tstack-ai#20448)

Fixes objectstack-ai#19920
Clause-②: no (narrowing)

This PR takes items 1 and 3 of the remainder that seat 4's release on
objectstack-ai#19920 (comment 5865019059) names: `ApiError.code` and the flattened
list overlay's legacy `options` bag. Item 2, `ViewFilterRule.operator`,
is not changed: its input type is a contract choice, so it is analysed
as a fork (below) for the seat to take to triage.

## What changed

Only types change. No schema's parse, no value, and no export moves; no
export is added. The FROM column was read by a compiler-API census at
the base `0283cb924` and by probes against the source; the TO column is
also probed against the built `dist`.

| item | FROM | TO |
|:--|:--|:--|
| 1. `ApiError.code` (`api/error-code-ledger.zod.ts`,
`api/contract.zod.ts`) | `unknown`. `ErrorCode` was cast to `z.ZodType`
naming only its OUTPUT type parameter, and `z.ZodType`'s INPUT parameter
defaults to `unknown`, so the input type of `ApiErrorSchema` typed
`code` as `unknown`: `{ code: 42, message: 'x' }` compiled as an
`ApiError` while the schema refuses it at `code`. The same `unknown`
reached the `error.code` of every response type built on
`BaseResponseSchema` (58 input aliases, measured) and each `ApiError`
row of a batch result. `makeApiErrorSchema` repeated the one-parameter
cast for a caller-supplied vocabulary. | `ErrorCode`: the cast names
both parameters, each spelled with the existing `ErrorCode` type alias.
`makeApiErrorSchema`: both parameters named, the standard catalogue plus
the caller's codes. The `…Parsed` types do not move: their `code` was
already typed. |
| 3. The list overlay's `options` bag (`ui/view.zod.ts`) | A
string-keyed record of `unknown`, on the list overlay member and so on
`ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and
`AssembledViewArtifactParsed`, because `listViewKindBlocks()` returned a
record of string to `z.ZodTypeAny`. `options: { foo: 1, kanban: 42 }`
type-checked as all four while the member refuses both keys. | One
optional entry per list kind that names a block (`calendar`, `chart`,
`gallery`, `gantt`, `kanban`, `map`, `timeline`, `tree`), each the
kind's own block with every key optional. The return type is a mapped
type derived by the function's own rule (a value of the list shape's
`type` enum that is also a key of the shape), each entry typed by zod's
own `.partial()` answer through a typed helper, never a hand-written
copy. The runtime loop is byte-identical; one assertion on its result
states what the two derivations share, and the new pin file holds the
runtime key set equal to the type's. |

### A change beyond the order's route, forced by a measurement

The dispatch suggested dropping `makeApiErrorSchema`'s cast. It stays:
its vocabulary is caller-supplied and spread into a `string[]`, so the
cast is what carries the caller's codes into the type; dropping it would
also change the returned schema class (to `ZodEnum`), a public-type
change beyond this item. The defect was the missing input parameter, and
that is what changed.

The `ErrorCode` cast exists because the spread erases the members to
`string`, not to dodge declaration size (mechanism assumption A1). But
naming the input parameter DID hit declaration size, measured, and that
fixed the spelling:

- **First spelling, inline union in both parameters** (`74a132da1`): the
built declarations grew 30,647,033 to 34,006,427 B (+3.36 MB, +11.0%);
`api/index.d.ts` alone +1,262,450 B. Declaration emit prints an inline
union literal by literal wherever a schema embeds `ApiErrorSchema` (78
sites in the `api` entry), and the input parameter doubled those prints.
- **Landed spelling, the `ErrorCode` alias** (`539295c9e`): the emitter
prints the alias by name, including for the output half the base already
printed inline. The declarations SHRINK instead (table below). The
bundler emits one new shared chunk, `error-code-ledger.zod` (31,949 B
`.d.ts`, 31,950 B `.d.mts`), for the name to be imported from.

## Measurements (spec build, base `0283cb924` against head code
`539295c9e`)

- **TS7056**: 0 in every spec build of this round (base, `74a132da1`,
`539295c9e`).
- **Declaration files**: 128 at base, 130 at head; the build's own
`check-dts-references` resolves 394/394 relative references across the
130 (382/382 across 128 at base).

| declaration file | base | head | delta |
|:--|--:|--:|--:|
| `api/index.d.ts` (`.d.mts` the same, within 2 B) | 2,532,113 |
1,274,615 | -1,257,498 |
| `automation-api.zod` chunk `.d.ts` (and `.d.mts`) | 535,440 | 193,663
| -341,777 |
| `api-assembled/index.d.ts` (and `.d.mts`) | 184,582 | 86,529 | -98,053
|
| `contracts/index.d.ts` (and `.d.mts`) | 505,045 | 505,100 | +55 |
| `view.zod` chunk `.d.ts` (and `.d.mts`) — item 3 | 498,393 | 505,886 |
+7,493 |
| `error-code-ledger.zod` chunk, new (`.d.ts`) | 0 | 31,949 | +31,949 |
| all `.d.ts` / `.d.mts` files | 30,647,033 | 27,331,377 | -3,315,656
(-10.82%) |

- **Item 3 against the order's size rule** (A2: implement only if within
the same order as PR objectstack-ai#20369's remainder 5, +2,729 B per chunk, 0
TS7056): item 3 moves only the `view.zod` chunk, +7,493 B per chunk (2.7
times that figure, the same order of magnitude, +1.5% of the chunk), 0
TS7056, no `any` anywhere (`check:exported-any` green). Taken on that
reading; the ratio is stated so the seat can hold the rule to a tighter
reading if it meant one.

## Reverse verification (from committed state `539295c9e`, on disk,
through `scripts/ablation-replace.mjs`)

Each pin file compiled under `tsconfig.test.json`'s options. The pins
import `./contract.zod` / `./view.zod` relatively, so the subject is
`src` and no `dist` is on the resolution path.

| leg | reverted to (the base spelling) | pin file | result |
|:--|:--|:--|:--|
| control | nothing | all three pin files | 0 diagnostics |
| A | `ErrorCode` cast naming the output parameter only |
`api/api-error-code-type.test.ts` | 2 x TS2322, 3 x TS2578 |
| B | `makeApiErrorSchema`'s cast naming the output parameter only |
`api/api-error-code-type.test.ts` | 1 x TS2322, 2 x TS2578 |
| C | `listViewKindBlocks()` returning a record of string to
`z.ZodTypeAny` | `ui/view-overlay-options-type.test.ts` | 2 x TS2322, 9
x TS2578 |

Every leg: the tool reports the anchor hit once and the mutation landed
(blob changed), then the restore proven (blob equals the HEAD blob, `git
diff HEAD` empty); an independent `git hash-object` check of all three
files after the legs matches HEAD, and `git status --porcelain` is
empty.

## Tests and gates

Code is identical at `539295c9e` and `4358d1a33` (`4358d1a33` adds the
changeset only).

- **Spec**: build exit 0 (TS7056 x0); `typecheck` exit 0,
`check:test-typecheck: OK — ... 53 file(s) / 251 error(s) / 138 pinned
signature(s) held`, and `--listFilesOnly` puts both new pin files in its
540-test-file program; `vitest run --project local` at `4358d1a33`: 565
files passed, 16,604 tests passed, 1 todo; `check:generated`: all 15
generated artifacts up to date, with no tracked file moved by any build.
- **Consumers** (after building spec and the 12-package closure of
`metadata-protocol`): `@objectstack/metadata-protocol` typecheck exit 0
(192 test files in its program) and tests 189 files passed, 3 skipped,
2,745 tests passed, 19 skipped; `@objectstack/types` typecheck exit 0
(23 of 23 test files in its program) and tests 22 files, 685 passed;
`@objectstack/client` `tsc --noEmit` over `src` exit 0.
- **Probe against the built `dist`**, from a consumer program importing
`dist/api` and `dist/ui`: 0 diagnostics, where every `@ts-expect-error`
(a numeric `code` on `ApiError`, an invented one on `BaseResponse`, a
numeric `options.kanban` on `ViewMetadata`, an unknown kind on
`AssembledViewArtifact`) is consumed and a tuple compiles only if
`ApiError.code` is neither `unknown` nor `any`.
- **Gates**: `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `4358d1a33` derived 86 commands (the
dispatch's 75 plus 11); all 86 run, exit codes written to disk first: 83
exit 0 (`check:lean-entry-closure` after building `objectql`), 2 exit 3
PREREQUISITE NOT MET (`check:dual-build-cjs-loads`,
`check:type-check-debt`: both need the whole-packages build). `--ran`:
86 derived, 84 run, 2 NOT-MEASURED, 0 UNRUN. Readings of note:
`check-adr-0087-registration` reads `[BREAKING+clause-②-narrowing]
not-required (no-migration-prescription)`; `check:api-surface` "public
API surface + factory signatures unchanged"; `check:exported-any` "no
exported type resolves to any: 2384 types + 1446 schemas across 18 entry
points"; `check-empty-changeset` exit 0.
- **Lint, narrowed and proven**: `eslint --no-inline-config --format
json` over the 5 changed `.ts` files: 0 errors, 0 warnings; the
changeset is outside eslint's configuration. `eslint.config.mjs`:327
enables no type-aware linting for any file, so this diff cannot move an
untouched file's verdict. Repo-wide lint is CI's.
- **NOT MEASURED, left to CI**: spec `test:repo` (it held the verify
lock for the whole foreground window, about 595 s, without finishing,
twice); the `@objectstack/client` test-layer typecheck (its 12 dev
dependencies include `runtime` and `rest`, a 33-package build); the two
exit-3 gates above; the objectui and cloud builds.

## Consumer census

- **Item 1 in this repo.** 116 exported spec aliases carry
`ApiErrorSchema`'s shape (58 input names; their `…Parsed` twins were
already typed), found by walking each alias's properties, arrays and
union members. Outside spec, code names them in `@objectstack/client`
(return annotations, `as unknown as` casts and `['data']` reads),
`@objectstack/metadata-protocol` (`toRowApiError`'s cast from `any`
after a `safeParse` guard, and `as BatchUpdateResponse` casts) and
`@objectstack/types` (`Pick` of `ApiError`'s optional fields, not
`code`). All three typechecks are green above; neither named consumer
file needed an edit.
- **Item 3 in this repo.** Outside spec, `ViewMetadataSchema` and
`AssembledViewArtifactSchema` are called with `safeParse` in `objectql`,
`rest` and `metadata-protocol` tests and `objectql`'s `engine.ts`; both
schemas' own static types are the erased unions, so no typed `options`
read exists outside spec.
- **objectui at the pin `f8a9d0fb`.** `ApiError` appears only as `Pick`
of `userMessage` (two files); none of the four view types is named.
Neither narrowing reaches it (from reading, not compiling).
- **cloud**: no checkout in this container, NOT MEASURED.

## Item 2, `ViewFilterRule.operator`: not changed, a fork for triage

`operator` is `z.preprocess(normalizeFilterOperator,
z.enum(VIEW_FILTER_OPERATORS))`. zod types a preprocess's input as its
function's parameter type, and `normalizeFilterOperator` takes
`unknown`, so `{ field: 'status', operator: 42 }` compiles as a
`ViewFilterRule` (and as a rule on every carrier: `ListView.filter`, tab
filters, `Page.filterBy`) while the door refuses it. Who writes the
legacy spellings the fold accepts, measured:

- `examples/`: 0 legacy spellings on a view-filter carrier, 19 canonical
ones in 8 files. (The one legacy-looking hit, `operator: 'ne'` in
`app-showcase`'s `invoice.object.ts`, is a field's `lookupFilters`, a
separate closed dialect.)
- In-repo non-test code: 0 (every other hit is another dialect: lookup
filters, auth `where`, skill trigger conditions, analytics).
- objectui at the pin `f8a9d0fb`: the filter builder emits camelCase ids
(13 of its 22 option values are alias-table keys: `notEquals`,
`greaterThan`, `notIn`, `isNull`, …). Its two producers typed against
spec's `ViewFilterRule` (`viewFilterFold.ts`, `ObjectDataPage.tsx`) fold
through `normalizeFilterOperator` before typing, so the canonical id is
what reaches the type. objectui at `9f0c84a44` (its current head) emits
the 20 canonical ids only.
- Stored `sys_metadata` rows: the alias table exists for them; they are
read through the runtime parse, whose input is `unknown` whatever the
type says.

The three options, the four axes and the recommendation are in the
`os-dev-report` on objectstack-ai#19920 (`open_questions`). In short: A, canonical
enum only (type the preprocess function's parameter; the runtime fold is
untouched); B, the enum plus the alias-table spellings (needs the
table's keys typed as literals, and still cannot express the case-folded
variants the fold also accepts); C, leave `unknown` with a declared
reason. The recommendation is A.

## What stays on objectstack-ai#19920

A compiler-API census of the 2,337 non-generic exported aliases of
`packages/spec/src` (tests excluded, 11 generic skipped), with an
injected control module that must read lit (it did, at base and head):

- **Alias level**: 6 aliases resolve to `unknown` at base and at head,
and none belongs to this family: `FlowValueSlot`, `AssignmentValue` and
their `Parsed` (value slots), `GetPublishedMetaItemResponse` and its
`Parsed` (opaque by ruling).
- **Top-level keys**: 194 at base, 193 at head; the one that left is
`ApiError.code`, and none entered. The only family site left is
`ViewFilterRule.operator` (item 2). Every other key the census reads is
declared `z.unknown()` / `z.any()` (the door accepts anything, so the
type is honest), a third-party or zod type, a service map or a fixture;
`GetMetaItemLayeredResponse.code` and `ViewMetadata.defaults` were
checked by hand and are both declared `z.unknown()`.
- **Index signatures one level below a top-level key**: 391 at base, 387
at head; the four that left are `options` on `ViewMetadata`,
`ViewMetadataParsed`, `AssembledViewArtifact` and
`AssembledViewArtifactParsed`, and none entered. Blind spot, declared:
deeper nesting is not walked.

## Clause-②

Line 2 and the changeset (`b26d6506b`) both read `Clause-②: no
(narrowing)`. The changeset keeps its BREAKING banner and the ADR-0087
marker `not-required (no-migration-prescription)`, so
`check-adr-0087-registration` still reads the narrowing. The value is
`no` because this diff adds no export and moves no accept set; it only
narrows published types. That is the PR objectstack-ai#19919 / PR objectstack-ai#20260 shape for
this defect class.

## Acceptance notes

- `makeApiErrorSchema`'s generic return type still prints the standard
catalogue inline: 4 prints in its one declaration (a 4,311 B line in the
emitted `contract.zod` declaration), where the base printed 2. A local
generic alias would name it; not done here, being one bounded
declaration.
- A field's `lookupFilters` is its own closed operator dialect (`eq`,
`ne`, `gt`, `lt`, `gte`, `lte`, `contains`, `in`, `notIn`), whose
members are spellings `ViewFilterRule` treats as deprecated aliases.
Both are enforced; noted for item 2's triage, not filed.
- The two new pin files follow the two-program shape of
`view-overlay-viewkind-type.test.ts`: tsc judges the type half, vitest
the runtime half; the refusal cases assert the issue `code` and `path`,
not a bare failure.

Line 1 was changed from the partial-landing marker to this closing
keyword by the `domain:spec` seat 1
(`session_01B3TqpoQbTAfG7G74GMDWNW`): item 2
(`ViewFilterRule.operator`'s input type) now has its own card, objectstack-ai#20450,
for triage, which is the dispatch order's A4 condition for closing
objectstack-ai#19920 with this PR.

Line 2 and the `## Clause-②` section were amended by the same seat after
the at-tier record 5871015216 found the value `yes` wrong for this diff;
the changeset line moved with them in `b26d6506b`, and the claim on
objectstack-ai#19920 was amended in place. The stale `Part of` paragraph was removed.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 protocol:ui size/m tests tooling

Projects

None yet

1 participant