Skip to content

Commit 94f373d

Browse files
committed
docs(psl-relation-syntax): reflect D1 reversal — clean break, reject legacy, no format rewrite
D1 reversed (operator decision, 2026-06-26): the directional vocabulary (from/to/through/inverse) is the only accepted @relation syntax. Legacy fields:/references: and @relation(name:) are rejected at parse time with a guiding diagnostic. The format canonicalisation pass was removed. The repo migration is byte-identical; a downstream codemod is deferred (TML-2957). Updates design-notes.md (D1, D3, principles, alternatives, trade-offs) and spec.md (at-a-glance, non-goals, contract-impact, cross-cutting requirements, transitional-shape, project DoD, open questions). Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 256a7dd commit 94f373d

2 files changed

Lines changed: 32 additions & 46 deletions

File tree

projects/psl-relation-syntax/design-notes.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
> Synthesized design document for `psl-relation-syntax`. Read this if you want to understand **what the project's design is**, **what principles it serves**, and **what alternatives were considered and rejected**. This document is not a chronological log of decisions — it captures the settled design, standing independently of the discussions that produced it.
44
>
5-
> Owned by the Orchestrator. Authored directly. **Status: grammar settled (D1–D5) via `drive-discussion`, 2026-06-24. Slice-level mechanics deferred to slice specs.**
5+
> Owned by the Orchestrator. Authored directly. **Status: grammar settled (D1–D5) via `drive-discussion`, 2026-06-24; D1 reversed to a clean break on 2026-06-26 (operator decision — legacy input acceptance and the `format` rewrite removed). Slice-level mechanics deferred to slice specs.**
66
77
## Principles this design serves
88

99
- **Directionality reads naturally** — a relation is "from these local fields to that referenced key"; the vocabulary says so.
1010
- **Omit what can be inferred** — single fields need no brackets; a referenced `@id` needs no explicit `to:`; an unambiguous junction is named on one end only.
11-
- **No silent break** — legacy `fields:`/`references:`/`@relation(name:)` keep parsing; they are input-only and never survive a round-trip.
11+
- **Clean break**the directional vocabulary is the only accepted syntax; legacy `fields:`/`references:`/`@relation(name:)` are rejected with a guiding diagnostic. A reusable codemod for downstream users is deferred (TML-2957).
1212
- **Disambiguate by pointing, not by naming** — replace the free-floating `@relation(name: "...")` string with a direct reference to the relation field.
1313
- **Explicit over magic** — junction synthesis fires only when there is genuinely no junction to find; an authored junction is never silently ignored.
1414

@@ -23,15 +23,15 @@ Canonical vocabulary on `@relation`:
2323

2424
### Decisions
2525

