Skip to content

Commit d969acd

Browse files
chore: close out migration-graph-rendering project (TML-2746) (#775)
## Close-out: migration-graph-rendering Closes the `migration-graph-rendering` project (TML-2746). It began as a redesign of `migration graph`'s renderer and broadened into a revamp of the whole interrogative migration **read-command family** (`list` / `graph` / `status` / `log`) on a shared renderer + ledger foundation. All work has shipped; this PR migrates the durable knowledge into `docs/` and deletes the transient project scaffolding. ### Definition of Done — verified | Outcome | Evidence | |---|---| | Tier-3 renderer rebuilt (line/plane/occlusion) | #762 | | Back-arc convergence, configurable geometry, greedy lane colouring, layout fixes | #767 | | Ledger foundation (per-migration journal) | #665 | | `list`/`status`/`log` revamped on the shared renderer; dagre + `list --graph` retired | shipped; verified — `migration-list/status/log/graph.ts` use the shared renderer; no `dagre`/`tree-render`/`layout` renderers remain | | Read-command consistency (TML-2801) | re-validated this PR: 4/7 findings resolved, 2 partial, 1 open (4 small follow-ups noted below) | | Showcase real-world golden | on `main` | No unmet acceptance criteria. External-reference scan for `projects/migration-graph-rendering/` is empty (reference-strip step was a no-op). ### Durable knowledge migrated to `docs/` - **ADR 227** — Migration read commands share one graphical renderer with command-specific annotations. - **ADR 228** — The migration apply ledger is a per-migration journal. - **ADR 229** — The migration graph renderer uses a line/plane/occlusion model (the renderer's internal design — lines as the primitive, single-owner cells, occlusion over blended glyphs). All three verified against shipped code. - **`docs/reference/Migration Graph Visual Language.md`** — the glyph/layout vocabulary the renderer draws from (was the project's `mockups.md`). The read-command consistency audit was **re-validated** against current code (verdict: largely accomplished — 4/7 findings resolved, 2 partial, 1 open) and captured as a Linear follow-up ticket (**TML-2877**, related to TML-2801) rather than a committed doc, since what remains is actionable backlog, not long-lived reference. Transient artefacts (spec, plan, slice specs/plans/reviews, `decisions.md` — now ADR'd, `learnings.md`, the followups draft, `trace.jsonl`, prototype, the audit doc) deleted with the folder / moved to Linear. ### Follow-ups (tracked, not in this PR) - **TML-2877** — the four remaining read-command consistency items (show `--space` policy, log unscoped-semantics doc, check see-also, show/check decoration flags, + a parity-test extension). - **PR #773** — the demo fixture no-op self-edge fix + offline integrity guard. ### Retro — lessons - **A wholesale rewrite obsoletes fine-grained bug-slices.** Three glyph-bug slice specs (tee/marker bugs) were made moot by the line/plane/occlusion rewrite — they targeted deleted code. Re-triage the backlog after a rewrite; don't carry dead slices. - **Hand-authored goldens beat auto-snapshots for correctness.** `toMatchSnapshot()` self-certifies whatever the renderer emits; the hand-authored `golden-pipeline` oracle caught a convergence regression the snapshots happily recorded as "correct." - **Real-world fixtures expose layout bugs unit fixtures miss.** Validating against the `showcase` graph surfaced four distinct layout/colour bugs (disconnected-component interleave, asymmetric-diamond merge lane, trunk-continuation, greedy-colour wraparound) that the simple scenarios never hit. - **`@prisma-next/cli` runs vitest with `isolate: false`** — "passes locally" ≠ passes in CI (parallel state pollution). Candidate for a durable testing note. - **`fixtures:emit` can emit an integrity-failing fixture** — the emitter and `migration check` disagreed (a hash-collapse produced a no-op self-edge). Landed an offline demo integrity guard (#773). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Will Madden <madden@prisma.io>
1 parent ec5f7df commit d969acd

48 files changed

Lines changed: 402 additions & 4853 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/architecture docs/ADR-INDEX.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ This document provides a comprehensive index of all Architectural Decision Recor
8989
| 154 | Component-owned database dependencies | Sets component-owned verification as the target architecture; v1 uses adapter-owned ID-presence checks as a temporary compromise | [ADR 154 - Component-owned database dependencies.md](adrs/ADR%20154%20-%20Component-owned%20database%20dependencies.md) |
9090
| 161 | Explicit foreign key constraint and index configuration | Adds two independent knobs (`foreignKeys.constraints`, `foreignKeys.indexes`) to control FK constraint and FK-backing index emission in migration DDL | [ADR 161 - Explicit foreign key constraint and index configuration.md](adrs/ADR%20161%20-%20Explicit%20foreign%20key%20constraint%20and%20index%20configuration.md) |
9191
| 166 | Referential actions for foreign keys | Adds optional `onDelete` / `onUpdate` action semantics to foreign keys and Postgres planner DDL emission (`ON DELETE` / `ON UPDATE`) | [ADR 166 - Referential actions for foreign keys.md](adrs/ADR%20166%20-%20Referential%20actions%20for%20foreign%20keys.md) |
92+
| 227 | Migration read commands share one graphical renderer with command-specific annotations | `migration list`, `graph`, and `status` all draw the same condensed tree; commands diverge only in per-edge `MigrationEdgeAnnotation` overlays keyed by `migrationHash`. Dagre deleted. Trunk = live-contract chain. `@contract` is app-space-only. Machine output stays flat. | [ADR 227 - Migration read commands share one graphical renderer with command-specific annotations.md](adrs/ADR%20227%20-%20Migration%20read%20commands%20share%20one%20graphical%20renderer%20with%20command-specific%20annotations.md) |
93+
| 228 | Migration apply ledger is a per-migration journal | One ledger row per applied edge (`space` + `migrationName` + `migrationHash` + `from`/`to` + `operationCount` + `appliedAt`). `migration status` reads it for applied/pending classification; `migration log` reads the unscoped flat table as the real apply history. | [ADR 228 - Migration apply ledger is a per-migration journal.md](adrs/ADR%20228%20-%20Migration%20apply%20ledger%20is%20a%20per-migration%20journal.md) |
94+
| 229 | Migration graph renderer uses a line/plane/occlusion model | The renderer is modelled around lines (not cells): each edge is a routed line carrying its own identity and colour, cells hold a z-ordered stack of lines, and the topmost line is drawn while the rest are occluded. Layout guarantees one drawable owner per cell (no tees, two columns per lane), so colour is correct by construction with no junction logic. | [ADR 229 - Migration graph renderer uses a line-plane-occlusion model.md](adrs/ADR%20229%20-%20Migration%20graph%20renderer%20uses%20a%20line-plane-occlusion%20model.md) |
9295

9396
## Preflight & CI
9497

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# ADR 227 — Migration read commands share one graphical renderer with command-specific annotations
2+
3+
## Status
4+
5+
Accepted. Builds on [ADR 039 — Migration graph path resolution & integrity](./ADR%20039%20-%20Migration%20graph%20path%20resolution%20%26%20integrity.md) and [ADR 218 — Refs with paired contract snapshots](./ADR%20218%20-%20Refs%20with%20paired%20contract%20snapshots%20and%20universal%20graph-node%20invariant.md).
6+
7+
## A worked example
8+
9+
A contract space has two migrations: `init` brings the empty contract to `ef9de27`, and `add_email` brings `ef9de27` to `a94b7b4`. The database is sitting at `ef9de27`; the app currently emits `a94b7b4`.
10+
11+
Three commands let you look at that state, and they all draw the *same* picture — contracts as `` nodes, migrations as labelled edges, newest at the top, the empty contract at the bottom:
12+
13+
```text
14+
migration graph migration list migration status
15+
───────────────── ───────────────── ─────────────────
16+
○ a94b7b4 (@contract) ○ a94b7b4 ○ a94b7b4 (@contract)
17+
│↑ add_email │↑ add_email 2 ops │↑ add_email ⧗ pending
18+
○ ef9de27 (@db) ○ ef9de27 ○ ef9de27 (@db)
19+
│↑ init │↑ init 1 op │↑ init ✓ applied
20+
○ ∅ ○ ∅ ○ ∅
21+
```
22+
23+
The skeleton is identical. What differs is what each command *writes onto* it: `graph` labels the ref/marker nodes, `list` annotates each edge with its package facts (operation counts, invariants), and `status` annotates each edge with whether it has run against the live database.
24+
25+
## Decision
26+
27+
There is one graphical renderer for migration topology. `migration list`, `migration graph`, and `migration status` all call it. They differ only in the per-migration annotations they hand it — never in how the tree itself is laid out.
28+
29+
The renderer takes the graph topology and produces the lane geometry, the gutter, and the node placement entirely from the topology. Everything command-specific arrives as a sparse annotation map:
30+
31+
```ts
32+
// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-labels.ts
33+
export interface MigrationEdgeAnnotation {
34+
readonly status?: 'applied' | 'pending';
35+
readonly operationCount?: number;
36+
readonly invariants?: readonly string[];
37+
readonly pathHighlight?: 'on-path' | 'off-path';
38+
}
39+
```
40+
41+
A command builds a `ReadonlyMap<string, MigrationEdgeAnnotation>` keyed by `migrationHash`, populates only the keys it cares about, and passes it to the renderer. The renderer draws whatever keys are present and leaves the rest of the row plain.
42+
43+
## How it works
44+
45+
### The annotation map is sparse and additive
46+
47+
Each command owns a disjoint slice of `MigrationEdgeAnnotation`:
48+
49+
- **`migration graph`** adds no edge annotations. It annotates *nodes* — refs and the `@contract`/`@db` markers.
50+
- **`migration list`** sets `operationCount` and `invariants` (the facts about each migration package on disk), plus ref node overlays.
51+
- **`migration status`** sets `status: 'applied' | 'pending'` on each edge, plus the `@db` node marker.
52+
- **`migrate --show`** sets `pathHighlight: 'on-path' | 'off-path'`, which the renderer draws as a focus mode — the chosen path bright, everything else dimmed.
53+
54+
Because every command writes only its own keys, their annotations compose without conflict: `migration status` overlays its applied/pending information on top of the list's package facts by merging the two maps before rendering.
55+
56+
```ts
57+
// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-space-render.ts
58+
export function mergeMigrationEdgeAnnotations(
59+
listOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
60+
statusOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
61+
): ReadonlyMap<string, MigrationEdgeAnnotation>
62+
```
63+
64+
Adding a new kind of annotation later is a matter of adding a field; commands that don't set it are unaffected.
65+
66+
### The trunk is always the live contract
67+
68+
A space's history is rarely a single line — branches, abandoned chains, and refs pointing at older states all coexist. The renderer has to choose which chain runs straight up the left as the trunk and which render as indented side-branches.
69+
70+
That choice is fixed: the trunk is the chain containing the **live contract**the contract the app currently emits, the same one `migrate` advances toward when you give it no target. It is supplied to the renderer as `liveContractHash`:
71+
72+
```ts
73+
// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-space-render.ts
74+
export interface RenderMigrationGraphSpaceTreeInput {
75+
readonly liveContractHash: string;
76+
readonly isAppSpace?: boolean; // default true; false suppresses @contract in extension spaces
77+
// …
78+
}
79+
```
80+
81+
Anchoring on the live contract makes the picture mean "where the app's code thinks the schema is"the reference frame an author actually works in. All three commands pass the same `liveContractHash`, so a given space looks the same whichever command drew it. The rule is not configurable.
82+
83+
### Node markers
84+
85+
Two reserved markers sit on contract nodes:
86+
87+
- **`@contract`**the live working contract. It only appears in the application space; extension spaces have no working contract of their own, so passing `isAppSpace: false` suppresses it.
88+
- **`@db`**the database's current position. It is per-space and appears wherever a database is connected.
89+
90+
Both render in sigil form (`@contract`, `@db`) in the tree and in `--legend` output, and those are exactly the tokens `--from`/`--to` acceptthe graph shows you what you're allowed to type.
91+
92+
### Every space, by default
93+
94+
All three commands render every on-disk contract space, each as its own section with its own tree. `--space <id>` narrows to one. Per-space headings appear only when more than one space is present. Contract spaces are independent histories with no cross-space topology, so this is N independent trees, not one combined graph.
95+
96+
### Applied vs pending
97+
98+
`migration status` is the only command whose annotation depends on live database state. It reads the apply ledger ([ADR 228](./ADR%20228%20-%20Migration%20apply%20ledger%20is%20a%20per-migration%20journal.md)) and classifies each edge:
99+
100+
- **applied**the ledger has an entry for this migration's `migrationHash`. Drawn green, `✓ applied`.
101+
- **pending**on the shortest path from the database's current contract to the live contract, and not yet applied. Drawn yellow, `⧗ pending`.
102+
103+
Everything else is on disk but neither applied nor on the current path, and renders plain.
104+
105+
### Human picture, flat machine output
106+
107+
The tree is for people. `--json` output is per-command and flat, shaped for tooling rather than for reading:
108+
109+
- `migration list --json`a flat array of migration packages.
110+
- `migration graph --json``{ nodes, edges }`, the deduplicated contract topology.
111+
- `migration status --json`the list shape plus a per-migration `status` field.
112+
113+
The tree is never part of machine output, and this costs nothing to enforce: a non-TTY invocation auto-switches to JSON, so the renderer never runs in a pipe or script in the first place.
114+
115+
## Consequences
116+
117+
- **One renderer to maintain.** Improvements to layout, lane colouring, gutter, and label formatting land for all three commands at once.
118+
- **The trunk is uniform.** No command can drift onto a different trunk rule without changing the renderer itself.
119+
- **Machine output is independent of the picture.** `--json` consumers are unaffected by any change to graphical rendering.
120+
- **`list` and `graph` remain separate commands** even though their human output looks alikesee below.
121+
122+
## Alternatives considered
123+
124+
- **Two renderers in parallel**a force-directed graph for `graph`/`status` and a tree for a subset. Rejected. When the same data is drawn by two engines they drift: in practice the two picked different trunks (one the live contract, one a historical ref), so the same space looked different depending on which command you ran, and every visual change had to be made twice.
125+
126+
- **Merge `list` and `graph` into one command.** Rejected. Their machine output is durably different`list` emits the faithful on-disk package inventory (every package, including parallel and disconnected edges) while `graph` emits the deduplicated `{ nodes, edges }` topologyand they answer different questions ("what migration packages are on disk?" versus "what contract topology do they describe?"). Sharing a human picture does not make them one command.
127+
128+
- **A separate annotation type per command** instead of one shared interface. Rejected in favour of a single additive map. Per-command types would force the renderer to accept a union and lose the simple "draw the keys that are present" semantics that makes annotations compose.
129+
130+
- **A configurable trunk (`--trunk <ref>`).** Deferred, not rejected. Locking a single rulelive contract is the trunkwas the priority; a user-selectable trunk can be added later as a pure extension without disturbing the default.
131+
132+
## References
133+
134+
- [ADR 039Migration graph path resolution & integrity](./ADR%20039%20-%20Migration%20graph%20path%20resolution%20%26%20integrity.md) — the graph model and path computation this renderer visualizes.
135+
- [ADR 218Refs with paired contract snapshots and universal graph-node invariant](./ADR%20218%20-%20Refs%20with%20paired%20contract%20snapshots%20and%20universal%20graph-node%20invariant.md) — refs rendered as node overlays.
136+
- [ADR 228Migration apply ledger is a per-migration journal](./ADR%20228%20-%20Migration%20apply%20ledger%20is%20a%20per-migration%20journal.md) — the ledger that backs the `status` applied/pending overlay.
137+
- [ADR 229Migration graph renderer uses a line/plane/occlusion model](./ADR%20229%20-%20Migration%20graph%20renderer%20uses%20a%20line-plane-occlusion%20model.md) — how the shared renderer is built internally.

0 commit comments

Comments
 (0)