Skip to content

fix(spec): JoinedReportBlock, a ViewItem config and the overlay viewKind carry the shapes their doors accept (#19920) - #20369

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-19920-exported-types-remainder
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-19920-exported-types-remainder

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #19920
Clause-②: yes (narrowing)

This PR takes the four remainder items that seat 1's release on #19920 (comment 5858219255) lists, plus remainder 5 (the flattened list overlay's type and columns), on main after PR #20286. #19920 remains open because this round's census and probes found three more sites of the same family, outside this PR's surface. See "What stays on #19920" below.

What changed

Only types change. The runtime accept set does not move: no schema's parse, no value, and no removed or renamed export. The FROM column was probed on the base c5dcb3ba0, both against the source (compiler API, tsconfig.test.json options) and against the built dist.

item FROM TO
1. JoinedReportBlock (ui/report.zod.ts) unknown, because JoinedReportBlockSchema was annotated z.ZodTypeAny. Each blocks[] element of Report / ReportParsed (and so of defineReport's parameter) was unknown for the same reason. The schema's own input type: the annotation is removed. blocks[] elements are blocks.
2. A ViewItem's config (ui/view.zod.ts) unknown on both arms of ViewItem and ViewItemWire, because viewItemArmShape(viewKind, config) typed config as z.ZodTypeAny. The same unknown reached defineViewItem and the viewItem member of ViewMetadata, ViewMetadataParsed, AssembledViewArtifact and AssembledViewArtifactParsed. The arm's config type: a ListView config under viewKind: 'list', a FormView config under viewKind: 'form'. config is a generic parameter now, beside viewKind.
3. The flattened overlay members' viewKind The union of 'list' and 'form' on both members, because flattenedViewOverlayFields(kind) was not generic, so z.enum([kind]) widened. { type: 'grid', columns: ['name'], object, viewKind: 'form' } type-checked, through the list member, as all four union types above, while both doors refuse it. 'list' on the list overlay member, 'form' on the form overlay member: the function is generic in the kind.
4. The list overlay's type default Not named anywhere. One TSDoc paragraph on ViewMetadataParsed, and the same on AssembledViewArtifactParsed. It says, from the code, that the member declares type without .default('grid') and re-applies it in .overwrite(applyListOverlayTypeDefault). An .overwrite() returns the member's own output type, so type stays optional in the output type while every parse through that member returns it ('grid' when the body named none). On that member type is typed as the list shape's type enum.
5. The flattened list overlay's type and columns (ui/view.zod.ts) unknown on the list overlay member of ViewMetadata, ViewMetadataParsed, AssembledViewArtifact and AssembledViewArtifactParsed, on input and on output, because listOverlayPatchFields() read both keys off ListViewShapeSchema through a cast of its shape to a record of z.ZodTypeAny. { object, viewKind: 'list', columns: 42 } type-checked as all four while that member refuses it. The list shape's own types, both optional: type the list view type enum, columns the field list. The shape is read as typed; the schemas are the same objects, so no parse moves.

Changes beyond the five items, each forced, with what forced it

  1. Declaration size (item 2). With only the generic parameter, view.zod.d.ts grows by +170,655 B (500,535 to 671,190, +34%), with 0 TS7056. The cause is that ViewItemSchema, ViewItemWireSchema and VIEW_METADATA_MEMBERS.viewItem each spell both config types out in full, at 58,764 B, 59,326 B and +60,108 B.
    • What this PR does instead. Both schemas carry an explicit type annotation through two non-exported aliases read off viewItemArmShape itself. ViewItemArmShape is the ReturnType of typeof viewItemArmShape, instantiated. ViewItemWireArmShape is that plus viewItemWireFields(), written as one mapped type. The viewItem entry of VIEW_METADATA_MEMBERS is spelled ViewItemWireSchema as typeof ViewItemWireSchema, an assertion to the schema's own type, so the emitter names the schema instead of copying it.
    • Result. view.zod.d.ts is 496,008 B, 4,527 B smaller than on the base.
    • Identity proof. Measured with an Equal probe (the mutual-conditional form) against in-memory inferred twins of both schemas. It holds for each schema, for its z.input and z.output, and for the member. A control pair answers false.
    • A spelling I measured and did not take. A first spelling of the wire annotation used an intersection. It was assignable but not identical (Equal false), so it was replaced by the mapped form.
    • The deviation. No TS error forced this assertion. Declaration size forced it. So it deviates from the dispatch's mechanism assumption 2, which allowed assertions only where a TS error forces them.
  2. ADR-0122 (items 1 and 2). Three isomorphism pins in type-alias-convention.pin.test.ts turned into TS2344 ×3: Iso_ui_report__JoinedReportBlockSchema, Iso_ui_view__ViewItemSchema and Iso_ui_view__ViewItemWireSchema. They had held only because input and infer were the same erased unknown; typed, each schema carries defaults. The pins are deleted. Per check:spec-parsed-alias rule 2, three parsed-state aliases are added: JoinedReportBlockParsed, ViewItemParsed and ViewItemWireParsed. These are three new type exports on the ui entry; api-surface/ui.json and export-origins/ui.json were regenerated. The pin count is restated from 786 to 783 in the file's two prose places and its count history.
  3. test-typecheck-debt.json. The report.test.ts entry shrinks by two signatures (TS18046 "'b' is of type 'unknown'" and TS2571), because blocks[] is typed now. It was regenerated with gen:test-typecheck-debt.
  4. A pending release note corrected on purpose. See the next section.

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

The new entry is .changeset/19920-exported-types-remainder.md: @objectstack/spec minor, Clause-②: yes (narrowing) (the diff narrows published types and also adds three exported type names, per the at-tier record 5863387565), a BREAKING banner naming every type that narrows, FROM and TO per item (four bullets, remainder 5 included), a banner and ADR-0087 reason saying that no EXISTING export changes, and the ADR-0087 marker not-required (no-migration-prescription).

Measurements

  • TS7056: 0 in every spec build of this round: origin/main 5049a3cf, the pre-remainder-5 head 406335af and the head 777b5097.
  • Remainder 5's cost (406335af to 777b5097): the view.zod chunk grows 495,664 to 498,393 B (+2,729, the same for .d.mts); the page.zod chunk moves -3 B; every other declaration file is byte-identical; all 128 files +5,452 B.
  • The whole PR (5049a3cf to 777b5097):
module base head delta
view.zod chunk (.d.ts, and the same for .d.mts) 500,191 498,393 -1,798
page.zod chunk (report and assembled views) 306,012 311,003 +4,991
index.d.ts 3,277,798 3,283,480 +5,682
system/index.d.ts 3,644,976 3,647,050 +2,074
ui/index.d.ts 524,900 524,978 +78
all 128 .d.ts / .d.mts files 30,888,091 30,910,145 +22,054 (+0.071%)
  • Item 1's in-memory emit figures (+3,542 B of report.zod) are the previous run's and were not re-measured; the chunk table above supersedes them.

Reverse verification (on disk, from committed state, via scripts/ablation-replace.mjs)

All five legs re-run at 777b5097, from committed state, through scripts/ablation-replace.mjs in wrap mode; each pin file compiled under tsconfig.test.json's options:

leg reverted to pin file result
item 1 JoinedReportBlockSchema: z.ZodTypeAny joined-report-block-type.test.ts 7 x TS2578, 2 x TS18046
item 2 config: z.ZodTypeAny in viewItemArmShape view-item-config-type.test.ts 8 x TS2578, 1 x TS18046
item 3 non-generic flattenedViewOverlayFields view-overlay-viewkind-type.test.ts 11 x TS2578
item 4 the list overlay's type keeping the list default view-overlay-viewkind-type.test.ts 1 x TS2322
remainder 5 the cast of the shape to a record of z.ZodTypeAny view-overlay-viewkind-type.test.ts 6 x TS2578, 4 x TS2322

Every leg: the tool reports the mutation landed and the restore proven (blob after restore equals the HEAD blob, git diff HEAD empty), and a second check re-reads git hash-object. Control, no mutation: the three pin files compile with 0 diagnostics; git status --porcelain empty.

Tests and gates, at 777b5097

At 777b5097: spec build exit 0 (TS7056 x0); spec typecheck exit 0 (check:test-typecheck OK, 53 files / 253 errors / 140 signatures); spec check:generated exit 0; spec vitest local 560 files, 16,493 passed, 1 todo; spec test:repo 34 files, 620 passed; consumer typechecks exit 0 for lint, metadata, metadata-protocol and objectql after building their 12-package closure (rest NOT MEASURED locally, 26-package closure). dispatch-gates derived 86: 83 exit 0, check-empty-changeset exit 1 (the deliberate correction), 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt, exit 3); --ran: 86 derived, 84 run, 2 NOT-MEASURED, 0 UNRUN. Lint narrowed and proven: 7 changed .ts files, 0 errors, 0 warnings; the 5 other changed files are outside eslint's configuration; no type-aware linting (eslint.config.mjs:327).

