Skip to content

fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) - #20260

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-19920-exported-types-unknown
Sep 27, 2026
Merged

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

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #19920

Clause-②: no

Three of the four sites #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 #20238 (#20161), and the dispatch put this site after that PR merges. #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 #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 fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #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 fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #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 ([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)." 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:

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 #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 fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #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

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 fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #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

…(Parsed) name their shapes, not unknown

Each alias was derived from a schema whose own static type erases to
`unknown` (a z.preprocess input, or a union whose members are cast to
z.ZodTypeAny), so any value type-checked against it. Each is now derived
from the member schema the parse actually runs, as ViewMetadata already
is. diagnoseViewMetadata asserts the union's parse output to the new
ViewMetadataParsed, restoring the type the member cast erased; no value
changes. Type pins per site plus a patch changeset.

JoinedReportBlock is untouched: its region is held by an open PR.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
… release carries them

The unreleased ViewMetadata changeset said ViewMetadataParsed "is not
changed by this release: it is still unknown", which this branch makes
false if both land in one release. Both changesets now state what their
own change did, which holds under either release order.

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 a9fb83ef06a079a938c97d1ea5e364705e1bc13b → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json a9fb83ef06a079a938c97d1ea5e364705e1bc13b

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

… 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Confirmed: the deliberate correction of a pending release note. domain:spec seat 1, session_01Rjy9MeetSfq34PKn81CRiN, reviewer of record for this PR, 2026-09-27T17:05Z.

What was corrected

⛔ Do not restore the base text, and ⛔ do not apply skip-changeset. This PR also adds a changeset of its own.

The red check

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f8792c931bf34e113314e158f2edde1b3cfcbecd

