diff --git a/docs/architecture docs/ADR-INDEX.md b/docs/architecture docs/ADR-INDEX.md index 8c08835c79..4995e05acd 100644 --- a/docs/architecture docs/ADR-INDEX.md +++ b/docs/architecture docs/ADR-INDEX.md @@ -89,6 +89,9 @@ This document provides a comprehensive index of all Architectural Decision Recor | 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) | | 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) | | 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) | +| 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) | +| 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) | +| 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) | ## Preflight & CI diff --git a/docs/architecture docs/adrs/ADR 227 - Migration read commands share one graphical renderer with command-specific annotations.md b/docs/architecture docs/adrs/ADR 227 - Migration read commands share one graphical renderer with command-specific annotations.md new file mode 100644 index 0000000000..0fcdd0ffa4 --- /dev/null +++ b/docs/architecture docs/adrs/ADR 227 - Migration read commands share one graphical renderer with command-specific annotations.md @@ -0,0 +1,137 @@ +# ADR 227 — Migration read commands share one graphical renderer with command-specific annotations + +## Status + +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). + +## A worked example + +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`. + +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: + +```text +migration graph migration list migration status +───────────────── ───────────────── ───────────────── +○ a94b7b4 (@contract) ○ a94b7b4 ○ a94b7b4 (@contract) +│↑ add_email │↑ add_email 2 ops │↑ add_email ⧗ pending +○ ef9de27 (@db) ○ ef9de27 ○ ef9de27 (@db) +│↑ init │↑ init 1 op │↑ init ✓ applied +○ ∅ ○ ∅ ○ ∅ +``` + +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. + +## Decision + +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. + +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: + +```ts +// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-labels.ts +export interface MigrationEdgeAnnotation { + readonly status?: 'applied' | 'pending'; + readonly operationCount?: number; + readonly invariants?: readonly string[]; + readonly pathHighlight?: 'on-path' | 'off-path'; +} +``` + +A command builds a `ReadonlyMap` 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. + +## How it works + +### The annotation map is sparse and additive + +Each command owns a disjoint slice of `MigrationEdgeAnnotation`: + +- **`migration graph`** adds no edge annotations. It annotates *nodes* — refs and the `@contract`/`@db` markers. +- **`migration list`** sets `operationCount` and `invariants` (the facts about each migration package on disk), plus ref node overlays. +- **`migration status`** sets `status: 'applied' | 'pending'` on each edge, plus the `@db` node marker. +- **`migrate --show`** sets `pathHighlight: 'on-path' | 'off-path'`, which the renderer draws as a focus mode — the chosen path bright, everything else dimmed. + +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. + +```ts +// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-space-render.ts +export function mergeMigrationEdgeAnnotations( + listOverlay: ReadonlyMap, + statusOverlay: ReadonlyMap, +): ReadonlyMap +``` + +Adding a new kind of annotation later is a matter of adding a field; commands that don't set it are unaffected. + +### The trunk is always the live contract + +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. + +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`: + +```ts +// packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-space-render.ts +export interface RenderMigrationGraphSpaceTreeInput { + readonly liveContractHash: string; + readonly isAppSpace?: boolean; // default true; false suppresses @contract in extension spaces + // … +} +``` + +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. + +### Node markers + +Two reserved markers sit on contract nodes: + +- **`@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. +- **`@db`** — the database's current position. It is per-space and appears wherever a database is connected. + +Both render in sigil form (`@contract`, `@db`) in the tree and in `--legend` output, and those are exactly the tokens `--from`/`--to` accept — the graph shows you what you're allowed to type. + +### Every space, by default + +All three commands render every on-disk contract space, each as its own section with its own tree. `--space ` 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. + +### Applied vs pending + +`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: + +- **applied** — the ledger has an entry for this migration's `migrationHash`. Drawn green, `✓ applied`. +- **pending** — on the shortest path from the database's current contract to the live contract, and not yet applied. Drawn yellow, `⧗ pending`. + +Everything else is on disk but neither applied nor on the current path, and renders plain. + +### Human picture, flat machine output + +The tree is for people. `--json` output is per-command and flat, shaped for tooling rather than for reading: + +- `migration list --json` → a flat array of migration packages. +- `migration graph --json` → `{ nodes, edges }`, the deduplicated contract topology. +- `migration status --json` → the list shape plus a per-migration `status` field. + +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. + +## Consequences + +- **One renderer to maintain.** Improvements to layout, lane colouring, gutter, and label formatting land for all three commands at once. +- **The trunk is uniform.** No command can drift onto a different trunk rule without changing the renderer itself. +- **Machine output is independent of the picture.** `--json` consumers are unaffected by any change to graphical rendering. +- **`list` and `graph` remain separate commands** even though their human output looks alike — see below. + +## Alternatives considered + +- **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. + +- **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 }` topology — and 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. + +- **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. + +- **A configurable trunk (`--trunk `).** Deferred, not rejected. Locking a single rule — live contract is the trunk — was the priority; a user-selectable trunk can be added later as a pure extension without disturbing the default. + +## References + +- [ADR 039 — Migration graph path resolution & integrity](./ADR%20039%20-%20Migration%20graph%20path%20resolution%20%26%20integrity.md) — the graph model and path computation this renderer visualizes. +- [ADR 218 — Refs 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. +- [ADR 228 — Migration 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. +- [ADR 229 — Migration 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. diff --git a/docs/architecture docs/adrs/ADR 228 - Migration apply ledger is a per-migration journal.md b/docs/architecture docs/adrs/ADR 228 - Migration apply ledger is a per-migration journal.md new file mode 100644 index 0000000000..4f1a181b8d --- /dev/null +++ b/docs/architecture docs/adrs/ADR 228 - Migration apply ledger is a per-migration journal.md @@ -0,0 +1,99 @@ +# ADR 228 — Migration apply ledger is a per-migration journal + +## Status + +Accepted. Builds on [ADR 039 — Migration graph path resolution & integrity](./ADR%20039%20-%20Migration%20graph%20path%20resolution%20%26%20integrity.md) and [ADR 021 — Contract marker storage](./ADR%20021%20-%20Contract%20Marker%20Storage.md). + +## A worked example + +Someone applies two migrations to a database, decides the second was wrong, reverts it, then re-applies it. `migration log` shows exactly that, one row per thing that happened: + +```text +APPLIED AT MIGRATION FROM → TO OPS +2026-06-02 16:37:31 +02:00 init ∅ → ef9de27 1 +2026-06-02 16:38:02 +02:00 add_email ef9de27 → a94b7b4 2 +2026-06-02 16:40:11 +02:00 revert_email a94b7b4 → ef9de27 2 +2026-06-02 16:41:55 +02:00 add_email ef9de27 → a94b7b4 2 +``` + +Each row is one applied migration edge: which migration ran, what contract it moved the database from and to, when, and how many operations it carried. `add_email` appears twice because it ran twice — the journal records events, not a set of "migrations that exist." Reading the `from → to` column top to bottom replays the database's actual path. + +## Decision + +The apply ledger is a per-migration journal: every `migrate` run appends **one row per applied migration edge**. Two read commands consume it — `migration status`, to tell which migrations have run against the live database, and `migration log`, to show the apply history. + +A row is self-contained: + +```ts +// packages/1-framework/0-foundation/contract/src/types.ts +export interface LedgerEntryRecord { + readonly space: string; + readonly migrationName: string; // directory name of the migration package + readonly migrationHash: string; + readonly from: string | null; // null for the baseline (empty-database) edge + readonly to: string; + readonly appliedAt: Date; + readonly operationCount: number; +} +``` + +The ledger is append-only. Each row is written as its edge is applied, in walk order, inside the same per-space transaction as the migration itself, so the journal and the schema change commit together. `operationCount` is captured from the migration at write time, so a row needs no other source to be read. + +## How it works + +### Reading the ledger + +The ledger is exposed as a single read-only primitive on the control-family instance, alongside the other marker reads: + +```ts +// packages/1-framework/3-tooling/cli/src/control-api/types.ts +readLedger(space?: string): Promise +``` + +Pass a `space` to read one space's rows; omit it to read every space's rows in one call. The two consumers use it differently, and that difference drives the optional argument. + +### `migration status` — applied and pending + +`status` calls `readLedger` per space and classifies each migration on the graph against the live database: + +- **applied** — a ledger row exists carrying this migration's `migrationHash`. A migration that was reverted and re-applied still counts as applied; "applied" means "has ever run," and the full back-and-forth lives in `log`. +- **pending** — the migration is on the shortest path from the database's current contract to the live contract, and it is not applied. These are the migrations a plain `migrate` would run next. + +Everything else is on disk but neither applied nor on the current path, and renders plain. (`status` feeds this classification into the shared tree renderer as an edge annotation — see [ADR 227](./ADR%20227%20-%20Migration%20read%20commands%20share%20one%20graphical%20renderer%20with%20command-specific%20annotations.md).) + +Matching on `migrationHash` is what makes per-migration `status` possible: a journal with one row per edge can answer "did *this* migration run?"; a coarser record cannot. + +### `migration log` — the flat history + +`log` calls `readLedger()` with no space argument, gets the whole table, and renders it as the flat, time-ordered list shown above. It is the one read command that does **not** draw the tree — and deliberately so. The same edge can recur (apply → revert → re-apply), and a tree, which places each contract at one node, cannot represent the same edge appearing several times. A table can, so `log` uses one. + +Rows sort by `appliedAt` ascending, with `space` then `migrationName` breaking ties. There is no `--space` flag and no per-space sections; a `Space` column appears only when more than one space contributes rows. `log` does not label rows as apply / revert / re-apply — the `from → to` direction and the repetition carry that meaning to the reader without the command having to do graph analysis on top of a database read. + +`log` reads `migrationName` straight from the ledger row, never from disk, so a migration package deleted after it was applied still shows in the history under its original name. + +Timestamps follow one rule: machine output is timezone-stable, human output is local. `--json` always emits ISO-8601 UTC (`2026-06-02T14:37:31.000Z`). Human/TTY output renders in the local timezone with offset (`2026-06-02 16:37:31 +02:00`); `--utc` switches the human output to UTC. A non-TTY invocation auto-switches to `--json`, so a piped `log` is UTC by construction. + +### Cross-target parity + +Every adapter implements the same `readLedger(space?: string)` signature and returns the same `LedgerEntryRecord` shape, so `status` and `log` are written once against the interface and behave identically across targets. With `space` omitted, an adapter returns its full table unfiltered. + +## Consequences + +- `readLedger` is a pure read on the control-family instance, beside `readMarker` / `readAllMarkers`; it carries no write side-effects. +- `migration log` is online-only — the database is the source of truth for apply history; it never reconstructs the timeline from on-disk state. +- `operationCount` is denormalized onto the row to keep the journal self-contained. Only `migrationName` / `migrationHash` / `from` / `to` / `appliedAt` are needed by `status` and `log`, so heavier fields can be made opt-in later without changing either consumer. +- The ledger has no schema-migration path: a database whose ledger is in any other shape is not read; the next `migrate` writes rows in this shape. + +## Alternatives considered + +- **Reconstruct apply history from on-disk state** by walking `findPath(∅ → marker)`. Rejected. In a branching history the reconstruction can pick the wrong branch, and it conflates a migration's *creation* time with its *apply* time. Only the ledger records what actually ran, and when. + +- **One collapsed row per `migrate` invocation**, spanning the whole walked path from origin to destination. Rejected. It cannot answer per-migration questions — `status` can't tell whether one specific migration ran — and it cannot represent the order of the individual edges within a single run, which is exactly what `log` exists to show. + +- **A tree view for `log`.** Rejected. `log` reports events over time, and the same edge can occur repeatedly; a tree pins each contract to a single node and cannot show repetition. A flat table is the honest representation of a journal. + +## References + +- [ADR 039 — Migration graph path resolution & integrity](./ADR%20039%20-%20Migration%20graph%20path%20resolution%20%26%20integrity.md) — the graph walk that produces the edge sequence written to the ledger. +- [ADR 021 — Contract marker storage](./ADR%20021%20-%20Contract%20Marker%20Storage.md) — the database marker that provides `status`'s origin contract hash. +- [ADR 227 — Migration read commands share one graphical renderer with command-specific annotations](./ADR%20227%20-%20Migration%20read%20commands%20share%20one%20graphical%20renderer%20with%20command-specific%20annotations.md) — how the ledger-derived applied/pending overlay reaches the renderer. diff --git a/docs/architecture docs/adrs/ADR 229 - Migration graph renderer uses a line-plane-occlusion model.md b/docs/architecture docs/adrs/ADR 229 - Migration graph renderer uses a line-plane-occlusion model.md new file mode 100644 index 0000000000..83a15a9169 --- /dev/null +++ b/docs/architecture docs/adrs/ADR 229 - Migration graph renderer uses a line-plane-occlusion model.md @@ -0,0 +1,155 @@ +# ADR 229 — Migration graph renderer uses a line/plane/occlusion model + +## Status + +Accepted. Refines [ADR 227 — Migration read commands share one graphical renderer with command-specific annotations](./ADR%20227%20-%20Migration%20read%20commands%20share%20one%20graphical%20renderer%20with%20command-specific%20annotations.md), which establishes that one renderer draws the migration topology for every read command. This ADR is about how that renderer is built internally. + +## A worked example + +A space has three forward migrations and one rollback. `000_init` through `002_fwd_bc` advance the history; `003_rollback` then rolls back from `arc_c` to `arc_a`, skipping `arc_b`. Here is `migration graph` drawing it — newest contract at the top, the empty contract `∅` at the bottom: + +```text +○─╮ arc_c +│ │↓ 003_rollback +│↑│ 002_fwd_bc +○ │ arc_b +│↑│ 001_fwd_ab +○◂╯ arc_a +│↑ 000_init +○ ∅ +``` + +The forward history runs straight up the left column: each `○` is a contract, each `│↑` a forward migration. `003_rollback` cannot be a plain downward edge because it skips `arc_b`, so it is routed as an arc — it leaves `arc_c` through the corner `─╮` into a lane of its own to the right of the trunk, runs down that lane as `│`, and lands on `arc_a` at the `◂╯`, where the `◂` marks the arrival. + +Two things in that picture drive the whole design. The rollback is a different colour from the trunk, and it gets its own column rather than being crammed into the trunk's. That is deliberate: a single text cell can hold only one glyph in one colour, so two differently-coloured lines must never be made to share one. Where lines genuinely have to overlap — a routed arc passing over another line at a crossing — one of them has to win the cell and the other has to give way, and each must keep its own colour. Deciding that unambiguously is what the renderer is built around. + +## Decision + +The renderer is modelled around **lines**, drawn by **occlusion**, into cells with a **single owner**. + +- **A line is the primitive, not a cell.** Each migration edge becomes a routed line that carries its own identity — which migration it is, which lane it occupies, and (in highlight mode) whether it is on or off the chosen path. Colour is a property of the line and is always read off the line; it is never inferred from a cell's position in the grid. +- **Overlap is resolved by occlusion.** Each cell holds a z-ordered stack of the lines passing through it. The renderer draws the topmost line and clips the rest. There is no glyph that blends two lines. +- **Every cell has exactly one drawable owner.** The layout guarantees this before the renderer runs, so rendering a cell is a direction-to-glyph lookup and a colour read, with no junction logic and no colour arbitration. + +Because lanes are reused — a column that carries one edge near the top carries a different edge lower down — colour cannot be a property of a position. If it were, the renderer would have to reconstruct which edge owns a given cell, and that reconstruction is exactly where colour bleeds. Making the line the primitive and giving it first-class identity means a cell's colour always belongs honestly to one line. + +## How it works + +### Two phases: layout, then render + +Rendering is split into a phase that thinks and a phase that does not. + +**Layout** (`migration-graph-grid-layout.ts`) is where every geometry and topology decision lives. It routes each edge as a line across a grid of cells and records, for each cell a line passes through, the **directions** the line occupies there (which of up / down / left / right) and the **plane** it sits on (its z-order). A cell therefore holds an ordered set of the lines present in it, and layout guarantees the single-owner invariant before handing the grid on. + +**Render** (`migration-graph-occlusion-render.ts`) is a projection with no knowledge of topology. For each cell it takes the topmost line (lowest plane number), derives the glyph from that line's directions — a box-drawing character for the verticals and corners, with `○` / `∅` node markers and `↑ ↓ ⟲` arrows layered on top — takes the colour straight off that line, and occludes the rest. + +### The single-owner invariant + +Keeping each cell owned by one line is what makes colour correct by construction. Two rules enforce it: + +- **No tees.** The glyph alphabet is verticals, corners, arrows, and node markers — never `├ ┬ ┼`. A tee is the only glyph that bundles a through-line and a branch into one cell; without it, that bundling cannot happen. A fork or a merge is drawn as one continuous line (a `│` or a sweeping corner) plus, for every other branch, that branch's own corner in its own cell. +- **Two columns per lane.** Each lane occupies a **rail column** (the single-owner vertical) and a **connector column** (corners and horizontals). Turns happen in the connector column, never crammed into a rail. The count is the named parameter `colsPerLane` (default 2). + +With one owner per cell, the colour of every cell is simply the colour of its line — there is never a second branch to compromise with. + +### Planes and z-order + +A cell is a z-ordered stack of lines; the lowest plane number is drawn and everything above it is occluded. One rule then covers every kind of overlap: + +- **Crossings** — two lines pass through a cell; the top one is drawn, the lower one is clipped. +- **Forks and merges** — several lines meet at a node; the top line is drawn continuous, and each other line yields beneath it, cornering off into its own connector cell. A merge is not a special junction — it is one continuous line plus N yielding corners, which scales to any number of parents or children. + +Which line is on top is the one thing that differs by mode: + +- **Flat (multi-colour) mode → trunk on top.** The main lane stays an unbroken `│` and later parents corner in beneath it (`│─╮─╮…`) — a compact, git-log-style picture. +- **Focus mode (`migrate --show`) → the on-path line on top.** The chosen path is lifted above everything and drawn as one continuous prominent line sweeping through merges (`╰───╮`), while off-path branches yield beneath it. + +### Back-arcs + +A rollback runs against the forward grain of the DAG, so it is routed on its own back-lane to the right of the trunk and drawn on an upper plane, continuous. Where a back-arc crosses a forward vertical, the forward line clips and the back-arc runs through. + +When several rollbacks land on the same target node, they share one back-lane rather than each taking its own: the sources converge and a single landing closes onto the node. This keeps the graph narrow and removes crossings. Each arc keeps its own colour on the segment it owns; occlusion arbitrates the shared rail. + +### Colour + +In flat mode, lanes are coloured by greedy assignment. Walking the rows from the bottom up, each diverging branch and each back-arc takes the lowest palette colour not currently in use by a lane alive alongside it; a back-arc additionally avoids its origin branch's colour and the on-path green. Colours are released when a lane ends and reused later. This guarantees two things: branches visible at the same time are always distinguishable, and a rollback never wears the colour of the branch it springs from. Where back-arcs share a rail, the arc whose source is lowest in the display is drawn on top. + +In focus mode the lane palette is set aside: the on-path line is green and continuous, off-path lines are dim. Because colour is read off the owning line and a cell has a single owner, an off-path line can never bleed green into an on-path cell or vice versa. + +### Layout invariants + +Beyond the per-cell rules, the layout holds the overall shape together: + +- **Disconnected components** (independent histories with no shared contract) each render as their own block starting at lane 0, separated by a blank line, rather than interleaving. +- **Asymmetric diamonds** — a fork whose two arms differ in length and reconverge — keep the merge node, and any trunk that continues past it, on the lane-0 trunk; the shorter arm is the side-branch. + +These shapes are pinned by regression goldens under `cli/test/utils/formatters/`. + +### Geometry + +Spacing is parameterised, not hard-coded. `colsPerLane` (default 2) is a named constant on the grid options, threaded through both the layout and the renderer, so the density of the graph can change without editing renderer code. Other spacing constants (hash-column width, label gaps) are likewise named. + +### Data structures + +`migration-graph-model.ts` defines the grid the two phases share: + +```ts +type Direction = 'up' | 'down' | 'left' | 'right'; +type PathRole = 'on-path' | 'off-path'; + +// Identity, carried into every cell the line touches. +interface LineRef { + readonly migrationHash: string; + readonly dirName: string; + readonly lane: number; // selects the lane's colour + readonly role: PathRole | undefined; // set in focus mode; undefined in flat mode +} + +// One line's presence in one cell. +interface CellLine { + readonly line: LineRef; + readonly directions: ReadonlySet; + readonly plane: number; // z-order; lower number = drawn on top + readonly selfLoop?: boolean; // a ⟲ self-edge + readonly landingArrow?: boolean; // the ◂ where a back-arc lands on its target +} + +interface NodeRef { + readonly contractHash: string; + readonly isEmpty: boolean; // the ∅ baseline node + readonly lane: number; + readonly role: PathRole | undefined; +} + +interface Cell { + readonly node?: NodeRef; // a contract marker; never shares a cell with a line + readonly lines: readonly CellLine[]; // ordered set of lines present +} + +type Grid = readonly (readonly Cell[])[]; // rows × columns, row 0 = top of the display +``` + +Rendering a cell: pick the line with the minimum `plane`, derive its glyph from the union of that line's `directions`, colour it from the line, and layer any node or arrow marker on top. + +The glyph and layout vocabulary the renderer draws — every box-drawing character and what each fixture topology looks like — is catalogued in the [migration graph visual language](../../reference/Migration%20Graph%20Visual%20Language.md) reference. + +## Consequences + +- **Colour is correct by construction.** Because every cell has a single owning line and colour is read off the line, there is no path by which one branch's colour reaches another branch's cell. The property holds without a verification pass. +- **The renderer has no junction logic.** No tee glyphs, no priority rules at crossings, no colour arbitration — the render phase is a direction-to-glyph lookup and a colour read. All topology lives in layout. +- **Merges scale uniformly.** A merge is one continuous line plus N yielding corners, so a node with any number of parents or children draws with the same rule; there is no special case per arity. +- **The two modes differ in exactly one knob.** Flat vs focus changes only which line sits on top of the stack; everything downstream is identical. +- **Traceability through a crossing is given up.** Occlusion clips the lower line at a crossing, so a reader cannot follow a single line unbroken through every cell it passes. This is the deliberate trade for unambiguous per-cell colour — see Alternatives. + +## Alternatives considered + +- **Colour keyed by cell position rather than by line.** Store colour on the grid cell and infer, at render time, which edge owns each cell. Rejected. Lanes are reused down the height of the graph, so a position does not identify an edge; the renderer would have to reconstruct ownership, and that reconstruction is precisely where colour bleeds between branches. Carrying identity on the line removes the reconstruction entirely. + +- **Blended junction glyphs instead of occlusion.** Draw a crossing as a combined glyph — a `┼` where two lines meet — coloured by whichever line wins a priority rule. This keeps both lines traceable through the crossing, but a single glyph can only carry one colour, so it necessarily misrepresents the other line (a green `┼` sitting in the middle of a grey rollback arc). Rejected. Occlusion gives up traceability through a crossing in exchange for every cell's colour being honest about exactly one line, and unambiguous colour is the property the whole renderer is built around. + +- **Tee glyphs for forks and merges.** Allow `├ ┬ ┼` so a fork or merge fits in one cell. Rejected. A tee is the one glyph that bundles a through-line and a branch into a single cell, which breaks the single-owner invariant and reintroduces the question of what colour the shared cell is. Spending a second column per lane to draw the branch as its own corner is cheaper than the colour ambiguity a tee brings back. + +## References + +- [ADR 227 — Migration read commands share one graphical renderer with command-specific annotations](./ADR%20227%20-%20Migration%20read%20commands%20share%20one%20graphical%20renderer%20with%20command-specific%20annotations.md) — the command-level architecture this renderer sits under. +- [Migration graph visual language](../../reference/Migration%20Graph%20Visual%20Language.md) — the glyph and layout catalogue the renderer draws from. diff --git a/projects/migration-graph-rendering/mockups.md b/docs/reference/Migration Graph Visual Language.md similarity index 97% rename from projects/migration-graph-rendering/mockups.md rename to docs/reference/Migration Graph Visual Language.md index 06e6c0b8b6..1eab4b891b 100644 --- a/projects/migration-graph-rendering/mockups.md +++ b/docs/reference/Migration Graph Visual Language.md @@ -1,14 +1,15 @@ -# tier-3 `migration graph` — hand-drawn layout mockups +# Migration Graph Rendering — Visual Language -Design conversation artifact. These are **hand-drawn**, not generated — the point -is to settle the visual language before coding the renderer. Hashes/names are real, -taken from `prototype/gallery.md` (the fixture topologies). +The locked visual vocabulary for the Tier-3 `migration graph` / `list` / `status` +renderer: the glyph alphabet, lane/column layout, and the worked picture for each +fixture topology. Companion to the [architecture](./12.%20Migration%20Graph%20Rendering.md); +this file is the glyph/layout contract, that file is the model behind it. The +pictures use real fixture hashes/names. -Carries forward the device from the original -`migration-graph-display-scenarios.md` draft: **a direction arrow in the edge's own +The core device: **a direction arrow in the edge's own lane** (`↑` forward, `↓` rollback). -> **Color extension (TML-2773, slice `lane-colors-and-legend`).** These mockups are +> **Color extension.** These mockups are > drawn **monochrome** — every layout rule below must read unambiguously without > color (rule 4 is explicit about this: the lane that owns the label carries the > arrow, so a wide fan is unambiguous in monochrome). On top of that monochrome- diff --git a/projects/migration-graph-rendering/README.md b/projects/migration-graph-rendering/README.md deleted file mode 100644 index 2cdc4b103a..0000000000 --- a/projects/migration-graph-rendering/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# migration-graph-rendering - -Redesign of the `migration graph` (Tier 3) command's rendering: a condensed, -deterministic, annotated node-link diagram that draws **contracts as nodes and -migrations as edges**, with complete back-edges, replacing the current dagre -layout and its golden-path root/tip selection. - -Tracking ticket: [TML-2746](https://linear.app/prisma-company/issue/TML-2746). -The tolerant graph source already ships — `migration graph` loads through the -ContractSpace aggregate and holds a multi-root / multi-tip / cyclic-tolerant -`MigrationGraph` (`aggregate.app.graph()`). This slice replaces only the -golden-path mapper + dagre renderer on top of that source; it is related to, -but not blocked by, the consolidation project (TML-2739). - -> **Current status & authoritative roadmap: [`plan.md`](./plan.md).** The renderer -> was rebuilt on a line/plane/occlusion model (`render-redesign-core`, merged #762; -> `render-redesign-geometry`, in progress). The numbered slices below are the older, -> mostly-merged read-command-family track. Several fine-grained glyph-bug slices that -> predated the rewrite have been deleted as obsolete — see `plan.md`. - -## Slices - -1. **Redesign the Tier-3 renderer** — [`spec.md`](./spec.md). The condensed - annotated node-link diagram (shipped in PR #658). -2. **Retire `migration list --graph`** — - [`slices/remove-list-graph-renderer/spec.md`](./slices/remove-list-graph-renderer/spec.md) - ([TML-2765](https://linear.app/prisma-company/issue/TML-2765)). Now that the - Tier-3 tree is compact and correct, the Tier-2 list-graph gutter is the - redundant middle; this slice removes it, leaving one graph renderer. -3. **`migration graph` multi-space** — [`slices/migration-graph-space-flag/spec.md`](./slices/migration-graph-space-flag/spec.md) ([TML-2767](https://linear.app/prisma-company/issue/TML-2767)). **Cancelled / superseded** by D16 in slice 8 below. The all-spaces-by-default rule lands as part of the unified pretty-rendering pass in `render-polish-and-ledger-tests`. Spec retained for historical context. -4. **`migration list` renders the tree (human output)** — - [`slices/list-renders-tree/spec.md`](./slices/list-renders-tree/spec.md) - ([TML-2768](https://linear.app/prisma-company/issue/TML-2768)). `list`'s - pretty/TTY output adopts the shared tree renderer (package-annotated); its - `--json` stays flat for tooling. Introduces the shared `edgeAnnotationsByHash` - overlay (D11) that `status` extends. Completes the intent of TML-2697. Runs in - parallel with slices 6–7; land first where possible (D11). - -The project has broadened from the `graph` renderer into the whole **migration -read-command family** (`list` / `graph` / `status` / `log`). Cross-cutting design -decisions (the command-family model, shared renderer, space policy, `list`/`graph` -split, the ledger foundation) live in [`decisions.md`](./decisions.md). - -5. **Ledger foundation** ([TML-2769](https://linear.app/prisma-company/issue/TML-2769)) — - make the on-apply ledger readable; store migration hash + name; add - `readLedger`. Control-plane (all targets). **Merged** (PR #665). -6. **`status` = shared tree + DB-state overlay** — - [`slices/status-db-overlay/spec.md`](./slices/status-db-overlay/spec.md) - ([TML-2748](https://linear.app/prisma-company/issue/TML-2748)). Renders the - shared tree directly via the `graph --tree` engine + an applied/pending edge - overlay (`status` key on D11's `edgeAnnotationsByHash`) + the `(db)` node - marker; `--from`/`--to` (D9), `--space` (D4); applied comes from the ledger - (resolves [TML-2130](https://linear.app/prisma-company/issue/TML-2130)); deletes - dagre and makes the tree the default for `graph` (drops `--tree`). -7. **`log` reads the ledger** — - [`slices/log-reads-ledger/spec.md`](./slices/log-reads-ledger/spec.md) - ([TML-2770](https://linear.app/prisma-company/issue/TML-2770)). Flat, - chronological, single-table apply history straight from the DB ledger (all - spaces merged; no tree, no per-space sections); local-time human output with a - `--utc` flag, ISO-UTC JSON. - -Slices 4, 6, and 7 run **in parallel** off the merged ledger foundation, each on -its own branch/PR. The only shared surface is the tree renderer's -`edgeAnnotationsByHash` field (D11), touched by 4 and 6 (additively); `log` (7) -shares none of it. - -8. **Render polish + ledger test coverage** — [`slices/render-polish-and-ledger-tests/spec.md`](./slices/render-polish-and-ledger-tests/spec.md). The follow-up slice covering [TML-2812](https://linear.app/prisma-company/issue/TML-2812) (unify pretty rendering across `list` / `status` / `graph`, locking trunk-choice as D14 + per-row data as D15 + space iteration as D16), [TML-2811](https://linear.app/prisma-company/issue/TML-2811) (column alignment, D17), [TML-2773](https://linear.app/prisma-company/issue/TML-2773) (colored lanes + `--legend`, D18 / D19), and the two open items in [TML-2774](https://linear.app/prisma-company/issue/TML-2774) (cross-target op-count parity harness + Postgres op-count-mismatch throw test, D20). Subsumes the former `unify-pretty-rendering` and `lane-colors-and-legend` slice drafts. [TML-2767](https://linear.app/prisma-company/issue/TML-2767) (`migration graph` multi-space) is **superseded** by D16 and closed. - -Future siblings (not core): `migration path --from X --to Y` ([TML-2771](https://linear.app/prisma-company/issue/TML-2771)) and `ref show` invariants ([TML-2772](https://linear.app/prisma-company/issue/TML-2772)). - -Ledger cleanups (still deferred — out of any current slice's scope, no Linear tickets): - -- **Consolidate the per-edge breakdown onto the plan** — [`slices/edges-on-plan/spec.md`](./slices/edges-on-plan/spec.md). The ledger foundation threads `migrationEdges` as a sibling of `plan` on the runner options. Moving the breakdown onto `MigrationPlan.edges` would let the runner read one object instead of two. Filed only as a draft spec until picked up. -- **Stop spelling the empty-contract origin as a fake hash** — [`slices/empty-origin-as-null/spec.md`](./slices/empty-origin-as-null/spec.md). ∅ is modelled as `null` at the read boundary but as `sha256:empty` in storage / graph, bridged by a coercion helper. The `EMPTY_CONTRACT_HASH` value is wired into the `MigrationGraph` node-keying, walk algorithms, integrity checks, and ref parsing — non-trivial blast radius; the operator ruled in the TML-2769 review that the constant's value is "not our fight." Filed only as a draft spec until picked up. - -## Contents - -- [`spec.md`](./spec.md) — **slice 1's spec + dispatch plan.** Pins the - implementation architecture (render pipeline, module placement, scope), the - coherence rationale, edge cases, slice-DoD, and the six-dispatch decomposition. -- [`mockups.md`](./mockups.md) — **the locked visual language.** Hand-drawn - layout mockups across the full fixture set plus synthetic pathologicals, with - the layout rules the renderer must implement. This is the design of record. -- [`prototype/`](./prototype) — zero-build prototyping harness used to settle - the design. `proto.mjs` loads every fixture under - `examples/prisma-next-demo/migration-fixtures`, recomputes topology in plain - JS, runs a pluggable `render()`, and writes `gallery.md`. Run from the repo - root: `node projects/migration-graph-rendering/prototype/proto.mjs`. - -The harness is throwaway exploration; the real renderer consumes the aggregate's -tolerant `MigrationGraph` and classifies edges with the shared Tier-2 topology -pass. `mockups.md` is the durable artifact — it carries the visual contract into -implementation, and `spec.md` turns it into a plan. diff --git a/projects/migration-graph-rendering/decisions.md b/projects/migration-graph-rendering/decisions.md deleted file mode 100644 index 4bf7271bd9..0000000000 --- a/projects/migration-graph-rendering/decisions.md +++ /dev/null @@ -1,277 +0,0 @@ -# Design decisions — migration read-command family - -This project began as a redesign of `migration graph`'s renderer (TML-2746) but -has broadened into a coherent design for the whole family of **migration read -commands** — `list`, `graph`, `status`, `log` (and `show`). This file records -the cross-cutting decisions that span more than one slice. Slice-local detail -lives in each slice's `spec.md`. - -## The command family - -| Command | Question it answers | On/offline | Human (TTY) output | Machine output | -|---|---|---|---|---| -| `list` | "what migration packages are on disk?" | offline | shared tree, package-annotated | flat package array (`--json`, future text-only) | -| `graph` | "what contract topology do they describe?" | offline | shared tree, topology/overlay-annotated | `{ nodes, edges }` | -| `status` | "where is my DB relative to all on-disk migrations?" | online (offline with `--from`) | `list` + per-migration applied/pending overlay | `list`'s shape + a `status` field | -| `log` | "what actually ran, and when?" | online | **flat** `list`-format rows from the ledger (no tree) | ledger entries | -| `show` | "what's in this one package?" | offline | package detail | package detail | - -`status`, `list`, and `graph` describe **on-disk state** and are tree-shaped. -`log` describes **what actually happened** (the DB ledger — real apply order and -timestamps, including rollbacks/re-applies) and is the one command that stays -flat, because the same edge can recur and a graph can't represent repetition. - -Tickets: `list`→tree TML-2768, `graph` multi-space TML-2767, `status` TML-2748, -`log` TML-2770, ledger foundation TML-2769, and the future siblings `migration -path` TML-2771 / `ref show` invariants TML-2772. - -## Decisions - -### D1 — One shared graphical renderer, command-specific annotations - -The condensed Tier-3 tree renderer (TML-2746) is the **single** human/graphical -rendering engine. `list`, `graph`, and `status` all draw the same tree; they -diverge only in the **annotations** they overlay: - -- `list` → per-migration package facts (op counts, invariants, refs). -- `graph` → `(refs)` / `(contract)` node overlays. -- `status` → `(db)` marker + per-edge applied/pending/unreachable status glyphs. - -This is the project's thesis: one renderer to maintain, fed different overlay -inputs. The dagre renderer and the Tier-2 list-graph gutter are both retired -(TML-2748, TML-2765). - -### D2 — `list` and `graph` stay distinct commands - -They are **not** merged. They answer different questions and their **machine -output differs significantly**: `list` emits a flat package array (the faithful -on-disk inventory — every package, including parallel / duplicate / disconnected -edges); `graph` emits `{ nodes, edges }` (the deduplicated contract topology). -Their human output happens to share the tree (D1, D3), but the commands' purpose -and tooling contracts are durably separate, and may diverge further. - -### D3 — Graphical output is human-only; machine formats stay flat - -The tree is rendered **only** in the pretty/TTY human path. `--json` and any -future text-only format omit the tree and emit each command's flat data, for -tooling. This is free: piping a read command already auto-switches to JSON -(non-TTY ⇒ `--json`, per `resolveOutputFormat`), so the human renderer never -runs for a pipe/script in the first place. - -Rationale for putting the tree in `list`'s human output: `list`'s flat order is -**lexicographic by directory name**, not chronological (the timestamp prefix is -a naming convention recording *creation*, not application). For a branching -history that order shows no relationships and is close to unreadable, so in -practice you always need `list` + `graph` together — hence combine their -graphical output for humans. - -### D4 — Space policy: all spaces by default, `--space ` to narrow (read commands) - -Contract spaces are **independent histories** (no cross-space topology). All read -commands render **every** on-disk space by default, each as its own disconnected -per-space section/tree, with `--space ` to narrow to one. `migration list` -already does this; `migration graph` is brought into line (TML-2767). This gives -one mental model for `--space` across the family. - -### D5 — `--tree` becomes the default; dagre is deleted - -The condensed tree shipped behind an experimental `migration graph --tree` flag -to avoid disturbing `migration status` (which shared the dagre renderer). Once -`status` moves onto the shared renderer, `--tree` becomes the default (the flag -is dropped) and the dagre renderer + `@dagrejs/dagre` are deleted (TML-2748). - -### D6 — `status` is `migration list` + a DB-state overlay (TML-2748) - -`status` draws the **same** list as `migration list` (shared renderer, per-space -sections, policy B) and overlays, per migration, one of two states; everything -else is shown plain (it's the full list — no subgraph pruning): - -- **applied** — a ledger entry exists for this migration (exact match on - migration hash, D7). KISS: literal "ever ran"; a rolled-back migration still - counts as applied here — the timeline lives in `log` (D8). -- **pending** — on the shortest path from the DB's current contract hash to the - app contract, and not applied (runs next on `migrate`). - -`status --json` is `list`'s shape plus a per-migration `status` field. This -retires dagre (the last consumer) and makes the condensed tree the default -(`--tree` flag dropped). - -### D7 — Restructure the ledger into a readable per-migration journal (TML-2769) - -Every target writes an append-only ledger on apply, but nothing reads it — and -its shape is wrong for `status`/`log`. Investigation found it records **one -collapsed row per space-apply** (origin→destination spanning the whole walked -path), and the three target schemas have diverged (PG/SQLite have no `space` -column; Mongo has no `operations`). Both consumers need **one row per migration -edge**: `status` matches `migration_hash` exactly; `log` shows one row per apply -event. - -So restructure (it's simpler than today's): one row per applied edge, each -carrying `space` + `migration_name` (dirName) + `migration_hash` + per-edge -`from`/`to` + the edge's `operations` (slice of `plan.operations` by -`operationCount`) + `applied_at`. The edge's `operations` are kept on -every row — they make the journal a high-value audit record (exactly what ran). -`contract_json_before/after` stays too (nullable; only the apply's endpoints are -materialised — multi-edge interiors are null; no consumer reads them yet). Both -`operations` and `contract_json` are non-essential to `status`/`log` (which need -only name/hash/from/to/count) — if storage ever bites, drop them or give users -an opt-in/out control for non-essential ledger storage rather than removing the -audit value by default. Writes happen per-edge inside the per-space -transaction, in walk order, by threading `PerSpacePlan.migrationEdges` to the -runner. Add `readLedger({ driver, space })` to `ControlFamilyInstance` (beside -`readMarker`/`readAllMarkers`) returning `LedgerEntryRecord[]` in apply order -with cross-target parity, plumbed through the control client + a control-api -operation. Prototype — no back-compat migration of existing rows. - -### D8 — `log` reads the ledger; flat, no tree (TML-2770) - -`log` reads the ledger (D7) and renders the real apply history in `list` row -format, **flat** — in apply order, with names + `appliedAt`, including rollbacks -and re-applies. It is online-only (the DB is the source) and the only read -command not sourced from on-disk state. Today's `findPath(∅→marker)` -reconstruction is discarded (it can pick the wrong branch and mislabels creation -time as apply time). - -### D9 — `status` origin/target controls (`--from` / `--to`) - -Default origin is the DB marker, default target is the current contract. -`--to X` retargets to a ref/hash ("can I move this DB to X? what path?"). -`--from X` overrides the origin (offline-capable: "what would `migrate --from X ---to Y` do?"). The **applied** overlay shows iff the origin is the real DB — -overriding it makes applied-ness meaningless, so it drops. `status` requires a DB -unless `--from` supplies the origin. - -### D10 — Path-decision and invariants live elsewhere, not in `status` - -To keep `status`'s footer lean (headline + actionable `missing invariant(s)` -line only): - -- **Which-path-and-why** (path selection, tie-break reasons) → a future - `migration path --from X --to Y` command that draws the graph and highlights - the chosen path, and dry-runs alternative pathfinding (TML-2771). -- **A ref's declared required invariants** → `ref show` (ref metadata); `status` - surfaces only the *missing* set relative to the DB (TML-2772). - -### D11 — One shared edge-annotation overlay on the tree renderer (TML-2748 + TML-2768) - -D1 said `list`/`graph`/`status` share the tree and diverge only in annotations. -This pins the *mechanism*. The tree renderer already carries **node** overlays -(`refsByHash`, `contractHash`, `dbHash`, `activeRefName` on -`RenderMigrationGraphTreeOptions`). The commands that annotate **migrations** -(`list`'s package facts, `status`'s applied/pending) add **one** new optional -input, keyed by the join key every migration already has — `ClassifiedEdge.migrationHash`: - -```ts -interface MigrationEdgeAnnotation { - readonly status?: 'applied' | 'pending'; // status overlay - readonly operationCount?: number; // list package fact - readonly invariants?: readonly string[]; // list package fact -} -// new field on RenderMigrationGraphTreeOptions: -readonly edgeAnnotationsByHash?: ReadonlyMap; -``` - -The renderer draws whatever is present: `status: 'applied'` → green `✓` on the -migration row; `'pending'` → yellow `⧗`; `operationCount`/`invariants` → appended -to the migration row's data column; absent ⇒ plain. `refs` stay **node** overlays -(`refsByHash`) for every command (`list` shows refs today and keeps them). - -Each command populates only its own keys, so the field is **additive** and the two -slices that touch it (`list`→tree TML-2768, `status` TML-2748) don't collide: -whichever lands first introduces `edgeAnnotationsByHash` + `MigrationEdgeAnnotation` -with the full type above; the other rebases onto it. To minimise even that, **land -TML-2768 first** where schedules allow — it's the slice that naturally introduces -edge annotations (package facts), and `status` then only adds the `status` key. - -Overlay ownership per command (refining D1): `list` → `operationCount` + -`invariants` (edge) and `refs` (node); `graph` → `refsByHash` + `contractHash` -(node); `status` → `status` (edge) + `dbHash` (node, the real DB marker, shown -iff a DB is connected). No command shows the `(db)` marker offline. - -### D12 — `log` is a single flat table across all spaces, not per-space sections (TML-2770) - -`log` answers "what actually ran, and when?" from the DB ledger. The ledger is -**already one flat table** in storage — each row carries its `space`. The read API -was needlessly scoped per-space (`readLedger(space)`); this slice makes the space -argument **optional** so `readLedger()` (unscoped) returns the whole table directly -(adapters drop the space filter when it's omitted). `log` reads that flat table, -orders by `appliedAt` ascending (apply order), and shows a `space` column **only -when more than one space** contributes rows. It is **not** space-sectioned: no -`--space` flag, no per-space headings (KISS — "just render what's in the ledger"). Rows are -uniform: the same edge recurring (apply → rollback → re-apply) simply appears as -repeated rows; `log` does **not** semantically classify apply vs rollback vs -re-apply (that needs graph analysis a DB-sourced command shouldn't do). The -`from → to` direction and repetition reveal the timeline to the reader. - -### D13 — Timestamp rendering: local in human output, UTC in machine output (TML-2770) - -`appliedAt` renders in the **local timezone** in human/TTY output (with offset -for unambiguity, e.g. `2026-06-02 16:37:31 +02:00`); `--utc` switches human -output to UTC (`2026-06-02 16:37:31Z`). `--json` and any non-TTY/machine output -always emit ISO-8601 UTC (`2026-06-02T14:37:31.000Z`) regardless of `--utc` -(machine output is timezone-stable by contract; `--utc` only affects the human -renderer). Non-TTY already auto-switches to JSON, so a piped `log` is UTC by -construction. - -### D14 — Trunk-choice rule: the live-contract chain is the trunk (TML-2812) - -D1 commits the three commands to one shared renderer, but doesn't pin **which chain is the trunk** when the topology is disconnected. In practice the commands diverged on disk: `list` chose the live-contract chain, `status` and `graph` chose the historical-ref chain (often `f7a8eb5` for the demo fixture's `prod` ref). Same data, two different trunk picks — same rendering engine, different inputs to its trunk resolver. - -The locked rule: **the trunk is the chain containing the live contract** — the contract emitted from the on-disk schema, i.e. the same contract `migrate` defaults to advancing toward when no `--to` is passed. Disconnected sub-graphs (historical-ref chains, abandoned branches, parallel work, etc.) render as side-branches indented one level. - -Rationale: the live contract answers "where does the app's code think the schema is?" — it's the *current* anchor in every authoring workflow. Historical refs are secondary, possibly-stale artefacts. Picking the historical-ref chain as the trunk is misleading: it implies the historical state is "the main line" when the operator's actual reference frame is the live contract. - -This rule applies uniformly to `list`, `status`, and `graph`. `list` already implements it; TML-2812 propagates it to the other two and asserts it via a shared snapshot. A parametrised trunk-choice (e.g. `--trunk `) is not in scope — locking one rule is the priority. - -## Resolved open items (were under discussion) - -- `status` multi-space rendering → **full annotated per-space sections** (each - space its own tree + overlay), headings only when >1 space, matching `list`/ - `graph` policy D4 (D6 / S-decisions at TML-2748 pickup). Not a compact summary. -- `log` name-mapping → **use the ledger's own `migration_name`** (D7); no on-disk - lookup, so the "package gone / ambiguous" question is moot (D12). - -## Delivery: three parallel slices off the ledger foundation - -With the ledger foundation (D7, TML-2769) merged, three slices run in parallel, -each its own branch/PR: - -1. **`list` renders the tree** (TML-2768) — human output adopts the shared tree - with package-fact edge annotations (D11); JSON stays the flat package array - (D2/D3). Introduces `edgeAnnotationsByHash` on the renderer. -2. **`status` = tree + DB-state overlay** (TML-2748) — renders the shared tree - directly via the `graph --tree` engine + the `status` edge annotation (D6, - D11), `--from`/`--to` (D9), `--space` (D4); deletes dagre and makes the tree - the default (`--tree` flag dropped, D5). -3. **`log` reads the ledger** (TML-2770) — flat single-table apply history (D8, - D12, D13). - -Each slice's `spec.md` carries the locked design + dispatch plan with **no open -questions** (every edge case pre-decided). `slices/edges-on-plan` and -`slices/empty-origin-as-null` remain deferred ledger cleanups (TML-2774). - -## `migrate --show` answers "what will `migrate` do?" (supersedes TML-2771's `migration path` noun) - -Discussion (2026-06-04, pm + architect) on TML-2771. The driving job: help users build a correct mental model of the **graph** migration system (vs linear) by answering *"what will happen when I `migrate` to the prod ref?"* - -**D-MS1 — Ship `migrate --show`, not a new `migration path --from --to` read command.** The job is decision-support *at the moment of acting*, so it belongs on the verb the user already reaches for — zero discovery cost, reinforces the verb. A `migration path` noun aims at a different (offline exploration) moment and adds a command to learn. *Rejected:* the original `migration path` noun. - -**D-MS2 — Flag name `--show`, not `--dry-run`.** "dry-run" connotes *step-through-every-op-and-halt*; the intent is *show the chosen path*. `--show` carries that. (`--plan` rejected too — collides with the `migration plan` authoring command.) - -**D-MS3 — Output is three parts:** the Tier-3 graph tree with the chosen path (from-state → target) highlighted **bright green**, off-path nodes **dimmed and unlabelled**; plus a **linear, ordered list** of the migrations that will execute (unambiguous for scripts/loops). - -**D-MS4 — From-state honesty.** Default `--from` = **the live DB marker, read read-only** (a connection, but no write ⇒ "no impact"). A preview that starts from a different state than the real `migrate` can *lie*, which is worse than no preview. Explicit `--from X` = a clearly-labelled **offline hypothetical** (no connection). - -**D-MS5 — New reference-grammar token for "the live marker": `@db`.** Today nothing you can pass to `--from` means "go read the live DB marker." Add the `@db` sigil to `parseContractRef` (`migration-tools/src/refs/contract-ref.ts`), resolved via `readAllMarkers()`. The spike (below) confirmed `db` is **not** a `--from` resolver token today — it exists only as the file-backed `db` ref and the renderer's `DB_MARKER_NAME='db'` label — so `@db` introduces **no collision and no rename**. Reusable everywhere the contract-reference grammar is accepted. - -**D-MS6 — Faithfulness constraint (architectural).** `migrate --show` runs `migrate`'s **exact** path-finder seam — no parallel reimplementation — and shares the Tier-3 renderer with `graph`/`status`. `status --from`'s offline path-preview routes through the same seam. A sanity check that runs different code than the action can disagree with reality. - -**D-MS7 — Unify the reserved-marker render vocabulary with the reference tokens: draw `@db` / `@contract`, drop the angle brackets.** Today the shared overlay draws the reserved markers in angle brackets — `` (`migration-list-styler.ts:91-94`) — while user refs use parens — `(main, prod)` (lines 97-100). Now that `@db` (and, symmetrically, `@contract`) are *reference tokens* you can type into `--from`/`--to`, the graph should **draw them the same way you type them**: `@contract @db`, sigil-prefixed, no angle-bracket wrapper. User refs keep parens (they're not sigil'd). *Why:* what you see in the graph should be what you can type — the strongest version of the mental-model goal. This touches the **shared** Tier-3 overlay AND the **`--legend`** output — the legend's own example markers (`formatLegendExampleMarkers`, `migration-graph-tree-render.ts:744`) currently print the `` form, and its explanatory text must now teach the `@db`/`@contract` spelling *and* that those are the tokens you can type into `--from`/`--to`. Both surfaces are shared by `graph` / `status` / `list` (legend via `utils/legend.ts`), so the change moves as one vocabulary, not per-command (snapshot regen across all three). It ships as the **vocabulary-foundation dispatch** of the `migrate --show` slice (the preview can't render `@db`-highlighted while siblings still show `` or legend it as ``). `@db` resolves only with a connection (live marker); `@contract` is offline-resolvable (the working/desired contract the app carries, and `migrate --to`'s default). - -**Command boundaries this locks in (the mental-model payoff):** `migrate --show` = *what this action will do* (planner's chosen path) · `status` = *where my live DB sits* (ledger: applied/pending, relative to the connected DB) · `graph` = *the whole map* · `log` = *what already ran* · `migration plan` = *authoring a new migration* (writes to disk — not a viewer). - -**Assumption — VERIFIED by spike (2026-06-04).** The planner exposes a clean read-only "compute the path, don't execute" seam: `graphWalkStrategy()` (`migration-tools/src/aggregate/strategies/graph-walk.ts:51`) returns the ordered `PerSpacePlan` / `pathDecision.selectedPath` as a pure, no-write value; `runMigration()` (`cli/.../operations/migrate.ts:284`) is the execution boundary. `readAllMarkers()` is read-only. `migrate --show` = `readAllMarkers` + `graphWalkStrategy` + render, stopping before `runMigration`. **No extraction needed — stays a one-PR slice.** `status --from` already calls the shared core (`findPathWithDecision`, `migration-graph.ts:300`) that `graphWalkStrategy` wraps, so consistency holds with no convergence refactor. - -**Rejected alternatives:** the offline `migration path --from --to` noun (wrong user moment + extra command); `migrate --dry-run` (wrong semantics); folding the feature entirely into `status` (loses the at-the-verb sanity check — though the two *share* the engine + renderer). - -**D-MS3 revised (operator visual review of PR #735).** The preview renders the **whole** graph, not just the path: on-path = bright green across nodes/hashes/names **and lane lines**; off-path = **uniform dim grey, fully drawn** (not omitted/unlabelled). Two correctness rules the first cut got wrong: **`@contract` marks the app's working-contract node, not the `--to` target**, and **the floating `@contract` node renders only in the app space, never in an extension space** (e.g. `pgvector:` must not show a floating `@contract`). This second rule is **app-space-only and enforced structurally in the shared Tier-3 renderer** (an `isAppSpace` gate on the `@contract` marker + the floating working-contract node) — because it was a pre-existing bug in `migration graph` / `migration status` / **`migration list`** too (all passed the app `liveContractHash` to extension-space renders; the R4 fix updated graph + status but **missed `migration list`** — `migration-list-render.ts` `renderSpaceTreeBlock` calls `renderMigrationGraphSpaceTree` without `isAppSpace`, so it must thread the app space id through and pass it per space). `@db` is **not** app-gated — it's a per-space marker and legitimately appears in each space. The ordered list renders in the **graph's migration-row format + alignment, minus the gutter, in the same green**, printed directly — **not** via Clack `ui.log` (which injects the `│` gutter). diff --git a/projects/migration-graph-rendering/design/graph-render-redesign.md b/projects/migration-graph-rendering/design/graph-render-redesign.md deleted file mode 100644 index 72587d0380..0000000000 --- a/projects/migration-graph-rendering/design/graph-render-redesign.md +++ /dev/null @@ -1,185 +0,0 @@ -# Design: migration-graph layout + render redesign - -> **Design-of-record** for rebuilding the Tier-3 migration-graph renderer -> (`cli/src/utils/formatters/migration-graph-{layout,rows,tree-render,lane-colors}.ts`). -> This document is the **architecture** — the data structures and the rules. It is -> deliberately **independent of execution**: the slices that build it -> (`slices/render-redesign-*`) reference this doc; they do not redefine it. -> Settled in design discussion (architect + principal-engineer), 2026-06. - -## Why redesign - -The renderer works but is unmaintainable, and its colouring has been a long tail of -bleed bugs. The root cause is a **data-structure mismatch**: - -- **Colour is a property of a _line_** (an edge's routed path — its on/off-path role). -- The current grid stores **positions**: a row×column grid of `StructuralCell`s keyed - by lane/column. A lane is freed and **reused** by a different edge further down its - length, so the renderer must **reverse-engineer "which edge owns this cell"** from - position. That reverse-engineering is the source of every colour bug. - -Two concrete smells confirm it: - -1. The 14-variant `StructuralCell` union (`vertical-pass` / `branch-tee` / - `merge-corner` / `arc-crossing` / …) **is the set of glyph shapes**, and the - renderer is a `switch (cell.kind)` that looks up `palette[kind]`. The layout - pre-bakes the glyph; the renderer re-walks it. -2. Every cell variant carries an **optional `migrationHash?`** — bolted on late to - recover line identity the structure never modelled. Identity was never first-class. - -Every bug we fixed was **mis-attribution** (which line owns a cell), never -**mis-positioning** (the lines always went to the right cells). So the expensive -traversal + lane allocation is sound; the cell representation and the render are wrong. - -## The model - -**A _line_ is the primitive, not a cell.** Each migration edge becomes a routed line -that carries its own identity (the migration, and its on/off-path role). Each contract -becomes a node. Colour is asked of a line, never inferred from a position. - -### Phases - -**Layout** routes lines on a grid and is the only place geometry/topology decisions -live. It owns: - -- **Ownership** — each line ↔ a migration; each node ↔ a contract (or the empty - state). Identity travels with the line. -- **Overlap** — a cell holds an **ordered (z) set of lines present**, each with its - **local directions** (which of up/down/left/right it occupies in that cell). -- **Planes (z-order)** — see below; the layout assigns them. -- **The single-owner invariant** — the layout guarantees **every cell is owned by - exactly one line** (see § Planes: no tees + 2-col lanes). So a cell never needs more - than one glyph can express, and never holds two differently-coloured lines. - -**Render** is a **dumb projection** — no topology knowledge. Per cell: - -1. Take the **topmost** line. -2. **Glyph** = box-drawing character from that line's directions (verticals + corners), - with `○`/`∅` node markers and `↑ ↓ ⟲` arrows as overlays. -3. **Colour** = that line's colour (on-path / off-path; or the by-branch rotation in - normal mode). Read straight off the owning line — never arbitrated. -4. Lower lines are **occluded** (clipped) — not drawn at this cell. - -This makes the giant `switch` collapse to a direction→glyph lookup, and makes colour -**correct-by-construction**: exactly one owning line per cell, so a cell's colour is -never a compromise between two branches. - -### Planes + z-order (the load-bearing decision) - -A cell is a z-ordered stack of lines. **The topmost line in a cell is drawn; the rest -are occluded.** One rule governs **everything that overlaps**: - -- **Crossings** — two lines pass through a cell: the top is drawn, the lower clips. -- **Merges / forks** — several lines meet at a node: the **top branch is drawn - continuous; the others _yield_ beneath it** (each corners into its own connector - cell). A merge is *not* a special junction — it is "one continuous line + N yielding - corners", which scales to any number of parents/children. -- **Back-arcs** — see policy below. - -**The single-owner invariant — this is what dissolves the colour problem: no cell is -ever owned by two lines.** Achieved by: - -- **No tees.** The glyph alphabet is **verticals + corners + arrows + node markers** — - never `├ ┬ ┼`. A tee is the *only* glyph that bundles a through-line with a branch in - one cell; drop it and the bundling is gone. A fork/merge is the top line's continuous - `│`/corner plus each other line's own corner. -- **2 columns per lane** — a **lane column** (a single-owner vertical) and a - **connector column** (corners/horizontals, single-owner). Turns happen in the - connector column, never crammed into the trunk's column. (Columns-per-lane is a - configurable constant — see § Geometry.) - -Because every cell has exactly one owner, **colour is read straight off that line** — -there is never a colour to arbitrate between two branches. - -**Z-order assignment is mode-dependent — the only thing that differs between modes:** - -- **Normal (multi-colour) mode → trunk on top.** The main lane stays an unbroken `│`; - later parents corner in beneath it (`│─╮─╮…`). Compact, git-log-style. -- **Highlighted (`migrate --show`) mode → the on-path branch on top.** The chosen path - is lifted above everything and drawn as one continuous prominent line sweeping the - merge (`╰───╮`) — literally "the route the runner takes" — while off-path branches - yield beneath it, each owning its own (grey) corner cells. - -**Back-arc policy:** - -- **Forward DAG lines = base plane; back-arcs (rollbacks) = upper plane, drawn - continuous.** Where a back-arc crosses a forward vertical the **forward line clips** - and the back-arc runs through; colour is orthogonal (grey when off-path). -- **Back-arc convergence:** back-arcs landing on the **same target node share one - back-lane** — narrower, truer, fewer crossings. - -### Geometry is configurable - -Columns-per-lane and similar spacing constants must be **named parameters**, not -values hard-coded across the layout and render. Changing "3 columns per lane" must not -require rewriting the renderer. - -### Data-structure sketch (illustrative, not final) - -``` -type Direction = 'up' | 'down' | 'left' | 'right'; - -interface LineRef { // identity, carried into every cell the line touches - readonly migrationHash: string; - readonly role: PathRole; // 'on-path' | 'off-path' | undefined (normal mode) - // (branch/lane identity for the normal-mode rotation colour) -} - -interface CellLine { // one line's presence in one cell - readonly line: LineRef; - readonly directions: ReadonlySet; // which arms it occupies here - readonly plane: number; // z-order; higher = drawn on top -} - -interface Cell { - readonly node?: NodeRef; // contract marker, if any (never overlaps) - readonly lines: readonly CellLine[]; // ordered set of lines present -} - -type Grid = readonly (readonly Cell[])[]; // rows × columns -``` - -Render per cell: pick `max plane`; `glyph = boxChar(union of that plane's directions)`; -`colour = colourOf(that plane's winning line)`; node/arrow overlays last. - -## Rationale (the durable why) - -- **Correct-by-construction colour.** Identity is first-class and travels with the - line; the render never reverse-engineers ownership, so it cannot bleed. -- **The renderer becomes trivial.** Glyph = lookup, colour = the line's colour. No - cell-kind switch, no per-cell hash recovery. -- **Crossings vs junctions are one rule** (same plane connects, different plane - occludes) instead of a dozen `arc-*`/`merge-*` cell kinds. -- **Unambiguous colour is the goal**, which is why occlusion beats glyph-union: one - owner per cell means the colour always honestly belongs to a single branch. - -## Alternatives rejected - -- **Model A — glyph = union of directions, colour = priority** (crossings render as a - combined `┼` coloured by the winner). Keeps both lines traceable through a crossing, - but every crossing cell's colour necessarily **misrepresents one of its two lines** (a - green `┼` sitting in a grey arc). Rejected: unambiguous colour is the whole point. -- **Back-arcs in ever-higher unique planes.** Would let arcs never collide, but - **prevents convergence** (arcs to one target couldn't share a lane). Rejected in - favour of convergence + a single upper plane. -- **Per-cell `migrationHash` bolt-on (the current patch).** Recovers identity after the - fact instead of modelling it. It is the symptom we are removing. - -## Open questions (settle at spec time) - -- ~~Within-plane junction colour when a branch splits on-path/off-path.~~ **Resolved - by the design itself:** the no-tee / 2-col-lane / single-owner rule means a junction - is never one shared cell — the top branch is a continuous line, each other branch owns - its own corner cell, so there is no colour to arbitrate. The choice of *which* branch - is on top is the mode-dependent z-order (trunk-on-top normal / on-path-on-top - highlighted), not a colour rule. -- **Default columns-per-lane** value and the full set of geometry parameters to expose. - -## Relationship to the current code - -- Likely **reusable**: the traversal (which nodes/edges, row order) and the lane/column - allocation — positioning was never the bug. -- **Rewritten**: the cell representation (→ ordered lines with directions + plane), the - plane-assignment + back-arc convergence logic (new, in the layout), and the renderer - (→ occlusion box-char projection). The 14 cell-kinds + the render `switch` + the - `migrationHash?` bolt-on are retired. diff --git a/projects/migration-graph-rendering/learnings.md b/projects/migration-graph-rendering/learnings.md deleted file mode 100644 index f7940c5cf7..0000000000 --- a/projects/migration-graph-rendering/learnings.md +++ /dev/null @@ -1,12 +0,0 @@ -# Learnings — migration-graph-rendering - -> Orchestrator-maintained working ledger of patterns surfaced during this run (foot-guns, escapees, severity calibrations, classes of bug the spec didn't cover). Reviewed at close-out; cross-cutting lessons migrate to durable docs, project-local ones drop with the project folder. - -## `@prisma-next/cli` runs vitest `isolate: false` — "passes locally" ≠ "passes in CI" - -The cli package's vitest config (`isolate: false`, `fileParallelism: false`) lets module mocks (notably `config-loader`/`loadConfig`) bleed across test files. A test that forgets to mock `config-loader` itself can pass locally by inheriting a leaked mock from a neighbouring file, then fail deterministically in CI's different file ordering with `errorConfigFileNotFound` (real `loadConfig` runs, no config file in cwd). Hit in `migration-status-missing-db.test.ts` (read-command-consistency slice, D2): green locally, red in CI #726. - -**Rules going forward:** -- Every cli command test that drives a command through `loadConfig` MUST set up its own config-loader state (mock returning the intended config, or a real fixture dir with a config file) — never rely on ambient mock state. Pair with file-level `afterAll(() => { vi.doUnmock('../../src/config-loader'); vi.resetModules(); })` so it doesn't pollute neighbours either. -- "Reproduces on the base tree" / "full suite passed N×" are NOT sufficient evidence a failure is pre-existing/independent when `isolate:false` is in play — favorable ordering masks both directions. Validate under CI file-ordering (the actual failing combination) before declaring a failure "not ours." The orchestrator made this error: trusted local green + implementer claims and shipped a PR whose CI was red on our own tests. - diff --git a/projects/migration-graph-rendering/plan.md b/projects/migration-graph-rendering/plan.md deleted file mode 100644 index 2c02c92115..0000000000 --- a/projects/migration-graph-rendering/plan.md +++ /dev/null @@ -1,38 +0,0 @@ -# Project plan — migration-graph-rendering - -Single source of truth for what's done and what's left. Slice specs live under -[`slices/`](./slices/); the older read-command-family roadmap is in [`README.md`](./README.md). - -## Renderer redesign track (current focus) - -The Tier-3 renderer was rebuilt on a line/plane/occlusion model so colouring is -correct-by-construction (no tees — corners + occlusion only). - -| Slice | Status | What it delivers | -| ----- | ------ | ---------------- | -| [`render-redesign-core`](./slices/render-redesign-core/) | **Merged** (#762) | The line/plane/occlusion rewrite: lines as the primitive, z-ordered cells, occlusion projection. Retired the old `renderMigrationGraphTree` + the 14 `StructuralCell` kinds + all tee glyphs. | -| [`render-redesign-geometry`](./slices/render-redesign-geometry/) | **In progress** (#767) | Back-arc **convergence** (rollbacks to one target share a single back-lane); lift the `colsPerLane` spacing default to a named constant + a scaling test; hand-authored **convergence goldens**; the **converged showcase golden** candidate (real-world fixture, for operator review). | - -After `render-redesign-geometry` lands, the renderer redesign is **effectively complete**. - -## Deleted as dead (rewrite-obsoleted) - -These were drafted as individual glyph-bug slices against the **old** renderer, then made -moot by the wholesale rewrite. They are deleted (recoverable from git history); the new -corner/occlusion model cannot produce the bugs they describe, and the showcase golden -exercises the same shapes: - -- `converging-back-arcs` — "only one of N converging rollbacks lands" → convergence now lands all of them on one shared lane (delivered in `render-redesign-geometry`). -- `connector-crossing-glyph` — "a pass-through lane renders as a `┬`/`┴` tee instead of `┼`" → there are no tees; a crossing is two lines arbitrated by occlusion. -- `node-merge-landing-marker` — "a non-trunk merge landing drops the `○` marker" → a node is its own single-owner cell and is always drawn. - -## Optional follow-ups (low priority, not started, no dedicated spec) - -- **Sync the on-disk showcase demo fixture** (`examples/prisma-next-demo/fixtures/showcase/`) to the showcase golden so the live demo shows the multi-lane-merge + convergence shape. Demo polish only — the renderer behaviour is already covered by the showcase golden test. (Was the `showcase-multilane-merge` slice; touches `examples/`.) -- **Regenerate the `diamond` example fixture** with complete `end-contract.*` artifacts so it's loadable by path (today only an uncommitted demo-config edit reads it). (Was `diamond-fixture-regeneration`.) - -## Read-command-family track (older, mostly merged) - -A separate, largely-completed track — `migration list`/`status`/`log` rendering + the -ledger foundation. Sequenced in [`README.md`](./README.md) (slices 1–8). Not re-audited in -this cleanup. diff --git a/projects/migration-graph-rendering/prototype/gallery.md b/projects/migration-graph-rendering/prototype/gallery.md deleted file mode 100644 index d1a81c9df9..0000000000 --- a/projects/migration-graph-rendering/prototype/gallery.md +++ /dev/null @@ -1,268 +0,0 @@ -# tier-3 `migration graph` prototype gallery - -_Generated by `projects/migration-graph-rendering/prototype/proto.mjs` — 16 fixtures + 5 synthetic._ - -## complex (8 nodes, 8 edges) - -``` - 20260305T1000_add_tags 0276f92 → cd5c15b - 20260304T1000_add_comments 3b2d98d → 0276f92 - 20260303T1100_merge_bob 6656a6e → 3b2d98d - 20260303T1000_merge_alice 73e3abe → 3b2d98d - 20260302T1200_staging_posts ef9de27 → a94b7b4 - 20260302T1100_bob_add_avatar ef9de27 → 6656a6e - 20260302T1000_alice_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4, cd5c15b conv: 3b2d98d div: ef9de27 -``` - -## converging-branches (6 nodes, 7 edges) - -``` - 20260303T1200_merge_avatar 6656a6e → 3116048 - 20260303T1100_merge_posts a94b7b4 → 3116048 - 20260303T1000_merge_phone 73e3abe → 3116048 - 20260302T1200_add_avatar ef9de27 → 6656a6e - 20260302T1100_add_posts ef9de27 → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 3116048 conv: 3116048 div: ef9de27 -``` - -## diamond (5 nodes, 5 edges) - -``` - 20260303T1100_merge_bob 6656a6e → 3b2d98d - 20260303T1000_merge_alice 73e3abe → 3b2d98d - 20260302T1100_bob_add_avatar ef9de27 → 6656a6e - 20260302T1000_alice_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 3b2d98d conv: 3b2d98d div: ef9de27 -``` - -## diamond-sub-branch (7 nodes, 7 edges) - -``` - 20260303T1200_bob_experiment_2 becd3f1 → b01f4d9 - 20260303T1100_merge_bob 6656a6e → 3b2d98d - 20260303T1000_merge_alice 73e3abe → 3b2d98d - 20260302T1200_bob_experiment 6656a6e → becd3f1 - 20260302T1100_bob_add_avatar ef9de27 → 6656a6e - 20260302T1000_alice_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 3b2d98d, b01f4d9 conv: 3b2d98d div: ef9de27, 6656a6e -``` - -## kitchen-sink (10 nodes, 10 edges, 1 back) - -``` - 20260326T1428_migration cc527d2 → bdc08a6 - ↩ 20260308T1000_rollback e9bd4aa → 0276f92 - 20260307T1000_kitchen_sink 0276f92 → e9bd4aa - 20260306T1000_add_comments a94b7b4 → 0276f92 - 20260305T1000_add_posts c81f321 → a94b7b4 - 20260304T1000_change_default b1858bc → c81f321 - 20260303T1000_email_default 73e3abe → b1858bc - 20260302T1100_widen_email ef9de27 → cc527d2 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: e9bd4aa, bdc08a6 conv: — div: ef9de27 -``` - -## linear (3 nodes, 2 edges) - -``` - 20260302T1000_add_posts ef9de27 → a94b7b4 - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4 conv: — div: — -``` - -## long-spine (10 nodes, 9 edges) - -``` - 20260308T1000_add_everything cd5c15b → 3116048 - 20260307T1100_late_branch cd5c15b → becd3f1 - 20260307T1000_add_tags 0276f92 → cd5c15b - 20260306T1000_add_comments 6656a6e → 0276f92 - 20260305T1000_add_avatar a94b7b4 → 6656a6e - 20260304T1000_add_posts 3ee5d20 → a94b7b4 - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: becd3f1, 3116048 conv: — div: cd5c15b -``` - -## multi-branch (7 nodes, 9 edges) - -``` - 20260326T1432_foobar 3ee5d20 → bdc08a6 - 20260326T1431_migration 3ee5d20 → bdc08a6 - 20260326T1431_foobar 3ee5d20 → bdc08a6 - 20260326T1430_migration 3ee5d20 → bdc08a6 - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1200_add_avatar ef9de27 → 6656a6e - 20260302T1100_add_posts ef9de27 → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4, 6656a6e, bdc08a6 conv: bdc08a6 div: ef9de27, 3ee5d20 -``` - -## multi-rollback-branch (7 nodes, 7 edges, 1 back) - -``` - ↩ 20260306T1000_rollback_to_phone 0276f92 → 73e3abe - 20260305T1000_add_tags 0276f92 → cd5c15b - 20260304T1000_add_comments 3ee5d20 → 0276f92 - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1100_staging 73e3abe → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4, cd5c15b conv: — div: 73e3abe -``` - -## rollback (4 nodes, 5 edges, 2 back) - -``` - ↩ 20260305T1000_rollback_phone 73e3abe → ef9de27 - ↩ 20260304T1000_rollback_bio 3ee5d20 → 73e3abe - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 3ee5d20 conv: — div: — -``` - -## rollback-continue (5 nodes, 6 edges, 2 back) - -``` - 20260306T1000_add_posts ef9de27 → a94b7b4 - ↩ 20260305T1000_rollback_phone 73e3abe → ef9de27 - ↩ 20260304T1000_rollback_bio 3ee5d20 → 73e3abe - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 3ee5d20, a94b7b4 conv: — div: ef9de27 -``` - -## sequential-diamonds (8 nodes, 9 edges) - -``` - 20260305T1100_merge_2b a94b7b4 → cd5c15b - 20260305T1000_merge_2a 0276f92 → cd5c15b - 20260304T1100_add_posts_branch 3b2d98d → a94b7b4 - 20260304T1000_add_comments 3b2d98d → 0276f92 - 20260303T1100_merge_1b 6656a6e → 3b2d98d - 20260303T1000_merge_1a 73e3abe → 3b2d98d - 20260302T1100_bob_add_avatar ef9de27 → 6656a6e - 20260302T1000_alice_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: cd5c15b conv: 3b2d98d, cd5c15b div: ef9de27, 3b2d98d -``` - -## single-branch (5 nodes, 4 edges) - -``` - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1100_add_posts ef9de27 → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4, 3ee5d20 conv: — div: ef9de27 -``` - -## skip-rollback (5 nodes, 6 edges, 2 back) - -``` - ↩ 20260306T1000_rollback_to_init 3ee5d20 → ef9de27 - ↩ 20260305T1000_rollback_to_phone a94b7b4 → 73e3abe - 20260304T1000_add_posts 3ee5d20 → a94b7b4 - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4 conv: — div: — -``` - -## sub-branches (6 nodes, 5 edges) - -``` - 20260303T1100_add_avatar ef9de27 → 6656a6e - 20260303T1000_add_bio 73e3abe → 3ee5d20 - 20260302T1100_add_posts 73e3abe → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: a94b7b4, 3ee5d20, 6656a6e conv: — div: ef9de27, 73e3abe -``` - -## wide-fan (7 nodes, 6 edges) - -``` - 20260302T1400_add_settings ef9de27 → b01f4d9 - 20260302T1300_add_category ef9de27 → becd3f1 - 20260302T1200_add_avatar ef9de27 → 6656a6e - 20260302T1100_add_posts ef9de27 → a94b7b4 - 20260302T1000_add_phone ef9de27 → 73e3abe - 20260301T1000_init ∅ → ef9de27 - - roots: ∅ tips: 73e3abe, a94b7b4, 6656a6e, becd3f1, b01f4d9 conv: — div: ef9de27 -``` - -## synth-multi-root (4 nodes, 3 edges) - -``` - 0003_merge ccccccc → ddddddd - 0002_branch_b_init bbbbbbb → ccccccc - 0001_branch_a_init aaaaaaa → ccccccc - - roots: aaaaaaa, bbbbbbb tips: ddddddd conv: ccccccc div: — -``` - -## synth-dangling-parent (3 nodes, 2 edges) - -``` - 0002_continue eeeeeee → fffffff - 0001_after_prune ddddddd → eeeeeee - - roots: ddddddd tips: fffffff conv: — div: — -``` - -## synth-pure-cycle (2 nodes, 2 edges, 1 back) - -``` - ↩ 0002_rollback bbbbbbb → aaaaaaa - 0001_forward aaaaaaa → bbbbbbb - - roots: aaaaaaa tips: bbbbbbb conv: — div: — -``` - -## synth-forest (5 nodes, 3 edges) - -``` - 0003_other_root ccccccc → ddddddd - 0002_app_next aaaaaaa → bbbbbbb - 0001_app_init ∅ → aaaaaaa - - roots: ∅, ccccccc tips: bbbbbbb, ddddddd conv: — div: — -``` - -## synth-self-edge (3 nodes, 3 edges, 1 self) - -``` - 0003_next aaaaaaa → bbbbbbb - ⟲ 0002_noop aaaaaaa → aaaaaaa - 0001_init ∅ → aaaaaaa - - roots: ∅ tips: bbbbbbb conv: — div: — -``` diff --git a/projects/migration-graph-rendering/prototype/proto.mjs b/projects/migration-graph-rendering/prototype/proto.mjs deleted file mode 100644 index e87448b939..0000000000 --- a/projects/migration-graph-rendering/prototype/proto.mjs +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env node -// Throwaway prototyping harness for the tier-3 `migration graph` redesign. -// -// Loads every fixture under examples/prisma-next-demo/migration-fixtures, -// extracts its topology (contracts = nodes, migrations = edges), classifies -// edge kinds, runs a pluggable `render(graph)` and writes one gallery file -// with every case rendered side by side. -// -// Run from the repo root: -// node projects/migration-graph-rendering/prototype/proto.mjs -// -// Design loop: edit render() -> re-run -> -// open projects/migration-graph-rendering/prototype/gallery.md -> react -> repeat. -// -// Not production code. The real renderer will consume the consolidated -// tolerant model (TML-2739); here we recompute topology in plain JS so the -// loop stays zero-build. - -import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; - -const FIXTURES = 'examples/prisma-next-demo/migration-fixtures'; -const EMPTY = '∅'; - -// --------------------------------------------------------------------------- -// Load -// --------------------------------------------------------------------------- - -/** @typedef {{ dirName: string, from: string, to: string }} Edge */ -/** @typedef {{ name: string, edges: Edge[], nodes: string[], short: (h:string)=>string, refs: Record }} Graph */ - -function loadFixture(name) { - const appDir = join(FIXTURES, name, 'app'); - const migDirs = readdirSync(appDir) - .filter((d) => statSync(join(appDir, d)).isDirectory()) - .sort(); // dirName ascending; we sort per-view as needed - /** @type {Edge[]} */ - const edges = []; - for (const dirName of migDirs) { - const m = JSON.parse(readFileSync(join(appDir, dirName, 'migration.json'), 'utf8')); - edges.push({ dirName, from: m.from ?? EMPTY, to: m.to }); - } - // refs (optional) - /** @type {Record} */ - const refs = {}; - try { - const refsDir = join(FIXTURES, name, 'refs'); - for (const f of readdirSync(refsDir)) { - if (!f.endsWith('.json')) continue; - const r = JSON.parse(readFileSync(join(refsDir, f), 'utf8')); - refs[f.replace(/\.json$/, '')] = r.hash; - } - } catch { - /* no refs */ - } - - const nodeSet = new Set(); - for (const e of edges) { - nodeSet.add(e.from); - nodeSet.add(e.to); - } - const nodes = [...nodeSet]; - - // stable short labels per fixture - const shortMap = new Map(); - for (const h of nodes) { - shortMap.set(h, h === EMPTY ? EMPTY : h.replace(/^sha256:/, '').slice(0, 7)); - } - const short = (h) => shortMap.get(h) ?? h.replace(/^sha256:/, '').slice(0, 7); - - return { name, edges, nodes, short, refs }; -} - -// --------------------------------------------------------------------------- -// Topology: classify edge kinds (forward / back / self) via 3-colour DFS. -// Neighbour order pinned to dirName-descending for determinism. -// Seed from forward-in-degree-0 roots (∅ first, then lexicographic), then any -// unvisited remainder lexicographically (covers pure cycles / multi-root). -// --------------------------------------------------------------------------- - -function classify(graph) { - const { edges, nodes } = graph; - /** @type {Map} */ - const out = new Map(); - for (const n of nodes) out.set(n, []); - for (const e of edges) out.get(e.from).push(e); - for (const list of out.values()) list.sort((a, b) => (a.dirName < b.dirName ? 1 : -1)); // dirName desc - - /** @type {Map} */ - const kind = new Map(); - const WHITE = 0; - const GREY = 1; - const BLACK = 2; - const color = new Map(nodes.map((n) => [n, WHITE])); - - const indeg = new Map(nodes.map((n) => [n, 0])); - for (const e of edges) if (e.from !== e.to) indeg.set(e.to, (indeg.get(e.to) ?? 0) + 1); - const roots = nodes.filter((n) => (indeg.get(n) ?? 0) === 0); - roots.sort((a, b) => (a === EMPTY ? -1 : b === EMPTY ? 1 : a < b ? -1 : 1)); - const seeds = [...roots, ...nodes.filter((n) => !roots.includes(n)).sort()]; - - function dfs(u) { - color.set(u, GREY); - for (const e of out.get(u)) { - if (e.from === e.to) { - kind.set(e.dirName, 'self'); - continue; - } - const c = color.get(e.to); - if (c === GREY) kind.set(e.dirName, 'back'); - else { - if (!kind.has(e.dirName)) kind.set(e.dirName, 'fwd'); - if (c === WHITE) dfs(e.to); - } - } - color.set(u, BLACK); - } - for (const s of seeds) if (color.get(s) === WHITE) dfs(s); - for (const e of edges) if (!kind.has(e.dirName)) kind.set(e.dirName, 'fwd'); - - return kind; -} - -// --------------------------------------------------------------------------- -// render(graph, kind) <-- THE THING WE ITERATE ON -// -// v0 strawman: just a normalized edge list + degree summary. Replace the body -// with candidate node-per-row lane layouts as the design converges. -// --------------------------------------------------------------------------- - -function render(graph, kind) { - const { edges, nodes, short } = graph; - const tag = { fwd: ' ', back: '↩', self: '⟲' }; - - const indeg = new Map(nodes.map((n) => [n, 0])); - const outdeg = new Map(nodes.map((n) => [n, 0])); - for (const e of edges) { - if (kind.get(e.dirName) !== 'fwd') continue; - outdeg.set(e.from, (outdeg.get(e.from) ?? 0) + 1); - indeg.set(e.to, (indeg.get(e.to) ?? 0) + 1); - } - const roots = nodes.filter((n) => (indeg.get(n) ?? 0) === 0); - const tips = nodes.filter((n) => (outdeg.get(n) ?? 0) === 0); - const conv = nodes.filter((n) => (indeg.get(n) ?? 0) >= 2); - const div = nodes.filter((n) => (outdeg.get(n) ?? 0) >= 2); - - const lines = []; - // edges newest-first (dirName desc) — the language we talk in - for (const e of [...edges].sort((a, b) => (a.dirName < b.dirName ? 1 : -1))) { - const k = kind.get(e.dirName); - lines.push(` ${tag[k]} ${e.dirName.padEnd(34)} ${short(e.from).padStart(7)} → ${short(e.to)}`); - } - const meta = - ` roots: ${roots.map(short).join(', ') || '—'}` + - ` tips: ${tips.map(short).join(', ') || '—'}` + - ` conv: ${conv.map(short).join(', ') || '—'}` + - ` div: ${div.map(short).join(', ') || '—'}`; - return [...lines, '', meta].join('\n'); -} - -// --------------------------------------------------------------------------- -// Synthetic graphs: the model-permitted shapes the on-disk fixtures don't -// cover (no ∅ genesis, pruned ancestors, pure cycles, disconnected forests). -// Built in the same shape loadFixture returns. -// --------------------------------------------------------------------------- - -function makeGraph(name, rawEdges, refs = {}) { - const edges = rawEdges.map(([dirName, from, to]) => ({ dirName, from: from ?? EMPTY, to })); - const nodeSet = new Set(); - for (const e of edges) { - nodeSet.add(e.from); - nodeSet.add(e.to); - } - const nodes = [...nodeSet]; - const short = (h) => (h === EMPTY ? EMPTY : h.replace(/^sha256:/, '').slice(0, 7)); - return { name, edges, nodes, short, refs }; -} - -const SYNTH = [ - // two independent roots converging — no ∅ anywhere - makeGraph('synth-multi-root', [ - ['0001_branch_a_init', 'sha256:aaaaaaa0', 'sha256:ccccccc0'], - ['0002_branch_b_init', 'sha256:bbbbbbb0', 'sha256:ccccccc0'], - ['0003_merge', 'sha256:ccccccc0', 'sha256:ddddddd0'], - ]), - // a `from` whose producing migration was pruned (dangling parent → root) - makeGraph('synth-dangling-parent', [ - ['0001_after_prune', 'sha256:ddddddd0', 'sha256:eeeeeee0'], - ['0002_continue', 'sha256:eeeeeee0', 'sha256:fffffff0'], - ]), - // pure 2-cycle, no in-degree-0 root to seed from - makeGraph('synth-pure-cycle', [ - ['0001_forward', 'sha256:aaaaaaa0', 'sha256:bbbbbbb0'], - ['0002_rollback', 'sha256:bbbbbbb0', 'sha256:aaaaaaa0'], - ]), - // two disconnected components (one ∅-rooted, one dangling-rooted) - makeGraph('synth-forest', [ - ['0001_app_init', null, 'sha256:aaaaaaa0'], - ['0002_app_next', 'sha256:aaaaaaa0', 'sha256:bbbbbbb0'], - ['0003_other_root', 'sha256:ccccccc0', 'sha256:ddddddd0'], - ]), - // self-edge alongside forward edges - makeGraph('synth-self-edge', [ - ['0001_init', null, 'sha256:aaaaaaa0'], - ['0002_noop', 'sha256:aaaaaaa0', 'sha256:aaaaaaa0'], - ['0003_next', 'sha256:aaaaaaa0', 'sha256:bbbbbbb0'], - ]), -]; - -// --------------------------------------------------------------------------- -// Drive: render every fixture + synthetic case into one gallery file. -// --------------------------------------------------------------------------- - -const names = readdirSync(FIXTURES) - .filter((d) => statSync(join(FIXTURES, d)).isDirectory()) - .sort(); - -const graphs = [...names.map(loadFixture), ...SYNTH]; - -const out = ['# tier-3 `migration graph` prototype gallery', '']; -out.push( - `_Generated by \`projects/migration-graph-rendering/prototype/proto.mjs\` — ${names.length} fixtures + ${SYNTH.length} synthetic._`, - '', -); - -for (const g of graphs) { - const name = g.name; - const kind = classify(g); - const backs = [...kind.values()].filter((k) => k === 'back').length; - const selfs = [...kind.values()].filter((k) => k === 'self').length; - out.push( - `## ${name} (${g.nodes.length} nodes, ${g.edges.length} edges` + - `${backs ? `, ${backs} back` : ''}${selfs ? `, ${selfs} self` : ''})`, - '', - '```', - render(g, kind), - '```', - '', - ); -} - -writeFileSync('projects/migration-graph-rendering/prototype/gallery.md', out.join('\n')); -console.log( - `Wrote projects/migration-graph-rendering/prototype/gallery.md (${names.length} fixtures)`, -); diff --git a/projects/migration-graph-rendering/read-command-consistency-audit.md b/projects/migration-graph-rendering/read-command-consistency-audit.md deleted file mode 100644 index 56e8e84143..0000000000 --- a/projects/migration-graph-rendering/read-command-consistency-audit.md +++ /dev/null @@ -1,131 +0,0 @@ -# Read-command consistency audit (TML-2801) - -Audits the migration **read** verbs — `status`, `list`, `graph`, `log`, `show`, `check` — for consistency now that the read-command redesign has shipped (`list`/`graph`/`status`/`log` split, dagre retired, ledger foundation). Where the earlier [`cli-audit.md`](../migration-domain-model/cli-audit.md) asked *which verbs should exist and where they live* (the F1–F8 surface-shape gaps, now largely implemented), this audit asks whether the implemented read verbs are **consistent with each other and with the [CLI Style Guide](../../docs/CLI%20Style%20Guide.md)** along the six axes named in the ticket: - -1. consistent params (`--to`, `--space`, …) -2. consistent formatting -3. consistent behaviours (e.g. how they handle multiple spaces) -4. consistent naming -5. clear, user-oriented help text -6. clear, user-oriented, structured errors with explicit "why" and "fix" - -This document is a state comparison + defect classification. It is not an implementation plan or a sequencing proposal; remediation is tracked as follow-up tickets (see [Remediation](#remediation)). - -Out of scope: the control-api ↔ CLI surface reconciliation ([TML-2780](https://linear.app/prisma-company/issue/TML-2780)) — that's the internal API naming, not the user-facing read surface. `migrate` and the authoring verbs (`plan`, `new`) are write verbs and out of scope. `ref list` is read-shaped but lives under a different subject; noted where relevant, not audited here. - -## Method - -1. Enumerated the read verbs from `packages/1-framework/3-tooling/cli/src/commands/migration-*.ts`. -2. For each, read the command builder verbatim: `setCommandDescriptions`, `setCommandExamples`, `setCommandSeeAlso`, the `addGlobalOptions(command).option(...)` chain, the `--json` envelope, the error factories invoked, and DB/driver requirements. -3. Compared the resulting matrix across commands and against the Style Guide. -4. Classified each divergence as a **defect** (fix) or an **intended divergence** (document the rationale). Not every difference is a defect — `log` being unscoped and `check` carrying custom exit codes are deliberate. - -All file:line references below were read directly from source at audit time. - -## Surface matrix - -| Axis | `status` | `list` | `graph` | `log` | `show` | `check` | -|---|---|---|---|---|---|---| -| Live/offline | live (offline w/ `--from`) | offline | offline | live | offline\* | offline | -| `--space` | ✅ | ✅ | ✅ | ❌ (unscoped) | ❌ (app-only) | ❌ (app-only) | -| `--to` / `--from` | ✅ both | — | — | — | — | — | -| Positional ref | — | — | — | — | `` required | `[migration]` optional | -| `--ascii` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | -| `--legend` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | -| `--db` | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | -| Ref parser | `parseContractRef` | — | — | — | `parseMigrationRef` | `parseMigrationRef` | -| `--json` envelope | `{ ok, … }` | `{ ok, … }` | inline `{ ok, … }`, no schema | **bare array** | `{ ok, … }` | `{ ok, … }` | -| Exported JSON schema | type only | type only | ❌ | ❌ | type only | type only | -| Custom exit codes | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (`exit-codes.ts`) | - -\* `show` is nominally offline but instantiates a control client (`migration-show.ts:156`) it never connects. - -## Findings - -### F1. Spatial vocabulary is three different stories (behaviour) - -**Current.** Three commands accept `--space ` with all-spaces-by-default + narrow-to-one (`migration-status.ts:652`, `migration-list.ts:303`, `migration-graph.ts:211`, validated centrally in `runMigrationList`). `log` is deliberately unscoped (merges every space's ledger in apply order). `show` and `check` are silently app-space-only — no `--space`, no diagnostic, the word "app-space" buried in the long description (`migration-show.ts:262`, `migration-check.ts:278`). - -**Why it matters.** The ticket calls out "how they handle multiple spaces" as a first-class consistency axis. A user who learns `--space app` from `list` reasonably expects it on `show`/`check`; today it's silently absent. The all-spaces-vs-app-only split is real (per-migration `show`/`check` operate on one package), but it's undocumented at the surface. - -**Classification.** `log` unscoped = **intended** (document it). `show`/`check` app-only = **defect of signalling**, not of behaviour: either accept `--space ` (defaulting to app) or reject a passed `--space` with a structured error that says app-only and why. - -**Fix.** Decide the policy for per-migration verbs and make it explicit at the surface (flag or structured rejection). Document `log`'s unscoped semantics in its long description. - -### F2. Two ref grammars, three positional behaviours (params) - -**Current.** `status` resolves `--to`/`--from` via `parseContractRef` — the full reference grammar (hash, prefix, ref name, migration dir name, `^`, `./path`), as advertised in its option help (`migration-status.ts:653-660`). `show` and `check` take a positional and resolve via `parseMigrationRef` — narrower. `show` accepts a filesystem path (`migration-show.ts:276`); `check` does **not** (`migration-check.ts:295`, help text "directory name or hash"). `show`'s target is required; `check`'s is optional (no-arg = whole-graph check). - -**Why it matters.** "Consistent params" and "consistent naming." The same conceptual input — *name a migration* — has three different accepted grammars across two adjacent verbs, and only `status` honours the rich contract-reference grammar the [`cli-audit.md`](../migration-domain-model/cli-audit.md) F5 resolution settled on. - -**Classification.** Required-vs-optional positional = **intended** (`check` with no arg is a distinct, useful mode). Path-accepted-by-`show`-but-not-`check` = **defect** (arbitrary). Whether `show`/`check` should accept the *contract* grammar (ref names) is a **design question**, not an obvious defect — they resolve a migration package, not a contract. - -**Fix.** Align `show` and `check` on one `parseMigrationRef` grammar (decide path in/out, apply to both). Make the help text for both describe the *same* accepted forms. - -### F3. `--json` envelope is not uniform (naming) - -**Current.** `status`, `list`, `show`, `check` emit a `{ ok, … }` object. **`log` emits a bare array** (`migration-log.ts:134`). **`graph` builds its envelope inline** in the action with no co-located, exported schema (`migration-graph.ts` action constructs `{ ok, nodes, edges, summary }` by hand). The primary-payload field name varies with no rule: `spaces` (status/list), `migration` (show), `failures` (check), top-level `nodes`/`edges` (graph). - -**Why it matters.** Style Guide §JSON Semantics: "Each command's `--json` success shape MUST be defined as a schema (arktype or equivalent) co-located with the command … and exported on the package's public surface." Success/error docs "SHOULD share a discriminator field (typically `ok: boolean`)." `log`'s bare array has no `ok` discriminator and can't carry an error in the same shape; `graph` has no exported schema at all. Both violate the guide. - -**Classification.** **Defect** — direct Style-Guide non-conformance, and the most mechanically fixable. - -**Fix.** Wrap `log` in `{ ok: true, entries: [...] }`. Give `graph` a co-located exported output schema like its peers. Audit field-naming for a shared convention. - -### F4. Decoration flags (`--ascii`, `--legend`) absent from offline verbs that could use them (formatting) - -**Current.** `--ascii` and `--legend` appear on `list` and `graph`; `--legend` also on `status`; `--ascii` is **not** on `status` (`migration-status.ts:649-661`) even though status renders the same shared tree. `show` and `check` (both offline, both human-rendered) have neither. - -**Why it matters.** `status` draws the shared tree (the parity test locks list/graph/status rendering byte-for-byte) yet can't force ASCII glyphs the way list/graph can — a pipe-to-file user gets inconsistent control depending on which read verb they reach for. - -**Classification.** `status` missing `--ascii` = **likely defect** (same renderer, asymmetric control). `show`/`check` missing `--legend`/`--ascii` = **judgement call** — their output isn't the laned tree, so a legend may not apply; confirm and document. - -**Fix.** Add `--ascii` to `status`. Decide `show`/`check` deliberately and note the decision. - -### F5. Structured errors are assembled ad hoc, not through one path (errors) - -**Current.** Ref-resolution errors go through the shared `mapRefResolutionError` in `status` (`migration-status.ts:330,342`) and `show` (`migration-show.ts:235`) — but `check` builds them **inline** with hand-written strings (`migration-check.ts`, around the `parseMigrationRef` call), so its envelope and `meta` diverge from its peers. DB-requirement signalling differs too: `log` raises `errorDatabaseConnectionRequired` then a separate `errorDriverRequired` (`migration-log.ts:51,59`), while `status` bundles connection+driver into a single `errorDatabaseConnectionRequired` condition (`migration-status.ts:274`). - -**Why it matters.** Style Guide §Errors mandates a uniform why/fix/where layout, a PN code, and (for missing-input failures) `meta.missingFlags`. Inline error construction in `check` bypasses the shared factory and risks drift in exactly the why/fix wording the ticket asks to standardise. - -**Classification.** `check` inline ref errors = **defect** (route through `mapRefResolutionError`). `log` vs `status` driver-vs-connection split = **defect of consistency** — same precondition, two different decompositions and two different messages; pick one. - -**Fix.** Route `check`'s ref errors through `mapRefResolutionError`. Unify the missing-DB/missing-driver precondition into one shared check used by both `log` and `status`. - -### F6. Help-text shape drifts (help text) - -**Current.** See-also lists are near-uniform (each links its four siblings) **except `check`, which omits `migration show`** (`migration-check.ts:288-292`) — the one sibling that, like check, takes a positional migration reference. Example counts swing with no rule: `graph` 6, `list` 5, `status` 4, `log`/`check` 3, `show` 2 (`show` has no `--json` example despite emitting JSON). Long-description phrasing is inconsistent: list/graph/check explicitly say "Offline — does not consult the database"; `show` (also offline) doesn't. - -**Why it matters.** "Clear, user-oriented help text" and discoverability. The see-also graph should be symmetric for a coherent verb family; the missing `check → show` edge is a real navigation hole. - -**Classification.** **Defect** (low effort, high polish). - -**Fix.** Add `migration show` to `check`'s see-also. Give every command a `--json` example where it emits JSON. State offline/live consistently in every long description. Consider a soft norm of 3–4 examples covering the command's distinguishing flags. - -### F7. `check` carries custom exit codes and `exitOverride` (exit codes) - -**Current.** `check` calls `command.exitOverride()` (`migration-check.ts:293`) and returns `OK`/`PRECONDITION`/`INTEGRITY_FAILED` from a co-located `migration-check/exit-codes.ts`; every other read verb uses the framework's default `handleResult` 0/1/2 semantics. - -**Why it matters / classification.** **Intended, and correct** per Style Guide §Exit Codes (a verify verb legitimately defines command-specific outcome codes in a co-located, exported module). Flagged only so the audit's "find the odd one out" doesn't misread it as drift. No fix — but confirm the codes are documented in `--help`/README per the guide's requirement. - -## Cross-cutting observations - -- **The shared infrastructure is good and underused.** `runMigrationList` (space validation), `parseContractRef`/`parseMigrationRef` (ref grammar), `mapRefResolutionError` (error envelopes), `validateLegendOptions`, the shared tree renderer — the consistency primitives exist. Most defects are commands *not* reaching for the shared path (`check`'s inline errors, `graph`'s inline JSON, `status` missing `--ascii`), not missing infrastructure. -- **Consistency isn't test-enforced beyond rendering.** [`migration-read-commands-parity.test.ts`](../../packages/1-framework/3-tooling/cli/test/commands/migration-read-commands-parity.test.ts) locks *pretty-rendering* parity (byte-identical per-space sections) but nothing about flags, help shape, JSON envelopes, or error shapes. These can re-drift freely. Extending this test (or a sibling) to assert the agreed conventions would prevent regression. -- **The Style Guide is the right yardstick and is mostly honoured.** The clear violations are F3 (JSON schema/envelope) and parts of F5 (error uniformity); the rest are intra-family asymmetries the guide doesn't speak to directly. - -## Remediation - -Audit-only deliverable; fixes ship as follow-up tickets under the **Migration read-command redesign** project. Proposed clusters: - -| Ticket | Covers | Effort | -|---|---|---| -| Unify `--json` envelopes + export schemas | F3 (`log` bare array → `{ ok, … }`; `graph` co-located exported schema; field-name convention) | S | -| Align `show`/`check` ref grammar + positional help | F2 (one `parseMigrationRef` grammar; decide path in/out; matching help text) | S | -| Route `check` errors through `mapRefResolutionError`; unify missing-DB/driver precondition | F5 | S | -| Help-text polish across the family | F6 (`check`→`show` see-also; `--json` examples; offline/live phrasing) | XS | -| Decide + signal space policy for per-migration verbs; document `log` unscoped | F1 | S | -| Add `--ascii` to `status`; decide `show`/`check` decoration flags | F4 | XS | -| Extend parity test to lock flags / help / JSON-envelope / error-shape conventions | cross-cutting | M | - -`check`'s exit codes (F7) need no change beyond confirming they're documented. diff --git a/projects/migration-graph-rendering/read-command-consistency-followups.md b/projects/migration-graph-rendering/read-command-consistency-followups.md deleted file mode 100644 index 0bef10bb27..0000000000 --- a/projects/migration-graph-rendering/read-command-consistency-followups.md +++ /dev/null @@ -1,116 +0,0 @@ -# Read-command consistency — follow-up tickets (TML-2801) - -Draft tickets remediating the findings in [`read-command-consistency-audit.md`](./read-command-consistency-audit.md). Each is sized as one PR, consistent with this project's "independent PRs off a merged foundation" cadence. Copy into Linear under the **Migration read-command redesign** project (Terminal team) as needed; granularity is yours — they can be filed 1:1 or grouped (a sensible grouping is noted at the end). - -All paths are under `packages/1-framework/3-tooling/cli/`. - ---- - -## 1. Unify `--json` envelopes and export per-command output schemas - -**Findings:** F3 · **Effort:** S · **User-facing:** yes (JSON consumers) - -**Why.** Style Guide §JSON Semantics requires each command's `--json` success shape to be a co-located, exported schema, and success/error docs to share an `ok` discriminator. `migration log` emits a **bare array** (`src/commands/migration-log.ts:134`) — no `ok`, no room for an error in the same shape. `migration graph` builds `{ ok, nodes, edges, summary }` **inline** in its action with no exported schema. - -**Scope.** -- Wrap `log` output as `{ ok: true, entries: [...] }` (name TBD); update `serializeLedgerEntriesForJson` callers and any snapshot tests. -- Give `graph` a co-located exported output type/schema like its peers (`status`, `list`, `show`, `check` already export theirs). -- Sweep the primary-payload field names for a shared convention (`spaces` / `entries` / `migration` / `failures` / `nodes`+`edges`) and document the rule. - -**Acceptance.** Every read verb's `--json` is `{ ok, … }` with an exported schema; `log` JSON validates against it; a test asserts the envelope shape across all six. - ---- - -## 2. Align `show` / `check` migration-ref grammar and positional help - -**Findings:** F2 · **Effort:** S · **User-facing:** yes - -**Why.** `show` (`src/commands/migration-show.ts:276`) accepts a filesystem path; `check` (`src/commands/migration-check.ts:295`) does not, though both resolve a migration package via `parseMigrationRef`. The accepted forms differ arbitrarily and the help text describes different grammars. - -**Scope.** -- Decide one grammar for per-migration verbs (path in or out) and apply to both. -- Make both commands' positional help describe the identical accepted forms. -- Keep the deliberate required-(`show`)-vs-optional-(`check`, no-arg = whole-graph) distinction. - -**Open design Q (resolve in ticket, not assumed a defect):** should `show`/`check` accept the *contract* reference grammar (ref names, `^`) the way `status --to` does? They resolve a package, not a contract — likely no, but state the decision. - -**Acceptance.** `show` and `check` accept the same migration-ref forms; help text matches; tests cover the path decision both ways. - ---- - -## 3. Route `check` errors through the shared factory; unify the missing-DB precondition - -**Findings:** F5 · **Effort:** S · **User-facing:** yes (error wording) - -**Why.** `check` constructs ref-resolution errors inline with hand-written strings instead of `mapRefResolutionError` (used by `status` at `src/commands/migration-status.ts:330,342` and `show` at `src/commands/migration-show.ts:235`), so its envelope and `meta` drift. Separately, `log` raises `errorDatabaseConnectionRequired` then a distinct `errorDriverRequired` (`src/commands/migration-log.ts:51,59`) while `status` folds both into one `errorDatabaseConnectionRequired` condition (`src/commands/migration-status.ts:274`) — same precondition, two decompositions, two messages. - -**Scope.** -- Route `check`'s ref errors through `mapRefResolutionError`. -- Extract one shared "needs a live DB (connection + driver)" precondition check and use it in both `log` and `status`; settle on a single message and PN-code treatment. -- Confirm missing-input failures set `meta.missingFlags` per Style Guide §Errors. - -**Acceptance.** Identical why/fix/PN-code envelope for the same precondition across `log`/`status`; `check` ref errors match `show`/`status`; a test asserts the shared shape. - ---- - -## 4. Help-text polish across the read-verb family - -**Findings:** F6 · **Effort:** XS · **User-facing:** yes (help/discoverability) - -**Why.** `check`'s see-also omits `migration show` (`src/commands/migration-check.ts:288-292`) — the one sibling that also takes a positional migration ref. `show` has no `--json` example despite emitting JSON. Long descriptions state "Offline — does not consult the database" on list/graph/check but not on `show` (also offline). - -**Scope.** -- Add `migration show` to `check`'s see-also. -- Add a `--json` example to every command that emits JSON (notably `show`). -- State offline/live consistently in every long description. -- Adopt a soft norm of 3–4 examples covering each command's distinguishing flags. - -**Acceptance.** See-also graph is symmetric across the family; every JSON-emitting verb has a `--json` example; offline/live phrasing is uniform. - ---- - -## 5. Decide and signal the space policy for per-migration verbs; document `log` as unscoped - -**Findings:** F1 · **Effort:** S · **User-facing:** yes - -**Why.** `status`/`list`/`graph` take `--space` (all-by-default, narrow-to-one). `show`/`check` are silently app-only — no flag, no diagnostic, only the word "app-space" in the long description. `log` is intentionally unscoped but doesn't say so. - -**Scope.** -- For `show`/`check`: either accept `--space ` (default app) or reject a passed `--space` with a structured error stating app-only and why. -- Document `log`'s unscoped (all-spaces-merged) semantics in its long description. - -**Acceptance.** Passing `--space` to a per-migration verb produces a deliberate, documented result (handled or cleanly rejected); `log`'s scope is stated in `--help`. - ---- - -## 6. Add `--ascii` to `status`; decide decoration flags for `show` / `check` - -**Findings:** F4 · **Effort:** XS · **User-facing:** yes - -**Why.** `status` draws the same shared laned tree as `list`/`graph` (the parity test locks them byte-for-byte) but lacks `--ascii` (`src/commands/migration-status.ts:649-661`), so glyph control is asymmetric across verbs that share a renderer. - -**Scope.** -- Add `--ascii` to `status` (wire to the same `resolveGlyphMode` path). -- Decide deliberately whether `show`/`check` warrant `--legend`/`--ascii` (their output isn't the laned tree) and note the decision in the audit doc. - -**Acceptance.** `status --ascii` forces ASCII glyphs identically to `list`/`graph`; `show`/`check` decoration decision recorded. - ---- - -## 7. Extend the read-command parity test to lock the conventions - -**Findings:** cross-cutting · **Effort:** M · **User-facing:** no - -**Why.** [`test/commands/migration-read-commands-parity.test.ts`](../../packages/1-framework/3-tooling/cli/test/commands/migration-read-commands-parity.test.ts) currently locks only *pretty-rendering* parity. Flags, help shape, JSON envelopes, and error shapes can re-drift with no test catching it. - -**Scope.** Add assertions (in the parity test or a sibling) covering, across all six read verbs: `{ ok, … }` JSON envelope + exported schema presence; symmetric see-also graph; uniform offline/live phrasing; shared missing-DB error shape. Land **after** tickets 1–6 so it encodes the agreed end state. - -**Acceptance.** A regression that reintroduces any F1–F6 defect fails this test. - ---- - -## Notes - -- **F7 (custom exit codes on `check`)** needs no fix — it's Style-Guide-correct. One-line task: confirm the codes are documented in `check --help`/README. -- **Suggested grouping** if you prefer fewer tickets: **(A)** = #1 (JSON); **(B)** = #2 + #3 + #4 (ref grammar, errors, help — all touch the per-migration verbs and shared factories); **(C)** = #5 + #6 (space + decoration policy); **(D)** = #7 (parity lock, last). -- **Sequencing:** #7 lands last; the rest are independent and parallelisable. diff --git a/projects/migration-graph-rendering/slices/check-single-target-multi-space/code-review.md b/projects/migration-graph-rendering/slices/check-single-target-multi-space/code-review.md deleted file mode 100644 index 98b54526e0..0000000000 --- a/projects/migration-graph-rendering/slices/check-single-target-multi-space/code-review.md +++ /dev/null @@ -1,118 +0,0 @@ -# Code review — check-single-target-multi-space (TML-2835) - -> Reviewer maintains scoreboard/findings/round-notes/summary; orchestrator owns § Subagent IDs + § Orchestrator notes. - -## Summary - -- **Current verdict:** ANOTHER ROUND NEEDED -- **AC scoreboard totals:** 1 PASS / 0 FAIL / 0 NOT VERIFIED -- **Open findings:** 2 (1 should-fix, 1 low-process) - -The multi-space resolution, ambiguity semantics, `--space` narrowing, and the -restored D2 ref-error envelope are all correct and well-tested. Two issues hold -the round: (F-1, should-fix) the `--help` long description still claims single -target is "app-space" only — directly contradicting the shipped behaviour, a -doc-maintenance miss; (F-2, low-process) `wrong-grammar` short-circuits the -space loop and can mask a legitimate earlier-space hit in a contrived -ref-name/hash-prefix collision. F-1 is a one-line fix and gates the round. - -## Acceptance criteria scoreboard - -| AC ID | Description (short) | Dispatch | Status | Evidence | -| ----- | ------------------- | -------- | ------ | -------- | -| AC-1 | `check ` resolves a non-app-space migration; `--space` narrows single-target; cross-space ambiguous ref errors PRECONDITION; exit codes still documented in `--help` | D1 | PASS | non-app resolve: `migration-check.ts:458-498` + test `migration-check-single-target-multi-space.test.ts:158-171`; `--space` narrow + validation: `migration-check.ts:419-428` + tests `:173-250`; ambiguity PRECONDITION: `migration-check.ts:480-486` / `cli-errors.ts:381-403` + test `:252-266`; exit codes documented: `migration-check.ts:558-559` | - -## Subagent IDs - -- **Implementer:** D1 ran over three sonnet rounds (each truncated mid-report — a recurring harness issue this session): `af7e7259bb09f0b8d` (R1: multi-space resolution + errorAmbiguousMigrationRef + path generalization; left uncommitted/broken), `a1a31be10a3c058e4` (R2: fixed typecheck + 3 own-test failures; committed `c9d65fc14`; defensibly corrected a `db` named-ref test to PRECONDITION per spec), `afdf7ff2de5d9774a` (R3: started the D2-regression fix but truncated leaving a compile error). **Orchestrator finished R3 directly** (commit `f350b17d6`) — a 2-line recovery (`firstParseFailure` typo + use the named `RefResolutionError` import; route 0-hit ref failures through `mapRefResolutionError`) rather than a 4th dispatch. -- **Reviewer:** _(opus pass — recorded below)_ - -## Findings log - -### F-1 (should-fix) — stale `--help` text claims single-target is app-space only - -`migration-check.ts:556` still reads "…or a migration reference to check a -single **app-space** package." This slice's whole point is that a migration -reference now resolves across **all** contract spaces, so the help text now -contradicts the shipped behaviour. AC-1 explicitly carries "exit codes still -documented in `--help`", and CLAUDE.md's golden rule is to keep docs current. - -**In-PR action:** reword the clause to reflect multi-space single-target -resolution (e.g. "…or a migration reference to check a single package in any -contract space — narrow with `--space `, and an ambiguous reference is -reported as a precondition failure"). One-line edit; no code change. Consider -adding a `migration check --space ` example to `setCommandExamples` -(`:561-566`) while there. - -### F-2 (low-process) — `wrong-grammar` short-circuit can mask an earlier-space hit - -In the ref loop (`migration-check.ts:462-470`), a `wrong-grammar` failure from -any space `return`s immediately, even if an earlier in-scope space already -pushed a legitimate hit. `parseMigrationRef` emits `wrong-grammar` when the -input is a ref **name present in that space's refs** (`migration-ref.ts:37-45`) -— which is space-dependent, contradicting the inline comment's claim that -wrong-grammar "is space-independent." Reachable case: a hex-prefix string that -(a) uniquely matches a migration hash in `app` (a hit) and (b) is literally the -name of a ref in a later space — the later space's wrong-grammar return would -discard the app hit and emit a ref-name diagnostic instead of resolving in app. -Contrived (requires a ref named like a hash prefix), but the masking is real. - -**In-PR action:** treat wrong-grammar like `firstParseFailure` — record it and -only emit it after the loop when there are zero hits, rather than -short-circuiting mid-loop. Then drop/repair the "space-independent" comment. -Low severity given the contrivedness; acceptable to defer with an explicit -narrative note if the orchestrator prefers, but the comment is actively wrong -and should at least be corrected. - -## Round notes - -### Round 1 (opus reviewer) - -Walked `checkSingleTarget` end to end against the spec's § Chosen design. - -- **Multi-space resolution — correct.** Hit collection requires both a - successful `parseMigrationRef` and an on-disk package with the matching hash - (`:472-477`); 0 hits → not-found, 1 → check it, >1 → ambiguity. The - "resolved-in-graph-but-no-package" edge falls through cleanly to the - not-found result (`firstParseFailure` stays undefined because parse - succeeded), exactly as the spec asserts. -- **D2 envelope restored — correct.** The 0-hit-with-parse-failure path - (`:491-497`) routes the first parse failure through `mapRefResolutionError`, - and the single-app-space `does-not-exist` lock - (`migration-check-ref-error.test.ts:124-139`) asserts PN-RUN-3000 + meta — - the D2/TML-2801 contract is genuinely re-locked, not papered over. -- **Ambiguity test is a real >1-space lock.** The fixture plants the same - dirName in `app` and `postgis` with distinct hashes - (`...test.ts:101-109`); `findEdgeByDirName` matches per-space, so two hits - are produced and `MIGRATION.AMBIGUOUS_MIGRATION_REF` fires — defect-planting, - would fail if the loop collapsed to first-match. -- **`db` named-ref test is spec-aligned, not a paper-over.** A ref named `db` - is a contract ref; `parseMigrationRef` rejects it as wrong-grammar - (`migration-ref.ts:37-45`). The spec scopes single-target to dirName/hash, - not ref-names, so PRECONDITION is the correct expectation. -- **`--space` narrows before resolution** (`scopedSpaces` at `:430-431` feeds - both the path and ref branches) and validates identically to the holistic - path (`isValidSpaceId`/`errorInvalidSpaceId`, membership/`errorSpaceNotFound` - at `:419-428`, mirroring `runMigrationCheck:272-277`). Tests `:173-250` lock - invalid/unknown/narrow-miss. -- **Repo rules — clean.** `ifDefined` used for the optional `resolvedSpaceId` - under exactOptionalPropertyTypes (`:535,541`); no bare `as`, no `any`, no - zod, no cross-file reexports added; new `errorAmbiguousMigrationRef` follows - the existing why/fix/meta factory style. -- **Behaviour-change note (narrative, not a finding):** single-target now loads - the full read aggregate (`:344-349`) and shares the holistic path's - `5001`/`5002` integrity-refusal gate, where the old path read only the app - migrations dir. This is the spec's chosen design and arguably more correct, - but it does widen single-target's pre-resolution failure surface — worth a - line in the PR description so it isn't a surprise. -- **`checkManifestFilesPresent(matchedSpace)`** (`:510`) now runs on the - matched space rather than always-app — correct: the orphan-manifest scan - should target the space the package actually lives in. - -## Orchestrator notes - -**Build executed via the drive-build-workflow protocol directly** (not re-invoking the skill — its full body was loaded an hour ago for the TML-2801 slice; re-reading would only burn context). Same loop: dispatch → intent-validate DoD → opus reviewer pass → on SATISFIED push + open PR (operator wants autonomous + auto-PR). Single-dispatch slice. Model tiers per `drive/calibration/model-tier.md`: implementer sonnet (design pinned in spec, reuses TML-2801's enumerateCheckSpaces pattern, precise brief); reviewer opus. - -**Reviewer verdict: ANOTHER ROUND NEEDED** (AC-1 PASS behaviorally; F-1 should-fix + F-2 low-process). Orchestrator intent-validation: both findings valid (F-1 stale `--help` "app-space" prose; F-2 wrong-grammar short-circuit masking + wrong comment). **Both RESOLVED directly by the orchestrator** in commit `7d52f765e` rather than a 5th sonnet dispatch (the three implementer rounds all truncated mid-report; a 2–3 line doc+comment+control-flow fix is not worth another flaky dispatch): F-1 → reworded the long description to multi-space + added a `check --space app` example; F-2 → removed the mid-loop wrong-grammar `return` so failures are deferred to `firstParseFailure` and only surface (via the shared envelope) when no space yields a hit, and corrected the "space-independent" comment. Verified: typecheck clean; migration-check + help-text + parity suites 54/54; full cli suite green (background run, exit 0). Resolution orchestrator-validated, not re-sent to the reviewer (narrow, reviewer-specified, test-covered). The reviewer's narrative note (single-target now loads the full read aggregate → inherits the holistic integrity-refusal gate; a deliberate, spec-aligned widening) is carried into the PR description. - -**SLICE DoD MET:** AC-1 PASS + F-1/F-2 resolved; all gates green. Proceeding to push + PR. diff --git a/projects/migration-graph-rendering/slices/check-single-target-multi-space/plan.md b/projects/migration-graph-rendering/slices/check-single-target-multi-space/plan.md deleted file mode 100644 index 4154072760..0000000000 --- a/projects/migration-graph-rendering/slices/check-single-target-multi-space/plan.md +++ /dev/null @@ -1,13 +0,0 @@ -# Dispatch plan — check-single-target-multi-space - -One dispatch. The slice is a single coherent outcome — `migration check`'s single-target resolution path becomes multi-space — touching one command file, one shared error factory, and tests. The ambiguity-error factory and the path-resolution generalization are part of that one outcome (they'd fail dispatch-INVEST's _Valuable_ as standalone dispatches), so they ride along. PART 2 (exit-code docs) is already satisfied by TML-2801's `--help` text; the dispatch confirms it, no separate unit. Tests precede implementation (repo rule). - -### Dispatch 1: `migration check ` resolves across all contract spaces - -- **Outcome:** `checkSingleTarget` (`packages/1-framework/3-tooling/cli/src/commands/migration-check.ts:398`) no longer hardcodes the app space — it enumerates contract spaces via `enumerateCheckSpaces`, resolves the target against each in-scope space's graph + refs (or, for a path, the space whose dir contains it), and checks the matched package in its space. `--space ` narrows resolution to one space, validated exactly as the holistic path does (`isValidSpaceId` → `errorInvalidSpaceId`; existence → `errorSpaceNotFound`; `PRECONDITION`). A ref that resolves in more than one space errors `PRECONDITION` via a new `errorAmbiguousMigrationRef` factory in `cli-errors.ts` (names the spaces, says to qualify with `--space`). The human header shows the resolved space when non-app. Exit codes and `MigrationCheckResult` shape are unchanged; the codes remain documented in `--help` (`:483-484`). -- **Builds on:** the spec's chosen design + the multi-space machinery TML-2801 merged to main (`enumerateCheckSpaces:143`, `CheckSpace:126`, `runMigrationCheck`'s `--space` validation pattern). -- **Hands to:** slice-DoD met — single-target `check` is multi-space, with `--space` narrowing and a clear cross-space ambiguity error, all under test. -- **Focus:** `migration-check.ts` (`checkSingleTarget` + the aggregate-load/dispatch wiring so single-target gets the aggregate the holistic path already loads + the output space hint); `cli-errors.ts` (`errorAmbiguousMigrationRef`); generalizing the path-resolution base (`resolveAppTargetPath`) over space dirs; tests. **Out:** the holistic path (TML-2801, done), the other read verbs, arktype runtime schemas (TML-2836), `--space` for `show`/`log`. -- **Tests first (real regression locks):** (a) a migration planted in a **non-app** space is resolved + checked by `check ` (was `PRECONDITION` not-found before — confirm red first); (b) `check --space ` narrows, and `--space` with an invalid/unknown id errors `PRECONDITION`; (c) a ref that matches in two spaces errors `PRECONDITION` with the qualify-with-`--space` message. -- **Validation gate:** `pnpm --filter @prisma-next/cli typecheck` + the check command tests (targeted run; the full cli suite's pre-existing `isolate:false` ordering flake is unrelated) + `pnpm lint:deps`. -- **Escape hatch (from the spec):** if generalizing path resolution over space dirs proves materially harder than the ref path, keep filesystem-path targets app-relative, note a sub-follow-up in the wrap-up, and ship the ref-based multi-space resolution (the headline value). Halt-and-surface if cross-space ref semantics turn out to diverge from the spec's assumption. diff --git a/projects/migration-graph-rendering/slices/check-single-target-multi-space/spec.md b/projects/migration-graph-rendering/slices/check-single-target-multi-space/spec.md deleted file mode 100644 index 2ed07b6951..0000000000 --- a/projects/migration-graph-rendering/slices/check-single-target-multi-space/spec.md +++ /dev/null @@ -1,86 +0,0 @@ -# Slice: check-single-target-multi-space - -_In-project slice. Parent project: `projects/migration-graph-rendering/`. Outcome: `migration check ` resolves a migration in **any** contract space, completing the multi-space `check` work TML-2801 began (which left single-target app-only)._ - -## At a glance - -[TML-2801](https://linear.app/prisma-company/issue/TML-2801) made `migration check`'s **holistic** (no-arg) mode multi-space, but its **single-target** path — `checkSingleTarget` at `packages/1-framework/3-tooling/cli/src/commands/migration-check.ts:398` — was deliberately left app-space-only: it builds one hardcoded `appSpace: CheckSpace` (`:405`) and resolves the ref against the app graph + app refs only. This slice makes `check ` resolve across **all** contract spaces (reusing the already-exported `enumerateCheckSpaces`), with `--space ` to narrow and a clear error when a ref is ambiguous across spaces. - -PART 2 of [TML-2835](https://linear.app/prisma-company/issue/TML-2835) (document the custom exit codes) is **already satisfied** — TML-2801 put them in the `--help` long description (`migration-check.ts:483-484`). This slice confirms that and adds nothing there unless a per-command README reference section exists. - -## Chosen design - -Single-file change (`migration-check.ts`) plus a possible shared cli-errors factory, plus tests. - -### Resolution becomes multi-space - -The command shell already loads the read aggregate for the holistic path (`:353`, `enumerateCheckSpaces(loadedAggregate.value.aggregate, migrationsDir)`). Make that aggregate available to the single-target path too (load before dispatch, pass into `checkSingleTarget`), so single-target can enumerate spaces the same way. - -`checkSingleTarget` then: - -1. **Scope the spaces.** All spaces by default; if `--space ` is passed, validate it the same way the holistic path does (`isValidSpaceId` → `errorInvalidSpaceId`; existence → `errorSpaceNotFound`; both exit `PRECONDITION`) and narrow to that one. -2. **Resolve the target across in-scope spaces.** - - `looksLikePath(target)` → resolve the path to a migration dir within whichever space's `migrationsDir` contains it (generalize `resolveAppTargetPath` over the space dirs; a path is explicit, so it lands in at most one space — inherently unambiguous). - - else → `parseMigrationRef(target, { graph, refs })` against each in-scope space; collect every `(space, matchedPackage)` hit. -3. **Disambiguate the hits.** - - 0 → not-found (`PRECONDITION`), same envelope as today. - - exactly 1 → check that package (file-existence / hash / snapshot) in its space — the existing per-package checks, unchanged. - - >1 (a dirName or hash-prefix that resolves in multiple spaces) → **new ambiguity error** (`PRECONDITION`): names the spaces and tells the user to qualify with `--space `. Add a structured `errorAmbiguousMigrationRef` factory in `cli-errors.ts` mirroring the existing why/fix style. -4. **Indicate the resolved space** in the human header when the match is non-app (the header already prints the target; add a space detail row where it reads cleanly). - -### Worked example - -Before — a migration that lives in the `postgis` space: - -``` -$ prisma-next migration check 20260601T0000_install_postgis_extension -✖ Migration package for "20260601T0000_install_postgis_extension" not found on disk (exit 2) -``` - -After: - -``` -$ prisma-next migration check 20260601T0000_install_postgis_extension -✔ All checks passed (space: postgis, exit 0) -``` - -Exit codes (`OK=0` / `PRECONDITION=2` / `INTEGRITY_FAILED=4`, `migration-check/exit-codes.ts`) are unchanged and remain documented in `--help`. - -## Coherence rationale - -One reviewable change to one command's resolution path (`migration-check.ts`) plus its tests — it finishes the multi-space story the holistic path already established and shares that path's space-enumeration + validation. Rollback is one commit. - -## Scope - -**In:** `migration-check.ts` — `checkSingleTarget` + the aggregate-load/dispatch wiring + the human-output space hint; a new `errorAmbiguousMigrationRef` factory in `cli-errors.ts`; generalizing the path-resolution base over spaces; tests. Confirm exit-code docs. - -**Out:** the holistic-path behaviour (TML-2801, done); the other five read verbs; arktype runtime schemas ([TML-2836](https://linear.app/prisma-company/issue/TML-2836)); any `--space` semantics for `show`/`log`. No change to exit codes or the `MigrationCheckResult` shape. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| A ref (dirName or hash-prefix) matches a migration in more than one space | New ambiguity error (`PRECONDITION`), names the spaces, says "qualify with `--space `" | The headline new behaviour; needs a planted-fixture test. Mirrors git's ambiguous-ref UX. | -| Filesystem-path target (`looksLikePath`) | Resolve within the owning space's dir | A path is explicit → unambiguous. If generalizing `resolveAppTargetPath`'s base over spaces proves gnarly, keep it app-relative and file a sub-follow-up (non-app paths are rare) — escape hatch, surface it. | -| `--space ` given + ref not in that space | not-found within that space (`PRECONDITION`) | Mirrors the holistic path's `--space` narrowing. | - -## Slice-specific done conditions - -- [ ] `check ` resolves + checks a migration planted in a **non-app** space (was `PRECONDITION` not-found before); `--space ` narrows single-target; a ref ambiguous across spaces errors `PRECONDITION` with the qualify-with-`--space` message; exit codes remain documented in `migration check --help`. - -## Open Questions - -1. **Path-target generalization vs app-relative.** Working position: generalize path resolution to the owning space's dir; if non-trivial, keep paths app-relative and note a sub-follow-up (paths are a rare way to name a non-app migration). -2. **README exit-code mirror.** Working position: exit codes already live in `--help` (a Style-Guide-acceptable home); only add to the CLI package README if it has a per-command reference section — otherwise `--help` suffices. - -## Required-section notes - -- **Contract-impact:** none. **Adapter-impact:** none — operates on hashes/graphs via the aggregate, target-agnostic. **ADR:** none. - -## References - -- Parent project: [`projects/migration-graph-rendering/spec.md`](../../spec.md) -- Prior slice (the multi-space foundation): [`../read-command-consistency/spec.md`](../read-command-consistency/spec.md) (TML-2801) -- Linear issue: [TML-2835](https://linear.app/prisma-company/issue/TML-2835) -- Standard: [`docs/CLI Style Guide.md`](../../../../docs/CLI%20Style%20Guide.md) § Exit Codes -- Surfaces: `migration-check.ts` — `checkSingleTarget:398`, `enumerateCheckSpaces:143`, `runMigrationCheck:264`, `CheckSpace:126` diff --git a/projects/migration-graph-rendering/slices/edges-on-plan/spec.md b/projects/migration-graph-rendering/slices/edges-on-plan/spec.md deleted file mode 100644 index 49ab8d77d9..0000000000 --- a/projects/migration-graph-rendering/slices/edges-on-plan/spec.md +++ /dev/null @@ -1,69 +0,0 @@ -# Slice: consolidate the per-edge breakdown onto the migration plan - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to: the ledger-foundation slice (TML-2769) threads the per-edge breakdown to the runners as a **sibling field** (`migrationEdges`) next to `plan: MigrationPlan`. The edges are really the plan's own finer structure, so the two fields must be kept consistent by hand (a `Σ operationCount === plan.operations.length` guard exists only because they can desync). This slice moves the breakdown **onto the plan** so the runner reads `plan.edges`, the sibling field disappears, and the guard's reason to exist goes away._ - -## At a glance - -Today — two parallel fields the runner must reconcile: - -```ts -runner.execute({ - plan: MigrationPlan, // aggregate shape: origin → destination + flat operations[] - migrationEdges: [ // sibling: per-edge breakdown (dirName, hash, from, to, opCount) - { migrationHash, dirName, from, to, operationCount }, - ], - // … -}); -``` - -After — the breakdown is part of the plan: - -```ts -runner.execute({ - plan: { - ...MigrationPlan, - edges: [{ migrationHash, dirName, from, to, operationCount }], - }, - // … -}); -``` - -## Chosen design - -`MigrationPlan` carries only the aggregate shape — one `origin`→`destination` and a **flat** `operations[]`. The per-edge breakdown (per-edge `dirName`, `migrationHash`, intermediate `from`/`to`, per-edge `operationCount`) is what the ledger journal needs (one row per applied migration), and it is **not** a pure duplicate of the plan — only the endpoints (first edge's `from` = `plan.origin`, last edge's `to` = `plan.destination`) and the op-count total overlap. But threading it as a **sibling** of `plan` on the runner options is the smell: `PerSpacePlan` already carries `migrationEdges` (the planner builds it alongside the plan), and the producer copies it onto the runner options next to `plan`, so the runner receives two fields describing the same apply and must guard against their desync. - -This slice: - -- **Adds `readonly edges` to `MigrationPlan`** (in `framework-components`). The element type stays a **structural inline** object (`{ migrationHash; dirName; from; to; operationCount }`) for the same layering reason the sibling field is inline today: `framework-components` (layer 1-core) cannot import `AggregateMigrationEdgeRef` from `migration-tools` (layer 3-tooling). -- **Runners read `plan.edges`** instead of `options.migrationEdges` (mongo, postgres, sqlite). -- **Drops the sibling `migrationEdges`** from `MigrationRunnerPerSpaceOptions`, `MongoMigrationRunnerExecuteOptions`, and the SQL family's per-space option shape. -- **Producer stamps `edges` onto the plan it already builds.** The planner already holds `PerSpacePlan.migrationEdges`; it sets `plan.edges` from the same source rather than emitting a separate field. `apply.ts` no longer copies a sibling. -- **Retires the `Σ operationCount === plan.operations.length` desync guard** — once `edges` and `operations` are constructed together on one object, they can't drift independently. (Optionally keep it as a cheap internal `assert` in the runner; settle at pickup.) - -## Scope - -**In:** - -- `MigrationPlan.edges` (structural inline, framework-components). -- Runners read `plan.edges`; sibling `migrationEdges` removed from all runner-option shapes. -- Producer (`apply.ts` / planner) stamps `edges` onto the plan. -- Migrate every construction site — the package runner tests (`synthEdges(plan)` helpers) and the five example-app manual/chain tests — from sibling `migrationEdges` to `plan.edges`. - -**Out:** - -- The per-edge data itself (unchanged — same five fields). -- `PerSpacePlan.migrationEdges` naming on the planner side (internal; can stay or be renamed `edges` for symmetry — decide at pickup). -- The ∅-origin spelling (`from: ''` / `sha256:empty` / `null`) — see sibling slice `empty-origin-as-null`. - -## Open Questions - -1. **Keep the op-count guard as an internal assertion?** Once edges live on the plan the desync path is gone, but a cheap `assert(Σ edges.operationCount === operations.length)` in the runner still catches a malformed producer. Decide whether the assertion earns its keep. -2. **`edges` required vs optional on `MigrationPlan`.** The sibling field is currently required (synth/at-head plans carry a single synthesised edge). Keep it required for the same reason, or model at-head as an empty array — settle at pickup. -3. **Rename `PerSpacePlan.migrationEdges` → `edges`?** Cosmetic symmetry with the new plan field; optional. - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md`. -- Predecessor: `slices/ledger-foundation/spec.md` (TML-2769) introduced the sibling `migrationEdges`; this slice consolidates it. -- Surfaced by the TML-2769 / PR #665 review (the "single structure of migration runners" thread). The blast radius of making the sibling field required — five example-app call sites plus every runner-option test — is itself evidence that the breakdown belongs on the plan. -- Linear issue: _to be filed at pickup (standalone, related to TML-2769 / TML-2774)._ diff --git a/projects/migration-graph-rendering/slices/empty-origin-as-null/spec.md b/projects/migration-graph-rendering/slices/empty-origin-as-null/spec.md deleted file mode 100644 index 2b6e090333..0000000000 --- a/projects/migration-graph-rendering/slices/empty-origin-as-null/spec.md +++ /dev/null @@ -1,63 +0,0 @@ -# Slice: stop spelling the empty-contract origin as a fake hash - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to: the "no origin" state (the very first migration, from ∅) is modelled inconsistently. The read boundary models it honestly as `null` (`LedgerEntryRecord.from: string | null`), but the storage/graph layer spells it `sha256:empty` — a string that is **not a valid sha256 hash** masquerading as one. A coercion helper (`ledgerOriginFromStored`) exists only to bridge the two. This slice removes the typology lie so ∅ has one honest representation._ - -## At a glance - -The same "no origin" fact, spelled three ways: - -```ts -// read boundary — honest: null means "no origin" -interface LedgerEntryRecord { - readonly from: string | null; // ∅ ⇒ null - // … -} - -// storage / graph — a fake hash used as an in-band node key -const EMPTY_CONTRACT_HASH = 'sha256:empty'; // not a real sha256 - -// the bridge that only exists because of the divergence -ledgerOriginFromStored(stored); // '' | 'sha256:empty' | null → null -``` - -The question this slice answers: _if `from` permits `null`, why does `sha256:empty` exist at all?_ - -## Chosen design - -Deferred — the **shape** of the fix is settled, the exact cut is chosen at pickup because the blast radius reaches the graph layer. Two candidate cuts: - -1. **Model ∅ as `null` end-to-end.** Graph nodes keyed by `string | null`, edge `from` nullable, no sentinel anywhere. Honest, but the largest blast radius: `migration-graph` (the node map keys nodes by string hash), `graph-walk`, `check-integrity`, and the renderer all assume a non-null string key today. -2. **Keep a sentinel but drop the misleading `sha256:` prefix** (e.g. a bare `∅` / `empty` token). Smaller than (1), but still touches every site that compares against or emits the constant, and a sentinel string is still a weaker model than `null`. - -The operator has ruled that the **constant's value is owned by the graph/storage layer** ("not our fight" — TML-2769 review): this slice is about the **typology honesty at the boundary**, not about unilaterally reformatting a value the graph layer depends on. So the realistic scope is: pick the cut with the graph layer's owner, then land it. - -Cheap immediate win, independent of the cut (can land first): - -- **One-line doc note on `LedgerEntryRecord.from`** explaining `null` = ∅, and that the storage/graph spelling (`sha256:empty`) is normalised to `null` on read by `ledgerOriginFromStored`. Stops the next reader asking the same question. - -Already done in TML-2769 / PR #665 (not re-litigated here): the constant was **deduplicated** — Mongo no longer redefines its own `EMPTY_ORIGIN_HASH`; it imports the shared one. - -## Scope - -**In:** - -- The doc note on `LedgerEntryRecord.from` (immediate). -- Whichever ∅-spelling cut is agreed with the graph-layer owner: either ∅ = `null` end-to-end, or a de-prefixed sentinel. -- Collapse `ledgerOriginFromStored` accordingly (it disappears entirely under cut 1; it simplifies under cut 2). - -**Out:** - -- The ledger journal structure (TML-2769) and the per-edge breakdown (`edges-on-plan` slice) — orthogonal. -- Unilaterally changing the constant's value without the graph layer's owner — explicitly not in scope. - -## Open Questions - -1. **Which cut — `null` end-to-end or a de-prefixed sentinel?** Settle with whoever owns `migration-graph`'s node-keying. (1) is the honest model; (2) is the smaller change. The decision turns on how much the graph node map and renderer rely on a non-null string key. -2. **Does the graph node map tolerate a `null` key?** If yes, cut (1) is much cheaper than it looks. Investigate at pickup. - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md`. -- Predecessor: `slices/ledger-foundation/spec.md` (TML-2769) — introduced `LedgerEntryRecord.from: string | null` and the `ledgerOriginFromStored` bridge; deduped the constant. -- Surfaced by the TML-2769 / PR #665 review (the `sha256:empty`-is-not-a-hash comment and the `from`-permits-null-but-we-use-the-constant comment). -- Linear issue: _to be filed at pickup (standalone, related to TML-2769 / TML-2774)._ diff --git a/projects/migration-graph-rendering/slices/ledger-foundation/plan.md b/projects/migration-graph-rendering/slices/ledger-foundation/plan.md deleted file mode 100644 index f06a1f5635..0000000000 --- a/projects/migration-graph-rendering/slices/ledger-foundation/plan.md +++ /dev/null @@ -1,59 +0,0 @@ -# Plan: ledger foundation (TML-2769) - -One branch / one PR (`tml-2769-make-the-migration-ledger-readable`). Three -dispatches, sequenced; each leaves the workspace green. - -## Dispatch 1 — SQL write restructure + apply-layer threading - -**Outcome:** Postgres + SQLite ledgers record **one row per applied migration -edge** (space + name + hash + per-edge from/to + that edge's ops), written inside -the per-space transaction in walk order. Mongo unchanged (still green). - -- `apply.ts`: add `migrationEdges: r.entry.migrationEdges` to each - `perSpaceOptions` entry. -- `2-sql/9-family/.../migrations/types.ts`: add optional `migrationEdges` to - `SqlMigrationRunnerExecuteOptions`. Also add it (optional) to the **Mongo** - runner execute-options type so `apply.ts` typechecks (Mongo consumes it in - dispatch 2). -- PG + SQLite `statement-builders.ts`: `ensureLedgerTableStatement` gains - `space`, `migration_name`, `migration_hash`; `buildLedgerInsertStatement` - takes the per-edge input (`space`, `migrationName`, `migrationHash`, `from`, - `to`, `operations`, `contractJsonBefore/After`). -- PG + SQLite `runner.ts`: replace the single `recordLedgerEntry` with a per-edge - loop — for each `migrationEdges` entry, slice `plan.operations` by - `operationCount` (walk order) and insert a row. `contract_json` endpoints only - (single-edge: before=prior marker contract, after=destinationContract; - multi-edge interiors null). `synth` plans (no `migrationEdges`) keep a single - synthesised row keyed by plan destination. -- Tests (PG + SQLite adapter): single-edge, multi-edge (N rows, ops attributed - per edge, order), synth (one row), row carries space/name/hash. - -**Builds on:** nothing. **Hands to:** dispatch 2 (Mongo parity), dispatch 3 (reads). - -## Dispatch 2 — Mongo write parity - -**Outcome:** Mongo ledger docs match the per-edge journal shape. - -- `marker-ledger.ts` `writeLedgerEntry`: accept per-edge input; add - `migrationName`, `migrationHash`, `operations` to the doc (already has - `space`/`from`/`to`/`appliedAt`). -- Mongo runner: consume `migrationEdges` from execute options; per-edge loop - inside its write path, walk order. Synth → single doc. -- Tests (mongo adapter): single-edge, multi-edge, synth. - -**Builds on:** dispatch 1's apply-layer threading + execute-options field. - -## Dispatch 3 — `readLedger` SPI + reads + client plumbing - -**Outcome:** `readLedger({ driver, space })` returns a space's entries in apply -order with cross-target parity, reachable from the CLI. - -- `LedgerEntryRecord` in `@prisma-next/contract/types`. -- `readLedger` on `ControlFamilyInstance` (beside `readMarker`/`readAllMarkers`). -- PG/SQLite read statement (`… WHERE space = ? ORDER BY id`); Mongo aggregate - (`$match { type:'ledger', space }`, insertion order); per-family wiring. -- Control client (`cli/src/control-api/client.ts` + `types.ts`) + a - descriptor-free control-api operation, mirroring `readMarker`/`db-verify`. -- Tests: read round-trip per target + cross-target parity on `LedgerEntryRecord`. - -**Builds on:** dispatches 1–2 (rows exist to read). diff --git a/projects/migration-graph-rendering/slices/ledger-foundation/spec.md b/projects/migration-graph-rendering/slices/ledger-foundation/spec.md deleted file mode 100644 index 153ace06f9..0000000000 --- a/projects/migration-graph-rendering/slices/ledger-foundation/spec.md +++ /dev/null @@ -1,144 +0,0 @@ -# Slice: ledger foundation — a readable per-migration journal - -Linear: [TML-2769](https://linear.app/prisma-company/issue/TML-2769). Blocks -`status` ([TML-2748](https://linear.app/prisma-company/issue/TML-2748)) and `log` -([TML-2770](https://linear.app/prisma-company/issue/TML-2770)). Design context: -[`../../decisions.md`](../../decisions.md) (D7). - -## The decision - -Restructure the on-apply ledger into a **per-migration journal** — one row per -applied migration edge — and add a `readLedger` read API. Today the ledger is -write-only, its three target schemas have diverged, and it records **one -collapsed row per space-apply** (origin→destination spanning the whole walked -path). That shape can't answer the two questions `status` and `log` ask: - -- `status`: "is *this migration* applied?" → a ledger row exists whose - `migration_hash` matches the edge. Needs one row **per edge**. -- `log`: "what ran, in what order?" → one row **per apply event**, with the - migration's name, `from→to`, and timestamp. - -So the journal is per-edge, and every row records the **space id** of the -applied migration. - -## Target row shape (all targets, normalised) - -One row per applied migration edge: - -| Field | Source | Notes | -|---|---|---| -| `space` | apply space id | **new** — SQL ledgers have no space column today | -| `migration_name` | `edge.dirName` | **new** | -| `migration_hash` | `edge.migrationHash` | **new** — the exact-match key `status` uses | -| `from` (origin core hash) | `edge.from` | null only for the ∅ origin | -| `to` (destination core hash) | `edge.to` | | -| `operations` | slice of `plan.operations` by `edge.operationCount` (walk order) | the edge's authored ops | -| `contract_json_before` / `_after` | apply endpoints only (see below) | retained, nullable | -| `applied_at` / `created_at` | now() | append order = apply order | - -**`contract_json_before/after`:** kept (per the call to keep it until it's a -problem), but only the apply's **endpoints** are materialised — a single-edge -apply gets `before` = prior marker contract, `after` = `destinationContract`. -Multi-edge applies have no materialised intermediate snapshots, so interior -edges store `null`. No current consumer reads these columns; synthesising -intermediate contracts is out of scope. - -## Schema convergence (away from today's divergence) - -- **Postgres / SQLite** (identical today: `{id, created_at, origin/destination - core+profile hash, contract_json_before/after, operations}`): add `space`, - `migration_name`, `migration_hash`. `origin_core_hash`/`destination_core_hash` - become the per-edge `from`/`to`. -- **Mongo** (today `{type:'ledger', space, edgeId, from, to, appliedAt}`): add - `migrationName`, `migrationHash`, `operations`. Already has `space`/`from`/`to`/ - `appliedAt`. - -This is a prototype — no back-compat / migration of existing ledger rows. - -## Write path: one row per edge, atomic with marker on SQL - -`migrate`/`db update`/`db init` walk a path of edges per space and collapse them -into one `plan` before handing off to the runner (`apply.ts` → -`runner.execute`). The per-edge breakdown (`PerSpacePlan.migrationEdges`: -`{migrationHash, dirName, from, to, operationCount}`) is **not** currently passed -to the runner. Thread it to the runner's per-space execute options, and replace -the single `recordLedgerEntry` call with a loop that inserts **one ledger row per -edge** — attributing ops by slicing `plan.operations` with each edge's -`operationCount` in walk order. - -On **Postgres and SQLite**, those writes stay inside the per-space transaction -(atomic with marker advancement). Apply in walk order so append order is apply -order. - -On **Mongo**, DDL cannot run inside a transaction, so ledger writes and marker -advancement are **resumable**, not atomic — the per-edge journal invariant is a -SQL-only atomicity guarantee. - -`synth`-produced plans (`db init`/`db update` greenfield) carry a single -synthesised edge in `migrationEdges` (empty `dirName`, destination-keyed hash, -`from` wired from the live marker or the ∅ origin). Runners write one ledger row -from that edge like any other apply. - -## Read API: `readLedger` - -Add to `ControlFamilyInstance` (alongside `readMarker`/`readAllMarkers`): - -```ts -readLedger(options: { - readonly driver: ControlDriverInstance; - readonly space: string; -}): Promise; // append (apply) order -``` - -`LedgerEntryRecord` (new, beside `ContractMarkerRecord` in `@prisma-next/contract/types`): -`{ space, migrationName, migrationHash, from: string | null, to, appliedAt: Date, operationCount }`. -Implemented per target (PG/SQLite `SELECT … WHERE space = ? ORDER BY id`; Mongo -`$match: { type:'ledger', space }` sorted by insertion). The ∅-origin normaliser -lives beside `EMPTY_CONTRACT_HASH` in `@prisma-next/migration-tools`; SQLite- -specific ledger decode (`created_at`, TEXT `operations`) lives in the SQLite -adapter. Plumb through the control client (this slice); a descriptor-free -control-api operation ships with the `status` / `log` consumers (TML-2748 / -TML-2770). - -**Read leniency:** `readLedger` never throws on malformed or legacy rows. Docs -missing per-migration journal fields (`migrationName` / `migrationHash` on Mongo; -unreadable rows on SQL) are **skipped** so a single legacy entry cannot poison -`status` / `log`. Well-formed rows map to `LedgerEntryRecord` as before. - -## Done when - -- Ledger rows carry `space` + `migration_name` + `migration_hash` on Postgres, - SQLite, and Mongo, written **one per applied edge** (inside the per-space SQL - transaction on Postgres/SQLite; resumable on Mongo), in apply order. -- `readLedger({ driver, space })` returns that space's entries in apply order, - with cross-target parity (same `LedgerEntryRecord` shape from all three). -- The control client exposes `readLedger` (this slice). A control-api operation - wrapping it for the CLI is delivered with its consumers (TML-2748 / TML-2770). -- Tests: per-target write (single-edge, multi-edge, synth) + read round-trip, - cross-target parity on the read shape, and a control-client `readLedger` test. - -## Out of scope - -- `status` / `log` command behaviour and rendering (TML-2748 / TML-2770). -- Synthesising intermediate contract snapshots for multi-edge `contract_json`. -- Pruning / compaction of ledger rows; back-compat migration of old rows. - -## Reviewability - -One reviewer holds this in one sitting: it's a single coherent change — "the -ledger becomes a readable per-migration journal." Likely decomposes into a -write/restructure dispatch (schema convergence + per-edge write across the three -targets + apply-layer threading) and a read dispatch (`readLedger` SPI + per- -target reads + client plumbing). - -## References - -- Parent project: [`../../README.md`](../../README.md), [`../../decisions.md`](../../decisions.md). -- Write seam: `cli/src/control-api/operations/apply.ts`, the SQL family runner - (`packages/2-sql/9-family/.../migrations/runner` + `statement-builders.ts` for - PG/SQLite), the Mongo runner + `marker-ledger.ts`. -- Read seam mirror: `ControlFamilyInstance.readMarker`/`readAllMarkers` - (`framework-components/src/control/control-instances.ts`), CLI control client - (`cli/src/control-api/client.ts` + `types.ts`). -- Per-edge breakdown: `migration-tools` `AggregateMigrationEdgeRef` / - `PerSpacePlan` (`aggregate/planner-types.ts`). diff --git a/projects/migration-graph-rendering/slices/list-renders-tree/spec.md b/projects/migration-graph-rendering/slices/list-renders-tree/spec.md deleted file mode 100644 index 1ba333e1d1..0000000000 --- a/projects/migration-graph-rendering/slices/list-renders-tree/spec.md +++ /dev/null @@ -1,89 +0,0 @@ -# Slice: `migration list` renders the graph tree in human output - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to: `migration list`'s flat, lexicographically-ordered text is unreadable for a branching history. This slice routes `list`'s human (pretty/TTY) output through the shared tree renderer — package-annotated — while keeping its machine output flat for tooling. It also **introduces the shared edge-annotation overlay** (D11) that `status` (TML-2748) then extends. Tracking: [TML-2768](https://linear.app/prisma-company/issue/TML-2768)._ - -## At a glance - -Human (TTY) — `list` draws the shared tree; migration (edge) rows carry op count + `{invariants}`; refs ride the node overlay: - -``` -$ prisma-next migration list -app: -○ 3b2d98d (main) -│↑ 20260303_add_phone ef9de27 → 3b2d98d 2 ops {phone_present} -○ ef9de27 -│↑ 20260301_init ∅ → ef9de27 5 ops -○ ∅ -``` - -Machine (`--json`, or any pipe) — unchanged flat package array: - -``` -$ prisma-next migration list --json -{ "ok": true, "spaces": [ { "spaceId": "app", "migrations": [ … ] } ], "summary": "…" } -``` - -## Chosen design - -Per project decisions D1/D2/D3/D11: - -- **Pretty/TTY path** → render via the shared tree engine (`buildMigrationGraphRows` → `buildMigrationGraphLayout` → `renderMigrationGraphTree`), one disconnected tree per space (D4 — all on-disk spaces, `spaceId:` heading when multi-space; `--space` already narrows). `list` builds the per-space graph from the same `aggregate.space(id)` it already enumerates. -- **Edge annotations (D11)** → `list` populates `edgeAnnotationsByHash: Map` from each `MigrationListEntry` (which already carries `operationCount` + `providedInvariants`). **Refs are a node overlay** (`refsByHash`, keyed by each migration's `to` contract hash) — the same channel `graph` uses — not an edge annotation. -- **This slice introduces `edgeAnnotationsByHash` + `MigrationEdgeAnnotation`** on `RenderMigrationGraphTreeOptions` (D11), populating only the `operationCount`/`invariants` keys. `status` (TML-2748) later adds the `status` key. The renderer renders whichever keys are present; absent ⇒ plain row. -- **`--json` and future text-only** → unchanged flat package array (`MigrationListResult`). The tree never appears in a machine format. Free of pipe-safety concerns: `resolveOutputFormat` already returns `json` for non-TTY stdout, so the human renderer only runs interactively. - -`list` and `graph` share the renderer but differ in annotations and JSON (D2): `list` is package-centric (every on-disk package is an edge row, including parallel/duplicate/disconnected edges); `graph` is contract-centric (deduplicated nodes + `(contract)`/`(refs)` overlays). - -### Description-wording fix (no behaviour change to flat order) - -`migration list`'s description claims "latest first"; the flat sort is `compareDirNamesDescending` — lexicographic by dir name, not chronological. **The flat/`--json` order is unchanged** (kept byte-identical, lexicographic-descending by `dirName`); only the misleading "latest first" wording in the command description is corrected. (Tree order is topological regardless of the flat sort.) - -## Scope - -**In:** - -- Route `migration list` pretty/TTY output through the shared tree renderer with edge annotations (op count, invariants) + refs node overlay, per space. -- Introduce `edgeAnnotationsByHash` + `MigrationEdgeAnnotation` on `RenderMigrationGraphTreeOptions` (the D11 shared field) and render the `operationCount`/`invariants` keys. -- Keep `--json` flat and byte-identical. `--space` narrowing and `--ascii` glyph mode drive the tree. -- Correct the "latest first" description wording. -- Tests: pretty render across linear + branching + multi-space; `--json` shape byte-identical to today; `--space` narrowing; `--ascii` glyph mode on the tree. - -**Out:** - -- `migration graph` (separate command, separate JSON) — untouched here. -- The `MigrationListResult` JSON shape (stays the flat package array). -- The `status` overlay key on `edgeAnnotationsByHash` (TML-2748 adds it). -- Any flat-order change (explicitly unchanged). - -## Pre-decided edge cases - -| Edge case | Disposition | -|---|---| -| Parallel / duplicate edges (N packages, same `from → to`) | Each is its own edge row — `list` is package-faithful, unlike `graph`'s deduplicated nodes. The tree engine already renders parallel edges. | -| Disconnected packages (orphan `from`) | Rendered as a disjoint tree component (the renderer already handles disjoint forests). | -| A migration with no invariants / zero `ops` | Annotation shows `N ops`; `{invariants}` omitted when the set is empty. | -| Two migrations sharing a `to` that a ref points at | Ref renders once on the shared `to` node (node overlay dedups by hash). | -| `--ascii` | Drives the tree's glyph mode (box-drawing → ASCII), same as `graph --ascii`. | -| Empty space (no migrations) | Existing per-space empty-state line, unchanged. | -| `--space ` unknown | Existing `errorSpaceNotFound` (enumerates available ids), unchanged. | - -## Dispatch plan - -1. **Renderer: introduce the shared edge-annotation overlay (D11).** Add `MigrationEdgeAnnotation` + `edgeAnnotationsByHash` to `RenderMigrationGraphTreeOptions`; render `operationCount` (`N ops`) and `{invariants}` on the migration row when present. Pure renderer change + unit tests (snapshot rows with/without annotations). *Hands to 2 + 3; this is the field `status` rebases onto.* -2. **`list` → tree wiring.** In `migration-list.ts`, replace `renderMigrationListHumanOutput`'s flat path with the tree pipeline per space; build `edgeAnnotationsByHash` from `MigrationListEntry` (`operationCount`, `providedInvariants`) and `refsByHash` from entries' `to`+`refs`. Keep `--json` untouched. *Builds on 1.* -3. **Description fix + tests.** Correct "latest first" wording; add command tests (linear/branching/multi-space pretty render, `--json` byte-identical, `--space`, `--ascii`). *Builds on 2.* - -## Slice-specific done conditions - -- `migration list` (TTY) renders the package-annotated tree per space (op count + invariants on edges, refs on nodes); `migration list --json` is byte-identical to today; `--space` and `--ascii` behave; description no longer claims "latest first"; `edgeAnnotationsByHash`/`MigrationEdgeAnnotation` exist on the renderer for `status` to extend. - -## Sequencing - -Runs in parallel with `status` (TML-2748) and `log` (TML-2770). **Land this slice first** where schedules allow (D11): it introduces `edgeAnnotationsByHash`, which `status` extends with the `status` key — landing first means `status` rebases onto an existing field rather than both introducing it. No hard blocker either way (the field is additive). - -## References - -- Project decisions: `projects/migration-graph-rendering/decisions.md` (D1–D4, D11). -- Linear: [TML-2768](https://linear.app/prisma-company/issue/TML-2768); lineage [TML-2697](https://linear.app/prisma-company/issue/TML-2697). -- Shared renderer: `cli/src/utils/formatters/migration-graph-{rows,layout,tree-render}.ts`. -- `list` command + flat renderer: `cli/src/commands/migration-list.ts`, `cli/src/utils/formatters/migration-list-render.ts`, `migration-list-types.ts`. diff --git a/projects/migration-graph-rendering/slices/log-reads-ledger/spec.md b/projects/migration-graph-rendering/slices/log-reads-ledger/spec.md deleted file mode 100644 index 8a3f784a3d..0000000000 --- a/projects/migration-graph-rendering/slices/log-reads-ledger/spec.md +++ /dev/null @@ -1,95 +0,0 @@ -# Slice: `migration log` reads the DB ledger as a flat apply history - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to: `migration log` answers "what actually ran against this database, and when?" It reads the per-migration ledger journal (TML-2769) straight from the connected DB and prints it as one flat, chronological table — no graph, no per-space sections. It replaces today's `findPath(∅ → marker)` reconstruction from the on-disk graph, which lies whenever the DB and disk diverge. Tracking: [TML-2770](https://linear.app/prisma-company/issue/TML-2770)._ - -## At a glance - -Human (TTY) — flat table, oldest first, local time: - -``` -$ prisma-next migration log -2026-06-01 10:00:00 +02:00 20260301_init ∅ → ef9de27 5 ops -2026-06-02 10:00:00 +02:00 20260303_add_phone ef9de27 → 73e3abe 2 ops -2026-06-03 11:00:00 +02:00 20260305_rollback 73e3abe → ef9de27 2 ops -``` - -`--utc` (human only) renders the same table in UTC; `--json`/pipes are always ISO-8601 UTC: - -``` -$ prisma-next migration log --json -[ { "space": "app", "migrationName": "20260301_init", "migrationHash": "sha256:…", - "from": null, "to": "sha256:…", "appliedAt": "2026-06-01T08:00:00.000Z", - "operationCount": 5 }, … ] -``` - -When more than one space has ledger rows, a `space` column appears: - -``` -2026-06-01 10:00:00 +02:00 app 20260301_init ∅ → ef9de27 5 ops -2026-06-01 10:00:02 +02:00 audit 20260301_init ∅ → 9a1c2f3 3 ops -``` - -## Chosen design - -Per D8/D12/D13: - -- **Source = the DB ledger, read unscoped.** The ledger is one flat table in storage (every row carries `space`). This slice makes the read API's space argument **optional** end-to-end (SPI `readLedger({ driver, space? })`, client `readLedger()`, both adapters), so `readLedger()` with no space returns the **whole table**; the adapters drop the `WHERE space = ?` (SQL) / space `$match` (Mongo) when it's omitted. `log` calls `readLedger()` and sorts by `appliedAt` **ascending** (apply order). No on-disk graph, no `findPath` (replaces today's reconstruction), no per-space enumeration. (`status` keeps the *scoped* `readLedger(space)` form, unchanged.) -- **One flat table, not space-sectioned (D12).** No `--space` flag, no per-space headings. A `space` column is shown **only when >1 space** contributes rows (single-space — the common case — omits it as noise). -- **Uniform rows (D12 / KISS).** Every row is the same shape: `appliedAt · [space] · migrationName · from → to · N ops`. `log` does **not** classify apply vs rollback vs re-apply (that needs graph analysis a DB-sourced command shouldn't do). The same migration recurring (apply → rollback → re-apply) is just repeated rows; the `from → to` direction and repetition reveal the timeline. `from` of `null` renders `∅`. -- **Timestamps (D13).** Human/TTY → **local timezone** with offset for unambiguity (`2026-06-01 10:00:00 +02:00`). `--utc` → human output in UTC (`2026-06-01 08:00:00Z`). `--json` and any non-TTY/machine output → ISO-8601 UTC (`2026-06-01T08:00:00.000Z`) **regardless of `--utc`** (machine output is timezone-stable by contract). Non-TTY already auto-switches to JSON, so a piped `log` is UTC by construction. -- **`--json`** = the merged `LedgerEntryRecord[]`, sorted by `appliedAt` ascending, `appliedAt` serialized as the ISO-8601 UTC string. No wrapping object (it's a list). -- **Online only.** `log` reports DB truth, so it requires a connected DB; no offline/on-disk fallback (unlike `status`'s `--from`). Connection failure is the usual structured connect error. - -## Scope - -**In:** - -- Make the ledger read API's space argument **optional** (SPI `control-instances.ts`, client `readLedger()`, SQL adapter, Mongo adapter): unscoped read returns the whole table. Adapter unit/integration tests for the unscoped path (all spaces, deterministic order). -- `migration log` reads the unscoped ledger (`readLedger()`), sorted by `appliedAt` asc. -- Flat aligned table; `space` column only when >1 space; `from null → ∅`; `N ops`. -- `--utc` flag (human-only); local-tz default for human; ISO-UTC for JSON/non-TTY. -- `--json` = merged `LedgerEntryRecord[]` (ISO-UTC `appliedAt`), sorted asc. -- Empty-ledger message. -- Tests: single-space table; multi-space (space column); rollback/re-apply repetition; `--utc`; `--json` shape + UTC; empty ledger; DB-required error. - -**Out:** - -- Graph rendering of any kind (`log` is a table — D8). -- Per-space sectioning / `--space` (explicitly dropped — D12). -- Semantic apply/rollback/re-apply classification (D12). -- The ledger *write* path and `LedgerEntryRecord` shape (TML-2769, merged) — unchanged; this slice only widens the *read* to be unscoped. -- `status`'s use of the scoped `readLedger(space)` — unchanged. - -## Pre-decided edge cases - -| Edge case | Disposition | -|---|---| -| Empty ledger (no rows in any space) | Print `No migrations have been applied to this database.`; `--json` → `[]`. | -| Single space | `space` column omitted; table is `appliedAt · name · from→to · ops`. | -| Multiple spaces | `space` column shown; rows still globally sorted by `appliedAt` (interleaved across spaces). | -| Same migration applied → rolled back → re-applied | Three separate rows in apply order; no special glyph, the `from → to` directions tell the story. | -| `from` is `null` (initial / empty-origin) | Renders `∅` in the table; JSON keeps `null`. | -| Ties on `appliedAt` (same timestamp, e.g. coarse clock) | Stable secondary sort by `(space, migrationName)` so output is deterministic. | -| No DB connection | Structured connect error (DB required); no fallback. | -| A space with ledger rows but no marker | Still appears — the unscoped read returns rows regardless of markers (this is exactly why unscoped beats marker-enumeration). | - -## Dispatch plan - -1. **Unscope the ledger read.** Make `space` optional through the stack: SPI `readLedger({ driver, space? })` (`control-instances.ts`), client `readLedger()` (drop the `APP_SPACE_ID` default; pass `space: undefined`), SQL adapter (`control-adapter.ts`/`control-instance.ts` — conditional `WHERE space`), Mongo adapter (`mongo-control-adapter.ts`/`marker-ledger.ts` — conditional space `$match`). Adapter + client tests for the unscoped path (multiple spaces, deterministic order). *Hands to 3. Update the existing no-arg client test (`client.test.ts:888`) to the all-spaces expectation.* -2. **Table renderer.** Render `LedgerEntryRecord[]` → aligned flat table: optional `space` column (present iff >1 distinct space), `∅` for null `from`, `N ops`, and a timestamp formatter with three modes (local+offset / UTC-`Z` / ISO). Sort by `appliedAt` asc with `(space, migrationName)` tie-break. Unit tests (single vs multi-space columns; local vs `--utc`; empty; ties). *Hands to 3. Independent of 1.* -3. **Command rewrite.** Rewrite `migration-log.ts`: drop the `findPath` reconstruction; call `readLedger()` (1), render via (2) for human, emit the sorted list for `--json` (ISO-UTC). Add the `--utc` flag (human-only). Empty-ledger message. DB-required. Command tests. *Builds on 1+2.* - -## Slice-specific done conditions - -- `readLedger()` (unscoped) returns the whole ledger table across both adapters; `migration log` prints it as one flat chronological table (`space` column only when >1 space), oldest first; rollback/re-apply appear as repeated uniform rows; human time is local (`--utc` switches to UTC); `--json` is the `LedgerEntryRecord[]` in ISO-UTC sorted ascending; empty ledger prints the message / `[]`; no DB → structured error; no `findPath`/on-disk reconstruction remains in `migration-log.ts`; CI green. - -## Sequencing - -Parallel with `list`→tree (TML-2768) and `status` (TML-2748). Fully independent of the renderer overlay work (D11) — `log` does not use the tree at all, so it shares no code surface with the other two slices. - -## References - -- Project decisions: `projects/migration-graph-rendering/decisions.md` (D8, D12, D13). -- Linear: [TML-2770](https://linear.app/prisma-company/issue/TML-2770); lineage [TML-2697](https://linear.app/prisma-company/issue/TML-2697). -- Ledger read (to unscope): `ControlClient.readLedger` (`cli/src/control-api/client.ts`), SPI `control-instances.ts`, SQL `control-adapter.ts`/`control-instance.ts`, Mongo `mongo-control-adapter.ts`/`marker-ledger.ts`; `LedgerEntryRecord` (`framework/0-foundation/contract/src/types.ts`). -- Current log (to replace): `cli/src/commands/migration-log.ts` (`findPath`-based). diff --git a/projects/migration-graph-rendering/slices/migrate-show-preview/code-review.md b/projects/migration-graph-rendering/slices/migrate-show-preview/code-review.md deleted file mode 100644 index b5ef55461b..0000000000 --- a/projects/migration-graph-rendering/slices/migrate-show-preview/code-review.md +++ /dev/null @@ -1,230 +0,0 @@ -# Code review — `migrate-show-preview` (TML-2771) - -> The reviewer maintains this across rounds. Orchestrator owns § Subagent IDs / § Orchestrator notes. - -## Summary - -- **Current verdict:** SATISFIED (R8 — two-style seam re-verified by force-render after the `80dddb1cf` refactor: on-path == pgvector neutral single-path style byte-for-byte, off-path dim, run-list matches tree on-path; see R8 round note. Prior: R7 colour-model flip `61e6ecaa5`/`b9fe5833c`/`802ebbdb5`/`5e81813a0`; R6 ordering + forced colours; R5 app-space scoping; R4 `@contract` gate; R3 F2 accepted-closed) - -### Slice migrate-show-preview R8 — SATISFIED - -**Scope:** verify the two-style colour model after the seam refactor (`80dddb1cf`), which suppresses by-branch rotation in path-highlight mode and replaces it with on-path = neutral single-path style / off-path = dim grey, defined in one place. - -**Force-render (the whole point), `FORCE_COLOR=1`, `colorize:true`.** Built the demo topology (app chain ∅→76c1bd5→5618dca→6cee614→f7a8eb5 + fork ∅→1375f13, ext pgvector ∅→29059df), annotated `--from ∅ --to 6cee614` (on-path = initial/0742/0748, off-path = bookend/1431), rendered both the app tree and a standalone pgvector single-path tree, and inspected `JSON.stringify`'d ANSI. -- **On-path == pgvector neutral, byte-for-byte.** App on-path edge `initial` (∅-source) skeleton `\x1b[2m│\x1b[22m↑ \x1b[1mNAME\x1b[22m \x1b[2mSRC\x1b[22m \x1b[2m→\x1b[22m \x1b[96mHASH\x1b[39m` is identical to the pgvector `install_vector_v1` row. On-path name = `\x1b[1m` bold; lanes/node markers/arrows = ambient `\x1b[2m` dim (neutral); dest hash = `\x1b[96m` cyanBright (`destHash`); real source hash = `\x1b[2m\x1b[36m` dim-cyan (`sourceHash`); ∅-source = `\x1b[2m` dim glyph. **Zero rotation codes** on every on-path cell — frequency scan over all on-path rows: no `\x1b[31m` (red), no `\x1b[35m` (magenta), no `\x1b[92m` (greenBright). (Note: the spec prose says "hashes purple"; the actual neutral hash colours are cyan/cyanBright per `migration-list-styler.ts:85-86` — on-path faithfully mirrors the existing single-path render, which is the requirement.) -- **Off-path dim.** bookend / 1431 edge rows + f7a8eb5 / 1375f13 node rows + their lanes/markers/arrows + dest hashes all carry `\x1b[2m` (`forcedDim`), zero rotation codes. Off-path destination hash dims (`\x1b[2mf7a8eb5\x1b[22m`, `\x1b[2m1375f13\x1b[22m`) — the override reaches every character, no inner cyan leaks. -- **Run-list matches tree on-path.** `formatOnPathMigrationRow('0742', …)` name+hash columns `\x1b[1mNAME\x1b[22m \x1b[2m\x1b[36mHASH\x1b[39m\x1b[22m \x1b[2m→\x1b[22m \x1b[96mHASH\x1b[39m` are byte-for-byte identical to the tree on-path row's name+hash columns (gutter omitted by design). -- **`colorize:false` → zero `\x1b[`.** Re-rendered the annotated topology with `colorize:false` under `NO_COLOR=1`: output contains zero ANSI. (Under `FORCE_COLOR=1` the on-path name shows ambient `\x1b[1m` because `PATH_HIGHLIGHT_STYLES.onPath` uses colorette's ambient `bold()`, which honours `FORCE_COLOR` independently of the renderer's `colorize` flag — documented at `tree-render.ts:206-217` and in the suite's own comments; in real CLI use `colorize` tracks the TTY/NO_COLOR state, so piped `--show` emits no ANSI. Not a regression.) - -**Renderer colour test file:** `test/utils/formatters/migration-graph-tree-render.test.ts` — **83/83 PASS** (250ms). The `describe('PATH_HIGHLIGHT_STYLES neutral on-path style')` block (`:1973-2190`) asserts exactly this model and is green. (The whole-suite run surfaced one failure in a *different* file, `test/commands/migration-graph.test.ts` — a pre-existing `Config validation error`, unrelated to the colour work.) - -**Two-style seam (one-line-tweak confirmed):** `PATH_HIGHLIGHT_STYLES` at `migration-graph-tree-render.ts:200`. Both consumers route through it — the tree path (`:838` offPath, `:842` onPath) and the run-list `formatOnPathMigrationRow` (`:1140`). A future on-path colour change is one line inside `onPath` (`:213-218`, e.g. the `dirName`/`lane` styler); off-path is one line inside `offPath` (`:225-230`). - -**Read-only / faithfulness intact (yes):** the change is presentation-only. `executeMigrateShowCommand` (`commands/migrate.ts:147`) still calls `planMemberPath(...)` (which feeds `graphWalkStrategy`) and returns via `ok({…})` with **no `runMigration` call** anywhere in its body (grep over the command body: 0 hits). Control-flow seam unchanged by the colour refactor. - -**Findings:** none. Cleaned up the throwaway force-render script. -- **Dispatches SATISFIED:** D1, D2, D3, D4 clean -- **AC scoreboard totals:** 13 PASS / 0 FAIL / 0 NOT VERIFIED -- **Open findings:** 0 (F1 resolved `7206dfd95`; F2 routing-covered `b55f9a5a0` + accepted-closed. R7 colour model changed from bright-green to ordinary/dim — reviewer independently force-colour-rendered the operator's exact demo topology under `FORCE_COLOR=1` and inspected raw ANSI: no `\x1b[92m` remains; on-path ordinary, off-path dim, mixed-column off-path segments render correctly.) - -## Acceptance criteria scoreboard - -| AC ID | Description (short) | Dispatch | Status | Evidence | -| --- | --- | --- | --- | --- | -| AC-1 | Reserved markers render `@db`/`@contract` (no `<…>`) in overlay **and** `--legend`, across graph/status/list; snapshots regenerated + consistent | D1 | PASS | `migration-list-styler.ts` (`plainMarkers`/`markers`) + `migration-graph-tree-render.ts` (`formatLegendExampleMarkers`, legend text) drop brackets; styler/legend/graph tests updated; formatter grep for `` / `OUT OF SCOPE`. - -## Subagent IDs - -- **Implementer:** `a6794e1e5ade9c01c` (sonnet) — D1–D4 R1. Swap → `ab391f502ed0da567` (sonnet) for R2/F1 (harness exposes no resume tool; fresh subagent per round, recorded per continuity rule). -- **Reviewer:** `a8004f2c570c470fa` (opus) — R1. Swap → `a7f5d967cb649fe87` (opus) — R2 (same resume-unavailable reason). - -## Orchestrator notes (R3 — operator visual review of PR #735) - -- Operator gave 6 visual items on the `--show` output: 2 bugs (`@contract` placed at `--to` not the working contract; floating `@contract` leaking into extension spaces) + 4 refinements (off-path fully drawn dim grey; on-path lane lines green; ordered list not via Clack gutter; list in graph-row format + green). Spec § Output (D-MS3 revised) + decisions amended to match. -- Implementer swap (R3): `a488ab5e72b5ed40e` (sonnet). Commits `8b6792f68` (@contract bugs + off-path) / `bfd85a00c` (grey/green + list). DoD pre-check: diff confined to `migrate.ts` + the renderer + migrate-show test + docs; **no shared `__snapshots__` changed** (graph/status/list output untouched); `ui.output` replaces `ui.log` (Clack gutter gone); @contract-working-contract + extension-space tests present. -- Reviewer R3: `_(opus, recorded in round)_`. -- Judgment call to scrutinise: the command now renders **extension spaces too** (was app-only). Correct per spec multi-space behaviour (`migrate` spans app + extensions), but confirm extension spaces render their own heads/path with **no `@contract`**. → Reviewer R3 confirmed correct. -- **R3 verdict (orchestrator close):** reviewer R3 (`acff1f3bd487b7817`, opus) confirmed all six visual items correct on disk (both `@contract` bugs fixed, off-path grey / on-path green / no-Clack-list / graph-row format), no regression (read-only / faithfulness / shared output intact), multi-space expansion correct. The sole blocker was **F2** (should-fix: colour wrappers untested). -- **F2 accepted-closed.** R3 implementer (`a488ab5e72b5ed40e`) shipped the 6 fixes; F2-fix implementer (`ad4a992e51544dd31`) added routing coverage (commit `b55f9a5a0`) but found the test env runs `NO_COLOR=1`, no-op'ing colorette — so the literal green/grey ANSI codes can't be asserted without a source seam to force colour. Orchestrator decision (reviewer explicitly authorised the accept): close F2. Rationale — the regression-prone surface is the *routing* (now tested via the `will run` suffix + off-path-name-drawn), the `greenBright`/`dim` constant application is verified on-disk by the reviewer, and the operator visually confirms the hues on PR #735. A source-seam-for-colour-testability change is over-engineering for a should-fix on a visually-confirmed feature. Residual (documented): the literal hue is not asserted in-suite due to the `NO_COLOR` env. - -## Findings log - -### F1 — `migrate --show` assembles `graphWalkStrategy` inputs differently from real `migrate`; the previewed path can diverge - -**Severity:** must-fix - -**Where:** `packages/1-framework/3-tooling/cli/src/commands/migrate.ts:296,330,333` (and the dropped `--to`-ref invariants at `:183-201`) vs `packages/1-framework/3-tooling/cli/src/control-api/operations/migrate.ts:160,192-206`. Consumed by `graph-walk.ts:61` → `migration-graph.ts:325`. - -**What:** AC-5 requires the preview to be faithful — the path it shows must be the path `migrate` would actually run. The show command calls the same seam (`graphWalkStrategy`), but feeds it different invariant inputs: - -1. **Live-marker invariants are dropped.** On the live-marker path the command reads only `marker?.storageHash` (`:296`) and then builds `currentMarker = { storageHash: fromHash, invariants: [] }` (`:333`). Real `migrate` passes the full live marker, including its `invariants` (`operations/migrate.ts:160`). Inside `graphWalkStrategy`, `required = headRef.invariants \ markerInvariants` (`graph-walk.ts:61`); with the marker's invariants discarded, the preview's `required` set is the *full* head-ref invariant set instead of the remainder. -2. **`--to`-ref invariants are ignored.** The command extracts only `toResult.value.hash` (`:183-201`) and always targets with the *file head ref's* invariants (`:330`). Real `migrate` targets with the resolved ref's invariants (`refInvariants ?? headRef.invariants`, `operations/migrate.ts:192-199`). - -`required` is a genuine path-selection input — it flows into `findPathWithInvariants(graph, fromHash, toHash, required)` (`migration-graph.ts:325`) and changes which path is selected (and whether the already-at-target empty-path short-circuit at `:309` applies). So on any contract graph that uses invariants, the previewed ordered list / highlighted path can differ from what `migrate` actually runs. That is exactly the "the preview can lie" failure D-MS4 / D-MS6 exist to prevent. - -(Lower-severity sibling, same root cause: empty-graph members are silently `continue`d in the preview (`:322-325`) where real `migrate` either records an at-head resolution or fails loudly via `buildNeverPlannedFailure`. Fold into the same fix; not separately blocking.) - -**Why it matters:** The slice's whole purpose is a sanity check the user can trust before they advance the live DB. A preview whose path can differ from the real run on invariant-bearing graphs defeats the feature and silently misleads. The faithfulness *test* only asserts `graphWalkStrategy` was *called* (`migrate-show.test.ts:172-190`), so it does not catch this — the inputs are untested. - -**Recommended next action:** Make the two callers share the input assembly so divergence is structurally impossible. Either (preferred, the spike's optional wrapper) extract a `previewMigrationPath(member, …)` helper that computes `targetHash` / `targetInvariants` / `currentMarker` once and is called by both `executeMigrate` and `executeMigrateShowCommand`; or, at minimum, in the show command: (a) carry the live marker's `invariants` through `readAllMarkers()` into `currentMarker` instead of `[]`, and (b) capture the `--to` ref's invariants and use them as the target member's `headRef.invariants`. Then strengthen the faithfulness test to assert the `graphWalkStrategy` call arguments match what `executeMigrate` would pass for the same fixture (e.g. an invariant-bearing graph where full-vs-remainder `required` selects different paths), not merely that the function was called. - -**Status:** resolved (commit `7206dfd95`). Both original divergences fixed at a single assembly site (`planMemberPath`, `operations/migrate.ts:369`): (1) live-marker invariants now carried — show stores the full `marker ?? null` record (`commands/migrate.ts:316`) and the helper passes `currentMarker: liveMarker` (`:413`), so no stripped `{ invariants: [] }` shell exists on the live path; (2) `--to`-ref invariants captured from the resolved ref (`commands/migrate.ts:201-205`) and applied via `targetInvariants = refInvariants ?? headRef.invariants` (`:403-404`). Empty-graph case aligned: `at-head` → skip (show) / at-head-resolution (apply); `never-planned` → loud error in both. `executeMigrate` is a pure extraction — same error taxonomy (`buildNeverPlannedFailure`/`buildPathNotFoundFailure`/`errorNoInvariantPath`), same `atHeadResolutions`/`perSpacePlans` population, still hands to `runMigration`. Faithfulness tests strengthened to assert call-arg equivalence on an invariant-bearing `--to`-ref fixture; the second test would have failed on the original Bug #2 (asserts `headRef.invariants === ['inv-a']`, was `[]`). - -### F2 — the on-path-green / off-path-grey colour wrappers have no test coverage - -**Severity:** should-fix - -**Where:** `packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-tree-render.ts:749-752,757-764,777-780,482-489,495` (the `greenBright(...)`/`dim(...)` wrappers on gutter, name, hash, and `↑ will run` suffix added in commit `bfd85a00c`). No covering test: every `migrate-show.test.ts` case runs with `--no-color` + `stripAnsi`, and the renderer unit test `migration-graph-tree-render.test.ts` has zero `pathHighlight` cases (`grep pathHighlight` → only the two command test files). - -**What:** R3 items 3 and 4 (off-path fully drawn in uniform dim grey; on-path lane lines + hash in the same bright green as the name) are correct on disk — I verified the wrappers directly. But no automated test asserts the ANSI hues. The command tests strip colour; the renderer unit test was not extended for the path-highlight branch. The *structural* half of item 3 (off-path name is drawn, not omitted) is covered (`migrate-show.test.ts:415` asserts the off-path dirName now appears), but the *colour* half of 3 and all of 4 are unverified by CI. A future edit that drops `greenBright` from the gutter/hash, or stops dimming the off-path suffix, would pass every test. - -**Why it matters:** The operator's R3 feedback was specifically about colour. The fix is correct now and was visually confirmed on PR #735, but the visual contract regresses silently — exactly the kind of change CI should catch. The renderer already has many `colorize: true` assertions; adding one path-highlight case is cheap. - -**Recommended next action:** Add one assertion (renderer unit test preferred, or a non-`--no-color` command test) that, given an `edgeAnnotationsByHash` with one `on-path` and one `off-path` edge and `colorize: true`, asserts the on-path gutter+hash+name carry the green escape and the off-path gutter+hash+name+suffix carry the dim escape (e.g. assert the raw output contains `greenBright(...)`/`dim(...)` markers for the expected substrings, or compare against a small colorized inline snapshot). - -**Status:** open - -## Round notes - -### Slice migrate-show-preview R1 — ANOTHER ROUND NEEDED - -**Scope:** D1–D4. Commits `6d0a357f5..9bc81455f`. - -**Tasks:** D1 (relabel) clean. D2 (`@`-tokens) clean. D3 (`--show` read-only + list) — read-only clean, faithfulness fails (F1). D4 (graph viz) clean. - -**AC delta:** AC-1/AC-2/AC-3/AC-4/AC-6 NOT VERIFIED → PASS (commits as scoreboard). AC-5 NOT VERIFIED → FAIL — preview computes `required` invariants from inputs that diverge from real `migrate`, so the previewed path can differ (F1). - -**Findings:** F1 (must-fix). - -**For orchestrator:** none — F1 is in-PR addressable by the implementer (align `graphWalkStrategy` inputs / add the shared `previewMigrationPath` helper, then strengthen the faithfulness test). Transient-ID scan on the round's `+` diff: zero hits. - -### Slice migrate-show-preview R2 — SATISFIED - -**Scope:** F1 fix. Commit `7206dfd95` (on `9bc81455f`). - -**Tasks:** D3 faithfulness now clean — shared `planMemberPath` helper assembles `graphWalkStrategy` inputs once; both callers feed equivalent args. `executeMigrate` apply is a behaviour-preserving extraction. - -**AC delta:** AC-5 FAIL → PASS (commit `7206dfd95`, tests `migrate-show.test.ts:209-282`). Single assembly site `operations/migrate.ts:369`; callers `executeMigrate:163` + show `:346`. Both Bug #1 (live-marker invariants) and Bug #2 (`--to`-ref invariants) structurally fixed. - -**Findings:** F1 resolved. No new findings. - -**For orchestrator:** none. Transient-ID scan on R2 `+` diff: zero hits. Gates trusted (typecheck + cli 1209 + migration-tools 549 green per implementer; `migration-list-json-golden` concurrent flake pre-existing, also on TML-2780). - -### Slice migrate-show-preview R3 — ANOTHER ROUND NEEDED - -**Scope:** operator visual-review fixes. Commits `8b6792f68` (@contract bugs + off-path) + `bfd85a00c` (grey/green + list), on `d03a89cb9`. Diff confined to `commands/migrate.ts` + `utils/formatters/migration-graph-tree-render.ts` + `migrate-show.test.ts` (+ spec/decisions docs). - -**Items (all 6 correct on disk):** (1) `@contract`-placement BUG fixed — renderer gets working `contractHash` (`:433,442`), not `targetHash`; PASS. (2) floating-`@contract` BUG fixed — extensions get `spaceContractHash = undefined`; PASS. (3) off-path fully drawn dim grey (name/hash/gutter/suffix all `dim`, none blanked); correct on disk. (4) on-path lanes + hash `greenBright`; correct on disk. (5) list via `ui.output` not `ui.log` (`:851`); PASS. (6) list reuses graph data-column helpers in green, ad-hoc `1. name (…)` gone; PASS. - -**AC delta:** AC-6 evidence revised to the richer render (fully-drawn grey off-path + green lanes); stays PASS on correctness. Added AC-7 (working-contract `@contract`, app-only) PASS — `migrate-show.test.ts` `'@contract marks the working contract…'` + `'@contract does not appear in extension spaces'`, both non-tautological. Added AC-8 (list graph-row format, no Clack gutter) PASS — `migrate-show.test.ts:425-432`. Totals 8 PASS / 0 FAIL. - -**No-regression:** read-only intact (`executeMigrateShowCommand` returns `ok` at `:465`, never reaches `runMigration`); faithfulness intact (path still via `planMemberPath`/`graphWalkStrategy` `:346-359`, unchanged from R2); shared graph/status/list output unchanged — `pathHighlight` set only in `--show` (`migrate.ts:428`), no `__snapshots__` in the diff. - -**Multi-space judgment:** correct. The command now renders app + extensions per `allMembers` (`:253`), mirroring real multi-space `migrate`; extension spaces show their own head/path with no `@contract` and no floating app-contract node (test-confirmed). - -**Findings:** F2 (should-fix) — the on-path-green/off-path-grey colour wrappers (items 3+4) are correct on disk but covered by no test (`migrate-show.test.ts` all run `--no-color`; renderer unit test has no `pathHighlight` case). Blocks SATISFIED per all-severities-block; cheap to close with one colorized assertion. - -**For orchestrator:** F2 is in-PR addressable (add one path-highlight colour assertion). Note the operator already visually confirmed the colours on PR #735, so if you judge the visual contract sufficiently covered by human review you may accept F2 as a deferral — flagging the call to you. Transient-ID scan on R3 `+` diff: zero hits. Gates trusted (implementer reports typecheck + cli suite green, shared snapshots unchanged); pre-existing `control-api/client.test.ts` / `migration-list-json-golden` concurrent flakes are not regressions. - -### Slice migrate-show-preview R4 — SATISFIED - -**Scope:** follow-up fix — the app-space-only `@contract` rule was applied only to `migrate --show` in R3; `migration graph` and `migration status` had the same pre-existing bug (passed app `liveContractHash` to extension-space renders → spurious floating `@contract`). Commit `018f97184` on `d03a89cb9`. Diff: `migrate.ts`, `migration-graph.ts`, `migration-status.ts`, the two formatters, `migration-graph-tree-render.test.ts` (+ decisions.md). - -**Gate (correct + complete):** `@contract` marker gated behind `isAppSpace !== false` in `overlayNamesForContract` (`migration-graph-tree-render.ts:427-433`); the floating working-contract node is also gated by not forwarding `contractHash` to `buildMigrationGraphRows` for non-app spaces (`migrate.ts:431`, `migration-graph-space-render.ts:90-93`) — `detachedContractHash` (`migration-graph-rows.ts:274-279`) only floats a node when `contractHash` is passed, so dropping it removes the node, not just the label. BOTH gated. `@db` (`DB_MARKER_NAME`, `:434`) untouched — renders per-space. - -**Callers:** all three derive the flag from `spaceId === aggregate.app.spaceId` and pass it for every space (`migrate.ts:448-449` over `allMembers`; `migration-graph.ts:169,179`; `migration-status.ts:536,543`). None inverts it; no extension render path missed. - -**Default / single-space:** `isAppSpace?: boolean` defaults to app behaviour (`!== false`); single-space `graph`/`list`/`status` and any non-passing caller unchanged. No `__snapshots__` in the diff; +5 new tests, none modified. - -**No regression to `--show`:** the `spaceContractHash = isApp ? contractHash : undefined` workaround removed; now always passes `contractHash` + `isAppSpace: isApp` — behaviour-equivalent (extension spaces still get no `@contract`; app still marks the working contract). Read-only / faithfulness / green-grey paths untouched by this commit. - -**Tests:** `renderMigrationGraphTree isAppSpace gate` (5) genuinely assert: app shows `@contract`; extension shows none even when `contractHash` matches a node; extension produces no floating node (node-count + `extRows.nodes` does-not-contain check, `:253-264`, not label-only); `@db` still renders in extension spaces; omitting `isAppSpace` defaults to app behaviour. - -**AC delta:** AC-7 evidence widened from `migrate --show`-only to all three read commands, with the structural-gate mechanism recorded; stays PASS. Totals 8 PASS / 0 FAIL. - -**Findings:** none. Transient-ID scan on R4 `+` diff (`*.ts`): zero hits (the prior `BUG 1`/`BUG 2` comments were removed). Gates trusted (implementer reports typecheck + cli suite green, 1219 tests +5, no snapshot changes); pre-existing `control-api/client.test.ts` / `migration-list-json-golden` concurrent flakes are not regressions. - -**For orchestrator:** none. - -### Slice migrate-show-preview R5 — SATISFIED - -**Scope:** two operator-found bugs. Commit `8b48ccac4` (BUG 1: `--from`/`--to` app-space scoping) + `7aed826e7` (BUG 2: path-highlight lane-colour override), on `d03a89cb9`. Diff confined to `commands/migrate.ts`, `utils/formatters/migration-graph-tree-render.ts`, their two test files, and the spec § D-MS3/D-MS4 target updates. No `__snapshots__` changed; working tree clean; no `examples/` scratch. - -**BUG 1 (AC-10):** offline `--from` now sets the marker for the app space only (`commands/migrate.ts:295`), not a loop over `allMembers`; target app-gated at `:351`; connected path per-space at `:323-325`. Extensions are planned identically to real `executeMigrate` (`operations/migrate.ts:154-169`: own head ref + own marker) — the key faithfulness property holds. Test is a genuine multi-space repro (no throw / extension previewed greenfield→head / app `--from` suppression), non-tautological. - -**BUG 2 (AC-11):** `laneOverride` replaces `laneStylerForColumn` at the source (`tree-render.ts:273`), so the rotation hue is never emitted for any on/off-path cell kind (node marker, lanes, tees, corners, arcs, trailing) — the two non-`lane` paths (edge-lane arrow `:304`, arc-land-tee `:325`, node-marker pair `:251`) handled explicitly. Post-hoc gutter wrapper removed (`:838`). Node rows covered via `contractHighlights` (on-path wins, `:668-685`). Override gated on `edgeAnnotationsByHash` → graph/status/list unchanged. This is the structural fix for the surface F2 flagged as untested. - -**No regression:** read-only intact (`executeMigrateShowCommand` still returns before `runMigration`); faithfulness intact (`planMemberPath`/`graphWalkStrategy` seam unchanged, now also app-gated correctly); `@contract` app-space gate untouched; ordered-list format untouched. graph/status/list not regressed — no snapshot changes; override is `undefined` for those commands. - -**Findings:** none. Transient-ID scan on the R5 `+` diff (`*.ts`): zero hits (the `BUG 1`/`BUG 2` comments and test names are bug-narrative labels, not transient plan IDs). Gates trusted (implementer reports typecheck + cli suite green at 1222; graph/status/list snapshots unchanged; `examples/` scratch cleaned). Pre-existing `control-api/client.test.ts` / `migration-list-json-golden` concurrent flakes are not regressions. - -**For orchestrator:** the `NO_COLOR=1` test env makes literal green/grey/red ANSI unassertable; per the F2 accept precedent this is acceptable — hue correctness is verified by code inspection + operator visual review, and the regression-prone *routing* is now test-covered. - -### Slice migrate-show-preview R7 — SATISFIED - -**Scope:** four fixes. `61e6ecaa5` (FIX A: drop green highlight — on-path ordinary, off-path dim) + `b9fe5833c` (FIX B: `migration list` `@contract` app-space gate) + `802ebbdb5` (FIX C: global column alignment across spaces + run-list) + `5e81813a0` (FIX D: per-lane-segment classification). Diff confined to `commands/migrate.ts`, `commands/migration-list.ts`, the two formatters (`migration-graph-tree-render.ts`, `migration-list-render.ts`), and their three test files. No `__snapshots__` changed. - -**Colour model flipped (AC-6/AC-8/AC-11 supersede the R6 green-highlight evidence).** R6 verified `greenBright` on-path; the spec was then revised to "on-path = ORDINARY colours; off-path = DIM grey; NO bright-green highlight." FIX A removes the forced `greenBright` from `pathLaneOverride`, dirName, `hashColumnOverride`, and the will-run suffix. Grep for `greenBright`/`GREEN_BRIGHT` in production source (`migration-graph-tree-render.ts`, `migrate.ts`): **clean**. The R6 scoreboard wording for AC-6/AC-8/AC-11 (describing green) is now historical; current behaviour is ordinary-on-path / dim-off-path, force-rendered below. - -**FIX D (per-lane-segment classification) — PASS, force-rendered and adversarially probed.** I force-rendered the operator's exact demo topology (`--to 6cee614`: on-path ∅→76c1bd5→5618dca→6cee614, off-path 6cee614→f7a8eb5 + ∅→1375f13) and three synthetic adversarial topologies under `FORCE_COLOR=1`, inspecting `JSON.stringify`'d ANSI per row/column. Findings: -- `columnHighlights` "on-path wins" governs **only** `vertical-pass` cells (`tree-render.ts:843-846`). **Off-path edge-lane, name, and hash cells are classified by their own `migrationHash` annotation** (`:847-851`, `:956-988`), bypassing `columnHighlights` — so they always dim correctly regardless of how the column is stamped. -- The only column that is ever **mixed** (hosts both on- and off-path edges) is column 0, the trunk. Confirmed `--to 6cee614` mixed col 0: off m4_fourth + on m1/m2/m3. The off-path m4 edge-lane dims via its own hash (`\x1b[2m│\x1b[22m\x1b[2m↑\x1b[22m`). Crucially, **column 0's ordinary lane style is itself `dim()`** (`migration-list-styler.ts:88`, `stylerForLaneColumn` returns the dim lane for col 0). So an off-path `vertical-pass` misclassified as on-path in a mixed col 0 still renders dim — there is **no visible "trunk bright/ordinary when it should be grey" defect**. -- I deliberately tried to force an off-path lane onto a column ≥ 1 that is also stamped on-path (diamond merge, lane-reuse, two-fork). In every case the layout engine assigns the off-path branch its own column (its lane never coexists as a pass-through in an on-path column ≥ 1); the off-path segment's own edge/name/hash always dims via own-hash. No off-path segment rendered in a bright rotation colour. The bug the brief feared (off-path lane bright because "on-path wins" stamped its column) **does not reproduce**. - -**FIX A (on-path ordinary / off-path dim) — PASS, force-rendered.** `--to 6cee614` ANSI: on-path edge rows carry `\x1b[1m` (bold name) + `\x1b[96m`/`\x1b[36m` (ordinary cyan dest/source hash) + plain `↑ will run`; **no `\x1b[92m` anywhere** (frequency scan: 0). Off-path rows (m4_fourth, m0_fork, node f7a8eb5/1375f13) carry `\x1b[2m` on marker, hash, name, edge `from → to` **including the destination hash** (`\x1b[2mf7a8eb5\x1b[22m`, `\x1b[2m1375f13\x1b[22m`). NO_COLOR: rendered the same topology with `colorize:false` (annotations present) → **zero `\x1b[`**; the forced dim/bold are gated behind `opts.colorize` so piped/NO_COLOR `--show` emits no ANSI. - -**FIX B (`migration list` `@contract` gate) — PASS.** See AC-13. Threading correct; single-space unchanged; multi-space test non-tautological. - -**FIX C (global column alignment) — PASS with one note.** See AC-14. Graph sections align across spaces (`→` at col 41 in both app + pgvector). The run-list is internally aligned but shifted left by the graph gutter it omits (per spec "minus the graph gutter") — name col 2 / `→` col 37, sharing the same column WIDTHS. The operator's actual bug (app vs pgvector sections self-aligning and mismatching) is fixed. - -**No regression:** read-only intact (`executeMigrateShowCommand` returns before `runMigration`); faithfulness intact (`planMemberPath`/`graphWalkStrategy` seam unchanged); `@contract` app-space gate, app-scoped `--from`/`--to`, and canonical run-order (extensions first) all untouched. graph/status/single-space-list not regressed — path-highlight is a no-op without `edgeAnnotationsByHash`; the only `list` change is the multi-space `@contract` gate; no `__snapshots__` in the diff; the two affected formatter suites pass (92 tests). - -**Findings:** none. Transient-ID scan on the R7 `+` diff (`*.ts`/`*.mts`): zero hits (`TML-2771` is a Linear ticket, appears only in commit messages). Gates trusted (implementer reports typecheck + cli suite green, 1223 pass; the 4 `control-api/client.test.ts` live-DB failures are pre-existing, not regressions). - -**For orchestrator:** FIX C run-list is gutter-shifted by design (the spec's "minus the graph gutter" makes absolute-offset alignment with graph rows impossible); the data-column WIDTHS are shared and the cross-space graph mismatch — the operator's actual complaint — is fixed. Flagging in case the operator expects the run-list `→` at the same absolute column as the graph rows; if so that is a spec-clarification, not a defect in this fix. - -### Slice migrate-show-preview R6 — SATISFIED - -**Scope:** two operator-found bugs. Commit `bef989016` (run-list ordering) + `e59c0e6d3` (path-highlight ANSI colours), on `2c440bb7d`. Diff confined to `commands/migrate.ts`, `utils/formatters/migration-graph-tree-render.ts`, their two test files, and the spec § Output (items 1+2) target update. No `__snapshots__` changed; working tree clean. - -**PART A — ordering (AC-12):** `commands/migrate.ts:347-356` now iterates `[...aggregate.extensions, aggregate.app]` instead of `allMembers` (`[app, ...extensions]`). This is the same array order the runner consumes (`operations/migrate.ts:226` `canonicalOrder = [...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`); the "extensions alphabetically by spaceId, then app" convention is owned by `concatenateSpaceApplyInputs` (`run-migration.ts:55-58`) and both callers honour it by consuming `aggregate.extensions` in array order — byte-for-byte agreement. Test asserts `extDir` precedes both app migrations. PASS. - -**PART B — colours (AC-11), reviewer-force-rendered:** I built the actual repro topology and rendered it with `colorize:true` under `FORCE_COLOR=1`, inspecting `JSON.stringify`'d ANSI. On-path: every cell `\x1b[92m` (markers `○`/`∅`, contract-hash text, name, edge `from→to`, `↑ will run`, on-path lanes). Off-path: every cell `\x1b[2m` (marker, hash, name, edge `from→to` **including dest `1375f13`**, lanes). Connector: `\x1b[2m├─\x1b[22m\x1b[92m╯\x1b[39m` — off-path tee dim, on-path corner green. Frequency scan over the whole dump: only `92`/`39`/`2`/`22`; **zero** `31`/`35`/`36`/`96`/`33`/`34` — no rotation/cyan colour anywhere. Column-correct: green tracks the on-path branch's own lane, off-path lane is dim, no trunk bleed. - -**CRITICAL — NO_COLOR still works:** rendered the same topology with `colorize:false` (annotations still present) → output contains **zero** `\x1b[`. The forced stylers are gated behind `opts.colorize`, so piped / NO_COLOR / non-TTY `--show` emits no ANSI. PASS. - -**No regression:** read-only intact (`executeMigrateShowCommand` returns before `runMigration`); faithfulness intact (`planMemberPath`/`graphWalkStrategy` seam unchanged; the ordering fix uses the runner's own member arrays, strengthening faithfulness); `@contract` app-space gate + app-scoped `--from`/`--to` untouched. graph/status/list not regressed — path-highlight is a no-op without `edgeAnnotationsByHash`; no `__snapshots__` in the diff; the two affected suites pass (89 tests). - -**Findings:** none. Transient-ID scan on the R6 `+` diff (`*.ts`): zero hits. Gates trusted (implementer reports typecheck + cli 1224 +2 green); pre-existing flaky subprocess timeouts (`removed-verb-redirects.test.ts`, `version.test.ts`) fail identically on base — not regressions. - -**For orchestrator:** the R5 residual (literal hues unassertable under `NO_COLOR=1`) is now resolved — forced colours behind `colorize` make ANSI assertable in-suite without breaking piped output. - -## Orchestrator notes (R1 triage) - -- **Intent-validation of R1 verdict: pass-through.** F1 confirms the faithfulness concern (Orchestrator notes #1); the verdict reflects intent — a preview that can diverge from the real `migrate` defeats the slice's reason to exist. Looping to the implementer. -- **Direction for R2:** take the **preferred** fix — extract a shared `previewMigrationPath(...)` helper that assembles `targetHash` / target-invariants / `currentMarker` (incl. live-marker invariants and the resolved `--to`-ref invariants) and runs `graphWalkStrategy`, called by BOTH `executeMigrate` and `executeMigrateShowCommand`, so faithfulness is structural, not "keep two sites in sync." Fold in the empty-graph-member sibling (preview `continue` vs real at-head-resolution/loud-fail). Strengthen the faithfulness test to assert call-argument equivalence on an invariant-bearing fixture (full-vs-remainder `required` selects different paths). Refactoring `executeMigrate` to call the helper is in-scope and must stay behaviour-preserving (its apply path unchanged). - -- 4 feat commits `6d0a357f5` (D1 relabel) / `ffd292879` (D2 tokens) / `9c5bdd156` (D3 show+list) / `9bc81455f` (D4 graph). DoD pre-check: scope matches plan (migrate.ts, the 2 formatters, contract-ref + refs/types, tests); read-only + faithfulness tests present; no reserved-marker angle brackets remain; **control-api `executeMigrate`/`run-migration` bodies 0 lines changed** (no apply-path drift); deferred migration-tools apply-vocab untouched. Pre-existing concurrent-only flakes in `control-api/client.test.ts` (also seen on TML-2780) — not introduced here. -- **#1 reviewer focus — faithfulness depth (D-MS6).** The show path is a *new* `executeMigrateShowCommand` in `commands/migrate.ts` that calls `graphWalkStrategy()` directly; the control-api `executeMigrate` was NOT refactored to share a seam. The faithfulness test proves `graphWalkStrategy` is *called*, but not that it's called with the **same inputs/config** `executeMigrate` uses (aggregate, currentMarker, target-hash resolution, required invariants, per-space policy). Verify the previewed path is guaranteed identical to what real `migrate` would run — if the input assembly diverges, the preview can lie (the exact failure D-MS4/D-MS6 guard against). If it diverges, the fix is a shared `previewMigrationPath` helper both call (the spike's optional wrapper) or aligning the inputs. -- Also confirm: the `@db`-with-no-connection edge case has a clear error (D2's negative test was `@contract`-without-hash; the `@db` offline path is handled in the command — verify it's covered). - -## Orchestrator notes (R9 — green branch + alignment + message) - -- Operator's 3 tweaks: (1) on-path lane/branch glyphs GREEN (one-line in the `PATH_HIGHLIGHT_STYLES.onPath` seam, `forcedGreen`); (2) run-list `→` aligned with the graph `→` (list left-padded by `globalMaxEdgeTreePrefixWidth` + shared source-hash padding — verified both at column 42); (3) summary+header collapsed to `The following N migration(s) will run:`. Commit `2c827e7de`. -- The R8 test `off-path cells … no rotation codes` was reconciled: the on-path green branch is a CONTINUOUS green `│` that legitimately passes through off-path node rows; the assertion now checks the off-path node-marker + hash are dim (not the whole line). Green pass-through allowed; off-path own cells still dim, no red/magenta. -- Verification: implementer force-rendered the `--to 6cee614` topology (green on-path `│`, dim off-path node, aligned `→`, consolidated message). **Full cli suite green: 1232/1232, zero timeout flakes** (the earlier 500ms-timeout failures were machine load from parallel agents, not regressions). Typecheck clean. (No separate opus round this turn — 3 small force-render-verified tweaks + full green suite + operator is the visual verifier.) diff --git a/projects/migration-graph-rendering/slices/migrate-show-preview/plan.md b/projects/migration-graph-rendering/slices/migrate-show-preview/plan.md deleted file mode 100644 index f97b17fd48..0000000000 --- a/projects/migration-graph-rendering/slices/migrate-show-preview/plan.md +++ /dev/null @@ -1,33 +0,0 @@ -## Dispatch plan - -_Slice: `migrate-show-preview`. Four sequential dispatches, one PR (commit-per-dispatch). The `@`-vocabulary splits into two surfaces — render (D1) and reference grammar (D2) — because they're different code with different tests; the command then builds on both (D3 list, D4 graph). D4 has a non-linear hand-off: it builds on D3's on-path set **and** D1's relabelled overlay._ - -### Dispatch 1: Render-vocabulary relabel — reserved markers draw as `@db`/`@contract` - -- **Outcome:** The shared Tier-3 overlay draws reserved markers as `@contract`/`@db` (sigil, no angle brackets); user refs keep parens. The `--legend` example markers + explanatory text use the `@`-form and note they're typeable `--from`/`--to` tokens. `graph`/`status`/`list` snapshots are regenerated and consistent. `tsc` + cli tests green. -- **Builds on:** The spec's chosen design (D-MS7). -- **Hands to:** A green tree where every command rendering the shared overlay/legend speaks `@db`/`@contract` — the visual vocabulary D4 will highlight on. -- **Focus:** the marker-name styling in `migration-list-styler.ts` (the `<…>` branch, `:91-94`/`:19`) → `@`-prefix, drop brackets; `overlayNamesForContract` / styler in `migration-graph-tree-render.ts`; `formatLegendExampleMarkers` (`:744`) + the legend text in `renderMigrationGraphLegend` (`:765`). **Out:** the reference-grammar tokens (D2), `migrate --show` (D3/D4). **Gate:** `pnpm typecheck`; `pnpm --filter @prisma-next/cli test` green with `graph`/`status`/`list` snapshots updated; `rg "green\('<'\)|<\$\{names" packages/1-framework/3-tooling/cli/src/utils/formatters` shows no reserved-marker angle-bracket rendering remains. - -### Dispatch 2: `@db` + `@contract` reference-grammar tokens - -- **Outcome:** The contract-reference grammar accepts `@db` and `@contract` anywhere `--from`/`--to` are accepted. `@contract` resolves **offline** to the working/desired contract hash; `@db` resolves to the **live DB marker** (connection-required) — returned as a connection-requiring result the CLI layer resolves via `readAllMarkers()`, with a clear "needs a connection" error when offline. Unit tests cover both, plus the offline-`@db` error. `tsc` + cli tests green. -- **Builds on:** Independent of D1 (different surface); the spec's D-MS5. -- **Hands to:** `@db`/`@contract` are first-class reference inputs with a resolution helper any command can call — the explicit-`--from` path D3 wires. -- **Focus:** `parseContractRef` (`migration-tools/src/refs/contract-ref.ts`) recognising the `@`-sigil tokens (implementer picks the seam — parse-level vs context-injected reserved refs); the offline `@contract` resolution; the `@db` connection-requiring result + a shared CLI resolver helper. **Out:** rendering (D1); the `migrate --show` command (D3). **Gate:** `pnpm typecheck`; new `parseContractRef`/resolver unit tests green (positive `@contract`, positive `@db`-with-marker, negative `@db`-no-connection); `pnpm --filter @prisma-next/cli test`. - -### Dispatch 3: `migrate --show` — read-only path compute + ordered execution list - -- **Outcome:** `migrate --show [--from ] [--to ]` works as a **read-only** preview: default from-state = the live marker (`readAllMarkers`, read-only), explicit `--from X` = offline hypothetical (incl. `@db`/`@contract` via D2); the path is computed by `graphWalkStrategy()` — the same seam `migrate` uses — and the command **returns before `runMigration()`** (no writes). It prints the **linear, ordered list** of migrations that will run. Edge cases handled: no path, already-at-target (empty), multi-space, `@db`-with-no-connection. `tsc` + cli tests green. -- **Builds on:** Dispatch 2's `@db`/`@contract` resolver (for explicit `--from`); the planner seam confirmed by the spike (`graphWalkStrategy` / `runMigration` boundary). -- **Hands to:** A working `migrate --show` that prints the read-only ordered list and exposes the **on-path migration-hash set** — the input D4 highlights. -- **Focus:** `--show`/`--from` flags in `createMigrateCommand`; making the DB connection optional when `--from` is explicit; the read-only compute path (`readAllMarkers` → resolve from-state → `graphWalkStrategy` → stop before `runMigration`); the ordered-list formatter + `--json` parity if the family convention requires it; the four edge cases. **Out:** the graph visualization (D4). **Gate:** `pnpm typecheck`; `pnpm --filter @prisma-next/cli test`; new coverage proving (a) **read-only** — no path reaches `runMigration`/marker/ledger writes in `--show`, and (b) **faithfulness** — the path is computed via `graphWalkStrategy` (same seam as apply), asserted at the call site. - -### Dispatch 4: `migrate --show` graph visualization — green path + dimmed off-path - -- **Outcome:** `migrate --show` renders the Tier-3 graph with the chosen path (from-state → target) highlighted **bright green**, off-path nodes **dimmed and unlabelled**, and only path migrations named — alongside D3's ordered list. The graph shows the `@db`/`@contract` markers (D1's vocabulary). `tsc` + cli tests green; a snapshot/e2e fixture captures the highlighted output. -- **Builds on:** Dispatch 3's on-path migration-hash set **and** Dispatch 1's relabelled overlay (non-linear: needs both, not just D3). -- **Hands to:** The slice-DoD — `migrate --show` delivers the full two-part artifact (green-path graph + ordered list), faithfully and read-only, in the unified `@`-vocabulary. Ready for PR. -- **Focus:** extend the renderer's annotation mechanism (`MigrationEdgeAnnotation` / `edgeAnnotationsByHash`, `migration-graph-tree-render.ts:46-53,452`) with an on-path **highlight** annotation (green) + an **off-path dim** mode; wire the command to pass the on-path set + dim the rest; a fixture exercising the worked example (DB one migration behind target). **Out:** any new path-finding (reuse D3's); behavioural change to `migrate`. **Gate:** `pnpm typecheck`; `pnpm --filter @prisma-next/cli test` with the `--show` graph snapshot green; reviewer confirms the render reuses the existing annotation plumbing (no parallel renderer). - -_Dispatch-INVEST: D1 is a mechanical relabel + snapshot regen (one outcome, binary grep/snapshot gates). D2 is a single grammar addition with unit-level gates. D3 is a single-package feature (the read-only preview + list) with read-only + faithfulness as named, testable gates. D4 is a bounded renderer extension reusing existing annotation plumbing. Each is Small (fits one session, references scoped to the named files) and Independent given the D1→D2→D3→D4 hand-offs (D4's dual dependency surfaced). Total 4 ≤ 10. Ships as one PR, four commits._ diff --git a/projects/migration-graph-rendering/slices/migrate-show-preview/spec.md b/projects/migration-graph-rendering/slices/migrate-show-preview/spec.md deleted file mode 100644 index f206c19435..0000000000 --- a/projects/migration-graph-rendering/slices/migrate-show-preview/spec.md +++ /dev/null @@ -1,94 +0,0 @@ -# Slice: `migrate --show` — preview the path and ordered migrations `migrate` will run - -_Parent project `projects/migration-graph-rendering/`. Outcome: a user about to run `migrate` can first ask **"what will this do?"** and get a faithful, graph-shaped answer — building the mental model that the system is a contract graph, not a linear stack._ - -## At a glance - -`migrate --to ` today just applies — there's no way to preview the path it will take. This slice adds `migrate --show`: a **read-only** preview that runs `migrate`'s own path-finder from the live DB marker (default) to the target, then renders (1) the Tier-3 graph with the chosen path highlighted bright green and off-path nodes dimmed, and (2) a linear, ordered list of the migrations that will execute. It supersedes TML-2771's proposed `migration path --from --to` read command (see `decisions.md` § `migrate --show`, D-MS1). - -## Chosen design - -`migrate --show [--from ] [--to ] [--db ]` — a preview qualifier on `migrate`; never writes. - -**From-state (D-MS4):** -- Default (`--from` omitted): **read the live DB marker, read-only** (requires `--db`), so the preview starts from the *exact* state the real `migrate` would. No write ⇒ "no impact." -- Explicit `--from X`: a **labelled offline hypothetical** ("if you were at X…"), no connection. `X` accepts the existing contract-reference grammar. -- **`--from`/`--to` are app-space-scoped.** They override only the **app** member's from-state / target. **Extension** members are always previewed exactly as real `migrate` plans them — from their own live marker (or greenfield when offline) to their own head ref — never retargeted by the app-space `--from`/`--to`. (Feeding the app refs to an extension is the bug behind "No migration path … in space pgvector".) - -**Live-marker token (D-MS5) — decided `@db`, plus symmetric `@contract` (D-MS7):** add reserved reference tokens **`@db`** and **`@contract`** to `parseContractRef` (`migration-tools/src/refs/contract-ref.ts`), usable anywhere the contract-reference grammar is accepted — so `migrate --show --from @db --to prod` is the explicit form of the default. `@db` = "the live DB marker," resolved via `readAllMarkers()` (**requires a connection**); `@contract` = "the working/desired contract the app carries" (**offline-resolvable**; also `migrate --to`'s implicit default). The spike confirmed `db` is **not** a `--from` resolver token today (it exists only as the file-backed `db` ref `refs/db.json` and the renderer's `DB_MARKER_NAME='db'` label), so the `@`-sigil introduces **no collision and needs no rename** — the parser distinguishes it on the first character. - -**Render-vocabulary unification (D-MS7):** the shared Tier-3 overlay draws the reserved markers with the **`@`-sigil — `@contract @db` — dropping the angle-bracket form** `` (`migration-list-styler.ts:91-94`); user refs keep parens. The **`--legend`** output moves in lockstep: its example markers (`formatLegendExampleMarkers`, `migration-graph-tree-render.ts:744`) drop the `<…>` form, and its explanatory text teaches `@db`/`@contract` *and* that they're typeable `--from`/`--to` tokens. Both overlay and legend are shared by `graph` / `status` / `list` (legend via `utils/legend.ts`), so the change ships as this slice's **vocabulary-foundation dispatch** ahead of the preview — the preview can't render `@db`-highlighted while siblings still print or legend `` (snapshot regen across all three). - -**Output (D-MS3, revised per operator visual review):** two artifacts, one invocation: -1. The **Tier-3 graph tree** (shared renderer with `graph`/`status`), rendering the **whole** graph — nothing omitted. **Colour model — path-highlight mode is a DISTINCT colour scheme, not an overlay on the normal one.** In this mode the renderer classifies every glyph/migration/node as **on-path** or **off-path** — it does **NOT** use the normal by-branch rotating-colour logic (`LANE_COLOR_CYCLE`); branch rotation is **suppressed entirely**. There are exactly **two styles, defined in ONE place** (so the on-path/off-path colours are trivially tweakable in future): - - **On-path:** migration name **white/bold**, contract hashes the neutral single-path colour (cyan, same as a single-path section like `pgvector`), and the **on-path lane/branch glyphs (`│ ↑` + on-path node markers + the on-path trunk's own `├` tee) GREEN** — the green branch traces the path. (Name/hashes are neutral; only the branch is green.) **In merge/branch connector rows (`├─╯`), the bridging dash `─` and the corner `╯` that lead to an OFF-path branch are DIM (grey), not green — green is only the on-path trunk's own glyph.** - - **Off-path = dimmed grey** on every cell (marker, hash, name, edge `from → to` hashes incl. destination, lane segments). - Classification is on-path/off-path **per glyph**, following the actual traversed path — never by branch column index. Marker placement: **`@contract` marks the app's working-contract node and is rendered only in the app space — never in an extension space**; `@db` marks the live-marker node; the `--to` ref marks its own node in parens. (`@contract` is the working contract, *not* the `--to` target.) -2. A **linear, ordered list** of the migrations that will execute — rendered through the **IDENTICAL on-path row renderer as the tree's migration rows** (same code path / on-path style), with the graph gutter glyphs omitted. **Alignment anchor is the `→`/source-hash, NOT the name:** the list name sits at a normal **2-space indent**, and the name column is **padded wider** so the `∅ → hash` block (source hash onward) starts at the SAME absolute column as in the graph rows — i.e. every `→` in the whole output (graph + list) lines up, while the list name is at the 2-space indent (NOT pushed right by the gutter width). The source-hash column is padded (`padFromHashColumn`) so `∅`-source and full-hash-source rows put their `→` at the same column. Printed directly (no Clack `│` gutter). - - The summary + list header are **one consolidated line**: `The following migration(s) will run:` (NOT a separate "` migrations will run`" line plus a "`Will run, in order:`" header). **Column alignment is shared across ALL sections: the `app:` graph section, every extension graph section (e.g. `pgvector:`), AND this run-list must align their data columns (migration name, `from → to` hashes, `N ops`) to the same offsets** — compute the dirName/hash column widths GLOBALLY across all spaces and the list, not per-space. (Today each space self-aligns and the list uses a separate width → mismatch.) **The order MUST be the runner's canonical cross-space schedule order — extension spaces (alphabetically by space id) FIRST, then the app space — sourced from the SAME ordering mechanism the runner uses (`concatenateSpaceApplyInputs` / the runner's `applyOrder`), NOT reconstructed from a per-space loop.** The preview's run order must be byte-for-byte the order `migrate` actually applies (extensions before app) — this is part of the faithfulness constraint (D-MS6), not just presentation. Script/loop-friendly. - -**Faithfulness constraint (D-MS6) — confirmed by spike.** `migrate --show` computes its path through the **same seam the real `migrate` runs**: `readAllMarkers()` (read-only) for the from-state, then `graphWalkStrategy()` (`@prisma-next/migration-tools/aggregate`, `strategies/graph-walk.ts:51`) — a pure, no-write function that returns the ordered `PerSpacePlan` / `pathDecision.selectedPath`. The preview simply **stops before `runMigration()`** (the write boundary, `operations/migrate.ts:284`). No parallel reimplementation. The Tier-3 renderer is shared with `graph`/`status`. `status --from` already calls the same core path-finder (`findPathWithDecision`, `migration-graph.ts:300`) that `graphWalkStrategy` wraps, so the two commands are deterministically consistent **with no convergence refactor required** (an optional shared `previewMigrationPath` wrapper is a nicety, not a blocker). - -Worked sketch (DB one migration behind prod): - -``` -$ prisma-next migrate --show --to prod --db $DATABASE_URL - -app: - ○ a94b7b4 @contract ← app working contract (grey: off-path here) - │↑ add_posts ef9de27 → a94b7b4 1 ops ← GREEN: node, name, AND lane lines - ○ ef9de27 @db (prod) - │ old_change 3c1d0a2 → ef9de27 1 ops ← dim GREY: fully drawn, off-path - ○ 3c1d0a2 - ∅ - - add_posts ef9de27 → a94b7b4 1 ops ← list: graph row format, GREEN, no gutter -``` - -Green covers the whole on-path run (nodes, hashes, names, lanes); everything off-path is uniform grey but fully drawn. `@contract` sits on the working-contract node (app space only). Reserved markers use the `@`-sigil — the spelling you type into `--from`/`--to` (D-MS7), not ``. User refs keep parens (`(prod)`). The ordered list reuses the graph's migration-row formatter without the gutter, in the same green — not Clack `ui.log`. - -## Coherence rationale - -One command, one engine: add a read-only preview mode to `migrate` that reuses the planner seam and the Tier-3 renderer. The reference-grammar token (D-MS5) ships with it because the feature needs an explicit way to name the live marker. A reviewer holds "preview = run the planner, render the path, stop" in one sitting. - -## Scope - -**In:** `migrate --show` flag + read-only preview path; default `--from` = live marker (read-only); explicit `--from X` offline hypothetical; the `@db` + `@contract` reference tokens; the **render-vocabulary unification** (reserved markers draw as `@db`/`@contract`, angle brackets dropped) across the shared Tier-3 overlay **and the `--legend` output** (example markers + explanatory text) — including the resulting `graph`/`status`/`list` snapshot regen; the green-path/dim rendering; the ordered-execution list; `--json` parity for the ordered list if the family convention requires it. - -**Out:** -- Any change to `migrate`'s apply behaviour (preview is strictly additive + read-only). -- Refactoring `status --from` onto the shared seam **if** it already routes through it — touch only if it currently diverges (decide at dispatch time; may be a follow-up). -- `migration plan` (authoring) and `status`/`graph`/`log` semantics. -- Multi-path *comparison* / "show alternative routes" (a later exploration feature, not this sanity-check). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --- | --- | --- | -| No path from-state → target | Must render gracefully | Reuse the `status` no-path / wrong-grammar diagnostics; don't throw. | -| Already at target (empty path) | Show "nothing to run" | Green-highlight the single current node; empty ordered list. | -| Multi-space (`migrate` spans app + extensions) | Mirror `migrate`'s real behaviour | Render per-space path sections like `status`/`list` policy; the preview must match what the real multi-space `migrate` would do. | -| `--from db` (live token) with no `--db` connection | Structured error | "live marker needs a connection"; suggest an explicit `--from ` for offline. | - -## Slice-specific done conditions - -- [ ] `migrate --show` is read-only — preview returns before the `runMigration()` call (`operations/migrate.ts`); no marker/ledger mutation, no DDL reachable. -- [ ] The path is computed via `graphWalkStrategy()` (+ `readAllMarkers()` for the from-state), the same seam `migrate` uses — not a reimplementation; reviewer confirms from the call site. -- [ ] `@db` resolves to the live marker through `parseContractRef` (errors clearly with no connection); `@contract` resolves offline to the working contract; the distinction from the `db` ref is documented (glossary + help text). -- [ ] Reserved markers render as `@db`/`@contract` (no angle brackets) everywhere the shared Tier-3 overlay is used — `graph`/`status`/`list`/`migrate --show` — with snapshots regenerated and consistent. -- [ ] `--legend` reflects the new vocabulary: its example markers use `@db`/`@contract` (no `<…>`) and its text notes they're typeable `--from`/`--to` tokens; verified across `graph`/`status`/`list`. - -## Open Questions - -**None — all three closed by the planner-seam spike (2026-06-04).** - -1. ~~Clean read-only "compute path, don't execute" seam?~~ → **Yes.** `graphWalkStrategy()` returns the ordered `PerSpacePlan` with no writes; `runMigration()` (`operations/migrate.ts:284`) is the execution boundary the preview stops before. No extraction needed — stays a one-PR slice. -2. ~~Live-marker token spelling / `db`-ref collision?~~ → **`@db` sigil.** `db` is not a `--from` resolver token today, so there's no collision and no rename; add `@db` to `parseContractRef` (`refs/contract-ref.ts`), resolved via `readAllMarkers()`. -3. ~~Does `status --from` reimplement path-finding?~~ → **Shares the core.** `status --from` calls `findPathWithDecision` directly; `graphWalkStrategy` wraps the same function, so `migrate --show` is deterministically consistent with both `migrate` and `status`. No convergence refactor required. - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md`; decision: `decisions.md` § `migrate --show` (D-MS1–D-MS6) -- Linear issue: TML-2771 (re-titled to `migrate --show`) -- Glossary: `docs/glossary.md` § Migrate (verb) / Marker / Ref / Contract Reference grammar diff --git a/projects/migration-graph-rendering/slices/migration-fixture-audit/code-review.md b/projects/migration-graph-rendering/slices/migration-fixture-audit/code-review.md deleted file mode 100644 index dfa09f7a57..0000000000 --- a/projects/migration-graph-rendering/slices/migration-fixture-audit/code-review.md +++ /dev/null @@ -1,146 +0,0 @@ -# Code review — migration-fixture audit - -**Commit:** `835edccad72ef95bb63d168a28afcf8ad51226a0` on `regenerate-diamond-migration-fixture` -**Verdict:** **SATISFIED** — the ten redundant topology-only fixtures are deleted, the seven-fixture set is exactly as specified, the five regenerated survivors each render (tree + default) and pass `migration check`, topologies match the intended shapes, and the load-bearing multi-branch case is genuinely four distinct migration packages sharing `from`/`to` with distinct `migrationHash` (rendered as four parallel edges, no collision). `showcase`, `diamond`, the main config, CLI/packages, and `test-graphs.ts` are untouched. - -## Scoreboard - -| AC | Result | Evidence | -|---|---|---| -| 1. Deletions | **PASS** | All 10 dirs gone from the commit tree (`linear`, `single-branch`, `sub-branches`, `rollback`, `rollback-continue`, `multi-rollback-branch`, `diamond-sub-branch`, `complex`, `kitchen-sink`, `sequential-diamonds`) — confirmed via `git ls-tree -d 835edccad`. Remaining `migration-fixtures/` on disk = exactly {`converging-branches`, `diamond`, `long-spine`, `multi-branch`, `showcase`, `skip-rollback`, `wide-fan`} (7). `multi-branch` shows D entries only because it is a *regenerated survivor* (old topology-only files removed, full packages added) — the directory persists. | -| 2. Untouched | **PASS** | `git show --name-only 835edccad` matches none of: `prisma-next.config.ts`, `migration-fixtures/showcase`, `migration-fixtures/diamond/`, `test-graphs`, `packages/`, `cli/`. | -| 3. Renders + checks | **PASS** | All 5 `migration check` → `{ ok: true, failures: [], "All checks passed" }`. All 5 `--tree --format pretty` and default `--format pretty` render with no errors (trees below). | -| 4. Topology fidelity | **PASS** | wide-fan: init + 5 siblings off C1 (7 nodes/6 edges). converging-branches: 3 siblings off C1, 3 merges → union `042e80c`, `prod`→`042e80c` (6/7). skip-rollback: spine init→phone→bio→posts; `rollback_to_phone` `7e951c7→93be6c2` (existing add_phone node), `rollback_to_init` `827997c→789dd79` (existing init node) — crossing back-edges, no new leaves (5/6). long-spine: 7-node spine + 2 children off `add_tags` (`99de8c2`); `staging`→`6c66c89`, `prod`→`2636edd` (10/9). multi-branch: 3-way fork off C1 + `add_bio` off add_phone + 4 parallel edges; `feature`/`prod`/`staging` refs (7/9). | -| 5. Internal consistency | **PASS** | Spot-confirmed long-spine: every node's `end-contract.json` storageHash == its `migration.json` `to` (init `789dd79` … add_everything `2636edd`, all 9 match). The `migration check` pass covers this for all 5. | -| 6. multi-branch parallel edges | **PASS** (load-bearing) | The 4 packages `parallel_a..d` all carry `from sha256:827997c…` and `to sha256:d11106e…` (identical) but distinct `migrationHash` (`4cd497e…`, `c2d8545…`, `fdce4b9…`, `0e0ac47…`). The renderer draws 4 parallel edges into `d11106e`; `migration check` passes with no collision/dup-identity error. | -| 7. No remnants | **PASS** | No `pgvector`/`"vector"` refs in any survivor; all `extensionPacks` are `{}` (69×); all op target schemas are `__unbound__` (43×, app-space). No `_emit_scratch`/scratch/`.tmp`/emit-out files in the commit or untracked in the demo worktree. Survivors regenerated offline, so no old synthetic-hash mismatches (check pass corroborates). | - -## Per-fixture configs (all mirror the showcase/diamond pattern) - -| Fixture | contract source | migrations.dir | -|---|---|---| -| wide-fan | `./wide-fan-contract/settings.prisma` | `./migration-fixtures/wide-fan` | -| converging-branches | `./converging-branches-contract/union.prisma` | `./migration-fixtures/converging-branches` | -| skip-rollback | `./skip-rollback-contract/c1.prisma` | `./migration-fixtures/skip-rollback` | -| long-spine | `./long-spine-contract/everything.prisma` | `./migration-fixtures/long-spine` | -| multi-branch | `./multi-branch-contract/target.prisma` | `./migration-fixtures/multi-branch` | - -## `--tree` renders - -### wide-fan (7 nodes / 6 edges) -``` -* 93be6c2 -|^ 20260302T1000_add_phone 789dd79 -> 93be6c2 -| * afdcd8e -| |^ 20260302T1100_add_posts 789dd79 -> afdcd8e -| | * 7e3fa7f -| | |^ 20260302T1200_add_avatar 789dd79 -> 7e3fa7f -| | | * 2796854 -| | | |^ 20260302T1300_add_category 789dd79 -> 2796854 -| | | | * f224748 (contract) -| | | | |^ 20260302T1400_add_settings 789dd79 -> f224748 -+-+-+-+-/ -* 789dd79 -|^ 20260301T1000_init - -> 789dd79 -- -7 node(s), 6 edge(s) -``` - -### converging-branches (6 nodes / 7 edges) -``` -* 042e80c (prod, contract) -+-+-\ -|^| | 20260303T1000_merge_phone 93be6c2 -> 042e80c -| |^| 20260303T1100_merge_posts afdcd8e -> 042e80c -| | |^ 20260303T1200_merge_avatar 7e3fa7f -> 042e80c -* | | 93be6c2 -|^| | 20260302T1000_add_phone 789dd79 -> 93be6c2 -| * | afdcd8e -| |^| 20260302T1100_add_posts 789dd79 -> afdcd8e -| | * 7e3fa7f -| | |^ 20260302T1200_add_avatar 789dd79 -> 7e3fa7f -+-+-/ -* 789dd79 -|^ 20260301T1000_init - -> 789dd79 -- -6 node(s), 7 edge(s) -``` - -### skip-rollback (5 nodes / 6 edges) -``` -*-\ 7e951c7 -| |v 20260305T1000_rollback_to_phone 7e951c7 -> 93be6c2 -|^| 20260304T1000_add_posts 827997c -> 7e951c7 -*-+-\ 827997c -| | |v 20260306T1000_rollback_to_init 827997c -> 789dd79 -|^| | 20260303T1000_add_bio 93be6c2 -> 827997c -* 93be6c2 -*<--/ 789dd79 (contract) -|^ 20260301T1000_init - -> 789dd79 -- -5 node(s), 6 edge(s) -``` - -### long-spine (10 nodes / 9 edges) -``` -* 6c66c89 (staging) -|^ 20260307T1100_late_branch 99de8c2 -> 6c66c89 -| * 2636edd (prod, contract) -| |^ 20260308T1000_add_everything 99de8c2 -> 2636edd -+-/ -* 99de8c2 -|^ 20260307T1000_add_tags b34dc91 -> 99de8c2 -* b34dc91 -|^ 20260306T1000_add_comments 47f4a4f -> b34dc91 -* 47f4a4f -|^ 20260305T1000_add_avatar 7e951c7 -> 47f4a4f -* 7e951c7 -|^ 20260304T1000_add_posts 827997c -> 7e951c7 -* 827997c -|^ 20260303T1000_add_bio 93be6c2 -> 827997c -* 93be6c2 -|^ 20260302T1000_add_phone 789dd79 -> 93be6c2 -* 789dd79 -|^ 20260301T1000_init - -> 789dd79 -- -10 node(s), 9 edge(s) -``` - -### multi-branch (7 nodes / 9 edges) -``` -* d11106e (contract) -|^ 20260304T1000_parallel_d 827997c -> d11106e -|^ 20260304T1000_parallel_c 827997c -> d11106e -|^ 20260304T1000_parallel_b 827997c -> d11106e -|^ 20260304T1000_parallel_a 827997c -> d11106e -* 827997c (feature) -|^ 20260303T1000_add_bio 93be6c2 -> 827997c -* 93be6c2 (prod) -|^ 20260302T1000_add_phone 789dd79 -> 93be6c2 -| * afdcd8e (staging) -| |^ 20260302T1100_add_posts 789dd79 -> afdcd8e -| | * 7e3fa7f -| | |^ 20260302T1200_add_avatar 789dd79 -> 7e3fa7f -+-+-/ -* 789dd79 -|^ 20260301T1000_init - -> 789dd79 -- -7 node(s), 9 edge(s) -``` - -## multi-branch parallel-edge analysis (the load-bearing risk) - -| Package | from | to | migrationHash | -|---|---|---|---| -| parallel_a | `827997c…` | `d11106e…` | `4cd497ed…` | -| parallel_b | `827997c…` | `d11106e…` | `c2d85458…` | -| parallel_c | `827997c…` | `d11106e…` | `fdce4b93…` | -| parallel_d | `827997c…` | `d11106e…` | `0e0ac470…` | - -Identical `from`/`to` contract endpoints, four distinct `migrationHash` values → four distinct migration packages along the same contract transition. The renderer materialises this as four parallel edges into `d11106e`, and `migration check` reports `ok: true` — the read/check path tolerates same-endpoint multiplicity without a dup-identity collision. Risk cleared. - -## Findings - -- No blocking issues. No fixtures/source modified during review (only this `code-review.md` written). -- `multi-branch` legitimately appears with both deletions and additions in `git show --stat`: it is a regenerated survivor (old topology-only `migration.json`/`ops.json` removed, full packages + `start-contract`/`end-contract` added), not one of the 10 removed fixtures. Directory still present on disk. diff --git a/projects/migration-graph-rendering/slices/migration-fixture-audit/reorg-spec.md b/projects/migration-graph-rendering/slices/migration-fixture-audit/reorg-spec.md deleted file mode 100644 index b71d5abbc8..0000000000 --- a/projects/migration-graph-rendering/slices/migration-fixture-audit/reorg-spec.md +++ /dev/null @@ -1,58 +0,0 @@ -# Addendum spec: reorganize fixtures into `fixtures//{contract, migrations/}` - -## Problem - -The regenerated fixtures scattered assets across the demo root: -`migration-fixtures//app/…`, a separate top-level `-contract/` dir -per fixture, and a top-level `prisma-next..config.ts` per fixture. The -proliferation of `*-contract/` dirs is the specific complaint. - -## Target layout (every fixture, self-contained) - -``` -examples/prisma-next-demo/fixtures// - prisma-next.config.ts # contract: './contract.prisma'; migrations: { dir: './migrations' } - contract.prisma # the single head contract the config references - migrations/ - app/ - - refs/ -``` - -Render/QA each via: `prisma-next migration graph --config ./fixtures//prisma-next.config.ts`. - -Applies to **all 7** fixtures: `showcase`, `diamond`, `wide-fan`, -`converging-branches`, `skip-rollback`, `long-spine`, `multi-branch`. - -## Verified facts (config paths are config-relative) -- `resolveMigrationPaths` resolves `migrations.dir` against `resolve(configOption, '..')` (the config file's dir). -- `finalizeConfig` resolves `contract` source/output against the config file's dir. -So a config inside `fixtures//` using `./contract.prisma` + `./migrations` resolves correctly. - -## Moves (use `git mv` to preserve history / show as renames) -For each fixture ``: -1. `migration-fixtures//app` → `fixtures//migrations/app` (carries the node dirs + `app/refs/`). -2. The single head `.prisma` (the one the old config referenced) `-contract/.prisma` → `fixtures//contract.prisma`. -3. Author `fixtures//prisma-next.config.ts` (port `db.connection` from the old config) with `contract: './contract.prisma'` and `migrations: { dir: './migrations' }`. - -## Deletions -- The whole `examples/prisma-next-demo/migration-fixtures/` tree (after moves). -- All `examples/prisma-next-demo/*-contract/` dirs (after moving the one head `.prisma` each). -- All top-level `examples/prisma-next-demo/prisma-next..config.ts` (the 7 fixture configs). -- **Do NOT touch** the main `examples/prisma-next-demo/prisma-next.config.ts`. - -## Showcase consistency -`showcase` currently keeps emitted `showcase.json`/`.d.ts` and per-node -`start-contract.*`. For uniformity, slim it like the others: keep only -`fixtures/showcase/contract.prisma`, drop the emitted contract `.json`/`.d.ts`, -and remove per-node `start-contract.*`. **Verify** showcase still renders its -special shapes (forward cross-link, self-edge, disjoint cycle) and passes check. - -## References to update -- The usage comment in the showcase config (`--config ./prisma-next.showcase.config.ts` → `--config ./fixtures/showcase/prisma-next.config.ts`). -- Sweep docs/README for any reference to the old paths (`migration-fixtures/`, `*-contract/`, `prisma-next..config.ts`) and update. - -## Done when -- The 7 fixtures live under `fixtures//{prisma-next.config.ts, contract.prisma, migrations/app/…}`; no `migration-fixtures/`, no `*-contract/`, no top-level fixture configs remain. -- Each renders (default + `--tree`) and passes `migration check` via its new config, with identical node/edge counts to before. -- Main demo config untouched; history preserved via `git mv`. diff --git a/projects/migration-graph-rendering/slices/migration-fixture-audit/spec.md b/projects/migration-graph-rendering/slices/migration-fixture-audit/spec.md deleted file mode 100644 index bcad510160..0000000000 --- a/projects/migration-graph-rendering/slices/migration-fixture-audit/spec.md +++ /dev/null @@ -1,101 +0,0 @@ -# Slice: Audit & consolidate the demo migration fixtures - -## Problem - -`examples/prisma-next-demo/migration-fixtures/` holds 17 fixtures. They are -manual QA scenarios for the `migration graph` renderer (no test wires them). -Two issues: - -1. **15 of them are topology-only** (`migration.json` + `ops.json`, synthetic - hashes, no `end-contract.*`), so they are **unrenderable** — pointing the CLI - at them throws "missing destination contract snapshot" (the state `diamond` - was in before it was regenerated). Only `showcase` and `diamond` are complete. -2. **Heavy scenario overlap.** Now that the renderer + the comprehensive - `showcase` fixture exist, most single-shape fixtures are redundant: trivial - ones are strict subsets of larger ones (`linear ⊂ long-spine`, - `single-branch ⊂ multi-branch`, `rollback ⊂ skip-rollback`), and the - "combination" fixtures (`complex`, `kitchen-sink`, `diamond-sub-branch`, - `multi-rollback-branch`, `sequential-diamonds`) are subsumed by `showcase`. - -## Decision - -Keep a minimal set where each fixture isolates **one** distinct renderer -challenge, and make every kept fixture **renderable** (regenerated offline like -`diamond`). Delete the redundant ones. - -### Keep — already complete, leave as-is -- `showcase` — the all-in-one (only fixture with forward cross-link, self-edge, disjoint cycle). -- `diamond` — clean diamond divergence/convergence (regenerated in the prior commit). - -### Keep — regenerate to be renderable (offline `migration plan`, no DB; full showcase-style artifacts + per-fixture `prisma-next..config.ts`) -- `wide-fan` — one node → 5 children (fan-out width). -- `converging-branches` — 3 branches fan **in** to one node (N-way convergence). -- `skip-rollback` — node-**skipping** rollbacks ⇒ crossing back-arcs. -- `long-spine` — long vertical spine + late branch (height). -- `multi-branch` — 3-way fork + parallel edges (same `from`→`to`). **Caveat below.** - -### Delete (10) -`linear`, `single-branch`, `sub-branches`, `rollback`, `rollback-continue`, -`multi-rollback-branch`, `diamond-sub-branch`, `complex`, `kitchen-sink`, -`sequential-diamonds`. - -## Target topologies for the regenerated survivors - -Reproduce these **shapes** (node dir names + the `from→to` edge structure + -refs). Hashes will be freshly generated and differ from the old synthetic ones — -expected. Use a simple accreting schema (one `user` table; each `add_` adds a -nullable column ``; rollbacks remove columns; convergence = the union), -mirroring the `diamond` approach. No extensions. - -- **wide-fan**: `init`→C1; then 5 siblings off C1: `add_phone`, `add_posts`, - `add_avatar`, `add_category`, `add_settings` (each C1 + one column). refs: none. -- **converging-branches**: `init`→C1; 3 siblings off C1 (`add_phone`, - `add_posts`, `add_avatar`); then `merge_phone`/`merge_posts`/`merge_avatar` - each → the **same** union contract (C1+phone+posts+avatar). refs: `prod`. -- **skip-rollback**: spine `init→add_phone→add_bio→add_posts`; then - `rollback_to_phone` (`add_posts`→ the `add_phone` contract) and - `rollback_to_init` (`add_bio`→ the `init` contract) — both land on existing - node hashes, producing skip/crossing back-arcs. refs: none. -- **long-spine**: spine `init→add_phone→add_bio→add_posts→add_avatar→ - add_comments→add_tags`; then two children off `add_tags`: `late_branch` and - `add_everything`. refs: `prod`, `staging`. -- **multi-branch**: `init`→C1; 3 siblings off C1 (`add_phone`, `add_posts`, - `add_avatar`); `add_bio` off `add_phone`; then parallel edges off `add_bio` - (the original had 4 edges with identical `from`/`to` and duplicate names). - refs: `feature`, `prod`, `staging`. - -## Caveat — `multi-branch` parallel edges - -The offline planner produces a package keyed by its content; **four migrations -with identical `from`/`to` and identical ops would collide** (same migration -hash), so the real pipeline likely cannot reproduce genuine parallel edges. The -implementer must **attempt** it and, if the pipeline cannot create distinct -parallel edges between the same two nodes, **STOP and report** rather than -fabricate invalid packages. Options to decide then: hand-author the parallel -packages, approximate, or drop `multi-branch` (the in-memory `parallelEdges` -graph in `test-graphs.ts` already covers parallel-edge rendering for tests). - -## Done when - -- The 10 listed fixtures are deleted. -- Each regenerated survivor renders via its config - (`prisma-next migration graph --config ./prisma-next..config.ts`, default - + `--tree`) with no errors, and `migration check` passes for it (every - `end-contract` storageHash matches its `migration.json` `to`). -- `showcase` and `diamond` untouched; main `prisma-next.config.ts` untouched. -- No extension-space packages; no synthetic remnants in survivors. - -## Scope - -**In:** `examples/prisma-next-demo/migration-fixtures/**` (delete + regenerate), -new `examples/prisma-next-demo/-contract/**` sources, new -`examples/prisma-next-demo/prisma-next..config.ts` files. -**Out:** CLI/package source, `showcase`, `diamond`, main demo config, the -lane-colors PR (#674), `test-graphs.ts`. - -## Notes - -- Branch: continue on `regenerate-diamond-migration-fixture` (PR #677 broadens - from "regenerate diamond" to "audit & consolidate fixtures"). -- Generated dirs get a "now" timestamp prefix; rename to the canonical - `YYYYMMDDT…_slug` (identity is content-hash based, safe to rename). diff --git a/projects/migration-graph-rendering/slices/migration-graph-space-flag/spec.md b/projects/migration-graph-rendering/slices/migration-graph-space-flag/spec.md deleted file mode 100644 index 2a2654e95d..0000000000 --- a/projects/migration-graph-rendering/slices/migration-graph-space-flag/spec.md +++ /dev/null @@ -1,67 +0,0 @@ -# Slice: `migration graph` multi-space (all spaces by default, `--space ` to narrow) - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to the project's purpose: `migration graph` renders only the **app** contract space today, while `migration list` enumerates **all** on-disk spaces. This slice makes the read commands consistent — `graph` draws **every** on-disk contract space as a disconnected per-space tree by default, with `--space ` to narrow to one — matching `migration list`'s existing behaviour._ - -## At a glance - -``` -$ prisma-next migration graph -app: -○ 3b2d98d (contract) -│↑ add_phone ef9de27 → 3b2d98d -○ ef9de27 -│↑ init ∅ → ef9de27 -○ ∅ - -supabase-auth: -○ 9f2a1c0 -│↑ add_session 3bfce91 → 9f2a1c0 -○ 3bfce91 -│↑ init ∅ → 3bfce91 -○ ∅ -``` - -``` -$ prisma-next migration graph --space supabase-auth -○ 9f2a1c0 -│↑ add_session 3bfce91 → 9f2a1c0 -○ 3bfce91 -│↑ init ∅ → 3bfce91 -○ ∅ -``` - -By default `migration graph` draws **all** on-disk contract spaces, each as its own disconnected tree under a `spaceId:` heading (spaces are independent histories — there is no cross-space topology). `--space ` narrows to a single space. This mirrors `migration list`, which already enumerates all spaces with `--space` to narrow. - -## Chosen design - -The space policy is **all-spaces-disconnected by default, `--space ` narrows** (the read-command-consistency decision — same shape `migration list` already implements). `migration graph` loads `aggregate.app.graph()` today — hard-wired to the app space. This slice: - -- **Enumerates every on-disk space** (the same enumeration `migration list` uses — `migrationSpaceListEntriesFromAggregate` / `aggregate.space(id)`), rendering each space's `graph()` as its own tree under a `spaceId:` heading. Headings appear only when more than one space is present (matching the list renderer's `multiSpace` rule). -- **`--space `:** render only the named space's tree, no heading. Unknown space id ⇒ a clear, listing error (enumerate the available space ids); invalid id ⇒ the existing `errorInvalidSpaceId`. -- The renderer itself is space-agnostic — it already consumes a single `MigrationGraph`. The work is per-space iteration + heading composition + `--space` resolution, reusing `migration list`'s space-enumeration and error helpers (`isValidSpaceId`, `errorSpaceNotFound`). - -## Scope - -**In:** - -- All-spaces-by-default rendering on `migration graph` (per-space trees, `spaceId:` headings when multi-space). -- `--space ` flag (human + `--json`/`--dot` route through the selected space; multi-space JSON/DOT keys output by space id). -- Space resolution + unknown/invalid-id errors, reusing the `migration list` helpers. -- Help text / examples; tests across multi-space default, single `--space`, and unknown-space error. - -**Out:** - -- Cross-space edges / a unified multi-space topology (spaces are independent histories — disconnected trees only). -- The Tier-3 renderer's per-tree layout — untouched. - -## Open Questions - -1. **Flag spelling / value.** `--space ` is the established spelling on `migration list`; reuse it verbatim for consistency. -2. **`--space` + the eventual default tree renderer.** This slice should land after `--tree` becomes the default (TML-2748) or be written against `--tree` explicitly; confirm sequencing at pickup. -3. **Multi-space `--json`/`--dot` shape.** Single-space keeps today's shape; multi-space needs a per-space keyed envelope. Settle the exact shape at pickup (e.g. `{ spaces: [{ spaceId, nodes, edges }] }`). - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md`. -- Predecessor slice that drops the old per-space graph view: `slices/remove-list-graph-renderer/spec.md` (TML-2765) — its Open Question #1 defers this work to here. -- Linear issue: _to be filed at pickup (standalone, related to TML-2765)._ diff --git a/projects/migration-graph-rendering/slices/read-command-consistency/code-review.md b/projects/migration-graph-rendering/slices/read-command-consistency/code-review.md deleted file mode 100644 index 30f4fa094a..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-consistency/code-review.md +++ /dev/null @@ -1,99 +0,0 @@ -# Code review — read-command consistency (TML-2801) - -> Reviewer maintains this across rounds. Orchestrator writes the scaffold + § Subagent IDs + § Orchestrator notes only. - -## Summary - -- **Current verdict:** SATISFIED -- **Dispatches SATISFIED:** D1–D7 (all) -- **AC scoreboard totals:** 7 PASS / 0 FAIL / 0 NOT VERIFIED -- **Open findings:** 1 (F-1, should-fix — non-blocking; correctness asymmetry in a new flag, no regression) -- **Open escalations:** 0 - -The cumulative diff faithfully delivers all seven dispatch outcomes. Production code is clean against repo rules: no new bare `as`, no `any`, no zod, no needless comments, no cross-file reexport violations (the `CheckFailure` re-export is from an `exports`-style helper module and matches the pre-existing pattern). The new tests are real regression locks, not tautological. One should-fix correctness asymmetry (F-1) in the brand-new `--space` path is logged but does not block: it is a coverage gap in a new flag, not a regression, and the common (no-arg) path is fully covered. - -## Acceptance criteria scoreboard - -> One AC per dispatch outcome (plan.md). Update every round. - -| AC ID | Description (short) | Dispatch | Status | Evidence | -| ----- | ------------------- | -------- | ------ | -------- | -| AC-1 | All six read verbs emit `{ ok, … }` JSON with co-located exported schemas (log wrapped, graph extracted) | D1 (F3) | PASS | log `{ok:true,entries}` (migration-log.ts:42-45,135-139, incl. empty case); graph lifted to exported `MigrationGraphJsonResult` (migration-graph.ts:60-66,255-262); list/show/status/check already exported. Runtime locks: migration-log.test.ts:159-215 (real CLI, populated+empty), migration-show.test.ts:281-316 (real CLI), parity test §D1 lock. | -| AC-2 | `check` ref errors via `mapRefResolutionError`; shared missing-DB precondition used by log + status | D2 (F5) | PASS | check single-target ref errors → `mapRefResolutionError` (migration-check.ts:423); `requireLiveDatabase` (cli-errors.ts:354-371) used by log (migration-log.ts:57-65) + status (migration-status.ts:275-284), PRECONDITION exit preserved. `meta.missingFlags` = `['--db']` when connection missing, `[]` when only driver missing (no `--driver` flag exists → `[]` correct). Tests: require-live-database.test.ts (4 cases), parity §D2 lock asserts log/status PN-CLI-4005 + `missingFlags` agreement. | -| AC-3 | `check` accepts fs path via show's shared helper; both describe identical grammar | D3 (F2) | PASS | helper extracted to utils/migration-path-target.ts (looksLikePath/resolveAppTargetPath/findPackageByDirPath), imported by both show + check (migration-check.ts:52-56,414-419). Positional help aligned: both "directory name, hash/prefix, ref, or path" (migration-check.ts:497, migration-show.ts:248). Required(show)/optional(check) distinction preserved. Test: migration-check-path-target.test.ts, migration-show.test.ts:281. | -| AC-4 | `status` + `log` honour `--ascii`; status hardcoded `resolveGlyphMode(false)` removed | D4 (F4) | PASS | status `--ascii` option (migration-status.ts:669) threads `options.ascii===true` at both former-hardcoded sites (:381,:456); log `--ascii` (migration-log.ts:134) threaded into migration-log-table.ts (glyphMode optional, unicode default → other callers unaffected); show/check untouched. Tests: migration-log-table.test.ts (ASCII divider/glyphs), legend test. | -| AC-5 | check see-also includes show; `--json` examples everywhere JSON emitted; uniform offline/live phrasing | D5 (F6) | PASS | check see-also links migration show (migration-check.ts:493); every verb has a `--json` example incl. show (migration-show.ts:241); uniform phrasing — offline verbs "Offline — does not consult the database.", live verbs "Requires a database connection." Tests: migration-read-help-text.test.ts (see-also + per-verb `--json` example + offline/live phrasing), parity §D5 see-also symmetry. | -| AC-6 | `check` validates all spaces by default + `--space` to narrow (reuses aggregate.spaces()) | D6 (F1, spine) | PASS | `enumerateCheckSpaces` (migration-check.ts:143-167) projects `aggregate.spaces()`; `runMigrationCheck` (:264-284) runs per-space file-existence/snapshot/reachability/dangling-ref; `--space` validates via `isValidSpaceId`→`errorInvalidSpaceId`, unknown→`errorSpaceNotFound`, both PRECONDITION (:269-274). Exit codes + `{ok,failures,summary}` shape unchanged; single-target left app-only (documented :388-394). Tests: migration-check-multi-space.test.ts (plants non-app dangling-ref/unreachable defects → surface in no-arg; --space narrows; bad-id/unknown-id errors), parity §D6 lock. See F-1 re: integrity-violation drop under `--space`. | -| AC-7 | Parity test asserts envelope / see-also / missing-DB error / check multi-space across all six | D7 (DoD lock) | PASS | migration-read-commands-parity.test.ts extended with D1 envelope (6 verbs; list/graph/status/check CLI-driven, show/log type-shape locks with runtime coverage elsewhere — see F-1 note resolved), D5 see-also symmetry, D2 missing-DB PN-CLI-4005 + `missingFlags`, D6 multi-space + narrow + unknown-space. Pretty-parity intact. Locks would fail on any F1–F6 regression. | - -Status values: `PASS` / `FAIL` / `NOT VERIFIED — ` / `ACCEPTED DEFERRAL` / `OUT OF SCOPE`. - -## Subagent IDs - -- **Implementer:** D1 = `ad3585ca64f0606b4`. _Harness note: SendMessage/resume is not enabled in this context, so the persistent-subagent rule can't be honoured — a fresh implementer is spawned per dispatch with a full-context brief; continuity is carried by the committed prior work + on-disk spec/plan/review (the skill's documented no-resume fallback). Per-dispatch IDs recorded below._ - - D2 = `a87017f4f6804a242` (commit `784c2d5aa`) - - D3 = `ab02906c1f007a214` - - D4+D5 (batched) = `a080109cafb3bacc1` (D4 commit `aba6ea922`, D5 commit `1ce041f19`) - - D6 = `af0a190c08ae258f7` (spine; opus tier per model-tier calibration) - - D7 = `aa7058f79e4958d92` (commit `8cec4e131`) -- **Reviewer:** _(consolidated opus pass — recorded below)_ - -## Findings log - -### F-1 — `migration check --space ` silently drops space-attributable integrity violations (should-fix, correctness) - -**Where:** `packages/1-framework/3-tooling/cli/src/commands/migration-check.ts:363` — `if (options.space === undefined) { … loadAggregateIntegrityViolations … }`. - -**What:** In the holistic (no-target) path, aggregate integrity violations are appended only when `--space` is absent. When `--space ` is passed, the entire `loadAggregateIntegrityViolations` pass is skipped. But the per-space `checkSpace` (migration-check.ts:232-239) runs only file-existence, snapshot-consistency, reachability, and dangling-ref checks — it does **not** verify migration hashes. Migration-hash mismatch (`PN-MIG-CHECK-001`) and most other integrity-catalogue checks (`PN-MIG-CHECK-002/007/010/011/012/013/015/016`) live exclusively in the aggregate path, and the underlying `IntegrityViolation` variants carry `violation.spaceId` (see `integrity-violation-to-check-failure.ts`), i.e. they are space-attributable. Net: `migration check --space app` will pass on an app-space package whose stored hash is corrupted, whereas `migration check` (no flag) reports it. A user narrowing to a single space to validate it gets *weaker* validation than the default — a surprising asymmetry for a flag whose stated purpose (spec §F1) is just to *narrow scope*, not to drop checks. - -**Why should-fix not must-fix:** `--space` is a brand-new flag in this slice — this is a coverage gap in new surface, not a regression of any shipped behavior. The default (no-arg) path, which is the common case and the spine's headline deliverable, is fully correct and fully tested. No test locks the current drop-under-`--space` behavior as intended, so closing the gap is not fighting a deliberate contract. - -**Suggested in-PR action:** under `--space `, still run `loadAggregateIntegrityViolations` and filter the returned violations to `violation.spaceId === id` (skipping inherently cross-space variants such as `disjointness`), rather than skipping the pass wholesale. Add one parity/unit assertion that a planted hash-mismatch in the narrowed space surfaces under `--space`. This is a localized change in `executeMigrationCheckCommand` plus a fixture; it does not expand slice scope. - -## Round notes - -### Round 1 — consolidated Opus reviewer pass - -**Reviewed:** full cumulative diff `origin/main..HEAD` (7 commits bfb64d951..8cec4e131, +2203/-231 across 23 files). Read every production file in full (migration-check.ts, -log.ts, -graph.ts, -show.ts, -status.ts, cli-errors.ts, migration-path-target.ts, migration-log-table.ts, integrity-violation-to-check-failure.ts, core errors/control.ts) and the key test files (parity, multi-space, require-live-database, help-text, migration-log, read-commands-json-golden, migration-show). Cross-checked each against origin/main to separate new behavior from pre-existing. Trusted the implementers' green gates per brief; ran zero pnpm gates (no specific claim needed re-verification — the one isolation-flake claim is already documented as reproduced on the base tree). - -**AC deltas:** all 7 NOT VERIFIED → PASS. Evidence recorded inline in the scoreboard. - -**Repo-rule audit (clean):** no new bare `as` in the +452-line migration-check.ts or any production file (the two `as Record` casts at migration-check.ts:97-98 pre-date this slice); no `any` in production additions; no zod; arktype not required (no new runtime schemas — see narrative obs. 2); the `export type { CheckFailure }` re-export in migration-check.ts matches the pre-existing pattern and re-exports from a helper module, not a sibling command. - -**Test-quality audit:** the D7 type-shape locks for show/log flagged by the orchestrator are adequately backstopped at runtime — show by migration-show.test.ts:281-316 (drives the real `createMigrationShowCommand` with `--json`, asserts `parsed.ok===true`) and log by **migration-log.test.ts:159-215** (drives the real `createMigrationLogCommand` with `--json`, asserts `{ok:true,entries}` for populated AND empty ledgers). Note the orchestrator's D7 note cited `read-commands-json-golden.test.ts` as log's runtime lock, but that file re-wraps the envelope in a test-local `migrationLogJson` helper and does NOT exercise the production serialization — the real lock is migration-log.test.ts. Coverage is sufficient; not a finding. - -**Findings:** F-1 (should-fix, non-blocking) logged above. - -**Verdict:** SATISFIED. The slice delivers its single reviewable claim — the six read verbs are now consistent on params / JSON envelope / error path / decoration flags / help / space behaviour — and the extended parity test locks it. F-1 is a should-fix the orchestrator may either fold into this PR or carry as a fast follow-up; it does not undermine the consistency claim or any shipped behavior. - -## Orchestrator notes - -**Review cadence (orchestrator decision, autonomous-execution mode):** the operator asked for autonomous end-to-end execution of this consistency slice. Implementation dispatches D1–D6 are small, tightly-scoped, and each is intent-validated by the orchestrator at its DoD. Rather than spawn a reviewer round per dispatch, a single persistent reviewer runs a consolidated pass over the cumulative diff before PR-open, and D7 (the extended parity test) is itself a hard correctness lock on the agreed conventions. Deviation from strict per-dispatch review is recorded here per the artifact-contract visibility rule. - -**D1 intent-validation (PASS):** diff matches brief — log `{ ok: true, entries }` (empty case included), graph JSON lifted to exported type with byte-identical wire shape; tests red-first; cli package typecheck + tests green. Pre-existing repo-wide typecheck failures in unrelated adapter/sql packages (Turbo stale `.d.mts` in this worktree) noted by implementer — not a regression; revisit with a `pnpm build` before any repo-wide-typecheck gate. - -**D2 intent-validation (PASS):** check ref errors routed through `mapRefResolutionError` (envelope matches show/status) with PRECONDITION exit preserved by rendering the envelope directly rather than via handleResult — correct handling of the RUN-domain/exit-code nuance. `requireLiveDatabase` helper (src/utils/cli-errors.ts) unifies log+status missing-DB precondition with `meta.missingFlags`; status `--from` offline path intact. Additive `missingFlags` channel added to core `errorDatabaseConnectionRequired` (1-core/errors) — justified trivial-related fix, all call sites unaffected, requires `@prisma-next/errors` rebuild. Gates: cli typecheck + 1156 tests green. Implementer flagged a `vi.resetModules()`/`vi.doUnmock` pattern needed for config-loader-mocking tests under the repo's `isolate:false` vitest — thread into D7 if the parity test mocks config-loader. - -**Batching decision (orchestrator):** with no subagent resume, to control spawn cost the trivial surface-only dispatches D4 (--ascii) and D5 (help text) will be executed by one implementer as two separate commits. D3 (path-helper sharing) and D6 (spine) stay solo. Each commit remains dispatch-scoped. - -**D3 intent-validation (PASS):** path helper extracted to shared `src/utils/migration-path-target.ts` (looksLikePath / resolveAppTargetPath / findPackageByDirPath), imported by both show (refactor-only) and check; `check ` resolves (red-first), parseMigrationRef fallback + no-arg + exit codes intact; positional help aligned and renamed `[migration]`→`[target]`. Targeted gates green (cli typecheck + 15 tests). - -**TRACKED RISK — full cli test suite isolation flake (carry to D7 + pre-PR gate):** the package's vitest runs `isolate:false`/`fileParallelism:false`; config-loader-mock leakage across files makes the FULL `pnpm --filter @prisma-next/cli test` run order-dependent (`migration-list-json-golden.test.ts` intermittently sees a real `loadConfig` error). Implementer verified it's PRE-EXISTING (base tree fails worse) — not our regression. But D2+D3 added config-loader-mocking test files (`migration-status-missing-db`, `migration-check-ref-error`, `migration-check-path-target`). D7 must: (a) apply the `vi.resetModules()` + `afterAll` `vi.doUnmock` guard (per `cross-consumer-integrity.test.ts`) to any of our new mocking tests that lack it, and (b) confirm the FULL cli suite passes deterministically as the pre-PR gate. If the pre-existing leakage proves independent of our files and unfixable in-scope, escalate rather than ship a red full-suite. - -**D4 intent-validation (PASS):** status `--ascii` added, both hardcoded `resolveGlyphMode(false)` sites now thread `options.ascii === true`; log `--ascii` threaded into `migration-log-table.ts` (glyphMode optional, unicode default → other callers unaffected); show/check untouched. Red-first; typecheck + 34 targeted tests green. Commit `aba6ea922`. - -**D5 intent-validation (PASS):** check see-also now links migration show; every read verb has a `--json` example (show added); offline/live phrasing unified (status/log → "Requires a database connection." + status `--from` offline sentence; log notes all-spaces merge). New `migration-read-help-text.test.ts` red-first (8 failures). Typecheck + targeted green. Commit `1ce041f19`. Note: implementer hit an early wrong-checkout `cd` slip (ran against the main repo, not the worktree) and corrected it — final results + commits are worktree-local; worth a quick `git log` sanity check at PR time. - -**Model-tier calibration:** routed per `drive/calibration/model-tier.md` (not the memory one-liner). D6 (design-judgment/substrate change) → Opus; D7 (test assertions) → Sonnet; consolidated reviewer pass → Opus. - -**D6 intent-validation (PASS):** holistic check now per-space (all spaces default) via new exported `enumerateCheckSpaces` + `runMigrationCheck` policy core mirroring `runMigrationList`; per-space file-existence/snapshot/reachability/dangling-ref; `--space` reuses `isValidSpaceId` + `errorInvalidSpaceId`/`errorSpaceNotFound`, invalid→PRECONDITION; `loadAggregateIntegrityViolations` appended only in the unscoped case (defensible — narrowing means just-this-space); single-target left app-only with JSDoc follow-up note (per spec out-of-scope); exit codes + `{ok,failures,summary}` shape unchanged. Red-first multi-space fixture: planted non-app dangling-ref/unreachable failure now surfaces in no-arg check (silent before); `--space app|postgis` narrow; bad space→INVALID_SPACE_ID/SPACE_NOT_FOUND. Gates: typecheck + 36 related tests + lint:deps green. **Full `test/commands/` ran green 5× (557 each)** — materially de-risks the tracked isolation flake; our new tests are isolation-clean. - -**D7 intent-validation (PASS, with one logged observation):** parity test extended with 13 assertions — D1 envelopes (6 verbs), D5 see-also symmetry incl. check→show + back-link completeness (3), D2 log/status missing-DB envelope agreement w/ `meta.missingFlags` `['--db']` PN-CLI-4005 (1), D6 multi-space + `--space` narrow + bad-space SPACE_NOT_FOUND (3). Existing pretty-parity intact. Mock-isolation: our four config-loader-mocking files (D2–D6) ALREADY carried `afterAll(doUnmock+resetModules)` guards — no cleanup needed, consistent with D6's 5×-green run. **Tracked isolation-flake RESOLVED: provably pre-existing + independent** — full cli suite ran 4/5 green (1197 tests); the 5th run failed only on `client.test.ts` emit + `migration-list-json-golden` flakes that reproduce on the base tree with our test files stashed. Not our regression; implementer correctly declined to refactor unrelated test files. _Observation (low/process, non-blocking):_ the show + log `--json` envelope parity assertions are type-shape locks (construct the result type + verify shape) rather than CLI-driven, due to an ESM `vi.resetModules` instance constraint in the shared parity file; runtime regression protection for those two shapes lives in `migration-log.test.ts:159-215` (log — drives the real CLI for populated+empty; corrects my earlier mis-citation of the golden test, which re-wraps in a test-local helper) and `migration-show.test.ts:281-316` (show), so the convention is still pinned — just not all in one file. - -**Reviewer verdict (consolidated opus pass, agent `a44cf6597affe1f3e`): SATISFIED, 7/7 AC PASS, 1 should-fix finding (F-1).** Orchestrator intent-validation → **NOT pass-through; one more round.** F-1 (check `--space` drops `loadAggregateIntegrityViolations` wholesale → hash-mismatch + space-attributable integrity checks skipped when narrowing → `--space app` weaker than no-arg) contradicts the spec's intent for `--space` (narrow, not weaken), and the workflow's findings-discipline treats should-fix as blocking. It's a small, precisely-scoped fix (run the aggregate pass under `--space` too, filter by `violation.spaceId`, skip cross-space-only variants like disjointness) → dispatching D8 (F-1 remediation) rather than shipping or deferring. Reviewer's two non-finding observations (arktype-runtime-schema MUST met only structurally — consistent with peers, logged as a potential separate backlog item, NOT in scope here; code quality clean throughout) accepted as narrative. - -**F-1 RESOLVED by D8** (implementer `a46d2c670fa22ee49`, commit `b53d86042`): under `--space`, `loadAggregateIntegrityViolations` now always runs and is filtered to `v.kind !== 'disjointness' && v.spaceId === options.space` (verified all 12 non-disjointness IntegrityViolation variants carry `spaceId`; disjointness is inherently multi-space → excluded). No-arg path unchanged. Regression test: planted app-space hash-mismatch — `--space app` red before (exit 0)/green after (exit 4), `--space postgis` no false-positive, no-arg still catches it. Also tightened our own multi-space test file's mock cleanup to file-level. Gates: typecheck clean; multi-space tests 6/6; full suite 1196/1200 (the 4 = confirmed pre-existing `client.test.ts` isolation flake). **Resolution orchestrator-validated, not re-sent to the reviewer** — narrow, intent-clear, test-locked fix; recording here per the artifact-contract visibility rule. - -**SLICE DoD MET:** 7/7 ACs PASS + F-1 resolved. Extended parity test locks the conventions. Pushed + PR #726 opened per operator standing instruction. - -**CORRECTION (post-PR, operator-caught):** my "isolation flake is pre-existing and independent of our files" conclusion was WRONG for `migration-status-missing-db.test.ts` (added D2). CI fails its two cases with `errorConfigFileNotFound` — `loadConfig` runs UNMOCKED before the missing-DB precondition. The test was only green locally because a config-loader mock from another file leaked in under `isolate:false`; in CI's ordering the mock is absent. This is OUR defect (a test that isn't self-contained), not a pre-existing flake. Root error in my process: I trusted local full-suite green (favorable ordering) and the implementers' "reproduces on base tree" claims without validating under CI file-ordering. `client.test.ts` may still be genuinely pre-existing — the babysit agent (sonnet, `abe407b24e1e931db`) is disambiguating that, fixing our non-self-contained tests, and watching CI to green. diff --git a/projects/migration-graph-rendering/slices/read-command-consistency/plan.md b/projects/migration-graph-rendering/slices/read-command-consistency/plan.md deleted file mode 100644 index 68d661b065..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-consistency/plan.md +++ /dev/null @@ -1,54 +0,0 @@ -# Dispatch plan — read-command consistency - -Seven sequential dispatches. They touch overlapping sibling files (`check` appears in 2, 3, 6; `status` in 2, 4; `log` in 1, 2, 4), so order matters to avoid re-touching the same command twice for different reasons: settle each command's wiring before the spine lands on it. Each dispatch writes its targeted tests **before** implementation (repo rule). Dispatch 7 is the DoD lock and builds on the cumulative end state of 1–6, not just 6. - -**Escape hatch (spine, dispatch 6):** if `check` multi-space proves gnarlier than `aggregate.spaces()` makes it look (cross-space ref semantics needing design), cut **only** dispatch 6 to a sibling slice and drop its assertions from dispatch 7; dispatches 1–5 + the rest of 7 still ship as a complete consistency PR. - -### Dispatch 1: Unify `--json` envelopes + export schemas (F3) - -- **Outcome:** All six read verbs emit a `{ ok, … }` object with a co-located **exported** output type. `log` returns `{ ok: true, entries: [...] }` (no longer a bare array); `graph`'s inline `{ ok, nodes, edges, summary }` is lifted to an exported type like its peers. In-repo JSON snapshots/golden tests updated to the new shapes. -- **Builds on:** The spec's chosen design. -- **Hands to:** A uniform, exported JSON envelope across all six verbs — the shape dispatch 7 asserts. -- **Focus:** `migration-log.ts`, `migration-graph.ts`, their output-type modules, affected JSON golden tests. Not the human-output paths. - -### Dispatch 2: Unify the error path (F5) - -- **Outcome:** `check` resolves ref errors through the shared `mapRefResolutionError` (no inline string construction at `migration-check.ts:173–184`). A single shared "needs a live DB (connection + driver)" precondition helper is extracted and called by both `log` and `status`, producing one identical envelope (with `meta.missingFlags`) for that precondition. -- **Builds on:** Dispatch 1's settled JSON envelope (error docs share the `ok` discriminator). -- **Hands to:** One error-construction path for ref-resolution and missing-DB across the family — `check` no longer special-cased. -- **Focus:** `migration-check.ts`, `migration-log.ts`, `migration-status.ts`, the shared cli-errors/precondition helpers. Not the multi-space loops (dispatch 6). - -### Dispatch 3: Align `show`/`check` path grammar (F2) - -- **Outcome:** `check` accepts a filesystem path to a migration directory via the **same** helper `show` already uses (`looksLikePath` + path resolution); both verbs' positional help text describes the identical accepted forms (dir name / hash / ref / path). `check`'s no-arg whole-graph mode is unchanged. -- **Builds on:** Dispatch 2's `check` state (error path already routed through the shared factory). -- **Hands to:** `show` and `check` share one ref-resolution grammar and one path helper. -- **Focus:** `migration-check.ts`, the shared path helper currently private to `migration-show.ts` (promote/share it). Not `show`'s behaviour (already correct). - -### Dispatch 4: `--ascii` where a laned tree / table is drawn (F4) - -- **Outcome:** `status` exposes `--ascii` and threads `options.ascii === true` (the hardcoded `ui.resolveGlyphMode(false)` at `migration-status.ts:377,452` is gone); `log`'s table (`migration-log-table.ts`) honours `--ascii`. `show` and `check` are unchanged (no laned-tree glyphs). -- **Builds on:** The spec's chosen design (independent of 1–3). -- **Hands to:** Every verb that draws box-drawing glyphs can be forced to ASCII, matching `list`/`graph`. -- **Focus:** `migration-status.ts`, `migration-log.ts`, `migration-log-table.ts`, `resolveGlyphMode` wiring. Not `show`/`check`. - -### Dispatch 5: Help-text polish (F6) - -- **Outcome:** `check`'s see-also includes `migration show`; every JSON-emitting verb has a `--json` example (notably `show`); long descriptions state offline/live consistently across all six. -- **Builds on:** The spec's chosen design (independent; pure metadata edits). -- **Hands to:** A symmetric see-also graph + uniform help phrasing — the help conventions dispatch 7 asserts. -- **Focus:** The `setCommandDescriptions` / `setCommandExamples` / `setCommandSeeAlso` calls in the six command files. No behaviour change. - -### Dispatch 6 (SPINE): `check` multi-space (F1) - -- **Outcome:** `check`'s file-existence, reachability, and dangling-ref checks run **per contract space** (all spaces by default), and `check` accepts `--space ` to narrow — same policy as `list`/`graph`/`status`, reusing `aggregate.spaces()` and the `isValidSpaceId` / space-filter validation from `@prisma-next/migration-tools/spaces` (same `errorInvalidSpaceId` / `errorSpaceNotFound` factories `list` uses). Fixtures include a multi-space case so newly-surfaced non-app failures are intentional. -- **Builds on:** Dispatch 2 (`check` error path) + dispatch 3 (`check` path grammar) — lands on a `check` whose wiring is already settled. -- **Hands to:** `check` validates every space; `--space` narrows. `show` stays single-migration; `log` stays unscoped (documented in dispatch 5's phrasing pass). -- **Focus:** `migration-check.ts` check loops + `--space` option, multi-space fixtures. The custom exit codes (F7) are unchanged — confirm they're documented in `--help`. - -### Dispatch 7: Extend the parity test (DoD lock) - -- **Outcome:** `test/commands/migration-read-commands-parity.test.ts` (today: rendering parity only) asserts across all six verbs: `{ ok, … }` JSON envelope shape; symmetric see-also graph; the shared missing-DB error shape; `check`'s multi-space behaviour. A regression reintroducing any F1–F6 defect fails this test. -- **Builds on:** The cumulative end state of dispatches 1–6 (non-linear: asserts all of them, not just dispatch 6). -- **Hands to:** Slice-DoD met — consistency is test-locked. -- **Focus:** The parity test file (+ any shared test helpers). No production-code change; if an assertion fails, the defect is in the corresponding earlier dispatch's surface. diff --git a/projects/migration-graph-rendering/slices/read-command-consistency/spec.md b/projects/migration-graph-rendering/slices/read-command-consistency/spec.md deleted file mode 100644 index 3172220a79..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-consistency/spec.md +++ /dev/null @@ -1,95 +0,0 @@ -# Slice: read-command consistency - -_In-project slice. Parent project: `projects/migration-graph-rendering/`. Outcome: the migration read-verb family the project built (`list` / `graph` / `status` / `log`, plus `show` / `check`) is consistent at the surface — same param grammar, JSON envelope, error path, decoration flags, and space behaviour — and that consistency is test-locked._ - -## At a glance - -Brings the six migration **read** verbs — `status`, `list`, `graph`, `log`, `show`, `check` (in `packages/1-framework/3-tooling/cli/src/commands/migration-*.ts`) — into line on the six axes from [TML-2801](https://linear.app/prisma-company/issue/TML-2801): params, formatting, behaviour, naming, help text, and structured errors. Most of it is wiring existing shared primitives (`mapRefResolutionError`, `resolveGlyphMode`, the contract/migration ref parsers, exported `{ ok, … }` output schemas) into the commands that skipped them; one piece — making `check` validate every contract space — is a real behaviour change. The full state comparison is in [`../../read-command-consistency-audit.md`](../../read-command-consistency-audit.md) (findings F1–F7); the fix clusters in [`../../read-command-consistency-followups.md`](../../read-command-consistency-followups.md). - -## Chosen design - -Six fixes, addressed as one sweep across the sibling command files. Five are wiring; one (F1 / `check` multi-space) is the spine. - -### F3 — Unify `--json` envelopes + export schemas - -`log` emits a **bare array** ([migration-log.ts:134](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-log.ts)); `graph` builds `{ ok, nodes, edges, summary }` **inline** with no exported schema. Both violate Style Guide §JSON Semantics (co-located exported schema; shared `ok` discriminator). - -- `log` → `{ ok: true, entries: [...] }` (exported type). -- `graph` → co-located exported output type, like `status`/`list`/`show`/`check` already have. -- Sweep primary-payload field names for a documented convention. - -### F5 — One error path - -- `check` builds ref-resolution errors inline ([migration-check.ts:173–184](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-check.ts)); route them through the shared `mapRefResolutionError` that `status` ([:330,342](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-status.ts)) and `show` ([:235](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-show.ts)) use. -- `log` raises `errorDatabaseConnectionRequired` then a separate `errorDriverRequired` ([:51,59](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-log.ts)); `status` folds both into one condition ([:274](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-status.ts)). Extract one shared "needs a live DB (connection + driver)" precondition and call it from both. Confirm `meta.missingFlags` is set per Style Guide §Errors. - -### F2 — Align `show` / `check` path grammar (align up) - -`show` accepts a filesystem path to a migration dir (`looksLikePath` + path resolution); `check` passes the positional straight to `parseMigrationRef` ([:172](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-check.ts)), so it rejects paths. Give `check` path support by sharing `show`'s existing path helper. Both then accept the same forms (dir name / hash / ref / path); help text for both describes the identical grammar. The required-(`show`)-vs-optional-(`check` no-arg = whole-graph) positional distinction stays. - -### F6 — Help-text polish - -- `check`'s see-also gains `migration show` (currently omitted, [:288–292](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-check.ts)). -- Every JSON-emitting verb gets a `--json` example (notably `show`, which lacks one). -- Uniform "Offline — does not consult the database" / "Requires a database connection" phrasing in every long description (`show` is offline but doesn't say so). - -### F4 — `--ascii` where a laned tree is drawn - -`status` renders the shared laned tree but **hardcodes** `ui.resolveGlyphMode(false)` at [:377](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-status.ts) and [:452](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-status.ts) — it can never go ASCII. Add `--ascii` and thread `options.ascii === true`, matching `list`/`graph`. `log`'s table (`migration-log-table.ts`) uses box-drawing → add `--ascii` there too. `show` (op preview) and `check` (`✔`/`✗`/`fix:` lines) draw no laned-tree glyphs → no `--ascii`. - -### F1 — `check` multi-space (the spine) - -Today `check`'s explicit graph checks — file-existence, reachability, dangling-ref — iterate **app space only** ([:142–253](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-check.ts)); only `loadAggregateIntegrityViolations` ([:255](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-check.ts)) already spans all spaces via the aggregate. Extend the app-only loops to run **per contract space**, adopting the project's established policy: **all spaces by default, `--space ` to narrow** (same as `list`/`graph`/`status`). - -This is grounded, not new machinery: the aggregate already exposes `spaces()` / `space(id)` ([aggregate.ts:262–263](../../../../packages/1-framework/3-tooling/migration/src/aggregate/aggregate.ts)), each space carries its own graph, and `migrationSpaceListEntriesFromAggregate` ([migration-list.ts:98](../../../../packages/1-framework/3-tooling/cli/src/commands/migration-list.ts)) already iterates them. `check` reuses `aggregate.spaces()` + the shared `isValidSpaceId` / space-filter validation from `@prisma-next/migration-tools/spaces` (the same `errorInvalidSpaceId` / `errorSpaceNotFound` factories `list` uses). Net: `check` validates every space's graph, and `--space` narrows. - -`show` stays single-migration (no `--space` — the migration's space is already pinned by the reference). `log` stays unscoped (all spaces merged in apply order) — documented in its long description, not flagged. `check` keeps its custom exit codes (F7 — Style-Guide-correct, no change beyond confirming they're documented in `--help`). - -### DoD lock - -Extend [`test/commands/migration-read-commands-parity.test.ts`](../../../../packages/1-framework/3-tooling/cli/test/commands/migration-read-commands-parity.test.ts) (today: rendering parity only) to assert, across all six verbs: `{ ok, … }` JSON envelope shape; symmetric see-also graph; the shared missing-DB error shape; and `check`'s multi-space behaviour. A regression that reintroduces any F1–F6 defect must fail this test. - -## Coherence rationale - -One reviewable claim — "the six read verbs are now consistent" — evaluated once, against one extended parity test that proves it. Splitting into per-fix PRs would re-touch the same six sibling files and the same shared helpers repeatedly, serialising on conflicts and re-loading the same context for the reviewer each time. The diff is one sweep across `migration-*.ts` + the shared error/glyph/space helpers + the parity test, rollback-able as a unit. - -## Scope - -**In:** the six read-command files under `cli/src/commands/migration-{status,list,graph,log,show,check}.ts`; the shared helpers they wire to (`mapRefResolutionError`, the missing-DB precondition, `resolveGlyphMode`, `show`'s path helper, `migration-tools/spaces` validation); `migration-log-table.ts` (`--ascii`); exported output schemas for `graph` + `log`; `check`'s per-space check loops; the parity test extension. - -**Out:** -- Any change to `migrate` or the authoring verbs (`plan` / `new`) — write verbs, not in this family. -- The control-api ↔ CLI surface reconciliation ([TML-2780](https://linear.app/prisma-company/issue/TML-2780)) — internal API naming, separate slice. -- `ref list` consistency — adjacent subject, not audited here. -- Whether `show` / `check` should accept the **contract** reference grammar (ref names, `^`) — they resolve a package, not a contract; out by decision. -- Real `--space` filtering on `show` (rejected by design — single pinned migration). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| `log` JSON consumers depend on the bare-array shape | Accept the break | No external consumers in-repo; the envelope change is the point. Update in-repo snapshots/golden tests in the same diff. | -| `check` multi-space surfaces pre-existing integrity failures in non-app spaces | Expected, not a regression | `check` currently under-validates non-app spaces; newly-surfaced real failures are the fix working. Fixture set must include a multi-space case so the new failures are intentional, not surprising. | - -## Slice-specific done conditions - -- [ ] The extended parity test fails if any F1–F6 convention regresses (envelope, see-also symmetry, missing-DB error shape, `check` multi-space). - -## Open Questions - -1. **Does the `check` multi-space work stay in this slice?** Working position: **yes**, as the spine dispatch. Escape hatch — if cross-space graph reconstruction proves gnarlier than `aggregate.spaces()` makes it look (e.g. cross-space ref semantics need design), spin **only** the `check` multi-space piece into a sibling slice and ship the other five fixes as pure wiring. The plan should sequence `check` multi-space as its own dispatch so the cut is clean. -2. **`log` JSON payload field name** (`entries` vs other). Working position: `entries`. Settle when the schema is authored; low stakes. - -## Required-section notes - -- **Contract-impact:** none. No change to `packages/0-shared/contract/**` or framework-core. -- **Adapter-impact:** none functionally — the CLI read verbs are family-agnostic; `check` multi-space operates on hashes/graphs via the aggregate, not target SQL. The parity test is fixture-based (existing Postgres fixtures); no per-adapter code changes. -- **ADR pointer:** none. This is consistency wiring under the existing CLI Style Guide; no architectural shift. The Style Guide ([`docs/CLI Style Guide.md`](../../../../docs/CLI%20Style%20Guide.md)) is the governing standard. - -## References - -- Parent project: [`projects/migration-graph-rendering/spec.md`](../../spec.md) -- Audit + findings: [`../../read-command-consistency-audit.md`](../../read-command-consistency-audit.md), [`../../read-command-consistency-followups.md`](../../read-command-consistency-followups.md) -- Linear issue: [TML-2801](https://linear.app/prisma-company/issue/TML-2801) -- Standard: [`docs/CLI Style Guide.md`](../../../../docs/CLI%20Style%20Guide.md) §§ JSON Semantics, Errors, Exit Codes, Flag Conventions -- Prior surface-shape audit: [`projects/migration-domain-model/cli-audit.md`](../../../migration-domain-model/cli-audit.md) diff --git a/projects/migration-graph-rendering/slices/read-command-json-redesign/code-review.md b/projects/migration-graph-rendering/slices/read-command-json-redesign/code-review.md deleted file mode 100644 index 5b6ff25f12..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-json-redesign/code-review.md +++ /dev/null @@ -1,65 +0,0 @@ -# Code review — read-command-json-redesign (TML-2836) - -> Reviewer maintains scoreboard/findings/round-notes/summary; orchestrator owns § Subagent IDs + § Orchestrator notes. - -## Summary - -- **Current verdict:** SATISFIED -- **AC scoreboard totals:** 8 PASS / 0 FAIL / 0 NOT VERIFIED - -The six final shapes match the spec and use `name`/`space`/`hash`/`fromContract`/`toContract`/`currentContract`/`targetContract` identically. No retired name leaks anywhere (production or renderer reads). The arktype schemas are the genuine source of truth — every result type is `typeof Schema.infer`. The two-bodies `ok`/error model is honored and a consumer can distinguish check's integrity outcome (`failures`, no top-level `code`) from the error envelope (top-level `code`, unchanged). Empty-start `fromContract` is `null` everywhere including graph (F-1 closed). No bare `as`/`any` added in production. One low-process observation (F-2) about the parity file's log/show sections being redundant tautologies, but the real runtime locks exist in the dedicated command tests, so coverage is sound. - -## Acceptance criteria scoreboard - -| AC ID | Description (short) | Dispatch | Status | Evidence | -| ----- | ------------------- | -------- | ------ | -------- | -| AC-1 | Shared migration vocabulary renamed (name/space/fromContract/toContract/current+targetContract) across types + renderers; compiles | D1 | PASS | `migration-list.ts:112-120` (name/hash/fromContract/toContract); `migration-list-render.ts:75-118`, `migration-list-graph-topology.ts:322-325`, `migrations.ts:338-361` all read renamed fields; `migration-status.ts:560-565` currentContract/targetContract. Build+typecheck clean per gates. | -| AC-2 | Shared arktype sub-schemas exist; shared result types derived from them; `list --json` emits + validates against its schema | D2 | PASS | `json/schemas.ts:3-43` (`migrationEntrySchema`/`contractRefSchema`/`successEnvelopeBaseSchema` + list schemas); `migration-list-types.ts:1-5` re-exports the schema-derived types; `migration-read-commands-parity.test.ts:899-919` validates real `list --json` output against `migrationListResultSchema`. | -| AC-3 | `graph --json` nested per space (`spaces[].contracts/migrations`), schema-locked | D3 | PASS | `migration-graph.ts:183-195` builds `spaces[].contracts/migrations`; `read-commands-json-golden.test.ts:205-262` pins real output + validates against `migrationGraphJsonResultSchema`; empty-start `fromContract: null` at `migration-graph.ts:192`. | -| AC-4 | `status --json` currentContract/targetContract + structured diagnostics, schema-locked | D4 | PASS | `json/schemas.ts:78-121` discriminated diagnostic union (3 variants matching the 3 producers at `migration-status.ts:313/522/580`); `migration-status.test.ts:133-185` validates real output + status field; `format-status-summary.test.ts` exercises the folded-in MISSING_INVARIANTS in the human renderer. | -| AC-5 | `log --json` ledger `records` + renamed fields, schema-locked | D5 | PASS | `migration-log.ts:138-143` emits `records`; `migration-log-table.ts:191-203` `serializeLedgerEntriesForJson` maps migrationName→name etc.; `migration-log.test.ts:190-201` validates the REAL command stdout against `migrationLogResultSchema` and asserts `records[0].name`. | -| AC-6 | `show --json` drops dirPath + inner summary, schema-locked | D6 | PASS | `migration-show.ts:51-87` no `dirPath`, no inner summary, top-level summary at `:215`; `migration-show.test.ts:316-329` validates real `show --json` against `migrationShowResultSchema` and asserts `migration.name`/`migration.space`. | -| AC-7 | `check` failures on error-envelope vocab (`code`/`where`/`why`/`fix` + `space`), schema-locked (two ok:false bodies) | D7 | PASS | `json/schemas.ts:179-195` distinguishes `checkFailureSchema` (`space/code/where/why/fix`) from the error envelope; `migration-check.ts:628-660` routes the error envelope (`formatErrorJson`, top-level `code`) separately from the integrity outcome (`{ok,failures,summary}`); `ok===exit0` via OK/INTEGRITY_FAILED/PRECONDITION. `integrity-violation-to-check-failure.ts` maps all kinds with `space`+`code`. | -| AC-8 | Parity test validates all six against schemas + cross-command consistency + ok-mirrors-exit + topology rule | D8 | PASS | `migration-read-commands-parity.test.ts:891-1179`: real-output schema validation for list/graph/status/check; `assertNoRetiredNames` (`:878-889`) scans serialized JSON for all retired names + nodes/edges; empty-start null assertion (`:948-955`); ok-mirrors-exit (`:1059-1099`); topology (`:1101-1178`). log/show entries are type-shape locks (real locks live in their command tests — see F-2). | - -Status: `PASS` / `FAIL` / `NOT VERIFIED — `. - -## Subagent IDs - -- **Implementer:** per-dispatch (harness has no resume — fresh per dispatch, full-context briefs). IDs recorded below. - - D1 = `ab0aa065bfa6cda50` - - D2 = `a8e46fd764266b05b` - - D3 = `a5c51a675a4a2cd9d` (commit `9aa493fb4`; opus, structural) - - D4 = `a20d88b8ab6c52496` (status; commit `bd9033e9c`) - - D5 = `aa3a27cdb9b218395` (log records; commit `2b624914c`) - - D6 = `a821e5c3eb8e8c4b4` (show trim; commit `a514bafd8`) - - D7 = `aa41e4e038678514a` (check error-envelope vocab; commit `ef57aed15`) - - D8 = `a1a41c2e1e5d793d5` (parity lock + graph empty-start fix; commit `b0934c8fd`) -- **Reviewer:** `a33bf9f86d24c7a34` (consolidated opus pass — SATISFIED, 8/8 PASS, F-1 resolved, F-2 low-process/no-action). Orchestrator intent-validation: pass-through. F-2 accepted as non-blocking — the parity file's log/show sections are redundant type-shape checks, but real runtime shape is pinned in `migration-log.test.ts`/`migration-show.test.ts` (they validate real command output against the schema), so no coverage gap. **Slice DoD met → push + PR.** -- **Trace:** emitting to `projects/migration-graph-rendering/trace.jsonl` via the `drive-record-traces` emitter (started at D4 after the operator flagged the gap; D1–D3 + spec/plan backfilled — D1–D3 dispatch-spine with real durations, no fabricated round/brief events; forward emission full spine from D4). - -## Findings log - -### F-1 — empty-start `fromContract` must be `null`, not the `sha256:empty` sentinel (cross-command, should-fix) - -D3's `graph --json` emits `fromContract: "sha256:empty"` for baseline edges (the graph layer keys the empty origin as `EMPTY_CONTRACT_HASH`). `list`/`show` already coerce the empty start to `fromContract: null` at the read boundary. The settled design is **null everywhere at the empty start** ("same values everywhere"). Fix: coerce `EMPTY_CONTRACT_HASH → null` at each command's JSON boundary, and make the graph-migration schema's `fromContract` nullable. - -**Plan to close:** D4 (status) and D5 (log) are instructed to coerce empty→null so the inconsistency doesn't spread; D8's parity test asserts `fromContract` is `null` (never the sentinel) at the empty start across all six; graph's coercion (D3 surface) is fixed in the review round when D8's assertion flags it. Tracked here so it isn't lost. - -**Resolved (Round 1):** closed. Graph coerces `EMPTY_CONTRACT_HASH → null` at `migration-graph.ts:192`; the parity test asserts it (`migration-read-commands-parity.test.ts:948-955`); status has a dedicated empty-start-null test (`migration-status.test.ts:188-232`). `currentContract` legitimately stays `sha256:empty` at the empty start — that is the real DB-marker hash, not a migration endpoint, so the null rule does not apply to it (and the schema allows `string | null`). No other emit of `sha256:empty` in a `fromContract` position. - -### F-2 — parity file's log/show "schema validation" sections validate hand-built objects, not real command output (low-process) - -In `migration-read-commands-parity.test.ts`, the log and show entries (`:657-676`, `:609-632`, `:984-1008`, `:1010-1034`) build a sample object literal by hand and validate it against the schema. That cannot catch drift between the real command's `--json` and the schema — it only proves the schema accepts an object the author wrote to match it (close to tautological for drift detection). The test comments acknowledge this and point to the real locks. Those real locks are genuine and sufficient: `migration-log.test.ts:190-201` runs the actual command, captures stdout, asserts `records[0].name`, and validates against `migrationLogResultSchema`; `migration-show.test.ts:316-329` does the same for show. So overall coverage is sound; the parity-file log/show sections are redundant. No in-PR action required to ship — flagging as a process note: if these stay, a future reader may over-trust the parity file. Optional in-PR tightening: replace the two hand-built parity sections with the same real-command-capture pattern the list/graph/status/check sections already use (the log/show commands can be driven through `executeCommand` with mocked ledger/contract the same way their dedicated tests do). - -## Round notes - -**Round 1 (Opus reviewer).** Walked all six final shapes against the spec and read every touched command, schema, renderer, and test. Findings: F-1 confirmed closed (graph empty-start null landed; parity + status tests assert it). F-2 filed as low-process (parity file's log/show sections are tautological, but real runtime locks exist in the dedicated command tests, so the slice's claim holds). Verified: no retired name leaks (production or renderer reads, scanned via `assertNoRetiredNames` over serialized JSON and by reading the renderer diffs); result types genuinely `typeof Schema.infer`; the diagnostic union's 3 variants match the 3 status producers exactly; check's two-bodies model is honored and the error envelope is untouched (routed through `formatErrorJson`); `ok===exit0` holds across OK/INTEGRITY_FAILED/PRECONDITION; no bare `as`/`any` added in production (only an import alias); human-renderer layout preserved (field-access renames only). One uncovered-but-intentional human-output change: the show human renderer label `migrationHash:` → `hash:` (`migrations.ts:361`) is not asserted by `formatMigrationShowOutput`'s test, but it is a deliberate vocabulary alignment, not a defect. Verdict: SATISFIED. - -## Orchestrator notes - -**Build run via the drive-build-workflow protocol directly** (skill body already loaded earlier this session). Model tiers per `drive/calibration/model-tier.md`: D1 rename codemod → sonnet; D2 schema foundation → sonnet (precise pattern); D3 graph structural change → **opus** (substrate/design judgment); D4–D7 per-command → sonnet (compose D2's pattern); D8 parity test → sonnet; consolidated reviewer → opus. Each implementer commits before reporting (truncation guard seen earlier this session) and keeps reports short. - -**D1 intent-validation (PASS):** the four renames (`dirName→name`, `spaceId→space`, migration-entry `from/to→fromContract/toContract`, status `markerHash/targetHash→currentContract/targetContract`) applied across shared types + all renderers; graph's JSON edge left for D3 (correct per scope). 17 files. **Environment gotcha fixed:** cli typecheck initially showed 4 `StorageNamespace` `entries`-vs-`tables` errors in test fixtures — these were STALE built `dist/*.d.mts` (source has `entries`, the stale dist had `tables`; one erroring file wasn't even touched by D1, and origin/main carries the same fixtures). `pnpm build` refreshed the dist and typecheck went clean. **Lesson for the rest of this slice: a red cli typecheck citing sibling-package types is probably stale dist — `pnpm build` before trusting it.** Implementers should build-then-typecheck, or treat sibling-type errors skeptically. - -**D2 intent-validation (PASS):** shared arktype module `src/commands/json/schemas.ts` (`migrationEntrySchema`/`contractRefSchema`/`successEnvelopeBaseSchema` + the list-space + list-result schemas); shared result types derived from arktype; `list --json` validated against the exported schema in its golden test (`schema(value) instanceof type.errors`). Also did the spec's `migrationHash → hash` (migration's own id), updating construction + renderer reads. Pattern for D3–D7: compose with `.and(type({...}))`, spread `readonly` arrays at construction (arktype infers mutable `string[]`), validate golden output via `instanceof type.errors`. Gate: `pnpm build` then typecheck clean, 212 tests, lint:deps clean. diff --git a/projects/migration-graph-rendering/slices/read-command-json-redesign/plan.md b/projects/migration-graph-rendering/slices/read-command-json-redesign/plan.md deleted file mode 100644 index ae4ceba8f2..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-json-redesign/plan.md +++ /dev/null @@ -1,59 +0,0 @@ -# Dispatch plan — read-command-json-redesign - -Eight sequential dispatches. The cut is forced by two facts: the field renames live in **shared** types that the human renderers read, so the rename must land as one atomic codemod (D1); and the arktype schema is the **source of truth** the result types derive from, so the schema foundation lands once (D2) and each command then composes it. D3–D7 are per-command and independent of each other (each builds on D2, not on its predecessor) — kept sequential only because the harness has no subagent resume. D8 is the cross-command lock. Tests precede implementation in every dispatch. Standard gate unless noted: `pnpm --filter @prisma-next/cli typecheck` + the touched command's tests + `pnpm lint:deps`. - -### Dispatch 1: Rename the shared migration vocabulary (atomic codemod) - -- **Outcome:** across the shared types and every reader, `dirName → name`, `spaceId → space`, `from/to → fromContract/toContract` (and `markerHash/targetHash → currentContract/targetContract` in the status types). `migration-list-types.ts`, the status result types, and **all** human renderers that read these fields (`migration-list-render`, `migration-graph-tree-render`/`-space-render`, `migration-status-overlay`, `migration-log-table`) are updated in one change; the package typechecks; existing unit tests are updated to the new field names. No structural/shape change yet — pure rename. Field names are still defined by the current hand-written interfaces. -- **Builds on:** the spec. -- **Hands to:** the renamed migration-entry vocabulary, compiling, with renderers intact. -- **Focus:** the rename only. Not the arktype schemas (D2), not graph nesting (D3), not log/check/status/show structural changes (D4–D7). This is a mechanical fan-out — one outcome ("the vocabulary is renamed and everything compiles"), verified by typecheck + the touched tests. - -### Dispatch 2: arktype schema foundation + `list` on it - -- **Outcome:** a co-located, exported module of shared arktype sub-schemas — `migrationEntry` (`{ name, hash, fromContract, toContract, operationCount, createdAt, refs, providedInvariants }`), `contractRef` (`{ hash, refs }`), and the `{ ok, summary }` envelope base. The shared result types are **derived** from these schemas (`typeof Schema.infer`), replacing the hand-written interfaces (same field names from D1, so renderers are untouched). `migration list` builds its `--json` against the derived type and its golden test validates the real output against the exported schema. This establishes the pattern (schema → derived type → command emits → test validates) the other commands follow. -- **Builds on:** D1's renamed vocabulary. -- **Hands to:** the shared arktype sub-schemas + the schema-as-source-of-truth pattern, demonstrated end-to-end on `list`. -- **Focus:** the shared schema module + list. Not the other five commands' schemas (they compose this in D3–D7). - -### Dispatch 3: `graph` — nested per space, `contracts` + `migrations` - -- **Outcome:** `graph --json` changes from flat global `{ nodes, edges }` to `{ ok, spaces: [{ space, contracts: [{hash,refs}], migrations: […] }], summary }`, reusing the per-space enumeration `list`/`status` already use (`aggregate.space(id).graph()`); co-located arktype schema (composing D2's `contractRef` + migration sub-shape); graph golden/parity JSON updated and validated against the schema; the graph human renderer still works. -- **Builds on:** D2 (shared sub-schemas). -- **Hands to:** `graph --json` nested-per-space and schema-locked. -- **Focus:** graph's JSON construction + schema. The one real structural change in the slice. - -### Dispatch 4: `status` — `currentContract`/`targetContract` + structured diagnostics - -- **Outcome:** `status --json` uses `currentContract`/`targetContract` per space (rename from D1 carried into the JSON), and its `diagnostics` + former `missingInvariantsLine` become structured objects (`{ code, …, message }`; missing-invariants → `{ ref, invariants: [] }`) rather than prose; co-located arktype schema (composing D2's migration entry + the `status` field); status tests updated + validated. -- **Builds on:** D2; D1 (the marker/target rename). -- **Hands to:** `status --json` structured + schema-locked. -- **Focus:** status diagnostics structuring + schema. Not the tree rendering layout. - -### Dispatch 5: `log` — ledger `records` + schema - -- **Outcome:** `log --json` renames `entries → records`; `SerializedLedgerEntryRecord` fields align to the shared vocabulary (`migrationName → name`, `from/to → fromContract/toContract`, keep `space`/`appliedAt`/`operationCount`/`hash`); the log table renderer reads the renamed fields; co-located arktype schema; log golden/tests updated + validated. -- **Builds on:** D2. -- **Hands to:** `log --json` on `records` + schema-locked. -- **Focus:** log records + the log table renderer reads. - -### Dispatch 6: `show` — trim + schema - -- **Outcome:** `show --json` drops `migration.dirPath` and the inner per-migration `summary` (replaced by the top-level `summary`); the migration object uses the shared vocabulary + `operations` + `preview: { statements }`; co-located arktype schema (noting `preview` is family-shaped); show tests updated + validated. -- **Builds on:** D2. -- **Hands to:** `show --json` trimmed + schema-locked. -- **Focus:** show's single-migration shape + schema. - -### Dispatch 7: `check` — error-envelope vocabulary + `space` - -- **Outcome:** `check` failures become `{ space, code, where, why, fix }` (`pnCode → code`, add `space`), aligned to the shared error-envelope vocabulary (`integrity-violation-to-check-failure.ts` updated); co-located arktype schema that models check's two `ok:false` bodies (the `{ failures, summary }` outcome vs the shared error envelope) distinguishably; check tests updated + validated. -- **Builds on:** D2. -- **Hands to:** `check --json` on the error-envelope vocabulary + schema-locked. -- **Focus:** check failure shape + the `ok:false`-outcome-vs-error-envelope distinction. - -### Dispatch 8: cross-command consistency lock (parity test) - -- **Outcome:** `migration-read-commands-parity.test.ts` is extended to assert, across all six verbs: each `--json` output validates against its exported arktype schema; the shared field names (`name`, `space`, `hash`, `fromContract`/`toContract`) are used identically wherever they appear; the `ok`-mirrors-exit-code rule holds; the nested-vs-flat space-topology rule holds. A regression that reintroduces an old field name or shape fails this test. -- **Builds on:** the cumulative end state of D2–D7 (non-linear: asserts all of them). -- **Hands to:** slice-DoD met — the redesigned, schema-locked shapes are consistent and regression-protected. -- **Focus:** the parity/consistency assertions. No production change; a failure here means the defect is in the corresponding earlier dispatch. diff --git a/projects/migration-graph-rendering/slices/read-command-json-redesign/spec.md b/projects/migration-graph-rendering/slices/read-command-json-redesign/spec.md deleted file mode 100644 index c6156d4b28..0000000000 --- a/projects/migration-graph-rendering/slices/read-command-json-redesign/spec.md +++ /dev/null @@ -1,112 +0,0 @@ -# Slice: read-command-json-redesign - -_In-project slice. Parent project: `projects/migration-graph-rendering/`. Outcome: the six migration read commands emit consistent, self-describing, agent-facing `--json`, each locked by a co-located exported arktype schema._ - -## At a glance - -Rewrite the `--json` output of `migration list` / `graph` / `status` / `log` / `show` / `check` so the shapes are consistent and the field names are self-describing, and lock each shape with a runtime [arktype](https://arktype.io) schema (co-located with the command, exported on the package surface). This is the durable outcome of the design discussion on 2026-06-05 ([TML-2836](https://linear.app/prisma-company/issue/TML-2836), expanded from "just add schemas" to "redesign + lock"). The commands live in `packages/1-framework/3-tooling/cli/src/commands/migration-*.ts`; the CLI Style Guide § JSON Semantics governs. - -**Framing principle (drives every decision):** the JSON's primary reader is AI agents. So each command returns **structured fields as the source of truth, plus a human-readable `summary`**, and errors keep structured `why`/`fix`. A fact a consumer needs is never prose-only; a readable string riding alongside the structured data is welcome. - -No external consumers and 0.x semver → the shapes are rewritten freely, with no versioning or migration story. - -## Chosen design - -### Cross-cutting rules - -- **`ok` mirrors the exit code** (`ok === exit 0`). `check` returning `ok: false` when it finds integrity failures is correct — it exits 4. So `ok: false` has **two** bodies: `check`'s `{ ok: false, failures, summary }` (a real outcome at exit 4) and the shared error envelope `{ ok: false, code, why, fix, … }` (exit 1/2). The arktype schemas must admit both, and a consumer disambiguates by presence of `failures` vs `code` (or by exit code). -- **`name`** — a migration's name, everywhere (retires `dirName` and `log`'s `migrationName`). No `path` field for now; `show`'s `dirPath` is dropped (add a `path` later only on real need). -- **`space`** — the contract-space id, everywhere (retires `spaceId`). -- **Self-describing hashes:** a migration's contract endpoints are `fromContract` / `toContract` (`fromContract: null` for the empty start); the migration's own id is plain `hash`; `status`'s `markerHash` / `targetHash` become `currentContract` / `targetContract`; graph contracts are `{ hash, refs }`. -- **`summary`** stays on every command and is reworded to a consistent tone (today the strings are ad hoc). -- **Space topology — nested vs flat by whether the command carries per-space state:** - - **Nested** under `spaces[]` (the command holds per-space state): `list`, `graph` (per-space contracts+migrations), `status` (per-space current/target contract). - - **Flat** array, each item tagged with `space` (the command is a stream): `log` records, `check` failures. - - **Single** item with a `space` field: `show`. - -### Per-command target shapes - -```jsonc -// list -{ "ok": true, "summary": "…", - "spaces": [ { "space": "app", - "migrations": [ { "name": "…", "hash": "sha256:…", "fromContract": null, "toContract": "sha256:…", - "operationCount": 1, "createdAt": "…", "refs": [], "providedInvariants": [] } ] } ] } - -// graph (structural change: today's flat global nodes/edges → nested per space) -{ "ok": true, "summary": "…", - "spaces": [ { "space": "app", - "contracts": [ { "hash": "sha256:…", "refs": ["main"] } ], - "migrations": [ { "name": "…", "hash": "sha256:…", "fromContract": "sha256:…", "toContract": "sha256:…" } ] } ] } - -// status -{ "ok": true, "summary": "…", - "spaces": [ { "space": "app", "currentContract": "sha256:…", "targetContract": "sha256:…", - "migrations": [ { "name": "…", "hash": "…", "fromContract": "…", "toContract": "…", - "operationCount": 1, "createdAt": "…", "refs": [], "providedInvariants": [], - "status": "applied" } ] } ], - "diagnostics": [ { "code": "…", "message": "…" } ] } - -// log (entries → records; ledger apply events, not migration definitions) -{ "ok": true, "summary": "…", - "records": [ { "space": "app", "name": "…", "hash": "sha256:…", - "fromContract": "sha256:…", "toContract": "sha256:…", "appliedAt": "…", "operationCount": 1 } ] } - -// show (single item; dirPath + inner summary removed) -{ "ok": true, "summary": "…", - "migration": { "space": "app", "name": "…", "hash": "sha256:…", "fromContract": null, "toContract": "sha256:…", - "createdAt": "…", "operations": [ { "id": "…", "label": "…", "operationClass": "additive" } ], - "preview": { "statements": ["…"] } } } - -// check (failures take the error-envelope vocabulary + space) -{ "ok": false, "summary": "…", - "failures": [ { "space": "app", "code": "PN-MIG-CHECK-001", "where": "migrations/app/…", "why": "…", "fix": "…" } ] } -``` - -`status` migration entries carry every `list` field plus `status: "applied" | "pending" | null`. `status` diagnostics and the old `missingInvariantsLine` become structured objects (`missingInvariants → { ref, invariants: [...] }`), each carrying a `message` for the agent — never a bare prose line. `show.preview` stays `{ statements }` and is family-specific by nature (a document target renders differently). `refs`, `providedInvariants`, `operationCount` (where enumerating) vs full `operations` (in `show`), `createdAt` (authored) vs `appliedAt` (applied), and `operationClass` are kept as-is. - -### The schema lock - -Each command gets a co-located, exported arktype schema for its `--json` success shape (and `check`'s failure-outcome shape). The exported TypeScript result type is **derived from** the schema (`typeof Schema.infer`) so the command builds output against the same single source of truth. The golden/parity tests validate each command's real `--json` output against its schema, which is the runtime lock that prevents the shape and the schema drifting apart. - -## Coherence rationale - -One PR. The change is a single uniform sweep — the same renames and the same envelope rules applied across six sibling commands, plus their schemas and tests. Reviewing them together is the *only* way to judge the thing the slice is for ("are the six now consistent"); splitting shapes from schemas would re-touch every command file twice and let the schema drift from the shape it is supposed to pin. Large but coherent, and rollback-able as one unit. (Sizing was considered for a 2-slice split — shapes then schemas — and rejected for the re-touch + drift cost and the repo's preference for fewer, larger PRs.) - -## Scope - -**In:** the six commands' `if (flags.json)` construction + their exported result types (`migration-*.ts`); `migration-list-types.ts`, `migration-log-table.ts` (`SerializedLedgerEntryRecord` + `serializeLedgerEntriesForJson` → records), `integrity-violation-to-check-failure.ts` (`CheckFailure` → error-envelope vocab); the per-command arktype schemas (new); the human renderers that read the renamed fields (`migration-list-render`, `migration-graph-tree-render`/space-render, `migration-status-overlay`, `migration-log-table`); the golden + parity tests (`read-commands-json-golden`, `migration-list-json-golden`, `migration-read-commands-parity`). - -**Out:** the shared error envelope itself (we align `check` *to* it, not change it); the human (non-json) rendered *layout* (only field-name reads change, not the visual design — that was TML-2801's work); the commands' behaviour/flags (no new flags, no resolution changes); anything outside the six read verbs. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| Field renames ripple into the human renderers | In scope, expected | The result-type fields (`dirName`/`spaceId`/`from`/`to`/`markerHash`) are read by the tree/list/log renderers, not only the JSON path. Renaming the types touches those reads. Keep the rendered *layout* identical; only the field access changes. This is the largest non-obvious surface. | -| `graph` flat → nested-per-space | In scope, the one real structural change | Today graph builds global `nodes`/`edges`; the per-space form reuses the same per-space enumeration `list`/`status` already use (`aggregate.space(id).graph()`). Verify multi-space and single-space-elided fixtures still render and serialize. | -| `check` `ok:false` is an outcome, not an error envelope | Schema must model both | A consumer branching on `ok:false` must distinguish check's `{ failures }` from the error envelope `{ code }`. The schemas encode this; a parity test asserts the two are distinguishable. | -| `show.preview` is SQL-shaped | Accept, document | `{ statements }` is family-specific; not normalised across targets in this slice. | - -## Slice-specific done conditions - -- [ ] Every read command's `--json` matches its target shape above and validates against its co-located exported arktype schema (golden/parity tests assert real output parses); the human (non-json) output still renders correctly after the field renames. - -## Open Questions - -1. **Runtime self-validation.** Working position: the command builds output against the schema-derived type (compile-time), and the *tests* validate real output against the schema; the command does **not** re-validate its own output on every invocation (avoids per-call cost/noise). Revisit only if an agent-facing guarantee needs runtime enforcement. -2. **`status` diagnostic variants.** Working position: model the existing diagnostic kinds as a discriminated arktype union with a shared `{ code, message }` base; exact variant set pinned at implementation time from the current `StatusDiagnostic` producers. - -## Required-section notes - -- **Contract-impact:** none (no change to `packages/0-shared/contract/**` or framework-core; `LedgerEntryRecord` in `contract/src/types.ts` is the internal record — only the CLI's *serialized* form changes). -- **Adapter-impact:** the JSON is target-agnostic except `show.preview` (`{ statements }`), which is family-shaped by nature — noted, not normalised here. No per-adapter code changes. -- **ADR:** none. Governed by `docs/CLI Style Guide.md § JSON Semantics`; the agent-facing-JSON framing could be recorded there as part of the slice if it proves worth stating once. - -## References - -- Parent project: [`projects/migration-graph-rendering/spec.md`](../../spec.md) -- Prior slices: [`../read-command-consistency/spec.md`](../read-command-consistency/spec.md) (TML-2801 — unified the envelopes this redesigns), [`../check-single-target-multi-space/spec.md`](../check-single-target-multi-space/spec.md) (TML-2835) -- Linear issue: [TML-2836](https://linear.app/prisma-company/issue/TML-2836) -- Standard: [`docs/CLI Style Guide.md`](../../../../docs/CLI%20Style%20Guide.md) § JSON Semantics, § Errors -- Surfaces: `migration-{list,graph,status,log,show,check}.ts`; `migration-list-types.ts`; `migration-log-table.ts`; `integrity-violation-to-check-failure.ts` diff --git a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/code-review.md b/projects/migration-graph-rendering/slices/reconcile-control-api-surface/code-review.md deleted file mode 100644 index c8927ed35a..0000000000 --- a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/code-review.md +++ /dev/null @@ -1,62 +0,0 @@ -# Code review — `reconcile-control-api-surface` (TML-2780) - -> Initial scaffold. The reviewer maintains this document across rounds. The orchestrator and implementer read it but do not edit it (except the orchestrator's § Subagent IDs / § Orchestrator notes). - -## Summary - -- **Current verdict:** SATISFIED (round 1) -- **Dispatches SATISFIED:** D1, D2, D3, D4 -- **AC scoreboard totals:** 4 PASS / 0 FAIL / 0 NOT VERIFIED -- **Open findings:** 0 -- **Open escalations:** 0 - -## Acceptance criteria scoreboard - -> From `spec.md § Slice-specific done conditions`. Update every round. - -| AC ID | Description (short) | Dispatch | Status | Evidence | -| ----- | ------------------- | -------- | ------ | -------- | -| AC-1 | Glossary carries no migration-sense "applied"; Marker reads "migrated to"/"run"; tracker row added | D1 | PASS | `eca130276`; `rg -i "appl(ied\|ies\|ying)" docs/glossary.md` empty; Marker entry rewritten; tracker row added | -| AC-2 | `ControlClient.migrate` exists (no `migrationApply`); `ControlActionName`+`RunAction` carry `'migrate'`; cli-owned apply/migrationApply tokens gone | D2,D3 | PASS | `78576c44e`+`e98aade19`; cli/src apply-token grep empty; `migrate()` at client.ts:457; literals at types.ts:74 + run-migration.ts:29 | -| AC-3 | Files `run-migration.ts`+`db-run.ts`+`migrate.ts` exist; `apply.ts`+`db-apply.ts`+`migration-apply.ts` gone; all three carry strategy header docs | D4 | PASS | `6a7da65ae`; new files present + old gone; each carries command+strategy header doc | -| AC-4 | No behavioural diff: operation bodies unchanged except identifiers/imports | D2,D3,D4 | PASS | base-vs-final body diff per file: only identifier/comment renames + import paths; line-for-line parity, zero logic moved | - -Status values: `PASS` / `FAIL` / `NOT VERIFIED — ` / `ACCEPTED DEFERRAL — ` / `OUT OF SCOPE`. - -## Subagent IDs - -- **Implementer:** `a3d81e0dba4e5322c` (sonnet) — first spawned D1–D4 R1. -- **Reviewer:** `ad0cc9b433a8b0c57` (opus) — first spawned slice R1. - -## Findings log - -_(no findings yet)_ - -## Round notes - -### Slice R1 — SATISFIED - -**Scope:** D1–D4. Commits `eca130276..6a7da65ae` (`eca130276` glossary, `78576c44e` migrate, `e98aade19` apply→run, `6a7da65ae` file renames). 17 files. - -**Tasks:** D1 clean (glossary Marker + tracker row). D2 clean (`migrate()` + `Migrate*` family + `'migrate'` literal). D3 clean (`runMigration`/`RunAction`/`executeRun`/`executeMigrate`/`RUN_SPAN_ID` + "Running migration plan…" label). D4 clean (3 `git mv` + strategy header docs). - -**AC delta:** all four NOT VERIFIED → PASS. AC-1 `eca130276`. AC-2 `78576c44e`+`e98aade19`. AC-3 `6a7da65ae`. AC-4 confirmed by base-vs-final body diff. - -**AC-4 behavioural invariance:** verified by diffing each renamed file's final body against its pre-rename body at base `c159cd423` (normalising the in-scope identifier renames). Every residual line is an identifier rename, comment-prose rewording, or import-path/import-order change — line-for-line parity, no statement added/removed/reordered. `db-run.ts` has exact 423=423 line parity post-header; `migrate.ts` deltas are solely the type renames + biome collapsing the now-shorter `buildNeverPlannedFailure` signature onto one line (the `Contract` import is retained, used at the `contract: Contract` field). `RUN_SPAN_ID`/`spanId` value stays the wire-stable `'apply'` (constant *name* renamed, value intentionally unchanged). - -**Lockstep literals:** `'migrate'` agrees in `ControlActionName` (types.ts:74) and `RunAction` (run-migration.ts:29). `progressLabelForAction` switch is exhaustive (3 arms, no `default`, no stale `'migrationApply'`) — tsc-enforced. Both runtime-DATA asserts hand-updated (`apply.progress.test.ts:14`, `apply.test.ts:137,140`). - -**Scope discipline:** deferred items untouched — migration-tools `applyOrder`/`SpaceApplyInput`/`concatenate…`/`compute…ApplyPath` (diff clean), `MigrationApplyStep` retained (types.ts:596), `mode:'plan'|'apply'` retained (db-run.ts), formatters `formatMigrationApply*` retained. No back-compat alias added. JSDoc at types.ts:899-903 now documents real `MigrateOptions` fields (stale originHash/destinationHash/pendingMigrations gone). Transient-ID scan on `+` diff: zero hits. - -**MigrateRanEntry naming flag:** confirmed reads naturally — "migrate" namespace + "ran" (past tense of the `run` verb) is glossary-coherent. Result-object field names `applied`/`migrationsApplied` left as-is: renaming them is a public-shape change beyond naming-only scope (spec delegated only the *type* name). Accept. - -**Findings:** none. - -**For orchestrator:** Non-blocking, pre-existing (in base `c159cd423`, untouched here): `MigrateRanEntry` and the next interface each carry two stacked `/** */` JSDoc blocks (types.ts ~609-621, ~631-645). Out of this slice's naming-only scope — not filed. Worth a cleanup ticket if the team wants it tidied. - -## Orchestrator notes - -- **Decision (D1–D4 collapsed into one implementer delegation):** the four dispatches are tightly-coupled mechanical renames; ran them as one persistent-implementer delegation landing commit-per-dispatch with the grep+typecheck+cli-test gate after each. Per-commit gates all reported green. -- **Orchestrator DoD verification (pre-review):** 4 commits `eca130276` (glossary) / `78576c44e` (migrate rename) / `e98aade19` (apply→run) / `6a7da65ae` (file renames). cli/src grep for `migrationApply|applyMigration|ApplyAction|executeApply|APPLY_SPAN_ID|executeMigrationApply` → empty. `MigrationApplyStep` preserved (2 refs, types.ts). migration-tools deferred vocab untouched. Operation files renamed (run-migration.ts/db-run.ts/migrate.ts present; old gone). -- **origin/main advanced** to `b9c7119ae` (ADR 224, unrelated) after branching from `c159cd423`. Base is an ancestor of current main → GitHub three-dot diff is clean (17 files, no stray changes). No rebase required. -- Implementer's `MigrationApplyAppliedEntry` → **`MigrateRanEntry`** (free naming choice the spec delegated). Flagged to reviewer to confirm it reads naturally. diff --git a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/plan.md b/projects/migration-graph-rendering/slices/reconcile-control-api-surface/plan.md deleted file mode 100644 index 2842212f8c..0000000000 --- a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/plan.md +++ /dev/null @@ -1,40 +0,0 @@ -## Dispatch plan - -_Slice: `reconcile-control-api-surface`. Four sequential dispatches, one PR (commit-per-dispatch). Joints: language first (glossary), then the public verb (`migrate`), then the internal verb (`run`), then the file moves. Each commit is a self-contained reviewable unit; the split keeps "language vs public-API vs internal-rename vs file-move" legible in the diff._ - -### Dispatch 1: Retire "applied" from the glossary - -- **Outcome:** `docs/glossary.md` carries no "applied"/"applies"/"applying" referring to migrations. The Marker entry (line 202) reads "currently **migrated to**" and "a migration was **run**"; the Terminology Alignment Tracker table (~line 307) has a new row `apply (verb) / migrationApply → migrate (advance DB) / run (execute a migration)`, status **In progress**. -- **Builds on:** The spec's chosen design (the `migrate`/`run` two-level vocabulary). -- **Hands to:** A glossary that mandates the vocabulary the next three dispatches implement — the language is now the source of truth for the rename. -- **Focus:** `docs/glossary.md` only. **Out:** any code change; other docs' incidental "apply" usage. **Gate:** `rg -i "appl(ied|ies|ying)" docs/glossary.md` returns only non-migration hits (or empty); the tracker row exists. - -### Dispatch 2: Rename the public method surface to `migrate` - -- **Outcome:** `ControlClient` exposes `migrate(options)` (no `migrationApply`); the `MigrationApply*` option/result type family → `Migrate*`; the exported action literal is `'migrate'` in `ControlActionName`; every caller, test, README line, and cross-package doc comment that names the method or the literal is updated. `tsc` + the cli package suite are green. -- **Builds on:** Dispatch 1's glossary vocabulary. -- **Hands to:** A green tree where the public control-api verb is `migrate` end-to-end; the only remaining `apply`-vocab tokens are the cli-**internal** `applyMigration`/`ApplyAction`/`executeApply`/`executeMigrationApply` + the three filenames — all deferred to D3/D4. -- **Focus:** - - `types.ts:74` (`ControlActionName` member `'migrationApply'`→`'migrate'`), `types.ts:905` (interface decl), `client.ts:460` (impl) + `client.ts:462` (`connectWithProgress` arg). - - Type family `MigrationApply{Options,Result,Success,Failure,FailureCode,PathDecision,AppliedEntry}` → `Migrate{Options,Result,Success,Failure,FailureCode,PathDecision,RanEntry}` (impl picks the natural form for `…AppliedEntry`) across `types.ts`, `client.ts`, `migration-apply.ts`, `commands/migrate.ts`, `utils/cli-errors.ts`, `test/cli-errors.test.ts`. **Keep `MigrationApplyStep`** (deferred). - - Call site `commands/migrate.ts:315` (+ comment `:139`); the `action: 'migrationApply'`→`'migrate'` literal at `migration-apply.ts:289`; the `'migrationApply'` arm in `progressLabelForAction` (`apply.ts:258`) and the `ApplyAction` member (`apply.ts:24`) — these flip to `'migrate'` now even though the *type* `ApplyAction` is renamed in D3 (the literal is shared with the exported `ControlActionName`). - - Tests incl. the two runtime-literal asserts (`tsc` won't flag): `apply.progress.test.ts:13-14`, `apply.test.ts:136-140`; mock name in `migrate-to-contract.test.ts`. - - Docs: stale JSDoc `types.ts:899-902`; `README.md:1423`+`:1436`; postgres facade comment `control.ts:5`; `planner-types.ts:134` comment. - - **Out:** internal `apply`→`run` identifier renames + file moves (D3/D4); the spec's `Out` items. - - **Gate:** `pnpm typecheck`; `pnpm test:packages -- @prisma-next/cli` green; `rg "\bmigrationApply\b|MigrationApply(Options|Result|Success|Failure|FailureCode|PathDecision|AppliedEntry)" packages/1-framework/3-tooling/cli/src` empty. - -### Dispatch 3: Purge the cli-local `apply` vocabulary to `run` - -- **Outcome:** The internal shared primitive and its types speak `run`: `applyMigration`→`runMigration`, `ApplyAction`→`RunAction`, `ApplyMigrationInputs`/`…Value`/`…Result`→`RunMigration*`, `ApplyRunnerFailure`→`RunnerFailure`, `APPLY_SPAN_ID`→`RUN_SPAN_ID`, the `'Applying migration plan…'` progress label→`'Running migration plan…'`; `executeApply`→`executeRun`; `executeMigrationApply`/`ExecuteMigrationApplyOptions`→`executeMigrate`/`ExecuteMigrateOptions`. **No file moves yet** (filenames still `apply.ts`/`db-apply.ts`/`migration-apply.ts`). `tsc` + cli suite green. -- **Builds on:** Dispatch 2's hand-off — the public verb is already `migrate`, so the only `apply` tokens left are these internal identifiers. -- **Hands to:** A green tree where every cli-owned identifier speaks `run`/`migrate`; the only remaining `apply` tokens are the three filenames (D4) and the migration-tools imports (`applyOrder`/`SpaceApplyInput`, deferred follow-up). -- **Focus:** identifier renames within `control-api/operations/{apply,db-apply,migration-apply}.ts`, `client.ts`, and their tests; update the `RunAction` switch exhaustiveness. **Out:** file renames (D4); migration-tools' `applyOrder`/`SpaceApplyInput`/`concatenate…`/`compute…ApplyPath`; `mode:'apply'`; behavioural change. **Gate:** `pnpm typecheck`; cli suite green; `rg "applyMigration|ApplyAction|executeApply|ApplyRunnerFailure|ApplyMigration|APPLY_SPAN_ID" packages/1-framework/3-tooling/cli/src` empty. - -### Dispatch 4: Rename the operation files + add strategy header docs - -- **Outcome:** `git mv apply.ts run-migration.ts`, `db-apply.ts db-run.ts`, `migration-apply.ts migrate.ts`; all imports updated; each of the three files carries a header doc stating the command(s) it backs and its strategy. `tsc` + cli suite green; no behavioural diff. -- **Builds on:** Dispatch 3's hand-off — identifiers already speak `run`/`migrate`, so this dispatch only moves files and writes docs. -- **Hands to:** The slice-DoD: control-api operation files map onto the commands they back, fully documented, zero cli-owned `apply` tokens, glossary aligned. Ready for PR. -- **Focus:** the three `git mv`s; update importers (`client.ts` import of `./operations/migration-apply`→`./operations/migrate`, the `./apply`→`./run-migration` self-imports in `db-run.ts`/`migrate.ts` and tests under `test/control-api/`); add header docs to `run-migration.ts` / `db-run.ts` / `migrate.ts` (per spec §3). **Out:** any identifier change beyond import paths; behavioural change. **Gate:** `pnpm typecheck`; cli suite green; `test -f .../operations/run-migration.ts && test -f .../operations/db-run.ts && test -f .../operations/migrate.ts && ! test -e .../operations/apply.ts && ! test -e .../operations/db-apply.ts && ! test -e .../operations/migration-apply.ts`; `rg "migrationApply|applyMigration|ApplyAction|executeApply" packages/1-framework/3-tooling/cli/src` empty; reviewer confirms bodies unchanged except identifiers/imports. - -_Each dispatch passes dispatch-INVEST: D1 is a contained doc edit; D2/D3 are mechanical "fan-out" renames with binary grep + typecheck + test gates and named runtime-literal catches; D4 is file-move + import fix-up + doc headers. All are Small (each fits one executor session, references scoped to the named files), Estimable (binary gates), Independent given the D1→D2→D3→D4 hand-offs. No behavioural surface moves in any. Total 4 ≤ 10. Ships as one PR, four commits._ diff --git a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/spec.md b/projects/migration-graph-rendering/slices/reconcile-control-api-surface/spec.md deleted file mode 100644 index 0dfab97b62..0000000000 --- a/projects/migration-graph-rendering/slices/reconcile-control-api-surface/spec.md +++ /dev/null @@ -1,116 +0,0 @@ -# Slice: reconcile the control-api surface with the CLI command surface - -_Parent project `projects/migration-graph-rendering/`. Outcome: the control-api surface the read-command family (`migration status` / `log`) is growing against speaks the project's ubiquitous language — `migrate` for advancing the database, `run` for executing a migration's ops — with no method or type encoding the retired "apply migrations" phrasing._ - -## At a glance - -The CLI control-api surface (`ControlClient` methods + `cli/src/control-api/operations/*`) has drifted from both the command surface and the glossary: `client.migrationApply(...)` is the apply entry point, but the command that drives it is `migrate`, and **"apply (a) migration" is not in our ubiquitous language** — the glossary anchors `migration` as a noun, `migrate` as the verb for advancing a live DB, and `run` as the control-plane act of executing a migration's ops. This slice (1) updates the glossary to retire "applied", (2) renames the public method `migrationApply` → `migrate` (+ its `Migrate*` types and `'migrate'` action literal), and (3) purges the cli-local `apply` vocabulary to `run`, renaming + documenting the three operation files. Naming/structure only — **no behavioural change** to apply/plan/verify. - -## Chosen design - -The glossary's two-level vocabulary is the spine of the rename: - -| Concept | Ubiquitous-language verb | Where it lands | -| --- | --- | --- | -| Advancing a live DB along the migration graph | **`migrate`** | the `migrate` command → `ControlClient.migrate()` (public surface) | -| Executing a migration's ops via the runner | **`run`** | the cli-local shared primitive `runMigration` and the init/update/migrate operation files | - -### 1. Glossary — retire "applied" - -`docs/glossary.md:202` (Marker entry) is the only line carrying the word. Rewrite both instances: -- "tracks which contract is currently **applied**" → "tracks which contract the database is currently **migrated to**" (the marker's state is "where the DB sits in the graph"). -- "if a migration was **applied** but the application wasn't redeployed" → "if a migration was **run** but the application wasn't redeployed". - -Add a row to the Terminology Alignment Tracker table (`~line 307`): - -| User-facing term | Current internal term | Scope | Status | -| --- | --- | --- | --- | -| `migrate` (advance DB) / `run` (execute a migration) | `apply` (verb) / `migrationApply` | Control-api method + types, internal naming, docs | **In progress** | - -### 2. Public surface → `migrate` - -| Surface (exported / reachable) | Before | After | -| --- | --- | --- | -| `ControlClient` method | `migrationApply(options)` | `migrate(options)` | -| Option/result types | `MigrationApply{Options,Result,Success,Failure,FailureCode,PathDecision,AppliedEntry}` | `Migrate{Options,Result,Success,Failure,FailureCode,PathDecision,RanEntry}` (impl picks the natural suffix for the last two; `…AppliedEntry` → a `run`/`migrate` form) | -| Progress union literal | `'migrationApply'` in `ControlActionName` (exported) | `'migrate'` | - -`migrate` is the glossary verb **and** the command name — perfect 1:1 traceability (command `migrate` → `client.migrate()`), which is the ticket's core goal. (Earlier `applyMigrations` is rejected outright: "apply migrations" is off-language.) - -### 3. Cli-local `apply` → `run` - -All of these are defined and consumed **entirely within `cli/src/control-api/`** (investigation: zero references outside cli + migration-tools; the migration-tools half is deferred — see Scope/Out): - -| Before | After | File | -| --- | --- | --- | -| `applyMigration` (shared runner primitive) | `runMigration` | `apply.ts` → `run-migration.ts` | -| `ApplyAction = 'dbInit'\|'dbUpdate'\|'migrationApply'` | `RunAction = 'dbInit'\|'dbUpdate'\|'migrate'` | same file | -| `ApplyMigrationInputs` / `…Value` / `…Result` | `RunMigrationInputs` / `…Value` / `…Result` | same file | -| `ApplyRunnerFailure` | `RunnerFailure` | same file | -| `APPLY_SPAN_ID` / `'Applying migration plan…'` label | `RUN_SPAN_ID` / `'Running migration plan…'` | same file | -| `executeApply` (backs `db init` + `db update`) | `executeRun` | `db-apply.ts` → `db-run.ts` | -| `executeMigrationApply` / `ExecuteMigrationApplyOptions` (backs `migrate`) | `executeMigrate` / `ExecuteMigrateOptions` | `migration-apply.ts` → `migrate.ts` | - -Operation-file structure stays **split** (the synth-vs-replay heads are load-bearingly disjoint; merging was rejected in the prior design pass). Each file gains a header doc naming the command(s) it backs and its strategy: -- `run-migration.ts` — shared runner tail; backs no command directly. -- `db-run.ts` — backs `db init` / `db update`; strategy = introspect → `planMigration`, synth-for-app + graph-walk-extensions. -- `migrate.ts` — backs `migrate`; strategy = graph-walk-all-members, replay-only. - -### 4. Lockstep literal sites (a method find-replace will NOT catch these) - -- `ControlActionName` member — `types.ts:74` -- `RunAction` member + `progressLabelForAction` switch arm — `apply.ts:24,258` (no `default` ⇒ `tsc` exhaustiveness catches a stale arm) -- `connectWithProgress(..., 'migrationApply', ...)` — `client.ts:462` (typed ⇒ `tsc` catches) -- `action: 'migrationApply'` passed to the primitive — `migration-apply.ts:289` -- **Runtime-data assertions (`tsc` does NOT catch — hand-edit):** `test/control-api/apply.progress.test.ts:13-14`, `test/control-api/apply.test.ts:136-140` - -### 5. Doc/comment fixes carried along - -Stale JSDoc `types.ts:899-902` (documents `originHash`/`destinationHash`/`pendingMigrations` — none exist on the options type; rewrite to the real fields); `README.md:1423` + `:1436`; postgres facade comment `packages/3-extensions/postgres/src/exports/control.ts:5`; comment at `migration/src/aggregate/planner-types.ts:134`. - -## Coherence rationale - -One theme: bring the control-api migrate path and the glossary into agreement on `migrate`/`run`, eliminating "apply migrations." Every diff hunk is a glossary sentence, an identifier rename, a `git mv`, or a header comment — one concept moving, with `tsc` + two named runtime tests as the safety net. A reviewer holds it in one sitting. - -## Scope - -**In:** `docs/glossary.md` (line 202 + tracker row); the exported `migrate` method + `Migrate*` types + `'migrate'` literal; the cli-local `apply`→`run` purge (identifiers above) + 3 file renames + header docs; the `migrate.ts` call site in `commands/migrate.ts`; affected cli tests; the four doc/comment fixes. - -**Out (deliberately — keeps the slice in one package, and respects "ask first" surfaces):** -- **`migration-tools` `apply` sequencing vocabulary** — `applyOrder` (planner-types), `SpaceApplyInput`, `concatenateSpaceApplyInputs`, `computeExtensionSpaceApplyPath`/`ExtensionSpaceApplyPathOutcome`. A second package with its own planner tests, and "apply-*order*" is sequencing language a half-step off the verb. Residual: cli's `runMigration` will still consume an `applyOrder` field / `SpaceApplyInput` from migration-tools. **Flagged as the follow-up** to finish the glossary-tracker row. -- **`MigrationApplyStep`** — its JSDoc says the name is deliberately back-compat-kept and ADR 208 references it; renaming ripples into an ADR (ask-first surface). Defer with the migration-tools follow-up. -- **`mode: 'plan' | 'apply'`** — a distinct plan-vs-execute concept ("apply mode"), not "apply a migration". Untouched. -- Behavioural changes to apply/plan/verify (`executeRun`/`executeMigrate`/`runMigration` bodies unchanged except identifiers). -- Formatters `formatMigrationApplyOutput` / `formatMigrationApplyCommandOutput` (presentation; the former formats `db init`/`db update` output). -- The four integration journey-suite local `migrationApply()` helpers (owned by `projects/migration-domain-model` M2). -- Re-exporting the `Migrate*` option/result types from `src/exports/control-api.ts` (pre-existing gap; out of naming-only scope). -- Any deprecated alias / backwards-compat export (repo policy: clean break). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --- | --- | --- | -| Two tests assert `'migrationApply'` as runtime string data | Must hand-edit | `tsc` can't catch a bare-string assertion: `apply.progress.test.ts:13-14`, `apply.test.ts:136-140`. | -| `applyOrder` / `SpaceApplyInput` from migration-tools remain after the cli purge | Expected residual | Those are another package's API names; the DoD grep targets cli-owned `apply`/`migrationApply` tokens, not migration-tools imports. Finishing them is the deferred follow-up. | -| Journey-suite `migrationApply()` helpers collide with this rename | Do **not** touch | Local CLI-shelling helpers, not the control-api method; owned by another in-flight project. | -| `MigrationApplyStep` referenced by ADR 208 | Keep + defer | Renaming drifts an ADR (ask-first). | - -## Slice-specific done conditions - -- [ ] `docs/glossary.md` contains no "applied"/"applies"/"applying" referring to migrations; the Marker entry reads "migrated to"/"run"; the tracker table has the `apply → migrate/run` row. -- [ ] `rg "migrationApply|applyMigration|ApplyAction|executeApply|ApplyRunnerFailure|ApplyMigration" packages/1-framework/3-tooling/cli/src` returns empty (cli-owned apply/migrationApply tokens gone). `ControlClient` exposes `migrate`; `ControlActionName` + `RunAction` carry `'migrate'`. -- [ ] Files `run-migration.ts` + `db-run.ts` + `migrate.ts` exist; `apply.ts` + `db-apply.ts` + `migration-apply.ts` are gone; all three carry a header doc naming command + strategy. -- [ ] No behavioural diff: operation bodies unchanged except identifiers (reviewer confirms from the diff). - -## Open Questions - -1. Include the `migration-tools` `apply`-sequencing rename (`applyOrder`→`runOrder`, `SpaceApplyInput`→`SpaceRunInput`, …) in this slice, or ship it as the follow-up that closes the tracker row? Working position: **follow-up** — keeps this slice one-package and cleanly reviewable; the tracker row is marked "In progress" to record the remainder. -2. Rename `MigrationApplyStep` + update ADR 208 now, or with the follow-up? Working position: **follow-up** (ADR edits are ask-first). -3. Should the `Migrate*` option/result types be re-exported from `control-api.ts` so a programmatic `client.migrate(...)` consumer can name them? Working position: **no — out of scope** (public-surface addition, not a rename); flag as a possible follow-up. - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md` -- Linear issue: [TML-2780](https://linear.app/prisma-company/issue/TML-2780/reconcile-the-control-api-surface-with-the-cli-command-surface) — branch `tml-2780-reconcile-the-control-api-surface-with-the-cli-command` -- Glossary: `docs/glossary.md` §"Migration & Database Lifecycle" (`migrate` verb / `migration` noun) + Terminology Alignment Tracker -- Repo policy: CLAUDE.md "Don't add backwards-compat exports unless asked"; `.agents/rules/no-backward-compatibility.mdc` diff --git a/projects/migration-graph-rendering/slices/remove-list-graph-renderer/spec.md b/projects/migration-graph-rendering/slices/remove-list-graph-renderer/spec.md deleted file mode 100644 index 1a415870bc..0000000000 --- a/projects/migration-graph-rendering/slices/remove-list-graph-renderer/spec.md +++ /dev/null @@ -1,93 +0,0 @@ -# Slice: retire `migration list --graph` (Tier-2 list-graph renderer) - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to the project's purpose: now that the Tier-3 `migration graph` tree renderer draws the whole history compactly and correctly, the Tier-2 list-graph gutter is the redundant middle — this slice removes it, leaving exactly one graph renderer to maintain._ - -## At a glance - -Remove the `--graph` flag from `migration list` and delete its ASCII list-graph engine (`migration-list-graph-render.ts` + `migration-list-graph-layout.ts`), keeping plain `migration list` (chronological, all spaces) and `migration graph` (topological tree). The shared edge classifier (`migration-list-graph-topology.ts`) stays — the tree depends on it. - -## Chosen design - -`migration list --graph` overlays a `git log --graph`-style branch gutter on the chronological (latest-first) migration list. For a linear history it reads fine; for anything with merges + rollbacks the two orderings fight and the output degrades: back-edges collapse to flat `↩` rows with no connection to where they land, merge nodes float detached from their labels (`o │`), and the `├─┬─┐` / `├─┘` gutter reads as noise. The Tier-3 tree renderer now covers that job — topologically ordered, routed back-arcs, refs/db/contract overlays, disjoint components — strictly better. - -The two views that earn their keep are the **chronological log** (`migration list`, flat) and the **topological shape** (`migration graph`, tree). The hybrid is the one to drop. - -**Removed:** - -- `migration list`'s `--graph` flag and the `graph` branch of `renderMigrationListHumanOutput`. -- `migration-list-graph-render.ts`, `migration-list-graph-layout.ts`, and their tests/fixtures (`migration-list-graph-render.test.ts`, `migration-list-graph-layout.test.ts`, `migration-list-graph-fixtures.ts`). -- The Tier-2 reference doc `docs/reference/migration-list-graph-rendering.md` and its links from `docs/README.md`. - -**Kept:** - -- `migration list` (flat) — unchanged, including `--ascii`, which still selects the glyph mode for the flat list's kind column (`* ↩ ⟲` → ASCII). `--ascii` is **not** removed; its help text drops the "for --graph" qualifier. -- `migration-list-graph-topology.ts` — the forward/rollback/self classifier, shared with the Tier-3 tree (`migration-graph-rows.ts`). The `GlyphMode` type, currently re-exported through the deleted `migration-list-graph-render.ts`, is re-sourced from its real home (`../glyph-mode`). - -Before → after for `migration list`: - -| Surface | Before | After | -|---|---|---| -| `migration list` | flat list, all spaces | unchanged | -| `migration list --graph` | ASCII branch gutter | flag removed (errors as unknown option) | -| `migration list --ascii` | ASCII glyphs (flat + graph) | ASCII glyphs (flat) — unchanged behaviour for the flat list | -| `migration graph` | topological tree | unchanged | - -## Coherence rationale - -One reviewer holds this in one sitting: it deletes one renderer and the single flag that reaches it, behind an unchanged flat-list path and an unchanged Tier-3 tree. The call-site grep is the acceptance surface — after the removal, nothing imports `migration-list-graph-{render,layout}` and `migration list --graph` is gone. Rolls back as one unit. - -## Scope - -**In:** - -- Delete the Tier-2 list-graph renderer + layout modules and their tests/fixtures. -- Remove the `--graph` flag, its render branch, and `MigrationListHumanRenderOptions.graph` from `migration-list.ts`; re-source `GlyphMode`; fix the command description/examples. -- Delete `docs/reference/migration-list-graph-rendering.md`; delink from `docs/README.md`; scrub any Tier-2 cross-reference in `docs/reference/migration-graph-rendering.md`. -- Drop `--graph` cases from `migration-list.test.ts` and any CLI-journey/e2e coverage that drives `list --graph`. - -**Out:** - -- `migration-list-graph-topology.ts` (shared classifier) — kept. -- The flat `migration list` output and its `--ascii` glyph behaviour — kept. -- The Tier-3 tree renderer — untouched. -- Teaching `migration graph` to render non-app contract spaces (see Open Questions) — not folded in unless the operator says so. -- The dagre renderer + `migration status` — that's TML-2748, independent. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -|---|---|---| -| `--ascii` looks like a `--graph`-only flag | Keep `--ascii`; it drives the flat-list kind glyphs too | `renderMigrationListWithStyle` consumes `glyphMode`; only the help text's "for --graph" qualifier is wrong. | -| `GlyphMode` imported from the deleted renderer | Re-source from `../glyph-mode` | `migration-list.ts` imports `GlyphMode` via `migration-list-graph-render.ts`; that re-export disappears with the file. | -| `migration-list-graph-fixtures.ts` | Delete with the render test | Imported only by `migration-list-graph-render.test.ts`; the topology test does not use it. | - -## Slice-specific done conditions - -- [ ] `rg "migration-list-graph-(render|layout)"` over `src/` + `test/` returns zero hits, and `migration list --graph` exits as an unknown option; the shared topology classifier and its test remain green. - -## Open Questions - -1. **Multi-space graph visibility.** `migration list --graph` iterated every on-disk contract space; `migration graph` renders only the app space. Removing Tier-2 drops the only per-space *graph* view (flat `list` still shows all spaces). Working position: **accept the loss in this slice** — extension/non-app spaces rarely need a topology view, and a `migration graph --space ` follow-up can restore it if users ask. Fold a `--space` flag into this slice only if the operator prefers. - -## References - -- Parent project: `projects/migration-graph-rendering/spec.md` (Tier-3 redesign — this slice's predecessor). -- Linear issue: [TML-2765](https://linear.app/prisma-company/issue/TML-2765) (standalone, related to TML-2746 and TML-2748). -- Surfaces removed: `cli/src/utils/formatters/migration-list-graph-{render,layout}.ts` (+ tests/fixtures), `cli/src/commands/migration-list.ts` (`--graph`), `docs/reference/migration-list-graph-rendering.md`. -- Surfaces kept: `migration-list-graph-topology.ts` (shared with `migration-graph-rows.ts`), flat `migration list`, the Tier-3 tree renderer. - -## Dispatch plan - -### Dispatch 1: remove the `--graph` flag and delete the Tier-2 renderer - -- **Outcome:** `migration list --graph` is gone; `migration-list-graph-{render,layout}.ts` and their tests/fixtures are deleted; `migration-list.ts` drops the `graph` option/branch, re-sources `GlyphMode`, and fixes its description/examples; `migration-list.test.ts` drops `--graph` cases. Flat `list` (incl. `--ascii`) and the shared topology classifier stay green. -- **Builds on:** This spec; the grounded footprint (only `migration-list.ts` imports the deleted modules). -- **Hands to:** A green workspace where the call-site grep returns zero and `migration list` behaves identically except the removed flag. -- **Focus:** Code + test removal only. Docs in dispatch 2. - -### Dispatch 2: docs cleanup - -- **Outcome:** `docs/reference/migration-list-graph-rendering.md` deleted; `docs/README.md` delinked; any Tier-2 cross-reference in `docs/reference/migration-graph-rendering.md` scrubbed. Reference-link lint (if any) green. -- **Builds on:** Dispatch 1. -- **Hands to:** Slice-DoD: zero dangling references to the removed surface across docs + code. -- **Focus:** Docs only. No behaviour change. diff --git a/projects/migration-graph-rendering/slices/render-polish-and-ledger-tests/spec.md b/projects/migration-graph-rendering/slices/render-polish-and-ledger-tests/spec.md deleted file mode 100644 index ce071cb4cc..0000000000 --- a/projects/migration-graph-rendering/slices/render-polish-and-ledger-tests/spec.md +++ /dev/null @@ -1,395 +0,0 @@ -# Slice: render-polish-and-ledger-tests - -_Parent project `projects/migration-graph-rendering/`. Outcome: close the four follow-ups on the just-merged read-command family in one PR — unify pretty rendering across `list`/`status`/`graph` (D1 enforcement, locked trunk-choice rule), align migration data columns, color the gutter + add `--legend`, and close the two open test-coverage gaps in the ledger journal._ - -_Tracking: [TML-2812](https://linear.app/prisma-company/issue/TML-2812) (lead) + [TML-2811](https://linear.app/prisma-company/issue/TML-2811) + [TML-2773](https://linear.app/prisma-company/issue/TML-2773) + [TML-2774](https://linear.app/prisma-company/issue/TML-2774) (open items only). Supersedes the on-disk slice drafts `unify-pretty-rendering` and `lane-colors-and-legend`._ - -## At a glance - -``` -$ migration list $ migration status $ migration graph - -app: app: app: - ○ 1375f13 (contract) ○ 1375f13 (db, contract) ○ 1375f13 (contract) - │↑ 20260603_migration ∅ → 1375f13 10 ops │↑ 20260603_migration ∅ → 1375f13 ✓ applied │↑ 20260603_migration ∅ → 1375f13 10 ops - │ ○ f7a8eb5 (prod) │ ○ f7a8eb5 (prod) │ ○ f7a8eb5 (prod) - │ │↑ 20260518_bookend 6cee614 → f7a8eb5 0 ops │ │↑ 20260518_bookend 6cee614 → f7a8eb5 ⧗ pending │ │↑ 20260518_bookend 6cee614 → f7a8eb5 0 ops - │ … │ … │ … - ├──╯ ├──╯ ├──╯ - ∅ ∅ ∅ - -pgvector: pgvector: pgvector: - ○ 29059df (head, db, contract) ○ 29059df (head, db, contract) ○ 29059df (head, db, contract) - │↑ install_vector_v1 ∅ → 29059df 1 ops {pgvector:install-vector-v1} │↑ install_vector_v1 ∅ → 29059df 1 ops {pgvector:install-vector-v1} ✓ applied │↑ install_vector_v1 ∅ → 29059df 1 ops {pgvector:install-vector-v1} - ∅ ∅ ∅ - -7 migrations across 2 contract spaces up to date 7 nodes, 6 edges -``` - -Same input, three commands, byte-identical per-space sections — modulo `status`'s applied/pending overlay column and per-command footer. Three column-aligned migration rows per space; lane glyphs (`│ ├ ╯`) carry per-column color; the `○` node glyph takes its lane color while direction arrows (`↑ ↓ ⟲`) stay bright. `migration graph --legend` prints a key to stderr; `--ascii` / `NO_COLOR` / piped output stay text-clean. - -## What this slice closes - -Six independent follow-ups, all surfaced after the read-command-family slices ([#704](https://github.com/prisma/prisma-next/pull/704) `log` / [#705](https://github.com/prisma/prisma-next/pull/705) `status` / [#706](https://github.com/prisma/prisma-next/pull/706) `list`→tree) merged. Bundled because items 1–3 + 6 touch the same handful of rendering modules (`migration-graph-rows.ts`, `migration-graph-tree-render.ts`, `migration-list-data-column.ts`, `migration-list-styler.ts`), item 4 is a small test-coverage gap that should not orphan its own PR, and item 5 plus the no-path-guard half of item 6 are coupled one-line edits in `migration-status.ts`. One reviewer holds them in one sitting. - -1. **Unify pretty rendering** ([TML-2812](https://linear.app/prisma-company/issue/TML-2812)) — same DB state, same on-disk migrations: `list` / `status` / `graph` produce **byte-identical** pretty rendering of the per-space sections, modulo `status`'s overlay column and per-command footer. Closes the gap between project decision D1 ("one shared graphical renderer, command-specific annotations") and the on-disk reality where the three commands diverge in (a) **trunk choice** (live-contract chain vs historical-ref chain), (b) **per-row data** (full vs overlay-only vs none), and (c) **space iteration** (`graph` silently scopes to one space). -2. **Column alignment** ([TML-2811](https://linear.app/prisma-company/issue/TML-2811)) — pad the tree-prefix to a consistent visible width per render block so `dirName` starts at the same column on every migration row, regardless of how deep that row sits in the tree drawing. -3. **Colored lanes + `--legend`** ([TML-2773](https://linear.app/prisma-company/issue/TML-2773)) — `git log --graph`-style per-column coloring of the gutter; routed back-arcs colored as one hue per arc; node glyph `○` colored by lane; direction arrows stay bright; opt-in `--legend` block on `migration graph`. -4. **Ledger op-count test coverage** ([TML-2774](https://linear.app/prisma-company/issue/TML-2774) items 3 + 5, the two open follow-ups) — one cross-target table-driven harness asserting SQL + Mongo emit the same `operationCount` per applied edge for the same plan (including a skipped-op apply); covers the Postgres op-count-mismatch throw path that's currently uncovered. -5. **`migration status` default target + no-path wording** (D21, surfaced during QA) — fix `resolveTargetHashForSpace` so the default target is the live contract whenever there is one (matching `migrate`'s default and preventing the "(db, contract) ... cannot reach the selected target" contradiction); reword the no-path summary to name the actual missing thing (a migration path), name both endpoints, and lead with `migration plan` as the fix. -6. **System-marker rendering + fresh-DB no-path** (D22, surfaced post-D6) — render system markers (`contract`, `db`) in `<>` brackets to distinguish them syntactically from user-managed refs (`()`); fix the `markerHash !== undefined` guard in the path check so a fresh DB with the live contract disconnected from `∅` reports the no-path summary instead of "up to date". -7. **Cross-space data-column alignment + contract-node refs adjacent to hash + right-justified from-hash column** (D23, surfaced post-D7) — three coupled padding rules at the row→string join in the renderer. -8. **Cross-space dirName alignment + capitalised "Up to date"** (D24, surfaced post-D8 QA) — extend D23's cross-space alignment to the dirName column too; capitalise the no-pending-no-error summary headline. -9. **Trunk-choice rule honors `contractHash` for connected components** (D25, surfaced post-D8 QA) — fix the leaf-selection / rank step in `migration-graph-rows.ts` so the chain ending at the live contract wins the trunk position even when both chains share `∅`. D14 was specced; the algorithm wasn't fully wired. -10. **Legend wording + universal `--legend` across graph-drawing commands** (D26, surfaced post-D11 QA) — replace the marker/ref legend line with two parallel lines following D22's `<>` / `()` convention; hoist the legend helpers out of `migration-graph.ts` and wire `--legend` into `migration list` and `migration status`. - -## Locked decisions - -### D14 — Trunk-choice rule: live-contract chain (carried from project decisions) - -The trunk is the chain containing the **live contract** — the contract emitted from the on-disk schema, the same contract `migrate` defaults to advancing toward when no `--to` is passed. Disconnected sub-graphs render as side-branches indented one level. `list` already implements this via its `contractHash` argument to `buildMigrationGraphRows`; `status` and `graph` adopt it by passing the same value at their call sites. The trunk-choice algorithm itself is not parametrised by command — there's one rule, one input, three call sites that pass the same input. - -### D15 — Per-row data shape, with one overlay knob - -Every migration row, in every command, renders `dirName · from → to · N ops · {invariants}` (the full shape `list` uses today). `status` appends one overlay column at the right end — `✓ applied` / `⧗ pending` / blank. The shared `edgeAnnotationsByHash` contract introduced in D11 already carries the surface needed; `graph` adopts the list overlay verbatim, and `status` extends it (rather than replacing it) by composing a "list overlay + status column" annotation builder. - -### D16 — Space iteration: all spaces by default in all three commands - -`graph` adopts the all-spaces-by-default policy `list` and `status` already use (D4). `--space ` narrows uniformly. Each space renders as a per-space section with a `:` heading when more than one space is present. - -### D17 — Column alignment: per-render-block, not global - -Within a single render block (one space's per-space section, or a single-space rendering), compute the maximum visible width of the tree-prefix across all migration rows in the block, then right-pad each row's tree-prefix to that width before appending the data block. **Per-render-block, not global** — each space's tree depth may differ; alignment across spaces is not asserted (call it out if a future spec wants it). Visible-width calculation operates on rendered glyphs, not raw byte length, so wide / dim characters don't drift; padding is plain whitespace, not styled. Non-migration rows (contract nodes, blank/connector rows) are unaffected — only migration data rows participate in the alignment. - -### D18 — Lane coloring rules (locked from the start; no post-D1 refinement dispatch) - -Six rules, all locked, all implemented in one dispatch: - -1. **Vertical lanes (`│`) and structural connectors (`├ ┤ ╮ ╯ ┴ ┬ ┼`)** are colored by their column index, using a rotating palette over `colorette` hues. -2. **Column 0 stays uncolored** (default neutral / dim lane style); the palette rotates over columns ≥ 1. The single-lane linear case is therefore effectively monochrome. -3. **Routed back-arcs render in a single hue** (their owning back-lane color) across the whole arc — vertical back-lane, horizontal bridges, corners, `◂` landing. **Crossings (`┼`) stay dim/neutral** so neither overlapping arc "steals" the cell. -4. **Contract node glyph (`○`)** takes its column's lane color (column-0-neutral rule applies). In the `○◂` / `○─` arc-pair node markers, the `○` half takes the node's lane color and the connector half follows rule 3. -5. **Direction arrows (`↑ ↓ ⟲`) stay bright** — they encode direction, not branch identity, so they pop against the colored gutter. -6. **Data columns (`dirName`, `from → to`, `(refs)`)** are unchanged — color is a gutter / node-marker concern only. - -The palette is a 6-hue rotating `colorette` cycle (implementer's choice of hues; constraint: legible on both light and dark terminals, no clash with the green `(refs)` overlay or cyan hashes that would render adjacent tokens indistinguishable). Color is fully gated on the existing `colorize` flag — `--no-color` / `NO_COLOR` / non-TTY / piped output emits zero ANSI and existing plain-text goldens stay byte-identical. - -### D19 — `--legend` placement and gating - -A `--legend` boolean option on `migration graph` only (not `list`, not `status`). When set: print a legend block to **stderr** so stdout stays pure graph output and `migration graph | …` pipes cleanly. The legend honors the active glyph palette (unicode vs `--ascii`) and `colorize` state — the lane-color key only renders when `colorize` is true. `--legend` does **not** auto-enable any other flag (the original draft proposed auto-enabling `--tree`; that flag was already retired in the `status` slice, so there's nothing to auto-enable). - -### D21 — `migration status` default target is the live contract; no-path wording is path-shaped, not marker-shaped - -Two coupled fixes on `migration status`'s no-path failure mode, both surfaced during QA of this slice and folded in rather than spilled to follow-up tickets (the wording change is a one-line edit; the picker change is a 4-line edit; neither warrants a separate PR). - -**Picker (D14 alignment, radically simplified).** `migration status` has exactly **one** target per invocation. If the user passed `--to `, that's it. Otherwise it's the application's contract (`contractHash`). End of story. The picker collapses to a one-liner: - -```ts -function resolveTarget( - contractHash: string, - activeRefHash: string | undefined, -): string { - return activeRefHash ?? contractHash; -} -``` - -That deletes today's "graph membership" guard (`graph.nodes.has(activeRefHash)` / `graph.nodes.has(contractHash)`) and the single-leaf / head-ref fallbacks. The contract envelope can fail to read; the existing `CONTRACT.UNREADABLE` diagnostic already covers that case and `contractHash` defaults to `EMPTY_CONTRACT_HASH` when it does — the simplified picker returns that value and the no-path summary fires naturally, no special-casing needed. - -This also kills the dead `'Multiple valid migration paths — select a target with --to'` summary branch, the `MIGRATION.DIVERGED` diagnostic, and the `hasAmbiguousTarget` guard in `executeMigrationStatusCommand`'s loop. They are unreachable once the picker is total. Imports of `requireHeadRef` and `findReachableLeaves` in `migration-status.ts` are no longer used and get removed (other callers in the framework are unaffected — those helpers stay in their home modules). - -**Wording (no-path summary).** "Database marker cannot reach the selected target" mis-frames the failure: markers don't reach, paths exist or don't; "selected" implies the user picked. Three context-aware variants: - -- No `--to` (default = live contract): `No migration path from the database state (sha256:abc1234) to the application's contract (sha256:def5678). Run \`prisma-next migration plan --name \` to author one.` -- `--to `: `No migration path from the database state (sha256:abc1234) to the target (sha256:def5678 via \`prod\`). Run \`prisma-next migration plan --name \` to author one, or pass \`--to \` to pick a reachable target.` -- `--to `: same as the ref variant minus `via \`\``. - -Lead-fix is `migration plan` — the most common cause of the failure is "no migration has been authored from the DB state to the live contract yet", and `plan` is what the user needs. The `--to ` alternative only appears when `--to` was explicit (telling someone who didn't pick a target to "pick a different target" is incoherent). - -### D22 — System markers (``, ``) render in angle brackets; user refs (`(prod)`) keep their parentheses - -Two coupled fixes folded in after D6 surfaced (a) a fresh-DB false-"up to date" and (b) a `(db, db, contract)` collision when a user-authored ref happens to be named `db` (a real possibility — `db` is already the CLI namespace, so reserving it as a ref name is off the table). - -**Bracket syntax (data-source distinction).** Refs are **user-managed pointers persisted on disk** (under the migrations dir, edited via `prisma-next ref set`). The `contract` and `db` markers are **system-inferred pointers** — `contract` from the contract envelope on disk, `db` from the database marker record. They are categorically different things; rendering should reflect that. - -- System markers render in **angle brackets**: ``. -- User refs render in **parentheses**: `(prod, staging)`. -- Both pairs can appear on the same contract node when both have content. When they do, system markers render first, then user refs, separated by one space: ` (prod)`. When only one pair has content, render only that pair. -- Inside `<>`, names are sorted with `contract` first then `db` (``, never ``) — keeps `` adjacent to the data block in the common case where only it is present. -- Inside `()`, refs are sorted alphabetically (today's behaviour; unchanged). -- A `db` ref and the system `db` marker are **not deduped**. If both resolve to the same hash, they render together: ` (db)`. Different sources of truth, both surfaced. - -**Styling (unchanged surface area, just two new brackets).** `` is bold green (preserved from today's `CONTRACT_MARKER_NAME` convention). `` is plain green. Refs in `()` are plain green; the active ref is bold green (preserved). Brackets `<` `>` `(` `)` take the same hue as their bracket's contents. Plain-text (`colorize: false`) goldens stay byte-identical except for the `<>` characters replacing `()` around system markers. - -**Fresh-DB no-path detection.** Today's path-existence check at `migration-status.ts:458` is gated on `markerHash !== undefined`, so a fresh database (no marker recorded yet) skips the check entirely. Combined with `pendingCount === 0` when the live contract is disconnected from `∅`, the headline falls through to "up to date" — wrong, the DB is at `∅`, the live contract is unreachable. Fix: drop the `markerHash !== undefined` clause; let the check run against `originHash = markerHash ?? EMPTY_CONTRACT_HASH`. The `markerHash !== targetHash` predicate becomes `originHash !== targetHash`. The no-path summary fires correctly, with the same wording variants D21 already locked. - -### D23 — Cross-space data-column alignment, contract-node markers/refs adjacent to hash, right-justified from-hash column - -Three coupled rendering polish fixes surfaced post-D7 QA. Each is a small targeted edit at the row→string join point in the renderer. - -**Cross-space alignment (supersedes D17).** D17 specified per-render-block alignment ("each space's tree depth may differ; alignment across spaces is not asserted") and listed cross-space alignment as a follow-up. The follow-up is in. The new rule: compute `maxEdgeTreePrefixWidth` across **every migration row in every per-space block** in the current rendering, then pad every migration row to that single global width. Single-space renderings collapse to today's behaviour (one block contributes all the rows). The unified pipeline (D1) makes this a single-pass change at the layout module's entry point — collect all rows from all spaces first, compute the global max, hand the global width to the per-space tree-renderer. - -**Contract-node markers/refs adjacent to hash.** Today contract-node rows are padded to align their ` (refs)` text with the migration-data column ([dirName][from→to][ops][invariants]). That alignment is wasteful: markers and refs only ever appear on contract-node rows, never on migration rows, so there is no column they need to share. New rule: contract-node rows render ` (refs)`, with no padding between the hash and the markers/refs block. The existing `LABEL_GAP` constant (currently 2) provides the visual separation. Migration rows keep the cross-space-aligned data column from rule 1 — only contract-node rows lose the padding. - -**Right-justified from-hash column.** Today the from-hash column in the migration data block is left-padded (the value sits at the left of a fixed-width pad, trailing spaces fill to the `→`). Effect with regular 7-char hashes: invisible. Effect with `∅` (one char): six trailing spaces, then `→`, which reads as a gap. New rule: switch the from-hash column to right-padding (right-justified value). Regular hashes are unchanged (they fill the column). `∅` lands flush against the `→`. The to-hash column stays left-justified (it's followed by other tokens, not preceded by a glyph that should hug it). - -Goldens regenerate mechanically. Plain-text (`colorize: false`) goldens see whitespace-only changes per the three rules. The lane-color goldens (D3) and the legend goldens (D4) are unaffected (color is orthogonal to padding). - -### D24 — Cross-space dirName alignment + capitalised "Up to date" - -Two trivial follow-ups surfaced post-D8 QA. Each is one independent change in one helper. - -**Cross-space dirName alignment.** D23 / D8 aligned the **tree-prefix** width globally across per-space blocks but left the **dirName** column padded to each space's local max. That means the from-hash column (which sits immediately after dirName) lands at different absolute column positions across spaces — defeating the visual goal of cross-space alignment for everything *after* the tree-prefix. Extend the rule: compute a single global max dirName width across every migration row in every per-space block in the current rendering, pad every migration row's dirName to that width. Single-space renderings collapse to today's behaviour. Same orchestration point D8 used (`computeGlobalMaxEdgeTreePrefixWidth` + `renderMigrationGraphSpaceTrees`), parameterised with a parallel `globalMaxDirNameWidth`. - -**Capitalised "Up to date".** `buildStatusHeadline` currently emits the literal `'up to date'`. Change to `'Up to date'`. Sentences in the summary line start capitalised; this is the only outlier. - -### D25 — Trunk-choice rule honors `contractHash` for connected components too (D14 enforcement) - -D14 locked the trunk-choice rule: the trunk is the chain containing the **live contract**. D1 was meant to enforce this by threading `liveContractHash` through to `buildMigrationGraphRows` as `contractHash`. The threading works. The algorithm doesn't. - -**Bug.** `cli/src/utils/formatters/migration-graph-rows.ts` has two paths that consume `contractHash`: - -- `detachedContractHash`: when `contractHash` is not in `graph.nodes`, it's prepended as its own single-node component at the front. ✅ (works as expected.) -- `layerNodesByLongestForwardPath`: when `contractHash` IS in the graph (connected to the rest), this function ranks nodes by longest-forward-path-from-root and emits tips-first with lex tie-break. **It does not consider `contractHash` at all.** Whichever leaf has the highest forward-path rank wins the trunk position. - -The user's demo: `app` space has two leaves descending from `∅`: - -- `f7a8eb5` (4 hops from `∅` via the historical-namespaces chain) — rank 4. -- `1375f13` (1 hop from `∅` via the new `20260603T1537_migration`) — rank 1. - -Live contract is `1375f13`. By D14, `1375f13` should be the trunk's tip. By the algorithm's longest-path heuristic, `f7a8eb5` wins (rank 4 > rank 1) and the historical chain renders as the trunk; `1375f13` peels off as a side-branch. - -**Fix.** Bias the rank/leaf-selection step toward the chain ending at `contractHash`. The intervention is in `migration-graph-rows.ts`. Two acceptable shapes — implementer picks whichever reads cleaner: - -1. **Rank boost.** When `contractHash` is non-empty and present in the component being layered, boost its rank to `currentMax + 1` after the longest-path pass. The tips-first sort then emits it first, the layout assigns it column 0, and its ancestors back to the nearest fork point sit on column 0 too. -2. **Leaf preference.** Adjust `compareNodesTipsFirst` to take a "preferred-leaf" hint (= `contractHash` when the live contract is a leaf in this component). The hint wins the rank-tied lex tie-break, and the rank-boost path falls out as the same algorithm. - -Either way, single-node components, components without `contractHash` in them, and components where `contractHash === EMPTY_CONTRACT_HASH` keep today's behaviour. - -**Mid-chain edge case.** If `contractHash` is mid-chain (it has descendants — i.e. someone authored migrations beyond the live contract), the trunk should still anchor on the live contract, but the descendants render *above* it as a forward-extension side-branch. The implementer should either (a) handle this case by boosting only when `contractHash` is a leaf in the component, treating the non-leaf case as today's longest-path heuristic, or (b) handle it explicitly with a sensible layout. Option (a) is simpler and covers the common cases. Document the chosen behaviour in the dispatch's final report. - -### D26 — Legend wording follows D22's bracket convention; `--legend` is universal across graph-drawing commands - -D7 introduced the system-marker / user-ref split (`` for live markers, `(prod, staging)` for user-defined refs). The legend's marker/ref line still describes them with the old single-line collapse: ` (refs) db / contract markers`. That misses the teaching opportunity — the legend exists precisely to teach the bracket convention. - -**Wording.** Replace the single marker/ref line with two parallel lines: - -``` - live markers (contract on disk, database state) - (prod, staging) user-defined refs -``` - -Notes: -- The example bracket contents (`contract, db` and `prod, staging`) are illustrative literal strings in the key — they're not pulled from the user's actual project state. -- The styling pipeline must NOT apply the active-ref bold treatment to either example; a legend key isn't a graph node and there is no "active ref" in this context. -- The right column wording carries the conceptual split: live = inferred from external sources (contract file on disk; database marker); refs = user-managed. - -**Universality.** With D1's unified rendering pipeline, `migration list` and `migration status` both draw the same tree as `migration graph`. `--legend` should work on all three commands. - -The current helpers in `commands/migration-graph.ts` (`migrationGraphShowsLegend`, `validateMigrationGraphLegendOptions`) and `utils/cli-errors.ts` (`errorMigrationGraphLegendHumanOnly`, error code `MIGRATION.GRAPH_LEGEND_HUMAN_ONLY`) are graph-command-specific by name. Hoist and rename: - -- `migrationGraphShowsLegend` → `shouldShowLegend` (or similar) in a shared utils module. -- `validateMigrationGraphLegendOptions` → `validateLegendOptions` in the same shared module. -- `errorMigrationGraphLegendHumanOnly` → `errorLegendHumanOnly`; error message drops "graph" ("`--legend` is only available for human-readable output"); error code becomes `MIGRATION.LEGEND_HUMAN_ONLY`. - -Then wire `--legend` into `migration list` and `migration status` with the same option declaration and the same suppression rules: blocked when combined with `--json` / `--dot` / `--quiet`; printed alongside the human header on stderr only when the command is producing human output. - -**Out of scope.** No changes to the legend's *content* beyond the marker/ref line replacement — the contract / forward / rollback / self / applied / pending / empty-database / lane-swatch lines stay byte-identical. - -### D20 — Op-count parity is a cross-target obligation, asserted by one harness - -The SQL family and Mongo target both emit a `LedgerEntryRecord.operationCount` per applied edge. The contract: for the same plan applied successfully, every backend records the same `operationCount` per edge (including a skipped-op apply path, where the SQL adapter records `executedOperations` after idempotency skip-records and Mongo records planned ops). One table-driven harness in a shared test surface drives a representative plan through every backend (Postgres, SQLite, Mongo, family-sql synth) and asserts per-edge `operationCount` equality. The Postgres op-count-mismatch throw path (currently uncovered; SQLite + Mongo are covered) gains a targeted unit test in the same dispatch. - -## Acceptance - -- For any single input (same DB state, same on-disk migrations), `migration list`, `migration status`, and `migration graph` produce **byte-identical** pretty rendering of the per-space sections, modulo `status`'s overlay column and per-command footer. Asserted by a shared snapshot test that pipes the same fixture through all three renderers and diffs only the overlay column + footer. -- Trunk choice in all three commands: the chain containing the live contract is the trunk; historical-ref chains render as indented side-branches. -- Space iteration: all three commands render every on-disk space by default; `--space ` narrows uniformly; multi-space renderings carry `:` headings; the demo's `pgvector` space appears in `migration graph` output (regression test for the silent-elision bug surfaced during QA). -- Per-row data: `dirName · from → to · N ops · {invariants}` appears on every migration row in every command; `status` appends `✓ applied` / `⧗ pending`. -- Column alignment: in any rendering, the migration data block (dirName onward) starts at the same column offset for every migration row in that per-space section, including non-linear graphs (branches, rollback peels) and `--ascii` mode. -- Lane coloring: a colorized snapshot over a multi-lane fixture (kitchen-sink: diamond + routed back-arc + 3-way fan) asserts (a) lane color rotates by column index, (b) column 0 stays neutral, (c) each routed back-arc is one hue across all its owned cells while crossings stay neutral, (d) node `○` matches its lane color while arrows stay bright. Plain-text (`colorize: false`) goldens for every existing fixture remain byte-identical. -- `--legend`: snapshot coverage for `migration graph --legend` in unicode + `--ascii` × color on/off (4 cases), printed to stderr; stdout unchanged. `--json` / `--dot` paths reject `--legend` cleanly (legend is human-only). -- Op-count parity harness: one table-driven test exercises the same plan through Postgres, SQLite, Mongo, and the family-sql synth path and asserts per-edge `operationCount` equality. The Postgres op-count-mismatch throw path has a unit test covering the throw. -- `migration status` default target + no-path wording (D21): when `--to` is not provided, the picker returns the live contract for any non-empty `contractHash`. With the demo state (DB at live contract `1375f13`, disconnected historical chain ending at `f7a8eb5` reachable via `prod`), `migration status` (no flags) reports `up to date`. With `--to prod` against the same state, the no-path summary reads "No migration path from the database state (sha256:1375f13) to the target (sha256:f7a8eb5 via `prod`). Run `prisma-next migration plan --name ` to author one, or pass `--to ` to pick a reachable target." Unit tests pin all three wording variants. The two existing e2e tests in `migration-status-diagnostics.e2e.test.ts` that assert the old wording are updated to assert the new wording (and the post-`db update` scenario is updated to assert `up to date`, since the picker now correctly defaults to the live contract). -- D22 — system-marker rendering: every existing tree golden / snapshot covering system markers (`contract`, `db`) is regenerated with `` / `` in place of `(contract)` / `(db)`. User refs (e.g. `(prod)`) keep their parentheses. A new test in the tree renderer's unit suite pins the four cases: only system markers, only user refs, both, and a `db`-named user ref colliding with the system `db` marker (renders as ` (db)`). Plain-text (`colorize: false`) and `--ascii` paths stay byte-identical to today modulo the bracket characters. -- D22 — fresh-DB no-path detection: a new e2e in `migration-status-diagnostics.e2e.test.ts` exercises `db drop` + emit-only contract (no migration plans the live contract from `∅`) + `migration status` (no flags) and asserts the no-path summary against the live contract, not "up to date". -- CI green: full `pnpm test:packages` plus `pnpm test:integration` plus `pnpm test:e2e`. `pnpm fixtures:check` clean. The demo (`migration list` / `status` / `graph` against the disconnected-historical-chain demo state) renders consistently across the three commands. - -## Out of scope - -- **JSON shape unification.** `graph`'s `{ nodes, edges }` stays; `list` / `status` per-space arrays stay. JSON consumers diverge by design (project decisions D2 / D3). -- **Retiring or aliasing `migration graph`.** After this slice, `graph` and `list` produce identical pretty output; their JSON shapes still differ. Whether `graph` should remain a separate top-level command is a future call — deferred per operator. File a follow-up if/when the answer is clear. -- **`edges-on-plan` and `empty-origin-as-null`.** The two on-disk slice drafts under `projects/migration-graph-rendering/slices/` are explicitly **not** in this slice. Neither is in TML-2774's tracked scope. The empty-origin work is also genuinely heavy — `EMPTY_CONTRACT_HASH` is wired into the `MigrationGraph` node-keying, walk algorithms, integrity checks, and ref parsing — and the operator already ruled in the TML-2769 review that the constant's value is "not our fight." File standalone tickets if either is picked up. -- **Trunk-choice extensibility.** This slice locks one rule (live-contract spine). A future `--trunk ` flag is conceivable but not asked for here. -- ~~**Cross-space alignment.**~~ — supersedes D17 in D23 (cross-space alignment is now in scope; pad to a single global `maxEdgeTreePrefixWidth` across every migration row in every per-space block). - -## Pre-investigated edge cases - -| Edge case | Disposition | -|---|---| -| `--no-color` / `NO_COLOR` / non-TTY / piped | No lane colors; legend prints plain. `colorize` already short-circuits to the identity styler. Existing plain-text goldens stay byte-identical. | -| `--ascii` | Lane coloring still applies (color is orthogonal to glyph palette); the legend uses the ASCII glyph palette and reads `paletteFor(glyphMode)` from the same source as the renderer. | -| Lane freed and reused by a later branch | Keeps its column's color — `git log --graph` style; no per-branch identity tracking. The only exception is routed back-arcs (rule 3 above). | -| Single-space rendering | No `:` heading (existing convention preserved). | -| Single-lane linear graph | Effectively monochrome (column 0 stays neutral). | -| Demo's stale historical-ref chain (the `prod` ref pointing at `f7a8eb5`, disconnected from the live `1375f13` chain) | Renders as a side-branch in all three commands. Pinned by a snapshot over the demo state. | - -## References - -- Project: `projects/migration-graph-rendering/` (decisions D1/D2/D3/D4/D11/D14). -- Source modules touched: `packages/1-framework/3-tooling/cli/src/utils/formatters/{migration-graph-rows,migration-graph-tree-render,migration-graph-layout,migration-graph-lane-colors,migration-list-data-column,migration-list-render}.ts`, `packages/1-framework/3-tooling/cli/src/commands/{migration-list,migration-status,migration-graph}.ts`. -- Test surface for op-count parity: a new shared harness in `packages/1-framework/3-tooling/migration/test/` driving Postgres + SQLite (via the SQL family), Mongo (target), and the family-sql synth path; the PG op-count-mismatch throw test lives in `packages/3-targets/3-targets/postgres/test/`. -- Linear: TML-2812 (lead, project), TML-2811, TML-2773, TML-2774. TML-2767 closed as superseded by TML-2812. - -## Dispatch plan - -Eight dispatches. D1–D4 sit on the rendering surface (`packages/1-framework/3-tooling/cli/src/utils/formatters/`) and serialise — D2 builds on the unified pipeline D1 establishes, D3's lane-color selector is reused by D4's legend renderer. D5 sits on a different surface (`packages/1-framework/3-tooling/migration/test/` + the per-target test directories) and parallelises with the rendering work. D6 sits on `cli/src/commands/migration-status.ts` (no overlap with D1–D5's surfaces) and parallelises freely. D7 spans `migration-graph-tree-render.ts` + `migration-list-styler.ts` (system-marker brackets) plus a one-line edit in `migration-status.ts` (fresh-DB guard); it lands after D1–D4 and D6 so its golden regenerations don't conflict. D8 lands the three padding rules from D23 — cross-space alignment (touches the layout entry point that orchestrates per-space rendering), contract-node refs/markers adjacent to hash (touches the contract-node row builder in `migration-graph-tree-render.ts`), and right-justified from-hash column (touches `migration-list-data-column.ts`) — after D7's golden regenerations have settled. - -### Dispatch 1: unify the rendering pipeline across `list` / `status` / `graph` - -- **Outcome:** All three commands route through the same `buildMigrationGraphRows` → `buildMigrationGraphLayout` → `renderMigrationGraphTree` pipeline with the same trunk-choice input (live-contract chain, D14), the same per-row data overlay shape (`dirName · from → to · N ops · {invariants}`, D15), and the same space iteration policy (all on-disk spaces by default, `--space ` to narrow, D16). `migration graph` now renders the demo's `pgvector` space; `migration status` adopts the live-contract trunk; `migration status`'s overlay column composes onto the list overlay (rather than replacing it). A shared snapshot test pipes one fixture through all three renderers and asserts byte-identical per-space sections modulo `status`'s overlay column + per-command footer. -- **Builds on:** This spec; the existing shared pipeline (already called from all three commands today) + the `edgeAnnotationsByHash` contract from D11. -- **Hands to:** A unified call-site shape that the next dispatches build on. Trunk + per-row + space-iteration tests green; existing per-command goldens regenerated mechanically; D11 overlay extended in `cli/src/commands/migration-status-overlay.ts` to compose the list overlay with the status column. -- **Focus:** Behavioral unification only. No styling change (lane color is D3); no alignment change (D2); no test-coverage work (D5). - -### Dispatch 2: align migration data columns per render block - -- **Outcome:** In any per-space section produced by `list` / `status` / `graph`, the migration data block (dirName onward) starts at the same column offset on every migration row, regardless of how deep that row sits in the tree drawing. Computed per render block (D17): max visible width of the tree-prefix across migration rows in that block, right-padded with plain whitespace. Operates on visible glyph width, not raw byte length, so wide / dim / ANSI-styled glyphs don't drift. Non-migration rows (contract nodes, blank/connector rows) are unaffected. A new test over a deliberately non-linear fixture (rollback peel + diamond) asserts alignment in default + `--ascii` modes. -- **Builds on:** Dispatch 1's unified pipeline. The padding logic lives at the row→string join point in `migration-graph-tree-render.ts` (or its data-column helper). -- **Hands to:** Aligned data columns in all three commands; existing goldens regenerate mechanically (whitespace-only diffs); the kitchen-sink fixture pins the rule. -- **Focus:** Whitespace padding. No semantic change, no styling change. Only the join point between tree-prefix and data block changes. - -### Dispatch 3: per-column lane coloring with all six rules locked - -- **Outcome:** When `colorize` is true, the tree gutter renders per D18 — vertical lanes + connectors colored by column index over a 6-hue rotating palette (column 0 neutral, columns ≥ 1 colored), routed back-arcs single-hue per arc, crossings dim/neutral, node `○` matching its lane color, direction arrows staying bright, data columns unchanged. Plain-text (`colorize: false`) output is byte-identical to today across every existing fixture. A colorized snapshot over a kitchen-sink fixture asserts each of the six rules explicitly. -- **Builds on:** Dispatch 2 (same files, same join point — the alignment-padded layout is what the colorizer wraps). The existing `migration-graph-lane-colors.ts` module. -- **Hands to:** Colored gutter behind `colorize`; a column→color selector + a per-arc-id color resolver exported from the lane-colors module for D4's legend renderer to reuse. Plain-text goldens unchanged. -- **Focus:** Lane + node-marker coloring. No legend (D4); no layout-model change; no new flag. - -### Dispatch 4: `--legend` flag on `migration graph` - -- **Outcome:** `migration graph --legend` prints a palette-aware, color-aware legend block to **stderr** (D19), describing the glyph language (`○ ↑ ↓ ⟲ ∅ → (refs)`) and the lane-color cycle (only when `colorize` is true, reusing dispatch 3's selector). `--legend` is rejected with a clear error on `--json` / `--dot` (legend is human-only). `--ascii` swaps the legend's glyph palette to match. Snapshot coverage for the four cases (unicode + ascii × color on / off). `--help` examples and the reference doc mention `--legend`. -- **Builds on:** Dispatch 3's exported color selector. The existing `createMigrationGraphCommand` flag plumbing in `cli/src/commands/migration-graph.ts`. -- **Hands to:** New flag + legend renderer landed; legend goldens green; plain-text + colorized graph goldens from D3 unchanged; `--json` / `--dot` regression-green. -- **Focus:** Flag + legend block + docs. No change to lane-color mechanics. - -### Dispatch 5: cross-target op-count parity harness + Postgres throw test - -- **Outcome:** One table-driven harness drives a representative plan (greenfield baseline + a multi-edge apply, including a path that exercises an idempotency-skipped op) through Postgres, SQLite, Mongo, and the family-sql synth strategy, and asserts per-edge `LedgerEntryRecord.operationCount` equality across every backend (D20). A separate unit test in the Postgres adapter covers the op-count-mismatch throw path that's currently uncovered (SQLite + Mongo are covered). -- **Builds on:** This spec + the merged ledger journal (TML-2769). No dependency on D1–D4. -- **Hands to:** TML-2774 items 3 + 5 closed; ledger-journal cross-target invariant pinned in CI. -- **Focus:** Test coverage on the migration runners' ledger writes. No production-code change unless the harness surfaces a real parity bug, in which case the dispatch reports back rather than fixing in-line (a parity bug is its own ticket). - -### Dispatch 6: `migration status` default target + no-path wording - -- **Outcome:** Two coupled fixes per D21, both in `cli/src/commands/migration-status.ts`. (a) Picker collapses to `activeRefHash ?? contractHash` — explicit `--to` wins, otherwise the application's contract. The dead `MIGRATION.DIVERGED` diagnostic + `hasAmbiguousTarget` guard + `'Multiple valid migration paths'` summary branch are removed. Unused imports (`requireHeadRef`, `findReachableLeaves`) drop out. (b) The no-path summary is replaced by a context-aware builder `buildNoPathSummary({ markerHash, targetHash, explicitTarget, refName })` exported alongside `buildStatusHeadline`, with three variants: no `--to` (default → "the application's contract (…)"), `--to ` ("the target (… via \`\`)"), `--to ` ("the target (…)"). Lead-fix is `migration plan --name `; the `--to ` alternative appears only when `--to` was explicit. -- **Builds on:** This spec. No dependency on D1–D5; touches `migration-status.ts` only, which D1 already adapted but does not re-enter for this fix. -- **Hands to:** - - Picker change locked behind unit tests for the simplified two-line semantics: (i) `activeRefHash` non-undefined → returned regardless of `contractHash`; (ii) `activeRefHash` undefined → `contractHash` returned (including the `EMPTY_CONTRACT_HASH` case). - - Wording change locked behind unit tests for all three variants (`buildNoPathSummary({...})` exposed alongside `buildStatusHeadline` for direct testing). - - The two e2e assertions in `test/integration/test/cli-journeys/migration-status-diagnostics.e2e.test.ts` updated: the post-`db update` scenario (line ~286) now asserts `up to date` (picker correctly defaults to the live contract; DB matches it); the marker-on-wrong-branch + `--to production` scenario (line ~536) now asserts the new wording with the `via \`production\`` ref name. Any tests relying on the deleted `MIGRATION.DIVERGED` diagnostic / `'Multiple valid migration paths'` summary are deleted (the operator is comfortable removing the diagnostic; the simplified picker makes it unreachable). -- **Focus:** Picker simplification and message clarity in `migration-status.ts`. No change to overlay computation, no change to render pipeline, no new flags. Do **not** introduce a new ticket for either change — both land in this PR. - -### Dispatch 7: system-marker brackets + fresh-DB no-path guard - -- **Outcome:** Two coupled fixes per D22. - - (a) **System markers in `<>`.** In `cli/src/utils/formatters/migration-graph-tree-render.ts`, the `overlayNamesForContract` function (currently merging refs + `db` + `contract` into one list) splits into two lists: a system-marker list (``) and a ref list (`(prod, staging)`). Both are rendered when populated; markers render first, refs second, separated by one space. Inside `<>`, names are sorted with `contract` before `db`. Inside `()`, refs sort alphabetically (today's behaviour). Names are **not** deduped across the two lists — a user ref named `db` and the system `db` marker render as ` (db)` when they coincide on a hash. The styler in `cli/src/utils/formatters/migration-list-styler.ts` gains a `markers` formatter parallel to its `refs` formatter; `` stays bold-green, `` plain-green, `(refname)` plain-green, active ref bold-green. Brackets take the same hue as their contents. - - (b) **Fresh-DB no-path guard.** In `cli/src/commands/migration-status.ts`, the path-existence check around line 458 drops the `markerHash !== undefined` clause and replaces `markerHash !== targetHash` with `originHash !== targetHash` (where `originHash = markerHash ?? EMPTY_CONTRACT_HASH` is already computed). The `markerInGraph` clause stays — when the marker is `undefined`, `markerInGraph` is already `true` per its current definition. - -- **Builds on:** D6 (uses `buildNoPathSummary` for the fresh-DB path-failure case) and D1–D4 (which establish the unified rendering pipeline this dispatch's golden regenerations sit on top of). The styler split touches the same module D2's column-alignment changes touched but lands cleanly because alignment is a row-level concern and bracket rendering is a name-list concern. -- **Hands to:** - - **Unit tests** (`migration-graph-tree-render.test.ts`): four cases — only system markers, only user refs, both, and the ` (db)` collision. Plus existing snapshots regenerated mechanically (whitespace-clean: `(contract)` → `` / `(db)` → ``). - - **Styler unit test** (`migration-list-styler.test.ts` if it exists, otherwise an inline test in the renderer's suite): assert the four styling cases — `` bold-green, `` plain-green, `(prod)` plain-green, `(prod)` (active) bold-green — including bracket characters carrying their content's hue. - - **E2E** (`migration-status-diagnostics.e2e.test.ts`): one new scenario — `db drop` + emit a contract that has no migration plans into it + `migration status` (no flags) → asserts `'No migration path from the database state'` and `"to the application's contract"` substrings, NOT `'up to date'`. - - Existing snapshots: every renderer / command golden that covers system markers regenerates mechanically (single-string replace `\bcontract\b` → `` and `\bdb\b` → `` *only when those names appear inside today's `(...)` ref list*; user-ref `(db)` / `(contract)` strings stay parenthesised). The dispatch reports the count of regenerated snapshots in its final summary so the operator can spot-check. -- **Focus:** Two surgical edits — bracket syntax in the renderer + styler, one-line guard fix in `migration-status.ts`. No change to the column-alignment math, lane coloring, legend, or no-path wording (those are all locked in earlier dispatches). -- **Hard constraint:** Touch only `migration-graph-tree-render.ts`, `migration-list-styler.ts`, `migration-status.ts`, the matching test files, and snapshot fixtures regenerated mechanically as a result. **No other production files.** If a downstream consumer breaks (e.g. an extension or example app asserts on the `(contract)` rendering), report it and stop — do not file a ticket and do not fix in-line. - -### Dispatch 8: cross-space alignment + contract-node refs adjacent to hash + right-justified from-hash - -- **Outcome:** Three padding rules from D23, all in the renderer: - - (a) **Cross-space alignment.** `maxEdgeTreePrefixWidth` is computed across every migration row in every per-space block in the current rendering (one global value), then handed to the per-space tree-renderer. Single-space renderings collapse to today's behaviour. The orchestration point is the layout module's entry — the function that today renders one space's block in isolation needs a shared "max-width" input, and the multi-space wrapper computes it across all blocks before invoking each one. - - (b) **Contract-node refs/markers adjacent to hash.** Contract-node rows render ` (refs)` with no padding between the hash and the markers/refs. Migration rows are unaffected — they keep the cross-space-aligned data column from rule (a). Implementation: the contract-node row builder in `migration-graph-tree-render.ts` writes the markers/refs token immediately after the hash + `LABEL_GAP` instead of advancing to the data-column offset. - - (c) **Right-justified from-hash column.** In `migration-list-data-column.ts`, the from-hash field switches from left-padding to right-padding (i.e., the value sits at the right edge of the fixed-width column, leading spaces fill to the left). Regular hashes fill the column → unchanged visually. `∅` lands flush against the `→`. The to-hash column stays left-justified. - -- **Builds on:** D1 (unified rendering pipeline gives a single layout entry that can compute the global width), D2 (the per-block alignment logic D8 is generalising), D7 (its `` / `` brackets are the markers token D8 places adjacent to the hash). No dependency on D3/D4/D5/D6. - -- **Hands to:** - - **Unit tests** for cross-space alignment: a multi-space fixture with deliberately different tree depths (e.g. a 4-row `app:` block plus a 1-row `pgvector:` block) — assert that both spaces' `dirName` columns start at the same column offset (the deeper one's offset wins). Today's per-block test (D2) regression-pins single-space behaviour. - - **Unit tests** for contract-node ref placement: a contract node with ``, with ` (prod)`, with no markers/refs (renders just ``). Each case asserts the markers/refs sit at `` + `LABEL_GAP` spaces, NOT at the data-column offset. - - **Unit tests** for the right-justified from-hash column: one row with `∅` from, one row with a regular hash from. Assert that in both cases, the `→` lands at the same column offset. - - **E2E**: existing scenarios pass (whitespace-only diff in goldens, regenerate mechanically). No new e2e needed. - - Existing snapshots regenerate mechanically. - -- **Focus:** Whitespace-only changes at three specific join points (layout entry width-computation, contract-row builder, from-hash padder). No semantic, color, or content changes. - -- **Hard constraint:** Touch only the three named source files (`migration-graph-tree-render.ts`, the multi-space orchestration point — likely `migration-graph-space-render.ts` or the layout module — and `migration-list-data-column.ts`), their unit tests, and snapshot fixtures regenerated mechanically. No other production files. If you find that the cross-space orchestration requires a wider refactor than a single-pass max-width computation handed down via prop, stop and report — do not refactor the layout module structurally. - -### Dispatch 9: cross-space dirName alignment + capitalised "Up to date" - -- **Outcome:** Two trivial follow-ups per D24, both in surfaces D8 already touched. - - (a) **Cross-space dirName alignment.** Mirror D8's `globalMaxEdgeTreePrefixWidth` for dirName: compute a single global max dirName width across every migration row in every per-space block in the current rendering, then pad every migration row's dirName to that width. Reuse the orchestration point D8 introduced (`computeGlobalMaxEdgeTreePrefixWidth` / `renderMigrationGraphSpaceTrees`); add a parallel `globalMaxDirNameWidth` parameter and threading. Wire it through the same three caller sites D8 wired (`migration-list-render.ts`, `migration-graph.ts`, `migration-status.ts`). - - (b) **Capitalised "Up to date".** In `cli/src/commands/migration-status.ts`, `buildStatusHeadline`'s `pendingCount === 0` branch returns the literal `'up to date'`. Change to `'Up to date'`. One character. - -- **Builds on:** D8 (the orchestration plumbing); the rest of the slice is unaffected. - -- **Hands to:** - - **Unit tests:** extend the cross-space test from D8 to also assert that the from-hash column lands at the same absolute column offset across the two per-space blocks. The test from D8 today asserts the dirName start position (governed by tree-prefix width); add an additional assertion on the `→` arrow position (governed by tree-prefix width + dirName width + from-hash width — equality across blocks proves dirName is aligned globally). - - One unit test for the capitalisation: extend `format-status-summary.test.ts`'s `'reports up to date when nothing is pending'` test to assert `'Up to date'`. - - **E2E:** existing scenarios pass (whitespace + capitalisation only; goldens regenerate mechanically). - - Existing snapshots regenerate mechanically (the multi-space goldens now show globally-aligned from-hash columns; `'up to date'` snapshots flip to `'Up to date'`). - -- **Focus:** Two surgical edits — one paralleling D8's existing pattern, one a single-character literal. No structural change. - -- **Hard constraint:** Touch only `migration-graph-tree-render.ts`, `migration-graph-space-render.ts`, `migration-list-render.ts`, `migration-graph.ts`, `migration-status.ts` (same set as D8), their unit tests, and snapshot fixtures regenerated mechanically. No other production files. If you find a non-whitespace-or-capitalisation snapshot diff, stop and report. - -### Dispatch 11: trunk-choice rule honors `contractHash` for connected components - -- **Outcome:** D25's algorithmic fix in `cli/src/utils/formatters/migration-graph-rows.ts`. After D11 lands, the demo's `app` space renders with `1375f13` as the trunk's tip and the historical chain ending at `f7a8eb5` as the side-branch, in all three commands (`list` / `status` / `graph`). Single-node components, components without `contractHash` in them, and components where `contractHash === EMPTY_CONTRACT_HASH` are unchanged. - -- **Builds on:** D8/D9/D10 (this dispatch's whitespace-only goldens regenerate cleanly on top of the alignment changes); D1 (the unified pipeline already threads `liveContractHash` to `buildMigrationGraphRows`). No dependency on D2/D3/D4/D5/D6/D7. - -- **Hands to:** - - **Unit tests** in `migration-graph-rows.test.ts` (or wherever the row-model tests live) covering: (i) two leaves shared root, `contractHash` is the shorter-chain leaf → live-contract leaf wins trunk; (ii) two leaves shared root, `contractHash === undefined` → today's longest-path heuristic wins (regression-pin); (iii) `contractHash === EMPTY_CONTRACT_HASH` → today's heuristic (regression-pin); (iv) `contractHash` not in the graph → detached-contract path unchanged (regression-pin). - - **Renderer integration** in `migration-graph-tree-render.test.ts` over the demo's app-space topology fixture (or a synthetic equivalent): assert the rendered tree has `` at the top, on column 0, with the historical chain peeling off as a side-branch. - - **Snapshots** regenerate mechanically for any existing fixture whose topology has multiple leaves sharing a root with `contractHash` set; goldens with `contractHash === undefined` or detached-contract topology stay byte-identical. - -- **Focus:** Algorithmic fix in one helper (`layerNodesByLongestForwardPath` and/or its surrounding caller). Keep the change local; no refactor of the layout module beyond what's needed to bias the rank/leaf-selection toward `contractHash`. - -- **Mid-chain edge case:** Per D25, if `contractHash` has descendants in its component (mid-chain), prefer the simpler implementation (only bias when `contractHash` is a leaf; treat non-leaf as today's heuristic) and document the choice in the dispatch's final report. - -- **Hard constraint:** Touch only `migration-graph-rows.ts` (and `migration-list-graph-topology.ts` if it contains the topology classifier the rank step depends on), their unit tests, and snapshot fixtures regenerated mechanically. No other production files. If you find that the rank-boost approach requires a wider refactor of the layout module's column-assignment, stop and report — do not restructure the layout. - -### Dispatch 12: legend wording + universal `--legend` - -- **Outcome:** D26's two changes: - 1. The legend's marker/ref line collapses (` (refs) db / contract markers`) is replaced with two parallel lines following D22's bracket convention: - - ``` - live markers (contract on disk, database state) - (prod, staging) user-defined refs - ``` - - 2. The legend helpers are hoisted out of `commands/migration-graph.ts` into a shared utils module, renamed to drop the "graph" prefix, and wired into `migration list` and `migration status` with identical suppression rules (blocked when combined with `--json` / `--dot` / `--quiet`; printed alongside the human header on stderr). - -- **Builds on:** D7 (bracket convention), D11 (trunk fix is settled). Independent of D8/D9/D10's whitespace fixes. - -- **Hands to:** - - **Renderer:** `migration-graph-tree-render.ts` — replace the single marker/ref line with the two-line block. The example contents (`contract, db` and `prod, staging`) are literal strings; the active-ref bold treatment must NOT apply to either example. - - **Helpers hoisted:** `migrationGraphShowsLegend` → `shouldShowLegend`; `validateMigrationGraphLegendOptions` → `validateLegendOptions`; `errorMigrationGraphLegendHumanOnly` → `errorLegendHumanOnly`; error code `MIGRATION.GRAPH_LEGEND_HUMAN_ONLY` → `MIGRATION.LEGEND_HUMAN_ONLY`; error message drops "graph". Move them out of `commands/migration-graph.ts` and `utils/cli-errors.ts` (where appropriate) into a shared module — `utils/legend.ts` is fine, or fold into `utils/formatters/`. Pick whichever fits the existing layering. - - **Command wiring:** `migration list` and `migration status` get a `--legend` option declaration matching `migration graph`'s, the same `validateLegendOptions` invocation in their `.action(...)`, the same `shouldShowLegend(...)` gate in their command body, and the same call to `renderMigrationGraphLegend` (or the renamed equivalent) when the gate passes. - - **Tests:** - - **Unit** in `migration-graph-tree-render.test.ts`: legend snapshot updates for the two-line marker/ref block; assert the example strings are not bolded. - - **Command tests** for `migration list` and `migration status`: `--legend` prints the key on stderr; `--legend --json` returns the new error; `--legend --quiet` returns the same error; combined with `--space ` legend still prints (single-space mode doesn't suppress it). Mirror whatever assertions `migration graph`'s legend tests already have. - - **`--help` output** for list/status now mentions `--legend` — update help-output goldens if they exist. - - **Snapshots / inline goldens** regenerate mechanically. Inspect the legend diff: every change should be the marker/ref line replacement (and possibly help-text additions for `--legend` on list/status). Anything else is a regression — STOP and report. - -- **Focus:** Two narrowly-scoped changes — a wording update in one renderer function, and a code-move-plus-wiring of three small helpers across three command files. No algorithmic logic changes. - -- **Hard constraint:** Touch only the renderer file (`migration-graph-tree-render.ts`), the legend-helpers source (`commands/migration-graph.ts`, `utils/cli-errors.ts`, plus the new shared utils file), the three command files (`migration-graph.ts`, `migration-list.ts`, `migration-status.ts`), and their tests. No other production files. diff --git a/projects/migration-graph-rendering/slices/render-redesign-core/plan.md b/projects/migration-graph-rendering/slices/render-redesign-core/plan.md deleted file mode 100644 index 77cd1250a4..0000000000 --- a/projects/migration-graph-rendering/slices/render-redesign-core/plan.md +++ /dev/null @@ -1,184 +0,0 @@ -# Dispatch plan — `render-redesign-core` - -_Slice: rebuild the Tier-3 migration-graph renderer on the design doc's line/plane model -(`design/graph-render-redesign.md`), verified through an executable **scenario gallery** -(`spec.md § Verification surface`). Five sequential dispatches, one PR (commit-per-dispatch)._ - -## Shape: author the oracle (goldens) → build the real pipeline to match → cut over + delete - -The verification surface is a **hand-authored oracle** (`spec.md § Verification surface`): each -scenario/variant's expected output is a hand-authored 2D array of `{glyph, colour}` cells, -serialised by a trivial `renderCells` the gallery runs. The gallery **never invokes the real -renderer**. So the goldens are authored and **operator-approved first**, then the real pipeline -(model → occlusion renderer → layout) is built to **match** them. - -Order: -- **D1 (done):** topology catalogue (the inputs) + gallery harness + current-output capture = - the RED baseline (today's real output ≠ the goldens). -- **D2:** the oracle foundation — `Cell{glyph,colour}` + `renderCells` + the gallery showing - **goldens** in colour, and the hand-authored goldens for the **lock-the-look** set - (`linear`/`fork-2`/`merge-2`/`diamond` × rotating/highlight-trunk/highlight-alt). Operator - approves the corner language (`│─╯` rotating vs `╭─╯` highlighted, mode z-order). No real - renderer. -- **D3:** hand-author the **remaining** goldens (`fan-3`, `wide-fan`, `rollback-*`, - `self-loop`, `showcase` × variants); operator approves the full golden set — the complete, - human-approved spec of correct rendering. -- **D4:** build the real **model + occlusion renderer + forward layout**; forward scenarios' - real output **== golden**; green-only-on-path green for forward. -- **D5:** **back-arcs**; rollback scenarios' real output == golden; revert-to-red on - `rollback-cross:arc-1`. -- **D6:** cut every command onto the new pipeline; regenerate command snapshots (intentional - tee→corner); delete the old `StructuralCell`/tee path. - -The old path stays live and green until D6. - -> **Acceptance is human-in-the-loop.** Goldens (D2/D3) and every real-pipeline dispatch (D4/D5) -> only advance after the operator runs `pnpm gallery` and approves the colour. The golden is -> hand-authored, so "real output == golden" is a real test, not "renders what it renders". - -## Non-linear hand-offs - -- **D2 → D3** — D3 authors the rest of the goldens in the same `{glyph,colour}` form D2 establishes. -- **D4 builds on D2 + D3** — the real forward pipeline is asserted against the D2/D3 goldens. -- **D5 builds on D4 + D3** — back-arcs matched against the rollback goldens. -- **D6 builds on D4 + D5 + D1** — cuts commands onto the new pipeline; regenerates the - command-level (`graph-render.test.ts`) snapshots. - -_(Dispatch entries below still describe the earlier renderer-first framing for D3–D5; they are -superseded by the order above and will be rewritten once D2 proves the oracle out. D2's entry -is authoritative.)_ - ---- - -### Dispatch 1 — Scenario harness + catalogue + gallery + verbatim snapshots (RED baseline) - -- **Outcome:** The scenario catalogue (`spec.md § Scenario catalogue`) exists as data - (each scenario: contracts + edges + mode + on-path set, in its named variants). A - `renderScenario(name) → string` returns the **exact ANSI** output of **today's** renderer. - `pnpm --filter @prisma-next/cli gallery [filter]` prints scenarios in colour to the - terminal, filterable to one scenario/variant. A vitest file snapshots every - scenario/variant verbatim, plus the one **green-only-on-path** invariant assertion. The - baseline snapshots + that assertion **capture the current bleed as RED** on the - `highlight-alt` and `rollback-cross` variants (the assertion fails today; document which - variants are red). No renderer change. -- **Builds on:** the spec's verification surface + the design doc. -- **Hands to:** the executable verification surface every later dispatch is judged against, - and a documented RED baseline (which variants bleed today) to turn green. -- **Focus:** the catalogue data; fixture contract-graph construction; the gallery script - (filter arg, colour to terminal); the snapshot test; the green-only-on-path scanner. If - today's renderer crashes or can't express a scenario (e.g. `rollback-cross`), capture that - as the documented before-state, don't block. **Out:** any renderer/layout change. -- **Gate (binary):** `pnpm typecheck` green; `pnpm --filter @prisma-next/cli gallery` prints - all scenarios in colour and `gallery ` filters to one; the snapshot test runs and the - green-only-on-path assertion is **RED** on the named bleeding variants, green elsewhere; - the operator has run `pnpm gallery` and confirmed it shows the current state. - -### Dispatch 2 — Oracle foundation + lock-the-look goldens (AUTHORITATIVE entry) - -- **Outcome:** A `Cell{glyph, colour}` type + a trivial `renderCells(cells) → string` - (apply colour SGR per glyph, join rows) — the **only** logic the gallery runs; it does - **not** call any real renderer/layout. The gallery is switched to serialise hand-authored - `{glyph,colour}` arrays. **Hand-authored goldens** for the lock-the-look set — - `linear`/`fork-2`/`merge-2`/`diamond` × {rotating, highlight-trunk, highlight-alt} — encode - the design's corner language (no tees; `│─╯` rotating vs `╭─╯` highlighted; mode z-order). - `pnpm --filter @prisma-next/cli gallery [filter]` shows them in colour, filterable. **No - real renderer, no occlusion projection, no layout this dispatch.** The operator approves the - corner scheme before anything is built to match it. -- **Builds on:** D1's topology catalogue + gallery script (the gallery's render path is - swapped from today's-renderer to `renderCells`-over-goldens). -- **Hands to:** an **operator-approved** visual language as concrete `{glyph,colour}` goldens — - the spec the real pipeline (D4) must reproduce. -- **Focus:** `Cell{glyph,colour}` + `renderCells`; the hand-authored golden arrays for the four - scenarios × three variants; the gallery wiring + filter. Author an ergonomic format for the - arrays (e.g. parallel glyph-rows + colour-map) but the canonical model is the 2D - `{glyph,colour}` array. **Out:** the model `Grid`/`LineRef`/planes (D4); the occlusion - renderer (D4); any topology/layout code; back-arcs (D5); commands (D6); the old path. -- **Gate (binary):** `pnpm typecheck` green; `pnpm --filter @prisma-next/cli gallery` serialises - all four lock-the-look scenarios × three variants from hand-authored arrays in colour, and - `gallery merge-2:alt` filters to one; the gallery runs **no real renderer** (verify: - `renderCells` is its only render path); the **operator has run `pnpm gallery` and approved - the corner scheme**; old path + D1 tests untouched. - -### Dispatch 3 — New layout: forward DAG topologies → grid (real scenarios through the pipeline) - -- **Outcome:** A new layout builds the `Grid` from real forward topologies — each edge a - line carrying its `LineRef`, each contract a node, the **no-tee / 2-columns-per-lane** - single-owner discipline, plane assignment (all forward = base), mode-dependent z-order. - All forward scenarios (`linear`, `fork-2`, `merge-2`, `diamond`, `fan-3`, `wide-fan`, - `self-loop`) now render through the **new layout+renderer** in the gallery — rotating, - highlight-trunk, **and highlight-alt** all correct. The green-only-on-path assertion is - **green** for every forward scenario. No back-arcs, no convergence. Old path untouched. -- **Builds on:** D2's renderer + grid types. -- **Hands to:** a colour-correct forward pipeline — every forward scenario's `highlight-alt` - proven clean — for D4 to extend with back-arcs. -- **Focus:** the layout builder (reuse today's traversal + lane/column allocation — - positioning was never the bug); corner routing; single-owner invariant. **Out:** back-arcs - (D4); command cutover (D5). -- **Gate (binary):** `pnpm typecheck` green; layout unit tests green (every cell single-owner, - right identity/directions/plane, both modes); the forward scenarios render correctly and - the **operator has approved** their `pnpm gallery` colour incl. highlight-alt; green-only- - on-path assertion green for forward scenarios; `pnpm --filter @prisma-next/cli test` green. - -### Dispatch 4 — New layout: back-arcs on the upper plane (rollback scenarios) - -- **Outcome:** The layout routes rollback / back edges as **upper-plane** continuous lines; - where a back-arc crosses a forward vertical the **forward line clips**. The rollback - scenarios (`rollback-adjacent`, `rollback-arc`, `rollback-merge`, `rollback-cross`) + - their highlight variants render correctly in the gallery. The crux: `rollback-cross:arc-1` - — the on-path back-arc stays coloured through the crossing while the off-path arc is grey; - reverting the renderer makes the green-only-on-path assertion fail again (**revert-to-red - demonstrated**). Per-arc lanes kept (convergence = geometry slice). Old path untouched. -- **Builds on:** D3's forward grid + single-owner invariant. -- **Hands to:** the complete new pipeline (forward + back-arcs), every scenario in the - catalogue colour-correct — ready for the command cutover. -- **Focus:** back-arc plane assignment + forward-clip-at-crossing, on D3's builder. **Out:** - convergence (geometry slice); command cutover (D5). -- **Gate (binary):** `pnpm typecheck` green; layout tests green over the rollback fixtures - (back-arc on upper plane, crossed forward cell clips, single-owner holds); all rollback - scenarios render correctly and the **operator has approved** their gallery colour; - revert-to-red demonstrated on `rollback-cross:arc-1`; `pnpm --filter @prisma-next/cli test` - green. - -### Dispatch 5 — Cut all commands over + retire the old tee path - -- **Outcome:** `migrate --show`, `graph`, `status`, `list` switch to the new layout+renderer. - The command-level `graph-render.test.ts` snapshots are **regenerated** to the new corner - rendering — the **intentional** tee→corner change — and visually reviewed. The old - `StructuralCell` union, the render `switch`, the `migrationHash?` bolt-on, the - `branchTee`/`mergeTee` glyphs, and the dead old layout are **deleted**. The whole gallery - gets a final `pnpm gallery` sign-off. -- **Builds on:** D2's renderer + D3/D4's layout + D1's command snapshot context. -- **Hands to:** the slice-DoD — one line/plane pipeline serves every command; colour is - correct-by-construction; the cell-kind switch is gone. -- **Focus:** wiring the four commands; regenerating + eyeballing the command snapshots; - deleting the old path. **Out:** convergence + configurable geometry (geometry slice). -- **Gate (binary):** `pnpm typecheck` green; `pnpm --filter @prisma-next/cli test` green with - regenerated `graph-render` snapshots; `rg "StructuralCell|cell\.kind|branchTee|mergeTee|migrationHash\?" packages/1-framework/3-tooling/cli/src/utils/formatters` - returns **nothing**; operator signs off the final `pnpm gallery`. - ---- - -## Dispatch-INVEST check - -- **D1** — Independent (old path untouched), Valuable (builds the verification surface + the - RED baseline), Testable (gallery runs; named variants are red), Small (catalogue + script + - snapshot test). ✔ -- **D2** — substrate + the dumb renderer, validated on hand-built grids; Small because no - topology code; the operator-approval gate makes "the look is right" binary. ✔ -- **D3** — the forward layout; its gate is the forward scenarios going colour-correct - including the hard highlight-alt. ✔ -- **D4** — surgical back-arc extension; the named crux (`rollback-cross:arc-1`) + - revert-to-red is the testable gate. ✔ -- **D5** — mechanical cutover + snapshot regen + dead-code deletion, with a grep gate. ✔ - -**Sizing verdict:** one coherent slice, one PR, five commits (≤ 10). The gallery makes every -dispatch's correctness **visible to the operator in colour**, which is the property the -earlier round-trips lacked. Five ≤ 10. - -## References - -- Spec: [`spec.md`](spec.md) (§ Verification surface, § Scenario catalogue) -- Design-of-record: [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md) -- Sibling slice: [`../render-redesign-geometry/spec.md`](../render-redesign-geometry/spec.md) -- Renderer surface: `packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-{layout,rows,tree-render,lane-colors}.ts` -- Old mockups (tee language, for reference): `projects/migration-graph-rendering/mockups.md` diff --git a/projects/migration-graph-rendering/slices/render-redesign-core/spec.md b/projects/migration-graph-rendering/slices/render-redesign-core/spec.md deleted file mode 100644 index a94fdcfef4..0000000000 --- a/projects/migration-graph-rendering/slices/render-redesign-core/spec.md +++ /dev/null @@ -1,125 +0,0 @@ -# Slice: graph-render redesign — colour-correct line/plane pipeline - -_Parent project `projects/migration-graph-rendering/`. Outcome: the Tier-3 renderer becomes a dumb projection over a line/plane data model, so graph colouring is correct-by-construction (no bleed) and the cell-kind switch is gone._ - -> **Design-of-record:** [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md). This spec is **execution only** — it does not restate the model; read the design doc for the *what* and *why*. This slice implements the core (data model + planes + occlusion render); back-arc **convergence** and **configurable geometry** are the sibling slice `render-redesign-geometry`. - -## At a glance - -Replace the position-keyed `StructuralCell` grid + the `switch (cell.kind)` renderer with the design doc's model: lines carry identity, the layout assigns **planes** (forward = base, back-arcs = upper, continuous), and the renderer **occludes** (topmost plane wins per cell → box-char glyph + that line's colour). This kills the colour-bleed class of bugs at the source. - -## Chosen design - -Per the design doc. In scope for this slice: -- New data structures: `LineRef` / `CellLine` (directions + plane) / `Cell` / `Grid` (or the final names settled during build). -- Layout produces the new grid + **plane assignment** (forward DAG = base plane; each back-arc = upper plane, drawn continuous; forward verticals clip at crossings). **No convergence yet** — keep today's per-arc lanes (that's slice 2). -- Renderer = occlusion projection: topmost plane → `boxChar(union of its directions)` + colour from the winning line (on-path > off-path priority at same-plane junctions; by-branch rotation in normal mode); node/arrow overlays; lower planes clipped. -- Retire the 14 `StructuralCell` kinds, the render `switch`, and the per-cell `migrationHash?` bolt-on. -- **Single-owner glyph discipline (the core invariant):** the glyph alphabet is **verticals + corners + arrows + node markers — no tees (`├ ┬ ┼`)**; with 2 columns per lane, every cell is owned by exactly one line, so colour is read straight off it (never arbitrated). Merges/forks render as the **top branch continuous + the others yielding into their own corner cells**; **z-order is mode-dependent** — trunk-on-top in normal mode (`│─╮─╮`), on-path-branch-on-top in highlighted mode (`╰───╮`, the path sweeps over as one continuous line). -- **Out:** back-arc convergence; extracting geometry constants to parameters (both → `render-redesign-geometry`). Keep current geometry/spacing as-is. - -## Coherence rationale - -The layout's output type and the renderer that consumes it change together — they share the data-structure boundary, so they cannot land in separate PRs without breaking the consumer. One reviewer holds "lines + planes + occlusion projection" as a single change; every existing render test is the safety net. - -## Verification surface: the scenario gallery is a hand-authored oracle - -The gallery is an **independent oracle** — it **never invokes the real renderer or layout**. Each scenario/variant's expected output is a **hand-authored 2D array of `{glyph, colour}` cells** (the picture we want). A trivial `renderCells(cells) → string` serialises that array — applies the colour SGR to each glyph, joins rows — and **that is the only logic the gallery runs**. This is what makes the test non-tautological: the expected output is authored by hand, independent of the code under test. - -Three roles: - -- **Visual check (acceptance gate).** `pnpm --filter @prisma-next/cli gallery [filter]` serialises the hand-authored arrays to the terminal **in colour** (`FORCE_COLOR`), each under a labelled header, filterable to one `scenario:variant`. A human approves the picture by eye. -- **Golden fixture.** The serialised string of the **approved** array is the golden. The **real** pipeline (topology → layout → grid → occlusion renderer → string) is asserted, per scenario, to equal its golden. The hand-authored array is committed as the fixture; it changes only when a human re-approves via `pnpm gallery`, never a blind snapshot `--update`. -- **Documentation.** The catalogue + the hand-authored pictures are the doc. - -On top of the per-scenario golden match, **one targeted invariant** over the real output: the green SGR code appears **only inside on-path spans** (scenario-independent; the bug class we keep reintroducing). Its dual — every on-path cell is coloured, no grey gaps on the route — lands with the renderer rebuild (D3/D4). - -### Each scenario carries its explicit input — the golden is `render(input)` - -A golden is not a floating picture; it is the expected output **for a specific input graph**. Each scenario carries that input so the thing the human reasons from is exactly the thing the real pipeline (D4) is fed — the test is `render(input) === golden`. - -```ts -input: { - contracts: ['∅', 'root', 'trunk', 'alt'], // contract hashes (identifiers only) - migrations: [ // edges, from → to - { name: '000_init', from: '∅', to: 'root' }, - { name: '001_trunk_feature', from: 'root', to: 'trunk' }, - { name: '002_alt_feature', from: 'root', to: 'alt' }, - ], -}, -from: '∅', // focus variants: migrate --from (path origin / current DB state) -to: 'alt', // focus variants: migrate --to (path destination) -onPath: ['000_init', '002_alt_feature'], // focus variants: the highlighted route's migrations (derivable from from/to) -``` - -- **Ordering is determined by migrations, not contract hashes.** Every migration name carries a **3-digit prefix** (`000_`, `001_`, …) that stands in for a timestamp; lexicographic order of the prefixed name **is** chronological order. The layout orders by migration order; contract-hash lex order is **not** an input to layout. (Real contract hashes are sha256 — lexically arbitrary — so hash order would be meaningless.) -- **Migration-name labels are display-only.** They render in the gallery for human readability but are **stripped from the `render(input) === golden` comparison** (the assertion is structure + colour, not label text or its alignment). -- **`from` / `to` are the `migrate --from` / `--to` contracts** — the path's origin (current DB state) and destination. A focus variant *is* a `migrate --from X --to Y`; the highlighted path is what `migrate` computes between them. So focus variants must have **distinct `from`/`to`** to be distinct real invocations (e.g. a fan/diamond whose variants all share `∅ → merge` collapse to one real `migrate` — only one route is the computed path). -- `onPath` lists the migration names on the highlighted route (empty for `flat`); it is the expected result of computing the path from `from` to `to`. - -### Golden rows separate structure from identity — the label is looked up, never hand-drawn - -A golden row hand-authors **only the structural graph** (lanes, connectors, arrows, node markers) and **which migration/contract that row is** — never the label text, its alignment, or its colour. The renderer looks the name up in `input` and prints it. This keeps every bit of label alignment/formatting/colour logic out of the goldens, while still pinning which line maps to which migration/contract for test expectations. - -```ts -const fork2Flat = parseGrid([ - ['○', 'trunk', 'd' ], // node row: [glyphs, contract, colours] - ['│↑', '001_trunk_feature', 'dd' ], // migration row: [glyphs, migration, colours] - ['│ ○', 'alt', 'd.1' ], - ['│ │↑', '002_alt_feature', 'd.1d'], - ['│─╯ ', 'd11.'], // connector row: [glyphs, colours] (no identity) - ['○', 'root', 'd' ], - ['│↑', '000_init', 'dd' ], - ['○', '∅', 'd' ], -]); -``` - -- A row is `[glyphs, name, colours]` when it carries a contract or migration; `[glyphs, colours]` for a pure connector row. -- **`glyphs`** = structural characters only (`│ ╭ ╮ ╰ ╯ ─ ↑ ↓ ⟲ ○ ∅` + spaces) — no label text baked in. -- **`colours`** = one code per glyph character (`colours.length === glyphs.length`); the old "last code covers the label" rule is gone. -- **`name`** must exist in the scenario's `input` (contract hash or migration name) — `renderCells` looks it up and prints the label; an unknown name is an authoring error (a free consistency check between golden and input). -- The looked-up label is **gallery display only**; the `render(input) === golden` assertion compares structure + colour, and uses `name` to verify which migration/contract the real renderer placed on each row. - -**RED/GREEN.** The real pipeline starts on today's renderer, whose output ≠ the goldens (the current bleed) ⇒ RED. After the rebuild, real output == golden ⇒ GREEN; reverting the new renderer must break the match again. The hand-authored goldens are the spec of correct rendering; the human verifies them by eye via the gallery; the implementation must reproduce them. - -### Scenario catalogue (locked) — three-level: `scenario : strategy : variant` - -A golden is identified by three axes. **Strategy** is a first-class axis because it decides **z-order and the colour rule**, not just appearance: - -- **`flat`** — no chosen path. Every lane is a peer; colour **rotates by lane, numbered from 1** (lane 0 = colour `1`, lane 1 = `2`, …) — **no dim**; the **trunk stays on top** at merges/forks (`│─╯`). **Exactly one golden per scenario** (no variants). Edge cases — convergence, multiple lanes — arise from the topology itself, so no variants are needed. -- **`focus`** — one chosen path. The path is **lifted on top and drawn as one continuous line**; **colour follows the route, not the column** — the on-path line owns *every* cell it passes through, drawn green and continuous, **occluding** whatever it crosses; off-path lanes yield beneath it, dim. **Many variants per scenario**, each highlighting a **different path** (this is where the hard logic lives, so variants are deliberately chosen to traverse distinct routes). - -Identifier / filter syntax: `scenario` · `scenario:strategy` · `scenario:strategy:variant` (e.g. `merge-2:flat`, `merge-2:focus:alt`). - -| Scenario | `flat` | `focus` variants (each a distinct highlighted path) | -|---|---|---| -| `linear` | ✓ | `full` | -| `fork-2` | ✓ | `trunk`, `alt` | -| `merge-2` | ✓ | `trunk`, `alt` | -| `diamond` | ✓ | `trunk`, `alt` | -| `fan-3` | ✓ | `trunk`, `altA`, `altB` | -| `wide-fan` | ✓ | `trunk`, `alt` | -| `rollback-adjacent` | ✓ | `forward`, `through-rollback` | -| `rollback-arc` | ✓ | `trunk`, `through-arc` | -| `rollback-merge` (two rollbacks landing on the same node) | ✓ | `via-A`, `via-B` | -| `rollback-cross` (one back-arc crossing another) | ✓ | `arc-1`, `arc-2` | -| `self-loop` | ✓ | `through-loop` | - -`rollback-merge` renders as two separate back-lanes in this slice; convergence into one lane is the `render-redesign-geometry` slice. - -## Slice-specific done conditions - -- [ ] The captured-failure cases (the `focus:alt` highlight variants + `rollback-cross:focus:arc-1`) are RED against current code and GREEN after this slice — verified by the revert-to-red check (revert the new renderer, the colour tests fail; restore, they pass). -- [ ] `render(input) === golden` holds for every catalogue scenario, and the green-only-on-path invariant shows **zero** off-path green on every `focus` variant (the ground-truth check). -- [ ] The old `StructuralCell` kinds, the render `switch`, and the `migrationHash?` bolt-on no longer exist. -- [ ] Normal-mode (`graph`/`status`/`list`) rendering changes **intentionally**: the single-owner discipline replaces tees (`├`/`branchTee`/`mergeTee`) with corners, so the trunk stays a continuous `│` and parents corner in beneath it (`│─╮─╮`). The `graph-render.test.ts` snapshots are **regenerated** and the new corner rendering is visually reviewed. (Byte-identical is impossible here — it would contradict the no-tee rule. Only the *node labels, alignment, and lane assignment* stay equivalent; the *junction glyphs* change tee→corner.) - -## Open Questions - -None. (The within-plane junction-colour question is dissolved by the no-tee / single-owner discipline — see § Chosen design and the design doc. Default columns-per-lane is settled in the `render-redesign-geometry` slice.) - -## References - -- Design: [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md) -- Sibling slice: `../render-redesign-geometry/` (convergence + configurable geometry) -- Current tests to evolve: `cli/test/utils/formatters/migration-graph-colour-matrix.test.ts`, `…/migration-graph-cell-identity.test.ts` diff --git a/projects/migration-graph-rendering/slices/render-redesign-geometry/plan.md b/projects/migration-graph-rendering/slices/render-redesign-geometry/plan.md deleted file mode 100644 index 18ab24d8cd..0000000000 --- a/projects/migration-graph-rendering/slices/render-redesign-geometry/plan.md +++ /dev/null @@ -1,163 +0,0 @@ -# Dispatch plan — render-redesign-geometry - -Decomposes [`spec.md`](./spec.md) (back-arc convergence + configurable geometry) into -dispatches. Design-of-record: [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md) -(§ Planes → convergence; § Geometry is configurable). Builds on the merged -`render-redesign-core` line/plane/occlusion pipeline. - -Absorbs convergence (once drafted as a separate bug-slice against the old -`migration-graph-layout.ts` / tee renderer that #762 removed — that draft is deleted). - -## Grounding (current code) - -- **Layout:** `src/utils/formatters/migration-graph-grid-layout.ts`. Skipping - rollbacks currently get **one back-lane per arc** (`numBackLanes = - skippingRollbacks.length`, line ~283; `geomLaneOf` assigns a distinct rail per - arc). `backArcsByTarget` already exists but is unused for lane sharing. -- **Geometry:** `colsPerLane` is **already** a `GridOptions` param threaded through - `buildGrid`; the occlusion renderer reads the pre-widened grid, so widening needs - no renderer edit. Remaining hard-coded geometry is label-side: `hashLength` and - `dirNameWidth` in `migration-graph-labels.ts`. -- **Test harness:** `test/utils/formatters/migration-graph-scenario-gallery.ts` - (scenarios + `pnpm gallery`) and `migration-graph-gallery-snapshots.test.ts` - (vitest `toMatchSnapshot()` for verbatim ANSI, plus structural invariants: no-tee - alphabet, focus colours present). Existing rollback scenarios: `rollback-adjacent`, - `rollback-arc`, `rollback-merge` (co-**sourced** — same source, different targets, - NOT convergence), `rollback-cross`. **No converging scenario** (≥2 skipping rollbacks - → the *same* target). - -### Pinned definition of "converged" (structural, design decision) - -The design doc states the convergence *rule* but has no worked example in the new -corner model (the old tee-based draft used now-forbidden tees). Because -the harness snapshots bytes automatically, the meaningful RED/GREEN signal is a -**structural assertion**, not a hand-drawn glyph golden. "Converged" means: - -1. Skipping rollbacks that land on the **same target node** share **one** back-lane - column. Total grid width = `(numForwardLanes + numTargetGroups) * colsPerLane` - (today it is `(numForwardLanes + numSkippingArcs) * colsPerLane`). -2. Each source tees into the shared rail (corner, never a tee — occlusion arbitrates - the shared vertical's colour per the existing line/plane model). -3. A single landing closes at the target (one drawn `◂`/corner; others occluded). -4. Display order is unchanged — the tip stays topmost (convergence is a back-lane - routing change only; `computeDisplayOrder` is not touched). - -Exact glyphs are not pinned: they fall out of the renderer + occlusion in D2 and are -recorded in the auto-captured snapshot. The structural width/lane-count assertion is -what proves convergence. - -## Dispatches - -### Dispatch 1: Convergence scenario + RED structural assertion - -- **Outcome:** A `rollback-converge` scenario (two node-skipping rollbacks landing on - the *same* target, e.g. `∅→a→b→c→d` trunk + `d→a` + `c→a`; plus a three-arc variant - `d→a` + `c→a` + `b→a`) is added to the scenario gallery. A **structural convergence - assertion** (arcs sharing a target occupy exactly one back-lane column → grid width - per the pinned formula; tip stays topmost) is added, marked expected-fail (`it.fails`, - the core slice's RED convention) against today's per-arc layout. Red confirmed. -- **Builds on:** the merged `render-redesign-core` scenario gallery + occlusion pipeline. -- **Hands to:** a failing structural assertion pinning the narrowed converged shape, plus - the new scenario whose verbatim snapshot is auto-captured (per-arc bytes for now; D2 - re-records it converged). -- **Focus:** `migration-graph-scenario-gallery.ts` + `migration-graph-gallery-snapshots.test.ts` - only. No `src/` change. -- **Completed when:** the new structural assertion is RED for the expected reason (more - than one back-lane column for same-target arcs); `it.fails` passes (i.e. it genuinely - fails); every other gallery case still green; `pnpm test:packages -- @prisma-next/cli`. - -### Dispatch 2: Convergence layout (GREEN) - -- **Outcome:** In `migration-graph-grid-layout.ts`, skipping rollbacks are grouped by - target node; **one shared `geomLane` per target group** (not per arc); each source - tees into the shared rail; a single landing closes at the target; `numBackLanes` / - `totalCols` derive from the group count. Occlusion keeps each arc's own colour on the - segment it owns; the single-owner invariant holds. -- **Builds on:** Dispatch 1's failing convergence oracle. -- **Hands to:** back-arcs to a shared target render as one rail; the colour matrix and - all non-converging rollback scenarios are unchanged. -- **Focus:** back-arc planning + tee/landing emission + width computation in - `grid-layout.ts`. No colour-semantics change; no geometry-constant refactor. -- **Completed when:** D1's structural convergence assertion passes (flip `it.fails` → - `it`; **strengthen the tip-topmost check** when un-`fails`ing — assert `grid[0]`'s node - cell is the highest-rank tip, not merely that some node leads the grid, per the D1 - review); revert-to-red verified by reverting the layout change); the `rollback-converge` - snapshot is re-recorded to the converged output via `pnpm gallery` then - `--update-snapshots`; `rollback-arc` / `rollback-cross` / `rollback-merge` / - `rollback-adjacent` snapshots byte-identical (non-converging cases untouched); - `pnpm test:packages -- @prisma-next/cli` green. - -### Dispatch 3: Configurable geometry - -- **Outcome:** Every hard-coded geometry constant (label-side `hashLength` / - `dirNameWidth`, any connector-gap literal, and `colsPerLane`'s default) is a named - parameter on the options surface consumed by layout + labels. A test renders one - fixture at `colsPerLane` 2 vs 3 and asserts the output scales **with no renderer - edit**; the default keeps existing snapshots byte-identical. -- **Builds on:** Dispatch 2's converged layout (so the audited constant set is final). -- **Hands to:** geometry fully parameterized — a one-line constant change rescales the - output; defaults unchanged. -- **Focus:** the geometry-constant surface across `grid-layout.ts` + `labels.ts` - (+ `command-render.ts` plumbing). No topology or colour change. -- **Completed when:** a `colsPerLane` 2-vs-3 scaling test passes with no `src/` change - beyond the constant; `grep` finds no remaining hard-coded geometry literal in the - named files; default-path snapshots byte-identical; `pnpm test:packages -- cli` green. - -### Dispatch 4: Convergence golden oracle (hand-authored, independent) - -- **Outcome:** The hand-authored `golden-pipeline` oracle independently pins the **converged - one-lane** output. The converging `rollback-merge` golden in `gallery-goldens-backlink.ts` - (`rm_c→rm_a` + `rm_d→rm_a`, both landing on `rm_a`) — currently authored in the stale - **two-lane** form and broken by D2's convergence — is rewritten (flat + focus:via-A + - focus:via-B) to the one-lane converged picture, **derived from the design** (one shared - back-lane, sources tee in, single `○◂╯` landing; focus = on-path green continuous along - its route, off-path dim, no bleed). The renderer is then confirmed to match; the - green-only-on-path invariant passes. A 3-arc converging golden is added for breadth. - The `golden-pipeline` describe drops its now-obsolete "RED baseline — expected to fail" - framing (it becomes a real passing oracle). -- **Builds on:** D2's converged layout. (This is the strong independent oracle for AC-1 that - D1/D2's snapshot + width-assertion only weakly covered — and it fixes the CI Test job that - D2's convergence turned red against the stale golden.) -- **Hands to:** convergence is pinned by a hand-authored oracle; `golden-pipeline` 53/53 green. -- **Focus:** `gallery-goldens-backlink.ts` (rewrite/extend goldens) + `golden-pipeline.test.ts` - (describe rename). No `src/` change unless the design-derived golden reveals a real renderer - bug — in which case **halt and surface**, do not copy renderer output to force a pass. -- **Completed when:** the converging goldens are hand-authored to the design's one-lane form; - `pnpm --filter @prisma-next/cli test --run test/utils/formatters/golden-pipeline.test.ts` - is fully green (incl. green-only-on-path); the full formatter suite green; no `.snap` - change; transient-ID scan empty. - -## dispatch-INVEST check - -- **Independent:** D1 (tests only) → D2 (layout) → D3 (constants) are strictly - sequential; each hand-off is a named stable state. No concurrent work elsewhere. -- **Negotiable:** each names the outcome; the implementation path (exact grouping data - structure, constant names) is executor discovery. -- **Valuable:** D1 pins the contract, D2 delivers convergence, D3 delivers - configurability — each is a slice-DoD line, none is pure prep. -- **Estimable:** each `Completed when` is a binary test/grep gate. -- **Small:** convergence is one coherent layout change (D2); the geometry audit is a - bounded constant lift (D3); both fit one executor session. One reviewable PR. -- **Testable:** gated by `pnpm test:packages -- cli` + targeted greps. - -Hand-offs are linear; the final hand-off (D3) plus D2's colour-matrix-green check cover -all three slice-DoD conditions. One slice, one PR. - -## Model tiers - -Implementers: sonnet-mid. Reviewer pass: opus-high. - -## Open items (follow-ups, not in this PR) - -- **`assertGreenOnlyOnPath` in `golden-pipeline.test.ts` is vacuous.** `renderGrid` emits - no migration-name labels, so its `line.includes(offPathName)` check never fires — the - green-only-on-path guarantee is actually carried by the structure+colour `toEqual` (which - does compare per-glyph colour tokens, so it catches glyph-level bleed). Surfaced in the D4 - review. Harden by rewriting the invariant against parsed colour tokens, or delete it as - dead weight. Not in this PR. -- **`colsPerLane` is read independently by `buildGrid` and `renderGridRow`.** Today this - cannot diverge — every production caller uses the default and - `RenderMigrationGraphCommandInput` has no `colsPerLane` field — so it is not a bug. But - if `colsPerLane` is ever exposed to CLI users, the command-render path must thread one - option value to both phases (or derive width from the built grid) so the gutter reflows. - Surfaced in the D3 review. File a ticket if/when colsPerLane becomes user-facing. diff --git a/projects/migration-graph-rendering/slices/render-redesign-geometry/spec.md b/projects/migration-graph-rendering/slices/render-redesign-geometry/spec.md deleted file mode 100644 index ed63a7dd02..0000000000 --- a/projects/migration-graph-rendering/slices/render-redesign-geometry/spec.md +++ /dev/null @@ -1,41 +0,0 @@ -# Slice: graph-render redesign — back-arc convergence + configurable geometry - -_Parent project `projects/migration-graph-rendering/`. Outcome: rollback back-arcs to the same target collapse into one shared lane (narrower, truer output), and the layout/render geometry (columns-per-lane etc.) becomes configurable constants rather than hard-coded values._ - -> **Design-of-record:** [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md). Execution only. **Builds on** `render-redesign-core` (the line/plane/occlusion pipeline must exist first — convergence is a layout-routing change expressed in the new model). - -## At a glance - -Two enhancements on top of the correct pipeline: (1) **back-arc convergence** — back-arcs that land on the same target node share a single back-lane (sources tee in, one landing) instead of one lane each, which narrows the graph and removes crossings; (2) **configurable geometry** — extract the hard-coded spacing/columns-per-lane values into named parameters so the layout's density can change without rewriting the renderer. - -## Chosen design - -Per the design doc (§ Planes → convergence, § Geometry is configurable). In scope: -- **Convergence:** group back-arcs by target node; route one shared upper-plane back-lane per target, with each source teeing into it and a single landing at the target. (See the design doc's worked before/after — three `rollback_to_users_*` arcs → one lane.) -- **Configurable geometry:** identify every hard-coded geometry constant across the layout + renderer (columns-per-lane, gutter widths, label gaps, hash-column width, etc.), lift them into a single named-constants surface (or an options object) consumed by both phases. Changing "3 columns per lane" must be a one-line config change with no renderer edits. -- **Out:** the core data model / occlusion (that's `render-redesign-core`); any colour-semantics change. - -## Coherence rationale - -Both are layout-side parameterisations of an already-correct pipeline — convergence changes *which lane* a back-arc occupies; the geometry work changes *how wide* lanes are. One reviewer holds "the layout is now configurable and converges back-arcs"; the test suite proves the rendered output matches the converged + parameterised expectation. - -## Test-first discipline - -Author tests before implementation, red-first: -1. **Convergence tests** — a fixture with ≥2 back-arcs to one target asserts they share one back-lane (column count drops vs the per-arc layout; the rendered graph matches the design doc's converged example), and crossings reduce accordingly. -2. **Geometry-parameter tests** — render the same fixture at, e.g., 2 vs 3 columns-per-lane and assert the output scales as expected with **no** code change beyond the constant; assert the default produces today's spacing (no unintended visual change). - -## Slice-specific done conditions - -- [ ] Back-arcs to a shared target render as one lane; a multi-rollback fixture's width shrinks to the converged form and matches the design doc's example. -- [ ] Every geometry constant is a named parameter; a test flips columns-per-lane and the output scales with no renderer change; the default keeps existing output byte-identical. -- [ ] No regression to colour-correctness from `render-redesign-core` (the colour matrix stays green). - -## Open Questions - -1. Default columns-per-lane and the full set of geometry parameters to expose (design doc § Open questions). Working position: enumerate during build from the current hard-coded sites; default to today's values. - -## References - -- Design: [`../../design/graph-render-redesign.md`](../../design/graph-render-redesign.md) -- Prerequisite slice: `../render-redesign-core/` diff --git a/projects/migration-graph-rendering/slices/status-db-overlay/spec.md b/projects/migration-graph-rendering/slices/status-db-overlay/spec.md deleted file mode 100644 index 70b2e5a075..0000000000 --- a/projects/migration-graph-rendering/slices/status-db-overlay/spec.md +++ /dev/null @@ -1,94 +0,0 @@ -# Slice: `migration status` = the shared tree + a DB-state overlay; delete dagre - -_Parent project `projects/migration-graph-rendering/`. Outcome this slice contributes to: `migration status` tells the user where their connected database sits relative to all on-disk migrations. It renders the **same tree** as `migration list`/`graph` (the shared engine) and overlays, per migration, **applied** or **pending**; everything else is plain. It also retires the dagre renderer (its last consumer) and makes the condensed tree the default for `migration graph` (drops the experimental `--tree` flag). Tracking: [TML-2748](https://linear.app/prisma-company/issue/TML-2748)._ - -## At a glance - -``` -$ prisma-next migration status -app: -○ 3b2d98d (contract) (main) -│↑ 20260305_add_avatar 73e3abe → 3b2d98d ⧗ pending -○ 73e3abe (db) -│✓ 20260303_add_phone ef9de27 → 73e3abe ✓ applied -○ ef9de27 -│✓ 20260301_init ∅ → ef9de27 ✓ applied -○ ∅ - -1 pending — run `prisma-next migrate --to 3b2d98d` -``` - -- **applied** (green `✓`) — a ledger entry exists for this migration (exact `migrationHash` match, D7). Literal "ever ran" — a rolled-back migration still reads applied here; the timeline lives in `log`. -- **pending** (yellow `⧗`) — on the shortest path from the DB marker to the target contract, and not applied (runs next on `migrate`). -- everything else — plain (full list, no subgraph pruning). -- `(db)` marks the DB's current contract; `(contract)`/refs ride the existing node overlays. - -## Chosen design - -Per D1/D6/D9/D11: - -- **Render the shared tree directly** via the `graph --tree` engine (`buildMigrationGraphRows` → `buildMigrationGraphLayout` → `renderMigrationGraphTree`) — **not** dagre, **not** `list`'s flat renderer. `status` does not depend on `list` adopting the tree (TML-2768); it calls the engine itself. -- **Overlay via the shared edge-annotation field (D11):** `status` populates `edgeAnnotationsByHash: Map`. The `(db)` marker uses the **existing** `dbHash` node overlay (already in `RenderMigrationGraphTreeOptions`). If TML-2768 has landed, `edgeAnnotationsByHash` already exists and `status` only adds the `status` key; if not, `status` introduces the field per D11's type. -- **applied set — from the ledger, not the graph.** Per space, `applied = { e.migrationHash | readLedger(space) has a row with that hash }` (exact match). This **replaces** `deriveEdgeStatuses`'s current `findPath(∅→marker)`-derived applied set and resolves TML-2130 (applied must come from the ledger, not a graph walk). -- **pending set — shortest path, minus applied.** `pending = edges on findPath(dbMarker → target) that are not applied`. `target` = `--to` ref/hash if given, else the app contract hash (existing target resolution). -- **Multi-space (D4):** render **every** on-disk space as its own tree section (`spaceId:` heading when >1 space), matching `list`/`graph`. Per space: marker from `readAllMarkers()` (space→`ContractMarkerRecord`), ledger from `readLedger(spaceId)`, graph from `aggregate.space(spaceId)`. `--space ` narrows (reuses `list`'s `errorSpaceNotFound`). -- **Origin/target controls (D9):** default origin = DB marker, default target = app contract. `--to X` retargets to ref/hash. `--from X` overrides the origin (offline-capable). **The applied overlay shows iff the origin is the real DB** (online, no `--from`); with `--from`, applied-ness is meaningless and the overlay drops (pending still computes from `X → target`). `status` requires a connected DB **unless** `--from` supplies the origin. `(db)` node marker shows iff a DB is connected. -- **Summary footer (D10):** one short footer — a headline (`up to date` / `N pending — run prisma-next migrate --to X` / a divergence warning) plus a `missing invariant(s): …` line **only** when targeting a ref that declares required invariants the DB lacks. Path-selection/tie-break detail is **not** here (→ future `migration path`, TML-2771); a ref's *declared* invariants are **not** here (→ `ref show`, TML-2772) — `status` shows only the actionable *missing* set. -- **`--json`** = `list`'s shape (`{ ok, spaces: [{ spaceId, migrations: MigrationListEntry[] }], summary }`) augmented with a per-migration `status: 'applied' | 'pending' | null` field, plus top-level `markerHash: string | null` and `targetHash: string` per space section (`{ spaceId, markerHash, migrations: [{ …entry, status }] }`). Tree never appears in JSON (D3). -- **Delete dagre (D5):** remove `graph-render.ts`, `graph-migration-mapper.ts`, `graph-types.ts`, the `@dagrejs/dagre` dependency, and their tests; make the tree the default for `migration graph` (drop the experimental `--tree` flag; keep `--ascii`/`--legend`/`--dot`/`--json`). `rg 'dagre|graphRenderer|migrationGraphToRenderInput'` over `cli/` returns empty. - -## Scope - -**In:** - -- `migration status` renders the shared tree per space + applied/pending edge overlay + `(db)` node marker. -- applied from `readLedger` (exact hash); pending from `findPath(marker→target)` minus applied; per space. -- `--from`/`--to` per D9; `--space` per D4; the lean summary footer (headline + missing-invariants line). -- `--json` = list shape + `status` field + `markerHash`/`targetHash`. -- Delete dagre + mapper + types + dependency + tests; drop `migration graph`'s `--tree` flag (tree is default). -- Tests: online up-to-date / pending / divergence; `--from` (offline, applied dropped); `--to` ref+hash; multi-space; `--json` shape; `migration graph` default-is-tree; `rg dagre` empty. - -**Out:** - -- `migration list` / `migration graph` rendering themselves beyond dropping `--tree` (own tickets). -- The ledger read API (TML-2769, merged). -- `migration path` (TML-2771) and `ref show` invariants (TML-2772). -- Introducing package-fact edge annotations (that's TML-2768; `status` only adds the `status` key to the shared field). - -## Pre-decided edge cases - -| Edge case | Disposition | -|---|---| -| DB marker hash not in the on-disk graph (divergence) | Render the full tree, **no pending overlay** (no path computable), applied overlay still shown from the ledger; footer headline = divergence warning naming the marker hash. | -| Online, no DB connection and no `--from` | Hard error: a DB is required unless `--from` supplies the origin (D9). | -| `--from X` (offline) | Applied overlay drops; `(db)` marker not shown; pending = `findPath(X → target)` minus nothing (no applied set). | -| DB at the target already (at head) | No pending; footer = `up to date`. | -| A pending edge that is also applied (re-apply scenario) | applied wins (ledger is authoritative); never both — `applied` takes precedence in the overlay. | -| Migration hash applied but its package no longer on disk | The edge isn't in the on-disk graph, so it has no row to overlay; the ledger truth surfaces in `log`, not `status` (status is on-disk + overlay). | -| `--space ` unknown | `errorSpaceNotFound` (enumerates available ids), reused from `list`. | -| Empty space (no migrations) | Per-space empty-state line; no overlay. | -| `--to` ref with required invariants the DB lacks | `missing invariant(s): …` footer line; render proceeds. | - -## Dispatch plan - -1. **Renderer: `status` overlay key.** Ensure `edgeAnnotationsByHash`/`MigrationEdgeAnnotation` exist (rebase onto TML-2768, or introduce per D11) and render `status: 'applied'` → green `✓`, `'pending'` → yellow `⧗` on the migration row. `(db)` already works via `dbHash`. Renderer unit tests (applied/pending/plain rows; db marker). *Hands to 3.* -2. **Status computation (ledger-sourced).** Per space: build `applied` from `readLedger(space)` (exact `migrationHash` set), `pending` from `findPath(marker→target)` minus applied; honour `--from`/`--to` (applied dropped under `--from`). Replace the applied-from-graph path in `deriveEdgeStatuses` (resolves TML-2130). Pure-function unit tests (online/offline/divergence/at-head). *Hands to 3.* -3. **Command rewrite.** Rewrite `migration-status.ts` to: enumerate spaces (`readAllMarkers` + `aggregate.space`), render the shared tree per space with the overlays from 1+2, emit the lean footer, and emit the augmented `--json`. `--space` narrowing. *Builds on 1+2.* -4. **Delete dagre + flip `graph` default.** Remove `graph-render.ts`, `graph-migration-mapper.ts`, `graph-types.ts`, `@dagrejs/dagre` (package.json + lockfile via `pnpm install`), their tests + vitest refs; drop `migration graph`'s `--tree` flag (tree is default, keep `--ascii`/`--legend`/`--dot`); update graph command + tests. Verify `rg 'dagre|graphRenderer|migrationGraphToRenderInput'` over `cli/` is empty. *Independent of 1–3 except the final graph-command edit; sequence last to avoid churn.* - -## Slice-specific done conditions - -- `status` renders the full per-space tree + applied/pending overlay via the shared renderer; `--from`/`--to`/`--space` behave per D9/D4; applied comes from the ledger (exact hash), pending from shortest path minus applied; footer is headline + missing-invariants only; `--json` = list shape + `status` + `markerHash`/`targetHash`; dagre + mapper + types + dependency + tests are gone and `--tree` is the default (flag dropped); `rg 'dagre|graphRenderer|migrationGraphToRenderInput'` over `cli/` is empty; CI green. - -## Sequencing - -Parallel with `list`→tree (TML-2768) and `log` (TML-2770). Soft-depends on TML-2768 only for the `edgeAnnotationsByHash` field (D11) — rebase if TML-2768 lands first, else introduce the field here. The dagre deletion (dispatch 4) is self-contained. - -## References - -- Project decisions: `projects/migration-graph-rendering/decisions.md` (D1, D5, D6, D9, D10, D11). -- Linear: [TML-2748](https://linear.app/prisma-company/issue/TML-2748); resolves applied-from-ledger [TML-2130](https://linear.app/prisma-company/issue/TML-2130). -- Renderer: `cli/src/utils/formatters/migration-graph-{rows,layout,tree-render}.ts`. -- Current status + computation: `cli/src/commands/migration-status.ts` (`deriveEdgeStatuses`, `formatStatusSummary`). -- Ledger read: `ControlClient.readLedger(space)` (`cli/src/control-api/client.ts`), `readAllMarkers()`. -- Dagre to delete: `cli/src/utils/formatters/graph-render.ts`, `graph-migration-mapper.ts`, `graph-types.ts`. diff --git a/projects/migration-graph-rendering/spec.md b/projects/migration-graph-rendering/spec.md deleted file mode 100644 index f2fafbbd93..0000000000 --- a/projects/migration-graph-rendering/spec.md +++ /dev/null @@ -1,145 +0,0 @@ -# Slice: replace `migration graph`'s dagre renderer with a condensed, full-history diagram - -_Single-slice project. Outcome: `migration graph` renders the **whole** migration graph — every contract once, every migration as a labelled edge, back-edges and disjoint components included — in the condensed lane-per-column language locked in [`mockups.md`](./mockups.md), with no golden-path assumption._ - -## At a glance - -`migration graph` today routes its tolerant `MigrationGraph` (from `aggregate.app.graph()`) through `migrationGraphToRenderInput` — which assumes a single canonical linear history (`findPath`, `forwardChain`, a `spineTarget`, a phantom dashed edge to the current contract) — and then through a dagre auto-layout renderer that draws contracts and migrations as a large 2D node graph with per-marker glyph tags (`◆ db`, `◇ contract`). - -This slice replaces both layers for `migration graph` with a new layout engine + text renderer that: - -- places **every contract on exactly one `○ ` row** and **every migration on its own labelled edge row**, root(s) at the bottom, tip(s) at the top; -- carries direction in the edge's own lane (`│↑` forward, `│↓` rollback, `│⟲` self), with branch/merge spine (`├ ┐ ┴ ┬`) for divergence/convergence and routed solid-box arcs only for node-skipping rollbacks; -- decorates node rows with the `migration list --graph` `(refs)` overlay — user refs plus the reserved `db` and `contract` names — instead of per-marker glyph tags; -- makes no single-canonical-path assumption: multi-root, disjoint, cyclic, and detached-contract graphs all render without throwing. - -Worked example (DB one migration behind the current contract): - -``` -○ a94b7b4 (main, contract) -│↑ add_posts ef9de27 → a94b7b4 -○ ef9de27 (db, prod) -│↑ init ∅ → ef9de27 -○ ∅ -``` - -The complete visual language and the full topology gallery (linear, rollback, diamond, sequential-diamonds, 3-way fan, cross-link, kitchen-sink, routed skip-rollback, disjoint forest, dangling parent, self-edge, pure cycle, and the three overlay cases) is **locked** in [`mockups.md`](./mockups.md). That file is the design-of-record; this spec pins the implementation architecture, scope, and decomposition. - -## Chosen design - -### Data flow (before → after) - -The command already loads through the ContractSpace aggregate and holds a tolerant `MigrationGraph` plus `refs` and `contractHash` (see `migration-graph.ts`). Only the render path changes. - -| Stage | Today | This slice | -|---|---|---| -| Source | `aggregate.app.graph()` → `MigrationGraph` (tolerant) | unchanged | -| `--json` / `--dot` | read `graph.nodes` / `graph.migrationByHash` directly | unchanged (must stay byte-identical) | -| Default render | `migrationGraphToRenderInput` (golden-path) → `graphRenderer.render` (dagre) | new layout engine → new text renderer | - -The new render path is a pure pipeline over the `MigrationGraph`: - -1. **Row model** — classify each edge `forward` / `rollback` / `self` (DFS back-edge detection, the same classification `migration list --graph`'s topology pass already performs) and produce a deterministic vertical node ordering: roots at the bottom, tips at the top, disjoint components stacked with a blank separator. Pure data; no glyphs. Ordering tie-breaks follow the existing `dirName`-descending / enumerator order so snapshots are stable. -2. **Column model (grid)** — allocate a column per node and per in-flight edge lane; compute branch/merge spine connectors and long-edge lanes (a `git log --graph`-style allocator). Pure data: a per-row cell grid with lane assignments and spine glyphs. No back-arcs, no overlays yet. -3. **Text renderer** — emit the grid as text: node rows (`○ `), edge rows with the in-lane direction glyph and the authoritative `` data column plus the migration name. Adjacency cases first (every edge points at a layout-neighbour); node-skipping back-arcs and overlays layer on top. -4. **Routed back-arcs** — node-skipping rollbacks tee off their source node (`○─╮`), run a solid back-lane (`│`), and turn into their target (`◂╯` / `◂─╯`); overlapping arcs take adjacent back-lanes left-to-right, crossings `┼`. See [`mockups.md § Routed arcs`](./mockups.md). -5. **Node overlays** — append the `(refs)` parenthetical to node rows, reusing `migration-list-styler.ts`'s `refs` styler (which already styles the reserved `db` name to pop) and aligning the parenthetical to the same column as the edge-row `from → to` data. Order: user refs lexicographically, then `db`, then `contract`. A node nothing points at carries no decoration. The detached current contract (working schema emits a hash no migration produces) renders as a **floating node** carrying `(contract)` with **no incoming edge** — deliberately dropping today's phantom dashed connector. -6. **Glyph mode / ASCII fallback** — the renderer takes a glyph set; an ASCII set (and `--ascii` / non-UTF-terminal detection) swaps box-drawing for ASCII, orthogonal to `--no-color`. Glyph-mode detection is injected (pure) via `TerminalUI`, matching the Tier-2 arrangement. - -### Module placement - -New modules live CLI-side under `cli/src/utils/formatters/`, matching where the Tier-2 `migration-list-graph-{topology,layout,render}.ts` engine already lives (the edge classifier `classifyMigrationListGraphTopology` is the reuse anchor). Exact filenames are implementer discovery; the natural shape mirrors Tier-2. - -## Coherence rationale - -One reviewer holds this in one sitting: it rewires a single command's default-render path from one renderer to another, behind unchanged `--json` / `--dot` output and an unchanged source. The dispatches are internal increments of one pipeline (row model → grid → text → arcs → overlays → ASCII); the PR is correct only as a whole, and rolls back as one unit. The locked `mockups.md` gallery is the acceptance surface, so the chosen design does not drift mid-loop. - -## Scope - -**In:** - -- New layout engine + text renderer for `migration graph`'s default output, per [`mockups.md`](./mockups.md). -- Rewiring `migration-graph.ts`'s non-`json`/non-`dot` branch onto the new renderer. -- Reusing the Tier-2 edge classifier and the `(refs)` styler. -- Golden snapshots over the synthetic mockup topologies and the demo `examples/prisma-next-demo/migration-fixtures` pathological cases. -- A reference doc `docs/reference/migration-graph-rendering.md` (mirroring the Tier-2 `migration-list-graph-rendering.md`). - -**Out:** - -- `migration status`'s graph rendering. It shares `migrationGraphToRenderInput` + `graphRenderer` (the dagre renderer) and renders a *focused* root→relevant-node subgraph, a different intent from the full-history view. This slice **leaves the dagre renderer and the mapper in place** for `migration status` and does **not** delete them. Migrating `migration status` onto the new renderer (and then deleting dagre + the `@dagrejs/dagre` dependency) is **TML-2748** — a follow-up blocked by this slice. **On this slice's close, pick up TML-2748.** -- `migration list --graph` (Tier-2) — already shipped, untouched. -- The `--json` / `--dot` output shapes — frozen. -- Any change to the contract surface or target adapters (none). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -|---|---|---| -| Dagre renderer shared with `migration status` | Out of scope; leave dagre in place | Deleting `graph-render.ts` / `graph-migration-mapper.ts` would break `migration status`. Follow-up migrates status, then deletes. | -| `--json` / `--dot` read `graph` directly | Must stay byte-identical | Both branches bypass the render pipeline; regression-test their output. | -| Detached current contract (working schema emits an unproduced hash) | Behaviour change: floating `(contract)` node, no edge | Today draws a phantom dashed edge from the tip; the new design drops it deliberately (an edge means a migration; there is none). | -| Multi-root / disjoint / cyclic / dangling-parent graphs | Must render without throwing | The tolerant `MigrationGraph` permits all of these; golden them from the demo fixtures + synthetic cases. | -| Output determinism | Tie-break on `dirName`-desc / enumerator order | Required for stable goldens; the Tier-2 topology pass already does this. | - -## Slice-specific done conditions - -- [ ] Golden snapshots for the full topology gallery (synthetic mockup cases + demo `migration-fixtures`) are committed and match [`mockups.md`](./mockups.md); `migration graph --json` / `--dot` and `migration status` rendering have regression coverage proving they are unchanged. - -## Open Questions - -All three shaping questions are **resolved** (operator-confirmed); the decisions are folded into Chosen design above: - -1. **Edge classifier — shared.** Adapt `classifyMigrationListGraphTopology` to accept the `MigrationGraph` edge set so Tier-2 and Tier-3 cannot disagree on forward/rollback/self. -2. **Module placement — CLI-side, following the `migration list --graph` pattern.** New modules mirror `migration-list-graph-{topology,layout,render}.ts` under `cli/src/utils/formatters/`. -3. **Goldens — exact-text alignment fixtures, not a stable API.** The rendered text is explicitly not a contract; fixtures pin alignment/regression and may be regenerated freely. - -## References - -- Locked design-of-record: [`mockups.md`](./mockups.md); prototyping harness: [`prototype/`](./prototype/). -- Linear issue: TML-2746. Follow-up (blocked by this slice): **TML-2748** — migrate `migration status` off dagre and delete the dagre renderer + `@dagrejs/dagre`. -- Tier-2 precedent (the engine this mirrors): `cli/src/utils/formatters/migration-list-graph-{topology,layout,render}.ts`; reference doc `docs/reference/migration-list-graph-rendering.md`. -- Surfaces that change / are protected: `cli/src/commands/migration-graph.ts` (rewire), `migration-list-styler.ts` (`refs` reuse), `graph-render.ts` / `graph-migration-mapper.ts` (left in place for `migration status`). - -## Dispatch plan - -### Dispatch 1: row model — edge classification + vertical node ordering - -- **Outcome:** A pure function maps a `MigrationGraph` to an ordered list of node rows (roots bottom, tips top, disjoint components stacked) with each edge classified `forward` / `rollback` / `self`. Deterministic ordering (`dirName`-desc / enumerator tie-break). No glyphs, no columns. -- **Builds on:** The spec's chosen design; the existing `classifyMigrationListGraphTopology` (reuse anchor). -- **Hands to:** A `RowModel` (ordered nodes + classified, source/target-resolved edges) consumable by the column allocator. Unit tests over the synthetic + demo fixtures assert ordering and per-edge kind. -- **Focus:** Classification + ordering only. Lane allocation, glyphs, and rendering are later dispatches. - -### Dispatch 2: column model — lane allocation + branch/merge spine - -- **Outcome:** A pure function assigns a column to each node and each in-flight edge lane, and computes branch/merge spine connectors (`├ ┐ ┴ ┬`) and long-edge lanes, producing a per-row cell grid. -- **Builds on:** Dispatch 1's `RowModel`. -- **Hands to:** A `GridModel` (per-row cells with lane assignments + spine glyphs, no back-arcs, no overlays). Unit tests assert lane columns for diamond, 3-way fan, sequential-diamonds, and cross-link. -- **Focus:** Forward-grain lane allocation + spine. Node-skipping back-arcs are deferred to dispatch 4. - -### Dispatch 3: text renderer (adjacency cases) + rewire `migration graph` - -- **Outcome:** A renderer emits the `GridModel` as text — node rows, edge rows with in-lane direction glyph (`↑`/`↓`/`⟲`), and the `from → to` data column + migration name — for all **adjacency-only** topologies. `migration-graph.ts`'s default branch is rewired onto it (replacing `migrationGraphToRenderInput` + `graphRenderer.render`); `--json` / `--dot` branches untouched. -- **Builds on:** Dispatch 2's `GridModel`. -- **Hands to:** `migration graph` renders the condensed diagram for adjacency topologies (linear, rollback, diamond, sequential-diamonds, 3-way fan, cross-link, kitchen-sink, disjoint forest, dangling parent, self-edge, pure cycle); golden snapshots for those committed; `--json`/`--dot` regression-covered. -- **Focus:** Adjacency rendering + command rewiring. Routed arcs and overlays deferred. - -### Dispatch 4: routed back-arcs for node-skipping rollbacks - -- **Outcome:** Node-skipping rollbacks render as solid box-drawing arcs originating from the source node (`○─╮` / `│` / `◂╯` / `◂─╯`), with overlapping arcs in adjacent back-lanes (left-to-right) and crossings as `┼`, per [`mockups.md § Routed arcs`](./mockups.md). -- **Builds on:** Dispatch 3's renderer + the adjacency/node-skipping distinction in the grid. -- **Hands to:** The `skip-rollback` fixture renders per mockup; golden committed. -- **Focus:** Back-arc routing only. - -### Dispatch 5: node overlays — `(refs)`, `db`, `contract`, detached contract - -- **Outcome:** Node rows carry the `(refs)` parenthetical (reusing `migration-list-styler.ts`'s `refs` styler), aligned to the `from → to` data column, ordered user-refs-then-`db`-then-`contract`. The detached current contract renders as a floating `(contract)` node with no incoming edge (no phantom dashed edge). -- **Builds on:** Dispatch 3's renderer (node-row emission) + the command's `refs` / `contractHash` inputs. -- **Hands to:** The three overlay cases (DB-behind, everything-aligned, detached-contract) render per mockup; goldens committed. -- **Focus:** Overlay decoration + alignment. Reuses the existing styler rather than reintroducing per-marker glyph tags. - -### Dispatch 6: glyph-mode / ASCII fallback + reference doc - -- **Outcome:** The renderer takes a glyph set; an ASCII set is selected by `--ascii` / non-UTF-terminal detection (injected pure via `TerminalUI`), orthogonal to `--no-color`. The full fixture gallery has UTF + ASCII goldens. `docs/reference/migration-graph-rendering.md` documents the visual language. -- **Builds on:** Dispatches 3–5 (the complete renderer). -- **Hands to:** Slice-DoD: full gallery (UTF + ASCII) goldens green; `--json`/`--dot` + `migration status` regression-green; reference doc committed. -- **Focus:** Glyph-set indirection + docs. No new topology behaviour. diff --git a/projects/migration-graph-rendering/trace.jsonl b/projects/migration-graph-rendering/trace.jsonl deleted file mode 100644 index 896bad15a8..0000000000 --- a/projects/migration-graph-rendering/trace.jsonl +++ /dev/null @@ -1,74 +0,0 @@ -{"event_id":"5806a418-a070-4f90-8a12-8e5fd58cc3e3","schema_version":"1","ts":"2026-06-05T13:43:52.954Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/migration-graph-rendering/slices/read-command-json-redesign/spec.md","spec_kind":"slice","byte_length":11007,"edge_cases_count":4,"open_questions_count":2,"dod_items_count":1} -{"event_id":"5d031108-bd13-4138-8df5-1f3f469330c7","schema_version":"1","ts":"2026-06-05T13:43:53.060Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/migration-graph-rendering/slices/read-command-json-redesign/plan.md","plan_kind":"slice","byte_length":6917,"dispatch_count":8,"slice_count":null,"dispatch_size_distribution":{"S":0,"M":7,"L":1,"XL":0},"open_items_count":0} -{"event_id":"c909e3e9-8b30-4a1e-b2e2-4591a931119a","schema_version":"1","ts":"2026-06-05T13:43:53.146Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"BF0C7A16-8893-44F9-A94F-3A5A2B9D5813","dispatch_name":"implementer D1 — rename sweep","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"0191f8fe-90f7-48d1-831c-e7f6415cc440","schema_version":"1","ts":"2026-06-05T13:43:53.237Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"BF0C7A16-8893-44F9-A94F-3A5A2B9D5813","result":"completed","wall_clock_ms":1045895} -{"event_id":"2aaa7a7a-2b71-4488-8c18-6514261148dd","schema_version":"1","ts":"2026-06-05T13:43:53.352Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"8ACC9024-DB80-4D62-8B66-B7E0B45FB854","dispatch_name":"implementer D2 — arktype foundation","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"665a048c-2cbc-44be-8c8d-63862bdafb84","schema_version":"1","ts":"2026-06-05T13:43:53.447Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"8ACC9024-DB80-4D62-8B66-B7E0B45FB854","result":"completed","wall_clock_ms":484349} -{"event_id":"16a16b51-dea7-41b3-86eb-f3d1d7455912","schema_version":"1","ts":"2026-06-05T13:43:53.533Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"7D51BD1B-F00A-4810-A36C-9B982821FFDC","dispatch_name":"implementer D3 — graph nested per space","subagent_type":"general-purpose","model":"opus","parent_dispatch_id":null} -{"event_id":"75dbc549-338b-4297-beb4-0055305421ba","schema_version":"1","ts":"2026-06-05T13:43:53.617Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"7D51BD1B-F00A-4810-A36C-9B982821FFDC","result":"completed","wall_clock_ms":642820} -{"event_id":"1c77827a-8122-486f-9b10-06c27621b9b3","schema_version":"1","ts":"2026-06-05T13:44:25.953Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"788A031A-8973-4314-A72A-96C576943603","dispatch_name":"implementer D4 — status structured diagnostics","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"adaaa663-e342-4899-b623-7e0af7be291b","schema_version":"1","ts":"2026-06-05T13:44:26.046Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"788A031A-8973-4314-A72A-96C576943603","round_id":"E7E5F218-68A2-4CCC-B668-629EF36A9022","round_number":1} -{"event_id":"e6533bae-bcb7-48f5-87c3-fe01b3ca3d18","schema_version":"1","ts":"2026-06-05T13:57:36.014Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"788A031A-8973-4314-A72A-96C576943603","round_id":"E7E5F218-68A2-4CCC-B668-629EF36A9022","verdict":"satisfied","findings_filed":0,"wall_clock_ms":731562} -{"event_id":"eba9ac51-f912-4357-aa39-a5e0485ff161","schema_version":"1","ts":"2026-06-05T13:57:36.064Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"788A031A-8973-4314-A72A-96C576943603","result":"completed","wall_clock_ms":731562} -{"event_id":"93f74b04-d81d-4b17-947a-7cc293064411","schema_version":"1","ts":"2026-06-05T13:57:36.119Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"1368DE91-5AB8-4C72-AECE-FF75266A7F7E","dispatch_name":"implementer D5 — log records","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"cfba9745-8fa0-48f3-9bbd-5791611f6f18","schema_version":"1","ts":"2026-06-05T13:57:36.172Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"1368DE91-5AB8-4C72-AECE-FF75266A7F7E","round_id":"CCD9B654-0678-4041-B760-3C06F41CD5DB","round_number":1} -{"event_id":"29856d14-68b0-4a71-98da-2fe7e087a586","schema_version":"1","ts":"2026-06-05T14:05:20.553Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"1368DE91-5AB8-4C72-AECE-FF75266A7F7E","round_id":"CCD9B654-0678-4041-B760-3C06F41CD5DB","verdict":"satisfied","findings_filed":0,"wall_clock_ms":391041} -{"event_id":"3fde20f9-eb21-46c4-a4b4-a0c6d7bbd8f9","schema_version":"1","ts":"2026-06-05T14:05:20.603Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"1368DE91-5AB8-4C72-AECE-FF75266A7F7E","result":"completed","wall_clock_ms":391041} -{"event_id":"431169cd-84d4-4c74-8ecc-fb3130ce666e","schema_version":"1","ts":"2026-06-05T14:05:20.659Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"5C0AF009-1CA9-4E14-ABEB-7BDE4E7824CC","dispatch_name":"implementer D6 — show trim + schema","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"67eb8461-433f-4248-a2e7-2cf387e8919c","schema_version":"1","ts":"2026-06-05T14:05:20.715Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"5C0AF009-1CA9-4E14-ABEB-7BDE4E7824CC","round_id":"18B60A9D-85BD-4414-9379-999737852E21","round_number":1} -{"event_id":"51be1f89-117d-46f5-b2e8-297d61c1f5ca","schema_version":"1","ts":"2026-06-05T14:11:54.561Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"5C0AF009-1CA9-4E14-ABEB-7BDE4E7824CC","round_id":"18B60A9D-85BD-4414-9379-999737852E21","verdict":"satisfied","findings_filed":0,"wall_clock_ms":355253} -{"event_id":"344f5ca8-dfdc-4020-944d-3769e3b4836e","schema_version":"1","ts":"2026-06-05T14:11:54.612Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"5C0AF009-1CA9-4E14-ABEB-7BDE4E7824CC","result":"completed","wall_clock_ms":355253} -{"event_id":"ec5e8922-0f42-48a7-a863-4b1591e1d245","schema_version":"1","ts":"2026-06-05T14:11:54.673Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"9C363DC6-E52E-4324-BE9B-D4048762806F","dispatch_name":"implementer D7 — check error-envelope vocab","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"ef5b5441-3786-49db-ae8d-a7937fa0e314","schema_version":"1","ts":"2026-06-05T14:11:54.724Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"9C363DC6-E52E-4324-BE9B-D4048762806F","round_id":"49EBAB11-6F20-4771-A3DC-D66B993BE304","round_number":1} -{"event_id":"18114833-2e33-4264-85f4-d0920c6c5022","schema_version":"1","ts":"2026-06-05T14:19:15.775Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"9C363DC6-E52E-4324-BE9B-D4048762806F","round_id":"49EBAB11-6F20-4771-A3DC-D66B993BE304","verdict":"satisfied","findings_filed":0,"wall_clock_ms":381624} -{"event_id":"ede83693-e768-441d-b544-bf489ecfde50","schema_version":"1","ts":"2026-06-05T14:19:15.823Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"9C363DC6-E52E-4324-BE9B-D4048762806F","result":"completed","wall_clock_ms":381624} -{"event_id":"194659bb-43c1-4e3e-a4cd-3891f4af87b7","schema_version":"1","ts":"2026-06-05T14:19:15.876Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"1F2BD8A5-C4D9-462B-9EDA-89C655A37354","dispatch_name":"implementer D8 — parity lock + graph empty-start fix","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"c4c85267-590e-4a18-b04a-f308e568c270","schema_version":"1","ts":"2026-06-05T14:19:15.926Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"1F2BD8A5-C4D9-462B-9EDA-89C655A37354","round_id":"29A7F872-65B6-48AA-8C7C-9D8EB6263441","round_number":1} -{"event_id":"21b6ac00-5845-4361-963b-84db70972b80","schema_version":"1","ts":"2026-06-05T14:29:07.746Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"1F2BD8A5-C4D9-462B-9EDA-89C655A37354","round_id":"29A7F872-65B6-48AA-8C7C-9D8EB6263441","verdict":"satisfied","findings_filed":0,"wall_clock_ms":537019} -{"event_id":"d978504e-1ce5-4570-b76f-69a1be6979d8","schema_version":"1","ts":"2026-06-05T14:29:07.817Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"1F2BD8A5-C4D9-462B-9EDA-89C655A37354","result":"completed","wall_clock_ms":537019} -{"event_id":"ba749da6-3473-4e73-9a8c-22a9e38171fe","schema_version":"1","ts":"2026-06-07T09:28:49.000Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"plan-authored","slice_id":"render-redesign-core","dispatch_count":5,"plan_path":"slices/render-redesign-core/plan.md"} -{"event_id": "a46863bc-3c7b-4498-998e-535bf5df277d", "schema_version": "1", "ts": "2026-06-08T10:12:19.000Z", "project_run_id": "migration-graph-rendering", "orchestrator_agent_id": null, "event_type": "plan-authored", "slice_id": "render-redesign-geometry", "dispatch_count": 3, "plan_path": "slices/render-redesign-geometry/plan.md"} -{"event_id":"c0bddcca-fe2a-4a94-9460-06a9e60aedfc","schema_version":"1","ts":"2026-06-08T10:16:48.766Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"plan-amended","slice_id":"render-redesign-geometry","plan_path":"slices/render-redesign-geometry/plan.md","plan_kind":"slice","byte_length":7502,"bytes_delta":1925,"dispatch_count":3,"slice_count":null,"dispatch_size_distribution":null,"open_items_count":0,"reason":"corrected D1/D2 to the snapshot harness; pinned structural convergence definition","dispatches_added":0,"dispatches_removed":0,"dispatches_resized":2} -{"event_id":"5fe45c13-f24e-4d7c-aa51-c76bc3bf2df9","schema_version":"1","ts":"2026-06-08T10:17:17.326Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120","dispatch_name":"implementer D1 — convergence scenario + RED structural assertion","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"adba3c92-ec0a-455a-953f-80b2cf62a3f8","schema_version":"1","ts":"2026-06-08T10:17:17.379Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120","round_id":"E2A75996-242D-4BA3-965F-E26C0770AD4B","round_number":1} -{"event_id":"be16e3bd-a291-421f-bf79-7188d6109be2","schema_version":"1","ts":"2026-06-08T10:17:17.432Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120","round_id":"E2A75996-242D-4BA3-965F-E26C0770AD4B","brief_byte_length":0,"brief_content_hash":"pending","brief_disposition":"initial"} -{"event_id":"c2f44075-bff4-412e-9839-6c5374daa0aa","schema_version":"1","ts":"2026-06-08T10:33:44.391Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120","round_id":"E2A75996-242D-4BA3-965F-E26C0770AD4B","verdict":"satisfied","findings_filed":0,"wall_clock_ms":981227} -{"event_id":"8effdb9f-5e20-4816-b30c-56771abe8826","schema_version":"1","ts":"2026-06-08T10:33:44.441Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120","result":"completed","wall_clock_ms":981280} -{"event_id":"99369808-a8c2-4fa9-9721-a11603d094f4","schema_version":"1","ts":"2026-06-08T10:34:31.178Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7","dispatch_name":"implementer D2 — convergence layout (GREEN)","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"465E1D74-B7E9-4F4C-A442-CCE53D7C5120"} -{"event_id":"fa10fbe6-56e0-4e89-933d-530c002188c9","schema_version":"1","ts":"2026-06-08T10:34:31.230Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7","round_id":"5ABFD04A-8910-48D0-8016-DBCB32AABEAE","round_number":1} -{"event_id":"8d7ee478-56b3-4d48-9078-9418354efd88","schema_version":"1","ts":"2026-06-08T10:34:31.284Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7","round_id":"5ABFD04A-8910-48D0-8016-DBCB32AABEAE","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"7198c0c1-8b92-428e-bffc-bbff4e7ac245","schema_version":"1","ts":"2026-06-08T10:48:17.119Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7","round_id":"5ABFD04A-8910-48D0-8016-DBCB32AABEAE","verdict":"satisfied","findings_filed":0,"wall_clock_ms":825835} -{"event_id":"3564da34-0ce3-4854-a0fc-9035a4ad3a37","schema_version":"1","ts":"2026-06-08T10:48:17.183Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7","result":"completed","wall_clock_ms":825887} -{"event_id":"745d189e-15e7-424b-a20d-5c432b20a7e9","schema_version":"1","ts":"2026-06-08T10:48:57.221Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B","dispatch_name":"implementer D3 — configurable geometry + scaling test","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"4D10B22F-8ACE-434F-B279-2B5FBBCAADE7"} -{"event_id":"125a3caf-d144-40a5-87de-5b002b3443a4","schema_version":"1","ts":"2026-06-08T10:48:57.278Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B","round_id":"3F7534F4-213A-420F-8ABD-5D2E8726908A","round_number":1} -{"event_id":"8bd90fdc-ee02-4933-a996-d3b9845668bf","schema_version":"1","ts":"2026-06-08T10:48:57.335Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B","round_id":"3F7534F4-213A-420F-8ABD-5D2E8726908A","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"57011552-5920-4121-96c2-7f8aa6b71605","schema_version":"1","ts":"2026-06-08T10:57:08.325Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B","round_id":"3F7534F4-213A-420F-8ABD-5D2E8726908A","verdict":"satisfied","findings_filed":0,"wall_clock_ms":490997} -{"event_id":"c7efc315-663c-44dc-a6ff-7bf95f251ef3","schema_version":"1","ts":"2026-06-08T10:57:08.380Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B","result":"completed","wall_clock_ms":491054} -{"event_id":"a5d05e8c-ea46-4833-9d48-eb1843429fa0","schema_version":"1","ts":"2026-06-08T11:02:22.851Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"slice-completed","slice_slug":"render-redesign-geometry","result":"pr-opened","pr_ref":"#767"} -{"event_id":"f722a4e1-17b2-40d4-85f2-6c7b8de83430","schema_version":"1","ts":"2026-06-08T11:11:55.921Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE","dispatch_name":"implementer D4 — convergence golden oracle","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"51D01E1C-A9EC-4525-936F-CE5E729BD51B"} -{"event_id":"8ee65315-84a0-40d4-9b7d-27f3902641f8","schema_version":"1","ts":"2026-06-08T11:11:55.971Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE","round_id":"A6D724D9-AE7E-4E62-A5E6-C85336E16F48","round_number":1} -{"event_id":"36a1213f-20e1-4389-9c94-e65dece61231","schema_version":"1","ts":"2026-06-08T11:11:56.025Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE","round_id":"A6D724D9-AE7E-4E62-A5E6-C85336E16F48","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"661684d1-3c9b-42ec-94a0-a9c6f4b4914f","schema_version":"1","ts":"2026-06-08T11:32:38.385Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE","round_id":"A6D724D9-AE7E-4E62-A5E6-C85336E16F48","verdict":"satisfied","findings_filed":0,"wall_clock_ms":1242362} -{"event_id":"c860bfa2-22f3-4a2b-93c4-4340fad2eddc","schema_version":"1","ts":"2026-06-08T11:32:38.438Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE","result":"completed","wall_clock_ms":1242412} -{"event_id":"0fe40a68-be12-4a9e-81bf-998181859b18","schema_version":"1","ts":"2026-06-08T11:32:38.491Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"slice-completed","slice_slug":"render-redesign-geometry","result":"pr-updated-4-dispatches","pr_ref":"#767"} -{"event_id":"d60bd5b7-25e7-4046-a7be-3efc0c2ba746","schema_version":"1","ts":"2026-06-08T11:40:29.872Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"22B0223B-9E2A-4F50-AD2C-4BAED12769BB","dispatch_name":"implementer D5 — converged showcase golden candidate","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE"} -{"event_id":"54dbf761-480a-413b-8b5f-148e30454e4a","schema_version":"1","ts":"2026-06-08T11:40:30.039Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"22B0223B-9E2A-4F50-AD2C-4BAED12769BB","round_id":"BF2B8638-060B-45EF-8542-74D616073966","round_number":1} -{"event_id":"214fc00e-be5e-459f-82f4-a83dfb1c556e","schema_version":"1","ts":"2026-06-08T11:40:30.165Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"22B0223B-9E2A-4F50-AD2C-4BAED12769BB","round_id":"BF2B8638-060B-45EF-8542-74D616073966","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"595181ce-cc42-43f2-9b01-47c966c82533","schema_version":"1","ts":"2026-06-08T12:13:17.795Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"0E1BF2D8-4F5B-4CC7-A971-9AB2275DCBDB","dispatch_name":"implementer D5 — converged showcase golden candidate","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"6A6B6D31-D618-4725-B08F-B075C52550AE"} -{"event_id":"0003f93a-344b-4aa0-ab56-a1c8043a33d3","schema_version":"1","ts":"2026-06-08T12:13:17.852Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"0E1BF2D8-4F5B-4CC7-A971-9AB2275DCBDB","round_id":"2E7F6C69-6136-4273-B5DB-C82286F5E470","round_number":1} -{"event_id":"b5ca3127-27b3-4341-ac12-7313b853fb50","schema_version":"1","ts":"2026-06-08T12:13:17.904Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"0E1BF2D8-4F5B-4CC7-A971-9AB2275DCBDB","round_id":"2E7F6C69-6136-4273-B5DB-C82286F5E470","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"4b2549d7-2e7a-455c-a3a5-b0a1a75514d8","schema_version":"1","ts":"2026-06-08T13:19:19.409Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF","dispatch_name":"implementer D6 — fix disconnected-component + asymmetric-diamond layout bugs","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"5AAACCC0-432D-4B5C-B938-43BA2FDD20A7"} -{"event_id":"5f0e5be4-2b0a-4b5d-821f-9c8fe63bfdee","schema_version":"1","ts":"2026-06-08T13:19:19.468Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF","round_id":"5846F8D7-E0FF-499B-90C8-9ACD3ED85A28","round_number":1} -{"event_id":"8501a5fe-ac4d-4551-b19a-c008a30f9d00","schema_version":"1","ts":"2026-06-08T13:19:19.526Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF","round_id":"5846F8D7-E0FF-499B-90C8-9ACD3ED85A28","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"85f9e455-a290-47bd-aa34-a3733e2f12aa","schema_version":"1","ts":"2026-06-08T13:48:49.510Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF","round_id":"5846F8D7-E0FF-499B-90C8-9ACD3ED85A28","verdict":"satisfied","findings_filed":0,"wall_clock_ms":1769988} -{"event_id":"0c80776b-4561-49e1-bc3d-d8dfac3f15cc","schema_version":"1","ts":"2026-06-08T13:48:49.564Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF","result":"completed","wall_clock_ms":1770047} -{"event_id":"a363904d-b082-46d5-a221-c8b9294fef59","schema_version":"1","ts":"2026-06-08T14:00:31.659Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B","dispatch_name":"implementer D7 — fix trunk continuation past asymmetric-diamond merge","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"97853C34-E528-464E-B214-AEF9CFE0C6EF"} -{"event_id":"1790cd38-657c-4b10-8cfb-017d5756d1fd","schema_version":"1","ts":"2026-06-08T14:00:31.715Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B","round_id":"9CA60670-AFB1-4564-8801-5618EAB0565C","round_number":1} -{"event_id":"c927236e-0d6d-45ea-b423-a1830d0eb9b5","schema_version":"1","ts":"2026-06-08T14:00:31.772Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B","round_id":"9CA60670-AFB1-4564-8801-5618EAB0565C","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"af443fd6-fdc1-4c62-ace9-e8feb4b6e397","schema_version":"1","ts":"2026-06-08T14:09:11.227Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B","round_id":"9CA60670-AFB1-4564-8801-5618EAB0565C","verdict":"satisfied","findings_filed":0,"wall_clock_ms":519456} -{"event_id":"e7ea0b94-8ef2-4767-8695-eab06230f535","schema_version":"1","ts":"2026-06-08T14:09:11.282Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B","result":"completed","wall_clock_ms":519512} -{"event_id":"2d1f6203-8435-45c4-83fe-8a0e2b9a5366","schema_version":"1","ts":"2026-06-08T14:35:11.143Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"11717E47-29C3-4CE8-8A2D-02450AACB8CB","dispatch_name":"implementer D8 — greedy back-arc/lane colouring","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":"94E52B61-2284-40AF-BC6E-0FE86B36532B"} -{"event_id":"a2488379-6319-46c9-a475-165e92308158","schema_version":"1","ts":"2026-06-08T14:35:11.198Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"11717E47-29C3-4CE8-8A2D-02450AACB8CB","round_id":"E96A2E88-E019-4BA8-BE1B-E24CFFFB7850","round_number":1} -{"event_id":"ac79276a-ae30-420b-a3b2-d3186bf9ad23","schema_version":"1","ts":"2026-06-08T14:35:11.255Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"11717E47-29C3-4CE8-8A2D-02450AACB8CB","round_id":"E96A2E88-E019-4BA8-BE1B-E24CFFFB7850","brief_byte_length":0,"brief_content_hash":"inline","brief_disposition":"initial"} -{"event_id":"bc97f782-cf1a-42f8-be1f-245bc9f5bb83","schema_version":"1","ts":"2026-06-08T14:59:09.704Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"11717E47-29C3-4CE8-8A2D-02450AACB8CB","round_id":"E96A2E88-E019-4BA8-BE1B-E24CFFFB7850","verdict":"satisfied","findings_filed":0,"wall_clock_ms":1438446} -{"event_id":"a7bfcc3e-a722-494e-8f44-9302ec95b49f","schema_version":"1","ts":"2026-06-08T14:59:09.761Z","project_run_id":"migration-graph-rendering","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"11717E47-29C3-4CE8-8A2D-02450AACB8CB","result":"completed","wall_clock_ms":1438501}