You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: projects/psl-relation-syntax/design-notes.md
+14-12Lines changed: 14 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,13 +2,13 @@
2
2
3
3
> 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.
4
4
>
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.**
6
6
7
7
## Principles this design serves
8
8
9
9
-**Directionality reads naturally** — a relation is "from these local fields to that referenced key"; the vocabulary says so.
10
10
-**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).
12
12
-**Disambiguate by pointing, not by naming** — replace the free-floating `@relation(name: "...")` string with a direct reference to the relation field.
13
13
-**Explicit over magic** — junction synthesis fires only when there is genuinely no junction to find; an authored junction is never silently ignored.
14
14
@@ -23,15 +23,15 @@ Canonical vocabulary on `@relation`:
23
23
24
24
### Decisions
25
25
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.
29
29
-**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).
31
31
32
32
### Provisional slice slate
33
33
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.
3.`through: Junction.relationField` + `inverse:` disambiguation (D4 ambiguous case + 1:N back-relation; retires `name:`).
37
37
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
41
41
42
42
## Alternatives considered
43
43
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.)*
45
45
-**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.
46
46
-**`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).
-**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.
48
48
-**`through:` required on both ends.****Rejected because:** inferring the inverse honours omit-what's-inferable; explicit-both is reserved for genuine ambiguity.
49
49
-**"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.
50
50
@@ -55,16 +55,18 @@ _Deferred to slice specs; each carries a working position so execution proceeds
55
55
-**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.
56
56
-**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).
57
57
-**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.
59
59
60
60
## Accepted trade-offs
61
61
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.
0 commit comments