Consumer census

  • This repo. Outside packages/spec, the code that imports any narrowed name is:
    • defineReport in the two example report files above;
    • Report in qa/downstream-contract/src/pipeline.report.ts;
    • ReportSchema in one platform-objects test and one downstream-contract test;
    • ViewMetadataSchema in two metadata-protocol tests;
    • AssembledViewArtifactSchema in objectql's engine.ts and one objectql test (its own static type does not move).
    • Nothing outside spec names viewItemArmShape or flattenedViewOverlayFields, which are not exported. All consumer typechecks above are green.
  • objectui at the pin f8a9d0fb: one break, by design, for the seat to carry. packages/types/src/__tests__/report-chart-query-spec-parity.test.ts:725 is the inverted tripwire true satisfies IsUnknown of the spec's JoinedReportBlock. Compiled with that file's own two type definitions against this head's dist, it gives TS1360 ("Type 'true' does not satisfy the expected type 'false'"); line 726 (IsAny) still holds.
    • Its docblock says this failure is the instruction to re-run the triage and burn the local interface down, together with the ledger entry at scripts/check-spec-symbol-derivation.mjs:636.
    • It reds objectui's own type-check (tsc -p tsconfig.test.json) on its next spec bump. It does not red this repo's Console Pin Gate, which builds objectui without __tests__.
  • objectui's other hits. Five other objectui files reference ViewItemSchema / ViewMetadataSchema in code; every other census hit is a comment or objectui's own local JoinedReportBlock interface. From reading them, not from compiling them: they read .success / .error of a safeParse, or cast the schema as unknown as a local interface. None reads a typed config off a parse result.
  • Remainder 5 at objectui's pin f8a9d0fb. Six calls in four test files parse a body with ViewMetadataSchema, and none reads .data off the verdict, so the now-typed list-overlay type / columns reach no objectui read (from reading, not compiling).