26-
- **D1 — Single canonical keyword spelling; legacy input-only.** The whole toolchain always emits `from`/`to`/`through`/`inverse`. `fields:`/`references:`/positional-and-`name:` parse on input but never survive a round-trip. Both emit surfaces — the CST `format` command (`@prisma-next/psl-parser`'s `format/`) and the AST printer (`@prisma-next/psl-printer`) used by `contract infer` — render the canonical spelling. The `@relation` grammar is generic (named args scanned by string), so **no parser/grammar change is needed to accept the new keywords**.
27-
- **D2 — Retire `@relation(name:)` entirely.** Disambiguation is by pointing: `from`/`to` (FK declaration), `through:` (M:N junction side), `inverse:` (1:N back side). One mechanism per case; no string survivor across cardinalities.
28-
- **D3 — Conservative canonicalisation: keyword migration only.** `format`'s CST normalize pass swaps the argument *name* token (`fields``from`, `references``to`) and preserves everything else — values, brackets, redundant `Model.` qualifiers, comments/trivia. It does **not** drop inferable args or strip qualifiers ("single canonical" governs keywords, not inference depth). Aggressive normalisation is a possible future, out of scope here.
26+
- **D1 — Clean break: `from`/`to`/`through`/`inverse` only; legacy rejected.** The directional vocabulary is the sole accepted `@relation` syntax. Legacy `fields:`/`references:` and `@relation(name:)` are rejected at parse time with a guiding diagnostic (`PSL_LEGACY_FIELDS_REFERENCES` / `PSL_LEGACY_NAME`) that directs authors to the replacement. The `format` command no longer rewrites relation keywords — it was removed when legacy acceptance was dropped, since `format` cannot operate on now-invalid syntax. The `contract infer` AST printer (`@prisma-next/psl-printer`) renders the canonical spelling. The repo's own schemas (SQL and Mongo families) are migrated in-stack; a reusable downstream codemod is deferred (TML-2957). *(Reverses the original D1, which kept legacy as input-only with a `format`-based auto-rewrite. Operator decision, 2026-06-26.)*
27+
- **D2 — Retire `@relation(name:)` entirely.** Disambiguation is by pointing: `from`/`to` (FK declaration), `through:` (M:N junction side), `inverse:` (1:N back side). One mechanism per case; no string survivor across cardinalities. Legacy `name:` is rejected at parse time (per D1 clean break), not merely dropped from output.
28+
- **D3 — (Removed.)** The conservative `format` canonicalisation pass (keyword migration only) was built in S1·M2 and then removed when D1 was reversed — `format` no longer rewrites relations. The CST formatter still exists for layout normalisation; it simply has no relation-keyword rewrite.
2929
- **D4 — `through:` on one end; infer the inverse.** Unambiguous M:N: one navigable end declares `through: Junction`, the other's inverse list is inferred. Ambiguous (self-relation / multiple M:N between the same models): both ends declare and disambiguate via `through: Junction.relationField`.
30-
- **D5 — Bare-list precedence (backward-compatible).** Resolving a bare list (`Tag[]`): (1) other end has `through:` → this is its inferred inverse; (2) both ends bare **and** a junction model links them → recognise that authored junction (preserves shipped slice-5 behaviour); (3) both ends bare **and** no junction model → synthesise a model-less junction table (implicit M:N).
30+
- **D5 — Bare-list precedence.** Resolving a bare list (`Tag[]`): (1) other end has `through:` → this is its inferred inverse; (2) both ends bare **and** a junction model links them → recognise that authored junction (preserves shipped slice-5 behaviour); (3) both ends bare **and** no junction model → synthesise a model-less junction table (implicit M:N).
3131

3232
### Provisional slice slate
3333

34-
1. `from`/`to` FK foundation: accept legacy as input, resolver reads new keywords, CST `format` normalize pass + AST printer emit canonical, validation. FK relations only.
34+
1. `from`/`to` FK foundation: reject legacy `fields:`/`references:` at parse time, resolver reads `from`/`to` with `to:` inference, AST printer emits canonical, repo-wide migration to `from`/`to`. FK relations only.
3535
2. `through: Junction` explicit M:N (one-end declare + inverse inference, D4 unambiguous case).
3636
3. `through: Junction.relationField` + `inverse:` disambiguation (D4 ambiguous case + 1:N back-relation; retires `name:`).
3737
4. Implicit M:N — synthesise a model-less junction table + its migrations (D5 case 3).
@@ -41,10 +41,10 @@ Sequencing: 1 is the foundation; 2 → 3 sequential; 4 and 5 build on 2 (and 3 w
4141

4242
## Alternatives considered
4343

44-
- **Hard break from `fields:`/`references:`** — drop the Prisma spelling entirely. **Rejected because:** forces every downstream PSL author to migrate at once; parse-both + canonical-emit is a strict superset at little cost.
44+
- **Parse-both + canonical-emit (the original D1).** Legacy `fields:`/`references:`/`name:` would keep parsing as input-only, with `format` rewriting to canonical and the printer emitting canonical. **Rejected because:** the operator preferred a clean break — the prototype is pre-1.0, breaking changes are acceptable, and carrying a legacy dialect plus a `format`-based auto-migration that can't operate on now-invalid syntax adds surface area for little benefit. *(Reversal decision, 2026-06-26.)*
4545
- **Keep `@relation(name: "...")` for disambiguation.** **Rejected because:** the name is a free-floating token kept in sync across fields by convention; pointing at the relation field is direct and self-checking.
4646
- **`via:` for the 1:N back-relation pointer.** **Rejected because:** homophone of `through`, and `through` wrongly implies an intermediary where a 1:N back-relation is simply the inverse of one FK. `inverse:` is exact (Doctrine/JPA `inversedBy`/`mappedBy` precedent).
47-
- **Aggressive canonicalisation** (formatter drops inferable args / strips qualifiers). **Rejected (for now) because:** larger formatter with semantic schema-checks; deferred until there's evidence it's wanted.
47+
- **Aggressive canonicalisation** (formatter drops inferable args / strips qualifiers). **Rejected because:** the `format` rewrite was removed entirely when D1 was reversed, so there is no canonicalisation pass to make aggressive. The formatter does layout only.
4848
- **`through:` required on both ends.** **Rejected because:** inferring the inverse honours omit-what's-inferable; explicit-both is reserved for genuine ambiguity.
4949
- **"Both bare always synthesises" (literal).** **Rejected because:** it would silently ignore an authored junction model and break shipped slice-5 behaviour; D5's precedence synthesises only when no junction exists.
5050

@@ -55,16 +55,18 @@ _Deferred to slice specs; each carries a working position so execution proceeds
5555
- **Implicit-M:N synthesis mechanics** (slice 4) — synthesised table/column naming (Prisma's `_AToB`/`A`/`B`, or our own convention) and migration/DDL threading. Working position: mirror Prisma's convention unless DDL threading argues otherwise.
5656
- **Arrow-path grammar** (slice 5) — exact `a -> J.b -> J.c -> T.d` tokenisation + validation. Working position: a distinct lowering from implicit M:N (arrow-path keeps an authored junction *model* with scalar columns; implicit authors none).
5757
- **Diagnostics** — wording for the new arguments, and the "you authored a junction but never referenced it" guard implied by D5's case (2)/(3) boundary.
58-
- **`to:` value grammar** — accepts bare field / bracketed list; redundant `Model.` qualifier tolerated and preserved. Cross-model qualified paths are arrow-path territory, not slice 1.
58+
- **`to:` value grammar** — accepts bare field / bracketed list; a redundant `Model.` qualifier (`to: Post.id`) works via the member-access value grammar landed in slice 3. Cross-model qualified paths are arrow-path territory.
5959

6060
## Accepted trade-offs
6161

62-
- Two **input** dialects exist permanently (legacy + canonical); only **output** is single-dialect.
62+
- **Clean break migration cost.** Downstream PSL authors with legacy schemas must migrate manually (a codemod is deferred as TML-2957). The repo's own ~40 legacy sites are migrated in-stack, byte-identically (the emitted contracts are unchanged — only the PSL spelling moves).
63+
- **The `format` command no longer rewrites relation keywords.** It still normalises layout (indent, newlines) but does not touch `@relation` argument names. Legacy syntax is rejected at parse time, so there is nothing to canonicalise.
6364

6465
## References
6566

6667
- Project spec: [`./spec.md`](./spec.md)
6768
- Project plan: [`./plan.md`](./plan.md)
6869
- Straw-man: `wip/mn-psl-changes.diff`
6970
- Sibling project: `projects/sql-orm-many-to-many/` (runtime M:N; retains slice 7 / TML-2933, non-id unique junction targets)
70-
- Primary surfaces: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`; `packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`; `@prisma-next/psl-parser` (`src/format/`, generic attribute grammar); `@prisma-next/psl-printer` (AST printer for `contract infer`); CLI `format` command (`packages/1-framework/3-tooling/cli/src/commands/format.ts`).
71+
- Primary surfaces: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`; `packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`; `@prisma-next/psl-parser` (generic attribute grammar; member-access value parsing); `@prisma-next/psl-printer` (AST printer for `contract infer`); `packages/2-mongo/2-authoring/mongo-family/src/psl-helpers.ts` (Mongo relation parsing).
72+
- Deferred codemod: **TML-2957** (automated legacy → `from`/`to`/`through`/`inverse` migration for downstream users).

0 commit comments

Comments
 (0)