Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/spec-type-alias-parsed-convention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
"@objectstack/spec": minor
---

feat(spec): name the parsed state `XParsed` on every schema that has one (ADR-0122, #5551)

A Zod schema denotes two types — `z.input` (what an author writes: defaulted keys
optional, pre-transform) and `z.infer` (what `.parse()` returns) — and `packages/spec`
has been naming them two different ways with nothing written down about which is which.
Measurement on `origin/main`: **1384** bare aliases mean the parsed state, **86** mean
the author state, and three separate first-hand sources each described the 8-file
minority as "the house convention". No ADR recorded either spelling.

**[ADR-0122](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0122-schema-type-alias-naming-convention.md)
settles it: the bare name `X` is the AUTHOR state, `XParsed` is the PARSED state.** The
deciding argument is the keystroke every author writes first — `const c: Connector = { … }`
— which should be correct by default in every domain, without knowing which file you are in.

**This release is phase 1, and it is purely additive. Nothing is renamed or removed;
no existing annotation stops compiling.** It declares `XParsed` for the **657** aliases
whose schema genuinely has two distinct shapes, so that every consumer whose meaning
phase 2 will change already has a name to move to:

```ts
// before — one name, meaning the parsed state
export type Connector = z.infer< typeof ConnectorSchema >;
export type ConnectorInput = z.input< typeof ConnectorSchema >;

// after — the parsed state also has a name that will keep meaning it
export type Connector = z.infer< typeof ConnectorSchema >;
export type ConnectorParsed = z.infer< typeof ConnectorSchema >; // new
export type ConnectorInput = z.input< typeof ConnectorSchema >; // unchanged
```

Schemas whose `z.input` and `z.infer` are the *same* type (enums, plain unions, objects
with no defaults or transforms anywhere in their tree) deliberately get **no** `XParsed`
— a permanent synonym is a name you can only pick wrongly. All 718 of them are pinned
with compile-time assertions so the exemption cannot rot silently when one later gains
a `.default()`.

One name to note if you are upgrading across protocol 17: `FieldMapping` does **not**
gain a `FieldMappingParsed`. #5552 retired `FieldMapping.transform` and the whole
`FieldMappingTransform` union in the same release, and that key was the only reason the
schema had two shapes — so it is now isomorphic, and under this convention it correctly
keeps exactly one name.

**What to do now (optional, and cheap).** If you hold the result of a `.parse()` — or of
a `defineX()` factory, which returns it — move that annotation to `XParsed`:

```ts
-const c: Connector = ConnectorSchema.parse(raw);
+const c: ConnectorParsed = ConnectorSchema.parse(raw);
```

Annotations on values you *write* need no change now and will be correct after phase 2.
Doing nothing is also fine until then.

**What comes next.** Phase 2 flips the bare names to `z.input` and ships in a major, with
its own changeset and migration notes. `XInput` aliases are untouched by this release and
their fate is decided then.
18 changes: 18 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,24 @@ jobs:
- name: Merge-driver wiring gate
run: pnpm check:merge-driver

# ADR-0122 type-alias convention gate (#5551). `packages/spec` spelled the
# same idea two ways for its whole life — 1384 bare aliases meaning the
# PARSED state, 86 meaning the AUTHOR state — and three first-hand sources
# each called the 8-file minority "the house convention". ADR-0122 settles
# it (bare = author state, `XParsed` = parsed state) and lands phase 1
# additively: an `XParsed` wherever the parsed state is a distinct type, so
# the major-window flip of the bare names has a migration target everywhere
# it changes meaning. This gate is what stops that coverage from decaying —
# a new bare `z.infer` alias must either declare its `XParsed` or be pinned
# isomorphic in `packages/spec/src/type-alias-convention.pin.test.ts`, where
# tsc proves the exemption rather than a comment asserting it. Static text
# scan, no build needed, so it belongs in this job. Runs its own
# --self-test first: the detector can be broken while every alias is fine,
# and a scan that stops matching would report OK while reading nothing
# (#4868's family).
- name: Spec type-alias convention gate (ADR-0122)
run: pnpm check:spec-parsed-alias

typecheck:
name: TypeScript Type Check
runs-on: ubuntu-latest
Expand Down
Loading
Loading