What stays on #19920

A compiler-API census of the 2,337 non-generic exported aliases of packages/spec/src, excluding tests, with a control that must read lit: 6 aliases resolve to unknown before and after this PR, and none belongs to this family (FlowValueSlot, AssignmentValue and their Parsed, value slots; GetPublishedMetaItemResponse and its Parsed, opaque by ruling). At top-level-key level it counts 210 keys typed unknown before remainder 5 and 202 after; the 8 that left are remainder 5. Three sites of the family remain, outside this PR's surface:

  • ApiError.code (input): ErrorCode is cast to a one-argument z.ZodType, whose input defaults to unknown, and makeApiErrorSchema repeats the cast. { code: 42, message: 'x' } compiles as ApiError while ApiErrorSchema refuses it at code.
  • ViewFilterRule.operator (input): a z.preprocess, whose input is unknown (the cause InlineAction had). { field: 'status', operator: 42 } compiles while the door refuses it.
  • The list overlay's options bag: listViewKindBlocks() returns a record of z.ZodTypeAny, so the bag is typed as a string-keyed record of unknown. options: { foo: 1, kanban: 42 } compiles as ViewMetadata while the door refuses it. The census does not scan nested keys; a probe found this one.

Acceptance notes


Generated by Claude Code

…d carry their shapes

- JoinedReportBlockSchema drops its z.ZodTypeAny annotation, so
  JoinedReportBlock and Report/ReportParsed blocks[] are typed.
- viewItemArmShape takes its config schema as a generic parameter, so
  ViewItem / ViewItemWire config is the arm's ListView / FormView type.
- flattenedViewOverlayFields is generic in its kind, so each flattened
  overlay member's static viewKind keeps its own literal.
- ViewMetadataParsed / AssembledViewArtifactParsed TSDoc names the list
  overlay type default the parse applies and the output type lacks.
- Type-level pins for each.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…d by reference

Inferred, ViewItemSchema, ViewItemWireSchema and the viewItem member of
VIEW_METADATA_MEMBERS each spelled both config types out in full, +170 KB
of view.zod.d.ts. Both schemas are now annotated through ViewItemArmShape /
ViewItemWireArmShape (read off viewItemArmShape itself, identical to the
inferred types), and the member is asserted to its schema's own type, so
the declaration file names them instead.

Adds the changeset, and corrects the pending note that said
JoinedReportBlock still resolves to unknown.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…R-0122)

JoinedReportBlockSchema, ViewItemSchema and ViewItemWireSchema were pinned
input === infer only because their erased types were the same unknown on
both sides. Typed, each carries defaults, so the pins are false: they are
deleted and JoinedReportBlockParsed, ViewItemParsed and ViewItemWireParsed
are declared, as the convention prescribes.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…check ledger

The three Parsed aliases enter the ui entry's surface. The report.test.ts
ledger shrinks by two signatures: blocks[] is no longer unknown there.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…ory on a refused body

The pin count falls 786 -> 783 with the three deleted pins, stated in both
prose places and the count history. The defineViewItem type pin now wraps
the call: the factory parses, so calling it on the refused body threw at
module load.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…declared types narrow

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
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 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, packages/spec/src/ui/assembled-views.zod.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v12.mdx (via viewKind (literal, a string literal on a changed line))

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

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

Coarse fallback — 136 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 5049a3cf4e3dd591cf5fc23856cc3a42234c9267 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f5e4830d4d9e18648da12ea273f94e0df1b6e1e4 — the merge of head 777b5097834d92c9e9565878406aa2409c597811 into base 5049a3cf4e3dd591cf5fc23856cc3a42234c9267, 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 f5e4830d4d9e18648da12ea273f94e0df1b6e1e4 && git checkout f5e4830d4d9e18648da12ea273f94e0df1b6e1e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5049a3cf4e3dd591cf5fc23856cc3a42234c9267 777b5097834d92c9e9565878406aa2409c597811 && git checkout -B drift-repro 5049a3cf4e3dd591cf5fc23856cc3a42234c9267 && git merge --no-ff 777b5097834d92c9e9565878406aa2409c597811

node scripts/docs-audit/affected-docs.mjs --json 5049a3cf4e3dd591cf5fc23856cc3a42234c9267

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 35094e87739f152baa3b46c8a3250259c033208c
Local-runs: none

Inputs: card #19920 (body and all 11 comments), PR #20369 (body, 12-file list, net diff against merge-base 67047171 — identical to the local three-dot diff, 12 files, +440/−16), and the 35 check-runs on the head. Read-only throughout: git fetch plus git diff / git show against refs in the checkout; nothing built, run or re-run.