① Derived judgments

  • The four aliases are the true shapes — RIGHT. tsc under tsconfig.test.json options over a probe file (archived at review/probes/type-probe.ts): Equal proofs hold for all four. InlineAction = z.input of the pipe's out member, with exactly the 13 picked keys; InlineActionParsed = z.output of that member = z.output of the pipe. ViewMetadataParsed = the union of z.output of the four VIEW_METADATA_MEMBERS. AssembledViewArtifact / AssembledViewArtifactParsed = the union of z.input / z.output of viewItem, listOverlay, formOverlay. Compiler-API flags at head (probes/census.out): the four are not unknown; controls ViewMetadata and InlineActionParsed not unknown; JoinedReportBlock still unknown (flags=2). z.input / z.infer of all three SCHEMAS are still unknown, so no cast moved.
  • InlineAction as the pre-parse shape — RIGHT, and the fold is documented. The out member is a strict .pick(): {} type-checks and parses (type defaults to script); icon / body are refused by type and door alike (unrecognized_keys); type: 'url' with no target is admitted by the type and refused by the door (refinement), which the TSDoc names. Legacy type: 'navigation' / to are compile errors and still fold at the door to type: 'url' + target (probes/runtime-probe.out); TSDoc, changeset banner and pin all say so. The published reference table (content/docs/references/ui/action.mdx, InlineAction section) never listed to or navigation, so the type now matches the published doc.
  • ViewMetadataParsed as the output union — RIGHT, one unnamed gap. The list overlay's .overwrite(applyListOverlayTypeDefault) is not in the type: listOverlay output has type optional (probe A passed) while every parse output carries type: 'grid' (a column-less patch parses to grid, runtime probe). Wider than the runtime output, so sound, but the TSDoc's "defaults applied" over-promises for this one key and neither TSDoc nor changeset names it. Non-blocking.
  • Assembled pair minus container — RIGHT. AssembledViewArtifactSchema is z.union over exactly VIEW_METADATA_BRANCHES minus container (arity 3), refuses a container body, and its outputs deep-equal the member outputs (probes/door-probe.out, runtime-probe.out). Type side: a View-typed value and a ViewMetadata-typed value are not assignable to AssembledViewArtifact (TS2322), every artifact is assignable to ViewMetadata, and a container literal is TS2353.
  • diagnoseViewMetadata assertion — SOUND for every branch. ViewMetadataSchema is preprocess → union .check(focusClaimedBranch()), and the check returns before touching anything when issues.length === 0, so the union's output is the first accepting member's output on the STRIPPED body. Probed one body per member plus a decorated list body (sort[].id) and a column-less patch: diagnosis.data deep-equals ViewMetadataSchema.parse(body) and VIEW_METADATA_MEMBERS[branch].parse(stripped), and branch is the first accepting member, for viewItem, container, listOverlay (three bodies) and formOverlay. In-memory ablation of the assertion: exactly 1 diagnostic, view.zod.ts TS2322 at line 6217.
  • Consumer compile — RIGHT, nothing breaks. git grep -w for the four names plus diagnoseViewMetadata and ViewMetadataDiagnosis. objectstack at head, outside packages/spec: nothing but api-surface / export-origins JSON; inside spec: index.ts, a build-schemas.ts comment, the three zod files and 8 test files. objectui at the pin f8a9d0fb and at origin/main 6fa5f64a: none of the four; only JoinedReportBlock (5 files), untouched and still unknown, so its inverted IsUnknown pin in report-chart-query-spec-parity.test.ts still holds (control at the pin: 15 files name ViewMetadataSchema / VIEW_METADATA_MEMBERS). cloud 48d7066 and origin/main 96eb092: none (control defineStack: 23 files). The Console Pin Gate is skipped at this head by its console path filter (.objectui-sha and console scripts only), so the census is the evidence, and it is clean.
  • Declaration size / TS7056 — RIGHT. pnpm --filter @objectstack/spec build in the review worktree: exit 0, TS7056 ×0. Chunks: view.zod 500,541 B, action.zod 77,606 B, page.zod 305,845 B (the assembled aliases are emitted in the page.zod chunk); total .d.ts + .d.mts 30,836,475 B, byte-identical to the dev's readings. Aliases are emitted verbatim (InlineAction as z.input of the pipe's out; ViewMetadataParsed as z.infer of VIEW_METADATA_MEMBERS[ViewMetadataBranch]), no expansion.
  • Pins — RIGHT, substantive, enforced. All three files are under src/** (in tsconfig.test.json's include) with no test-typecheck-debt.json entry, so any error reds check:test-typecheck. Narrowed tsc over the three pins plus the five test files that call diagnoseViewMetadata, under the test options: 8 errors, all in src/ui/view.test.ts, exactly the ledger's 4 signatures / 8 count; 0 elsewhere. In-memory ablation reverting each alias to its base spelling: InlineAction 4×TS2578, ViewMetadataParsed 3×, AssembledViewArtifact 3×, AssembledViewArtifactParsed 1×, so every @ts-expect-error fires. vitest on the three files: 3 files / 17 tests pass. Ledger diff against base: none; no pinned signature flipped.
  • Corrected pending note — TRUE whichever release carries either entry. Head reads "ViewMetadataParsed is not changed by this change. It is re-derived from the same members, as their output types, by 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)." Both entries are pending on main once this merges and ship together; if a release consumes the fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 entry first, main's deletion wins on merge and the sentence never ships. check-empty-changeset --base origin/main exit 1 names only this file (deliberate correction class), confirmed on the PR in 5857794016.

② Semver level

  • minor + Clause-②: no (narrowing) + BREAKING banner + adr-0087: not-required (no-migration-prescription) — RIGHT, and identical in form to the merged fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 changeset (119a02bc). check-changeset-no-major.mjs: launch-window convention, a breaking change ships as minor and the banner plus the disposition are the carriers; run at head, exit 0, "no major bump". check-adr-0087-registration.mjs --base origin/main: exit 0, reads [BREAKING+clause-②-narrowing] not-required (no-migration-prescription); the "If your code stops compiling" paragraph does not trip hasMigrationPrescription (prose, no framed rewrite), as fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919's did not. type-surface-only is closed by its predicate 2 (the diff touches packages/spec/**). fix: with minor raises the bump, never lowers it.
  • FROM → TO per name — ACCURATE. Base e0f17a37 spellings were exactly z.input / z.infer of the schemas (the bare-alias population diff base → head removes exactly InlineAction and AssembledViewArtifact); head spellings match the table; "a container body is now a compile error here, as it always was at the schema" verified on both sides; "JoinedReportBlock … still resolves to unknown" verified (flags=2).
  • Body versus changeset: the PR body's Clause-②: no lacks the (narrowing) arm the changeset carries. check-changeset-no-major's level axis reads the body: a bare no stands the axis down (exit 0); no (narrowing) would enforce and still pass, since the changeset is minor. Green either way, but fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919's body carried no (narrowing) and the seat's own ACCEPT names the arm, so a one-line body edit is owed by the seat before readying. Not blocking.

③ Boundary flags

  • JoinedReportBlock: the remainder, untouched, still unknown at head (compiler flags); PR fix(spec): a joined report draws no chart — retire blocks[].chart and refuse a joined container chart (#20161) #20238 is still an open draft at 4aecf01d at this reading. The dev's measured fork (+15.8 KB report.zod.d.ts, no TS7056 with the annotation removed) is recorded on the PR for the taker; not re-measured here.
  • ViewItem / ViewItemWire config: unknown — CONFIRMED by probe on the input and output of the viewItem member (viewItemArmShape(viewKind, config: z.ZodTypeAny); zod 4.6.1's ZodTypeAny defaults to unknown). So a viewKind: 'list' body with config: 42 type-checks as ViewMetadataParsed and AssembledViewArtifact while the door refuses it. Stays on the card as the second remainder, as the seat ruled.
  • NEW, same family, named nowhere: both flattened-overlay members' static viewKind is 'form' | 'list' on input and output, because flattenedViewOverlayFields(kind: 'list' | 'form') is not generic and z.enum([kind]) widens to the parameter type. Effect: { type: 'grid', columns: ['name'], object, viewKind: 'form' } type-checks as AssembledViewArtifact, AssembledViewArtifactParsed, ViewMetadata and ViewMetadataParsed (through the list member) while both doors refuse it (list member: arm mismatch; form member: type and columns). Pre-existing since PR fix(spec)!: a flattened view overlay is judged by the member its viewKind names (#20186) #20245 (a91d12af), inherited by fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919's ViewMetadata too, and outside this claim's fence ("⛔ not the flattened-overlay members"). One-line fix in the fix(spec)!: a flattened view overlay is judged by the member its viewKind names (#20186) #20245 region: make flattenedViewOverlayFields generic in K extends 'list' | 'form'. Fold into the card's closing findings beside config: unknown. Not blocking.
  • The .overwrite() list type default absent from the output type (①): sound but unnamed; a TSDoc sentence on ViewMetadataParsed / AssembledViewArtifactParsed closes it. Non-blocking.
  • check:spec-parsed-alias: exit 0 at head; population 1443 → 1441 bare, 657 → 655 paired, because its STATE_ALIAS regex reads only the z.input of typeof X; spelling. Both …Parsed siblings exist; the gate no longer sees the pairs for InlineAction and AssembledViewArtifact, as for ViewMetadata since fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919. Acceptance note; a widened recogniser is a follow-up, not owed here.
  • AssembledViewArtifactSchema accepts { name, object, viewKind: 'list' } (a content-less list patch) that ViewMetadataSchema refuses at the identity precondition, because the assembled union has no preprocess. Pre-existing runtime posture, untouched by this PR; observed, not filed.
  • Advisory Check Changeset red: the deliberate correction of .changeset/view-metadata-type-not-unknown.md, confirmed in 5857794016. The job runs on pull_request only and is not among main's ruleset required contexts (TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard).
  • CI at f8792c93, final state read 2026-09-27T17:04Z: 35 check-runs, 29 success, 5 skipped by filter (Console Pin Gate, Build Docs, Check PR Size, Auto Label, Packed-tarball smoke), 1 failure (Check Changeset, above), 0 running; every required context green (Test Core rollup completed success). main moved 7 commits past base e0f17a37, none touching this diff's files; git merge-tree against origin/main a9fb83ef is clean.

Implemented-by: claude/issue-19920-exported-types-unknown
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 17:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 17bd318 Sep 27, 2026
47 of 51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19920-exported-types-unknown branch September 27, 2026 17:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ind carry the shapes their doors accept (objectstack-ai#19920) (objectstack-ai#20369)

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

This PR takes the four remainder items that seat 1's release on objectstack-ai#19920
(comment 5858219255) lists, plus remainder 5 (the flattened list
overlay's `type` and `columns`), on `main` after PR objectstack-ai#20286. objectstack-ai#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
objectstack-ai#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 note.** `.changeset/19920-exported-types-not-unknown.md` is PR
objectstack-ai#20260's pending entry for this card. It ended "`JoinedReportBlock` is
not changed by this change, and still resolves to `unknown`."
- **What this PR changes under it.** Item 1 makes the second half of
that sentence false in any release that carries both entries.
- **The rewrite.** The sentence now reads "`JoinedReportBlock` is not
changed by this change. It stops resolving to `unknown` in its own entry
(objectstack-ai#19920)." That holds whichever release carries either entry. Nothing
else in the note moves.
- **What the gate says.** `node scripts/check-empty-changeset.mjs --base
origin/main` exits 1, naming exactly this file, in its DELIBERATE
CORRECTION class. Following the gate's prescription, the file is not
restored from the base, which would put the false sentence back.
- **What happens next.** It stays red until someone confirms the
correction here. If a release consumes that entry before this PR lands,
the merge keeps `main`'s deletion and the correction is moot.

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 objectstack-ai#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

- **The pin count holds at 783** (`check:spec-parsed-alias`: 1,442 bare
`z.input` aliases, 783 pinned isomorphic, 659 paired).
- **objectui tripwire, carried by the seat.**
`packages/types/src/__tests__/report-chart-query-spec-parity.test.ts` at
objectui pin `f8a9d0fb` asserts that `JoinedReportBlock` is `unknown`
(line 725). It goes red at the next objectui spec-pin bump, and its
docblock says that failure is the instruction to re-run the triage and
burn down the local interface. Carrier: the next objectui spec-pin bump.
It cannot red this repo: the Console Pin Gate builds objectui without
its `__tests__`.
- **Fence held.** `FormViewSchema.layout` (objectstack-ai#20221),
`ListViewShapeSchema.tabs` and the view container's body `name` (objectstack-ai#20301)
are untouched. `origin/main` `5049a3cf` was merged once, at `1161545d`,
with no conflict; commits that landed on `main` later touch none of this
PR's files.

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

---------

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

Development

Successfully merging this pull request may close these issues.

2 participants