① Derived judgments

Accept set: none moves — RIGHT. The source edits are type-level only: JoinedReportBlockSchema loses its z.ZodTypeAny annotation (expression unchanged); viewItemArmShape and flattenedViewOverlayFields gain a generic parameter (bodies unchanged); ViewItemSchema / ViewItemWireSchema gain explicit type annotations (value expressions unchanged); VIEW_METADATA_MEMBERS.viewItem gains an assertion to its own type. No parse, default, refinement, key, tombstone or export is removed or renamed (api-surface/ui.json: +3 / −0). Test Core ×6, Dogfood ×3, Temporal Conformance and Spec property liveness are green on the head.

Public surface, each change named:

  • JoinedReportBlock: FROM unknown TO the input type of JoinedReportBlockSchema — narrowing, RIGHT (card site 4). Carried to every blocks[] element of Report / ReportParsed (report.zod.ts:486, z.array(JoinedReportBlockSchema)) and to defineReport's parameter (:596) — RIGHT; both named in the banner. Pinned by 7 TS2578-style directives in joined-report-block-type.test.ts.
  • JoinedReportBlockSchema's declared type: FROM z.ZodTypeAny TO inferred — a published const's type narrows, RIGHT, named in the changeset.
  • ViewItem / ViewItemWire config: FROM unknown TO the arm's own config (ListView on list, FormView on form) — narrowing, RIGHT (remainder item 2). Carried to defineViewItem's parameter and return (view.zod.ts:5113) and to the viewItem member of ViewMetadata, ViewMetadataParsed, AssembledViewArtifact, AssembledViewArtifactParsed — RIGHT, all named. Pinned by 8 directives in view-item-config-type.test.ts.
  • ViewItemSchema / ViewItemWireSchema declared types: FROM inferred TO an explicit discriminated-union annotation over two non-exported arm aliases — RIGHT as a surface statement, named. The "identical to the inferred type" claim is the dev's in-memory Equal probe and is not gate-verified; what the gates prove is that the annotation compiles on every Type Check lane and the config pins hold, so the narrowing is real and the annotation is now the contract.
  • Flattened overlay members' viewKind: FROM the list / form union on both members TO each member's own literal — narrowing on the four union types, RIGHT (remainder item 3), named; pinned by view-overlay-viewkind-type.test.ts.
  • NEW exports JoinedReportBlockParsed, ViewItemParsed, ViewItemWireParsed on the ui entry (api-surface/ui.json and export-origins/ui.json regenerated, +3 each): additive growth of the public surface. Adding them is RIGHT — check:spec-parsed-alias rule 2 ("the parsed state is named, or the schema is pinned isomorphic") forces a named XParsed once the three isomorphism pins cannot hold (input and infer differ by defaults once typed); the pins are deleted and the count restated 786→783 in both prose places and the history. Named in the changeset. Its DECLARATION is wrong — ②.
  • VIEW_METADATA_MEMBERS.viewItem type: unchanged (typeof ViewItemWireSchema) — RIGHT.
  • TSDoc on ViewMetadataParsed / AssembledViewArtifactParsed (remainder item 4): the .overwrite() type default and the list overlay's type / columns unknown — doc-only, RIGHT, pinned by two compile-time lines that go red the day that site is typed.
  • test-typecheck-debt.json: report.test.ts loses TS18046 and TS2571 — consistent with blocks[] now typed; Type Check · debt ledger green. RIGHT.
  • The one new site found (listOverlay type / columns unknown) stays on the card and the PR stays Part of — RIGHT for a family card; the Part-of guard is green.

Nothing in the derivations is wrong or missing.

② Semver level

  • Level minor on @objectstack/spec — RIGHT. A published-type narrowing ships minor in the launch window (the fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 / fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260 shape), and a new exported symbol takes at least minor; both roads meet here. Not patch, not skip-changeset, no major. The BREAKING banner names every narrowed carrier FROM→TO with the "if your code stops compiling" line; the ADR-0087 marker not-required (no-migration-prescription) is present and right (nothing objectstack migrate meta can reach). Arm (narrowing) — RIGHT.
  • Clause-②: value — WRONG. PR body: Clause-②: no; changeset: Clause-②: no (narrowing). The fleet's one reader (scripts/pm/clause2-line.mjs) says the value answers whether the diff widens an accept set OR expands the public surface. This diff adds three exported names to the ui entry and had to regenerate the public-surface ledger to admit them. The fleet's written rule for exactly this case is .changeset/17779-dashboard-metric-family-single-measure.md: yes (narrowing) because "the published surface GAINS one exported symbol … a new exported symbol is the mechanical floor for in-seat contract review"; every no (narrowing) entry that explains itself (17499, 19514, 19751, 19757, 19889, 19977, 20080, 20116) does so with "no exported symbol moves", and 17499 names "check:api-surface clean with no regeneration" as the test — which this PR does not meet. The family precedents fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 and fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260 declared no (narrowing) and touched no api-surface file; this PR is the first in the family to add names, and the claim's Clause-②: no was copied before the diff existed (the same claim says "adds three type exports"). The truthful spelling is Clause-②: yes (narrowing) — the reader's own case for "widens one surface and narrows another; both facts are read". No CI outcome changes with the fix (the LEVEL axis under yes wants at least minor on a moved package — already minor; ADR-0087 already reads the arm), but the line ships to consumers in CHANGELOG.md and the body line is the carrier of record, so it must be true.
    • Remedy, one changeset-only commit plus one body edit: PR body line → Clause-②: yes (narrowing); changeset line → Clause-②: yes (narrowing), and tighten the banner's "no schema's parse, no export and no value changes" to "no existing export changes" (three are added). No source, test or generated file moves; the fresh head then takes a same-head record.
  • The deliberate correction — RIGHT and MINIMAL, confirmed. .changeset/19920-exported-types-not-unknown.md (PR fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260's pending note) moves by exactly one line, +1 / −1: "…is not changed by this change, and still resolves to unknown." becomes "…is not changed by this change. It stops resolving to unknown in its own entry ([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)." Nothing else in the note moves. The note still exists on origin/main (862b6ce8) with the original sentence, so the correction is live, not moot: a release carrying both entries would otherwise print a false sentence. The gate's own prescription for this class is "do NOT restore it — say so on the PR and get it confirmed", and the PR body does so in its own section, naming the note and what changed under it. The red's annotation names exactly that file and no other.
  • Other reds: NONE. 35 check-runs on the head: 31 success, 3 skipped, 1 failure. The failure is Check Changeset — a pull_request-only job in pr-automation.yml, not among the seven required contexts on main (TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, Lint & Repo Gates, Governed Surface Queue Guard — all seven success on this head). The three skips are filter-driven, not red: Build Docs (docs filter: apps/docs/**, content/**, pnpm-lock.yaml), Console Pin Gate (console filter: .objectui-sha, the console scripts, ci.yml), Packed-tarball smoke (opt-in); none of those paths is in the diff.

③ Boundary flags

No os-dev-report exists (clock-out, release 5862804129); the flags are the PR body's.

  1. Deviation: annotations and one assertion forced by declaration size, not by a TS error (ViewItemSchema, ViewItemWireSchema, viewItem: … as typeof ViewItemWireSchema) — ANSWERED, accepted. The card body itself says to measure the declaration-size cost before touching a ZodTypeAny cast ([finding] AssembledInstalledPackage.manifest erodes to an index-signature type in the published .d.ts, so the assembled arm absorbs the authoring arm #19324); it was measured (+170,655 B of view.zod.d.ts inferred versus −4,527 B annotated, 0 TS7056 either way), the assertion is to the schema's own type, the arm aliases are non-exported and read off viewItemArmShape so they cannot drift, and the claim's condition — each such change named — is met in the PR body. Within the fence.
  2. Three new exports and three deleted isomorphism pins — ANSWERED in ①: gate-forced (check:spec-parsed-alias rule 2), named, ledgers regenerated. What is owed is the declaration value (②).
  3. Confirmation of the pending-note correction — ANSWERED in ②: right, minimal, confirmed by this record's reading.
  4. Part of versus a closing keyword (the new listOverlay type / columns site) — ESCALATED to the seat, non-blocking. Part of is right as the PR stands: the site is the same family as item 2, seat 1's release bundled family remainders on this card on purpose, and the Part-of guard is green. The seat either keeps it on [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 as remainder 5 with the measured fix (read ListViewShapeSchema.shape without the cast, +3,154 B, 0 TS7056) or files it; either way the item-4 TSDoc and the two pin lines in view-overlay-viewkind-type.test.ts are corrected with it, by design.
  5. objectui tripwire at pin f8a9d0fb (packages/types/src/__tests__/report-chart-query-spec-parity.test.ts:725, true satisfies IsUnknown of JoinedReportBlock, TS1360 against this head's dist) — ESCALATED to the seat, non-blocking for this repo. Verified structurally: the Console Pin Gate runs turbo run build / pnpm run build only, never objectui's tsconfig.test.json, so it cannot red here — and it was skipped on this head by the console filter, so it is NOT MEASURED. AGENTS.md's ship-together rule (Post-Task Add Changesets and GitHub Actions automation #4) covers removals and renames the sibling imports; this is a narrowing whose sibling break is a designed tripwire whose docblock says the failure is the instruction. The seat carries an objectui follow-up (burn down the local JoinedReportBlock interface; ledger at scripts/check-spec-symbol-derivation.mjs:636) with or before the next pin bump.
  6. Concurrency — origin/main moved once on this diff's files since the merge-base: PR feat(spec)!: form layout accepts only vertical | horizontal — the inline and grid arms retired (#20221) #20262 (spec(ui): layout: 'inline' | 'grid' on object-form / FormView parse green and render as vertical — enforce-or-remove (ADR-0049) #20221) landed FormViewSchema.layout at view.zod.ts 4038–4121, disjoint from every hunk here (4895+, 5027+, 5078+, 5292+, 5923+, 6713+, 6750+); none of the other 11 files moved. Open PRs feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #20357 (spec(ui): retire list.tabs and the view container's body name (2 keys); listViews + ViewTabBar and the row name already deliver both #20301) and feat(spec)!: retire the connector resilience family — health (probe + breaker), status and nested webhooks, sixteen keys nothing read (#20273) #20350 (spec(integration): retire the connector health-probe, circuit-breaker, authored status and nested webhooks keys (16), which nothing enforces #20273) share files: ordinary concurrency, the rework head merges main once (the pin count and test-typecheck-debt.json are the collision-prone files if feat(spec)!: retire the connector resilience family — health (probe + breaker), status and nested webhooks, sixteen keys nothing read (#20273) #20350 lands first). Fence held: FormViewSchema.layout, ListViewShapeSchema.tabs and the container body name are untouched.
  7. NOT MEASURED locally by the dev (check:dual-build-cjs-loads, check:type-check-debt, the example-app and workspace typechecks) — answered by CI on this head: Build Core, Type Check · debt ledger, Type Check · workspace, Type Check · consumer gates and Type Check · source gates are all green.

Implemented-by: claude/issue-19920-exported-types-remainder
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: FAIL


Generated by Claude Code

…ames are added

The diff narrows published types and also adds three exported names
(JoinedReportBlockParsed, ViewItemParsed, ViewItemWireParsed), so the line
reads `yes (narrowing)`. The BREAKING banner and the ADR-0087 marker reason
now say that no EXISTING export changes; the marker token is unchanged.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
… shape's types

listOverlayPatchFields() read `type` and `columns` off ListViewShapeSchema
through a cast of its shape to a record of z.ZodTypeAny, which typed both
keys `unknown` on the list overlay member. So `{ object, viewKind: 'list',
columns: 42 }` type-checked as ViewMetadata, ViewMetadataParsed,
AssembledViewArtifact and AssembledViewArtifactParsed while that member
refuses it. The shape is now read as typed; the schemas are the same
objects, so no parse moves.

The item-4 TSDoc sentences on ViewMetadataParsed and
AssembledViewArtifactParsed no longer say the two keys are `unknown`, and
the pin that held them `unknown` is replaced by pins that they are typed,
with four refused-body probes and a runtime half. The changeset names the
fourth narrowing.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Sep 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 777b5097834d92c9e9565878406aa2409c597811
Local-runs: none

Inputs: card #19920 (body and all 13 comments), PR #20369 (body, 12-file list, net diff against merge base 5049a3cf; GitHub's diff and the local git diff 5049a3cf..777b5097 agree at 12 files, +482/−20, and the merge base against today's main 15bf186f is still 5049a3cf), and the 42 check-runs on the head. Read-only throughout: git fetch plus git show / git diff / git grep against refs in the checkout; nothing built, run or re-run.

① Derived judgments

Accept set: none moves — RIGHT. Every source edit is at the type level and leaves the value expression as it was:

  • report.zod.ts: JoinedReportBlockSchema drops its z.ZodTypeAny annotation; the lazySchema factory of strictObject(...) with .superRefine(checkReportOrder) is unchanged. The schema is not recursive and no probe in recursive-schema-input-assertions.ts names it, so nothing pinned the annotation.
  • view.zod.ts: viewItemArmShape and flattenedViewOverlayFields gain a generic parameter with bodies unchanged; flattenedViewOverlayFields is called only with the literals 'list' (:5901) and 'form' (:5930). listOverlayPatchFields() reads ListViewShapeSchema.shape as typed instead of through a cast to a record of z.ZodTypeAny: the same shape.columns.optional() and shape.type.unwrap().optional().meta({ default }) on the same objects (type carries .default('grid') and columns is declared on that shape), so the served JSON Schema and the parse are byte-identical. ViewItemSchema / ViewItemWireSchema gain explicit annotations; VIEW_METADATA_MEMBERS.viewItem gains an assertion to its own type. No parse, default, refinement, key, tombstone or export is removed or renamed. Test Core ×6, Dogfood ×3, Temporal Conformance, Spec property liveness and Build Core are green on the head.

Public surface, each change named:

  • JoinedReportBlock: FROM unknown TO the input type of JoinedReportBlockSchema — narrowing, RIGHT (card site 4, the last of the card's four). Carried to every blocks[] element of Report / ReportParsed (z.array(JoinedReportBlockSchema) at :486) and defineReport's parameter (:596) — RIGHT, all named in the banner. Pinned by joined-report-block-type.test.ts.
  • JoinedReportBlockSchema's declared type: FROM z.ZodTypeAny TO inferred — a published const's type narrows, RIGHT, named in the changeset.
  • ViewItem / ViewItemWire config: FROM unknown TO the arm's own config (typeof ListViewSchema on list, typeof FormViewSchema on form; .describe() returns the same type) — narrowing, RIGHT (remainder 2). Carried to defineViewItem and to the viewItem member of ViewMetadata, ViewMetadataParsed, AssembledViewArtifact, AssembledViewArtifactParsed — RIGHT, named. Pinned by view-item-config-type.test.ts.
  • ViewItemSchema / ViewItemWireSchema declared types: FROM inferred TO an explicit discriminated-union annotation over two non-exported arm aliases — RIGHT as a surface statement. Checked structurally: lazySchema returns its factory's own type, strictObject returns a closedObject of z.object(...).strict() (so $strict), the wire arms end in .strip() (so $strip), the arm alias is the instantiated ReturnType of viewItemArmShape itself, and the wire alias's mapped type is exact because viewItemBaseShape() and viewItemWireFields() (isPinned, sortOrder, columnState) share no key. Full identity with the inferred twin is the dev's Equal probe, not gate-verified; every Type Check lane proves the annotation compiles and the config pins hold, so the annotation is now the contract.
  • Flattened overlay members' viewKind: FROM 'list' | 'form' on both members TO each member's own literal — narrowing on the four union types, RIGHT (remainder 3), named; pinned by view-overlay-viewkind-type.test.ts.
  • Flattened list overlay's type / columns (remainder 5): FROM unknown TO the list shape's own optional types on the list overlay member of the four union types, input and output — narrowing, RIGHT, named as the changeset's fourth bullet; pinned by the IsUnknown tuple, two member probes and four union probes in view-overlay-viewkind-type.test.ts.
  • NEW exports JoinedReportBlockParsed, ViewItemParsed, ViewItemWireParsed on the ui entry — additive growth, RIGHT and forced: typed, each schema carries a default (type with .default('tabular') on the block; the list config's type default on the arm), so input and infer differ and check:spec-parsed-alias rule 2 wants the named Parsed sibling. The three isomorphism pins are deleted with the count restated 786→783 in both prose places and the history (783 pinned + 659 paired = 1,442 bare, consistent). api-surface/ui.json and export-origins/ui.json regenerated (+3 each); no other api-surface ledger lists these names (root.json does not carry JoinedReportBlock), so nothing else owes regeneration; check:generated is green in Lint & Repo Gates.
  • VIEW_METADATA_MEMBERS.viewItem type: unchanged (typeof ViewItemWireSchema) — RIGHT.
  • TSDoc on ViewMetadataParsed / AssembledViewArtifactParsed (remainder 4): the .overwrite(applyListOverlayTypeDefault) default, absent from the output type, type typed as the list shape's enum — doc-only, RIGHT; applyListOverlayTypeDefault is typed T → T, so the sentence is true of the code; pinned by IsOptionalKey in the overlay pin file. The earlier sentence claiming type / columns unknown on that member is gone from both source files and from the PR body.
  • test-typecheck-debt.json: report.test.ts loses TS18046 and TS2571 — consistent with blocks[] now typed; Type Check · debt ledger green. RIGHT.

Nothing derived is wrong or missing.

② Semver level

  • Level minor on @objectstack/spec — RIGHT. A new exported symbol takes at least minor; a published-type narrowing ships minor in the launch window (the fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 / fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260 shape). Not patch, not skip-changeset, no major.
  • Clause-②: yes (narrowing) — RIGHT, in both carriers. The changeset's line 7 and the PR body's line 2 both read exactly Clause-②: yes (narrowing); no Clause-②: no survives anywhere in the body. This is the reader's own case (scripts/pm/clause2-line.mjs: a diff that widens one surface and narrows another, both facts read) and the fleet's precedent for the same shape (.changeset/17779-dashboard-metric-family-single-measure.md, on main). REWORK item 1 holds: the BREAKING banner now says no existing export changes and three parsed-state type names are added, nothing removed or renamed; the ADR-0087 reason says the same; the marker token is unchanged, not-required (no-migration-prescription), and right (nothing objectstack migrate meta can reach). The banner names every narrowed carrier (JoinedReportBlock, Report, ReportParsed, ViewItem, ViewItemWire, the four union types, defineReport / defineViewItem) with FROM → TO per item and the "if your code stops compiling" line. Lint & Repo Gates, which runs check-adr-0087-registration and check-changeset-no-major, is green.
  • The DELIBERATE CORRECTION — still RIGHT and MINIMAL, confirmed on this head. .changeset/19920-exported-types-not-unknown.md moves by exactly one line, +1 / −1: "…is not changed by this change, and still resolves to unknown." becomes "…is not changed by this change. It stops resolving to unknown in its own entry ([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)." Nothing else in the note moves. It still exists on origin/main (15bf186f, blob bce741af) with the original sentence, so the correction is live, not moot, and the new sentence is true in any release that carries either entry. The red's annotations on this head (Check Changeset, job 108811698945) name exactly that file and no other, with the gate's two-class text; the class is DELIBERATE CORRECTION, not COLLISION (the PR's own entry is 19920-exported-types-remainder.md, a different filename), whose prescribed remedy is do NOT restore it, say so on the PR and get it confirmed. The PR body does so in its own section. This record confirms it.
  • Other reds: NONE. 42 check-runs on the head: the only failures are the two Check Changeset runs (the same pull_request-only job across two pr-automation runs on this head). All seven required contexts on main, read from the branch rules (TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, Lint & Repo Gates, Governed Surface Queue Guard), are success. The skips are filter-driven, not red: Build Docs (docs filter), Console Pin Gate (console filter: .objectui-sha, the console scripts, ci.yml, none in the diff), Packed-tarball smoke (opt-in), and Auto Label / Check PR Size on the second run only.

③ Boundary flags

The dev's report is 5864550924 (open_questions: none; 7 deviations; 4 out_of_scope_findings), plus the four REWORK items and the PR body's flags.

  1. REWORK item 3, the single origin/main merge — HOLDS. This round added exactly one merge, 1161545d (parents 35094e877, the FAIL-record head, and 5049a3cf); the other merge in the PR, 04f5aef5, predates 35094e877 and was already in the head the FAIL record reviewed. No conflict; the merge brought PR feat(spec)!: form layout accepts only vertical | horizontal — the inline and grid arms retired (#20221) #20262's FormViewSchema.layout region (+39/−3 in view.zod.ts between 67047171 and 5049a3cf), disjoint from every hunk here. The dev's "not merged a second time" is verified: the 10 commits on main since 5049a3cf touch none of the 12 files, and the merge base is unchanged.
  2. REWORK item 2, remainder 5 — HOLDS. Fixed by the PR body's own measured route (read ListViewShapeSchema.shape as typed); re-measured at +2,729 B per view.zod chunk with 0 TS7056 (the dev's measurement; the chunk table is not gate-verified, and the whole PR leaves the view.zod chunk 1,798 B smaller than the base). The item-4 TSDoc and the pin lines were corrected with it.
  3. REWORK item 4, the report and its pr_body_delta — HOLDS. Every delta row is applied in the body (line 1 Part of #19920, line 2 Clause-②: yes (narrowing), table row 5, "five items", the new Measurements, Reverse verification, Tests and gates, Consumer census, What stays, Acceptance notes); no stale "four items", no old head sha, no Clause-②: no.
  4. Part of #19920 versus Fixes — ANSWERED: Part of is RIGHT as the PR stands. The REWORK's own condition for Fixes was "if no site of the card remains" after a family census; the dev's widened census and probes found three family sites (ApiError.code, ViewFilterRule.operator, the list overlay's options bag), and seat 1's review and release made [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 the family's closing card (5857792889, 5858219255). The Part-of guard and "The card this PR closes must claim this branch" are green. The seat may instead close the card on its four named sites and file the three on a fresh family card; that is a body-line edit plus a card, not a code change, and not blocking either way.
  5. Whether any of the three new sites belongs inside this PR — ANSWERED: NO, each stays on the card for the seat. None meets the adjacent-defect rule the seat applied to remainder 5 (same class, mechanical, unclaimed, same gate family), and each is outside claim 5863213367's file surface, which says stop on breach: ApiError.code lives in api/error-code-ledger.zod.ts / api/contract.zod.ts (a different entry, with in-repo consumers in metadata-protocol and types); ViewFilterRule.operator is a z.preprocess at view.zod.ts:923 whose input type is a contract choice (whether the legacy spellings the preprocess folds stay in the input type), not a mechanical edit; the options bag is listViewKindBlocks() (:5669), a runtime loop over the kinds returning a record of z.ZodTypeAny, whose typed form is a mapped type over the kind blocks with an unmeasured declaration cost. Escalated to the seat for routing only (this card or a new one), non-blocking.
  6. Deviation: annotations and one assertion forced by declaration size, not by a TS error — ANSWERED, accepted, unchanged since the FAIL record: measured (+170,655 B inferred versus −4,527 B annotated, 0 TS7056 either way), the assertion is to the schema's own type, the aliases are non-exported and read off viewItemArmShape so they cannot drift, and each such change is named in the PR body as the claim requires.
  7. Report deviations 1–4 (worktree from the remote branch, merge-first route order, widened census, items 1–4 ablations re-run) — process notes with no contract effect; the history shows no rewrite (35094e877 remains an ancestor of the head).
  8. NOT MEASURED locally by the dev (check:dual-build-cjs-loads, check:type-check-debt, the @objectstack/rest typecheck) — answered by CI on this head: Build Core, Type Check · debt ledger, Type Check · workspace, Type Check · consumer gates and Type Check · source gates are all green.
  9. objectui tripwire at pin f8a9d0fb (report-chart-query-spec-parity.test.ts:725, true satisfies IsUnknown of JoinedReportBlock) — ESCALATED to the seat, non-blocking, unchanged: a designed tripwire whose docblock says the failure is the instruction; the Console Pin Gate builds objectui without __tests__ and was skipped on this head by the console filter, so it is NOT MEASURED here. Carrier: the next objectui spec-pin bump, recorded in the PR's Acceptance notes.
  10. Nit, non-blocking: the PR title names three of the five items; the changeset's title, the line that ships, names all four narrowings. No carrier reads the PR title.
  11. Fence held: FormViewSchema.layout (spec(ui): layout: 'inline' | 'grid' on object-form / FormView parse green and render as vertical — enforce-or-remove (ADR-0049) #20221), ListViewShapeSchema.tabs and the container body name (spec(ui): retire list.tabs and the view container's body name (2 keys); listViews + ViewTabBar and the row name already deliver both #20301) are untouched by every hunk. Docs Drift flags content/docs/releases/v12.mdx as release-owned and read-only; it is not edited here.

Implemented-by: claude/issue-19920-exported-types-remainder
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 06:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 681868c Sep 28, 2026
42 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19920-exported-types-remainder branch September 28, 2026 06:56
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants