Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
912126a
docs(migration): lock migration graph (Tier 3) rendering design (TML-…
wmadden May 30, 2026
c16240d
docs(migration): add node overlays (refs / DB / current contract) to …
wmadden May 30, 2026
0255474
docs(migration): align node overlays to data column; drop cryptic pru…
wmadden May 30, 2026
3c0d9fc
Merge remote-tracking branch 'origin/main' into tml-2746-redesign-mig…
wmadden May 30, 2026
2912abb
Merge remote-tracking branch 'origin/main' into tml-2746-redesign-mig…
wmadden May 30, 2026
e74e00f
docs(migration-graph-rendering): land slice spec, dispatch plan, and …
wmadden May 30, 2026
57e06fd
refactor(cli): extract shared DFS classifier core in migration-list-g…
wmadden May 30, 2026
16bf013
feat(cli): add migration-graph row model (edge classification + node …
wmadden May 30, 2026
810b2fc
feat(cli): add migration-graph grid layout model (structural cell roles)
wmadden May 30, 2026
8a82692
fix(cli): principled migration-graph lane allocator (D2 R2)
wmadden May 30, 2026
74faf54
test(cli): lock migration-graph layout suite to mockup geometry
wmadden May 31, 2026
552d770
test(cli): fix cross-link convergence adjacency + pin horizontal-fill…
wmadden May 31, 2026
13c0dec
fix(cli): render longer migration-graph branches before shorter siblings
wmadden May 31, 2026
56db549
test(cli): render migration-graph layout fixtures as inline ASCII sna…
wmadden May 31, 2026
03d554b
fix(cli): lay out migration-graph cross-link with column-absolute con…
wmadden May 31, 2026
e24c785
test(cli): assert correct single-lane rollback rendering as expected-…
wmadden May 31, 2026
f5b9f7b
test(cli): lock divergence-family layouts as expected-failures
wmadden May 31, 2026
960d644
refactor(cli): rewrite migration-graph allocator around canonical lan…
wmadden May 31, 2026
aadb8d1
feat(cli): add migration graph --tree condensed renderer
wmadden May 31, 2026
121a452
fix(cli): emit migration graph --tree to stdout without clack rail
wmadden May 31, 2026
5dc6d34
feat(cli): render detached contract as floating tree node
wmadden May 31, 2026
b737a92
refactor(cli): extract detachedContractHash predicate + cover empty-g…
wmadden May 31, 2026
e1940e4
fix(examples): move demo migration-fixture refs into per-space refs dir
wmadden May 31, 2026
82e8a07
fix(cli): align divergence-apex node hash to its own lane
wmadden May 31, 2026
0ca2065
docs(migration-graph): pull divergence-apex rows to their own lane in…
wmadden May 31, 2026
ab161e8
test(cli): lock routed-back-arc targets for node-skipping rollbacks
wmadden May 31, 2026
c8c0031
feat(cli): route node-skipping rollback arcs in migration graph tree
wmadden May 31, 2026
2d6ba3e
test(cli): drop stale it.fails note from routed-arc test comment
wmadden May 31, 2026
7b6f27f
fix(cli): revert out-of-scope edgeLabelColumn gutter check
wmadden May 31, 2026
94c860e
fix(cli): harden routed rollback arcs per review
wmadden May 31, 2026
c58d1a9
feat(cli): add ASCII glyph fallback for migration graph --tree
wmadden May 31, 2026
6898000
docs(cli): document --tree/--ascii in migration graph description
wmadden May 31, 2026
d51b92e
fix(cli): order migration graph by longest forward path
wmadden May 31, 2026
1652ef9
fix(cli): draw routed back-arc connectors across spanned lanes
wmadden May 31, 2026
ddbf999
fix(cli): address review nits on routed-arc + layering
wmadden May 31, 2026
884e2f5
fix(cli): render empty contract node as ∅ glyph in migration graph tree
wmadden May 31, 2026
ae40aa9
fix(cli): harden empty-node glyph rendering and fold rows golden
wmadden May 31, 2026
7a903c8
Merge remote-tracking branch 'origin/main' into tml-2746-redesign-mig…
wmadden May 31, 2026
a242ce6
fix(cli): keep graph node markers and edge arrows bright
wmadden May 31, 2026
57c4cce
fix(cli): pad empty contract source in graph tree edge rows
wmadden Jun 1, 2026
bdfbaa2
docs(project): add slice to retire migration list --graph
wmadden Jun 1, 2026
210e3c4
fix(cli): emphasize contract marker, render db as a plain ref
wmadden Jun 1, 2026
9ba964f
test(demo): add comprehensive migration-graph showcase fixture
wmadden Jun 1, 2026
b8afbef
Merge branch 'main' into tml-2746-redesign-migration-graph-tier-3-con…
wmadden Jun 1, 2026
e2ab3e1
fix(demo): migrate showcase contracts to domain.namespaces shape
wmadden Jun 1, 2026
9767c3a
fix(cli): honor hashLength in tree graph hash abbreviation
wmadden Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions docs/reference/migration-graph-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# `migration graph --tree` — condensed annotated-tree rendering

This document is the rendering contract for `migration graph --tree`: how it draws the
offline migration graph as a condensed annotated tree, the row/grid model, the glyph
palette (Unicode and ASCII), routed back-arcs, and its relationship to the other
migration views. It is reference material for anyone maintaining or extending the
`--tree` output.

For the Tier-2 edge-per-row annotated tree (`migration list --graph`), see
[migration-list-graph-rendering.md](./migration-list-graph-rendering.md).

## Problem framing

`migration graph` (without `--tree`) renders a **node graph** laid out by dagre:
contracts are nodes, migrations are edges between them. That answers “what is the
whole topology?” including markers and back-edges drawn as graph geometry.

`migration graph --tree` is a different drawing contract: a **condensed annotated
tree** in the spirit of `git log --graph`, but with **one contract node row per hash**
and **one migration row per edge**, both carrying the same `dirName` and
`from → to` data column as the flat list. The gutter is a multi-lane box-drawing
spine (forward branches, merge joins, and routed rollback arcs) rather than dagre
coordinates.

## The model

1. **One contract = one node row** — `○ <hash>` (Unicode) or `* <hash>` (ASCII), with
optional `(refs)` / `db` / `contract` overlays on the same line as the hash.
2. **One migration = one edge row** — gutter lane(s) + `dirName` + `from → to` (always
forward `→` in the data column; edge *kind* is shown in the gutter arrow, not by
reversing the data column).
3. **Vertical order** — tip at the top, roots at the bottom; within each weakly
connected component, DFS post-order with `dirName`-descending neighbour order (same
tie-break as the Tier-2 topology pass). Disjoint components are separated by a blank
row.
4. **Detached contract** — when the workspace contract hash does not appear in the
graph, a floating node row is emitted above the main component (with `(contract)`
overlay when applicable).

Topology classification (forward / rollback / self) and convergence/divergence
facts come from `migration-tools` via `classifyMigrationGraphTopology` on
`MigrationGraph`; row order and grid placement live in
`migration-graph-rows.ts` + `migration-graph-layout.ts`; glyph rendering lives in
`migration-graph-tree-render.ts`.

## Gutter spine

### In-lane direction glyphs

Each migration row occupies a **lane column** in the gutter. The lane cell for the
migration’s own column shows a vertical bar plus a direction glyph:

| Kind | Unicode | ASCII | Meaning |
|---|---|---|---|
| forward | `↑` | `^` | normal forward migration along the spine |
| rollback | `↓` | `v` | back-edge (target is an ancestor of the source) |
| self | `⟲` | `@` | `from == to` |

Pass-through lanes on the same row show only the vertical bar (`│` / `|`).

### Branch and merge connectors

When several forward branches diverge from one contract or converge into one, dedicated
**connector rows** (no `dirName`) draw the fan or join:

- **Branch below a divergence** — `├─` tee, co-sourced `┬─`, corner `╮` (Unicode); `+-`,
`+-`, `\ ` (ASCII).
- **Merge above a convergence** — `├─` / `┴─` tees, corner `╯` (Unicode); `+-`, `/ `
(ASCII).

### Routed back-arcs (skip-rollback)

When a rollback skips intermediate nodes, the layout routes a **back-arc** in extra
lanes instead of a single adjacent `↓`:

- **Source tee** — node row with `○─` / `*-` (arc tee) marking where the arc leaves.
- **Back-lane** — `│↓` / `|v` on the rollback row in a dedicated lane; `──` / `--`
horizontal bridges and `┼─` / `+-` crossings where arcs cross the forward spine.
- **Landing** — node row with `○◂` / `*<` (arc land) plus `╯` / `/ ` corner, or
`◂╯` / `*</` composite on one row when the landing shares cells with the spine.
- **Co-sourced arcs** — `┬─` / `+-` when two back-arcs share a branch point.

Adjacent rollbacks (target is the node directly below on the spine) stay a plain `↓` /
`v` in the primary lane — no arc routing.

## Data column and overlays

Every edge row ends with:

```
<dirName> <from> → <to>
```

- **Hashes** — 7-character abbreviated contract hashes (`abbreviateContractHash`).
- **Empty baseline** — `∅` (Unicode) or `-` (ASCII) for `EMPTY_CONTRACT_HASH`, shared
with Tier-2 via `migrationListEmptySource`.
- **Forward arrow** — `→` (Unicode) or `->` (ASCII), shared with Tier-2 via
`migrationListForwardArrow`. Rollback rows still show `from → to`; the gutter
`↓`/`v` carries the kind signal.
- **Self-edge** — both hashes are shown (`from → to` with identical abbreviations).

Node rows show the contract hash in the label column. Overlays (same decoration rules
as `migration list`):

- **`(refs)`** — ref names from `refsByHash`, sorted lexicographically.
- **`db`** — when the node hash matches the database marker hash.
- **`contract`** — when the node hash matches the workspace contract hash (and is not
the empty baseline).

## Glyph palette

Lanes are **two visible columns** per grid cell (glyph + padding space), so every
structural role has a fixed width. Color (`MigrationListStyler`) is orthogonal to
glyph mode: `--no-color` disables ANSI styling; `--ascii` swaps glyphs only.

### Complete Unicode ↔ ASCII table

| Role | Unicode | ASCII |
|---|---|---|
| contract node | `○ ` | `* ` |
| arc source (tee on node) | `○─` | `*-` |
| arc landing (on node) | `○◂` | `*<` |
| vertical pass | `│ ` | `| ` |
| forward in-lane arrow | `↑` | `^` |
| rollback in-lane arrow | `↓` | `v` |
| self in-lane arrow | `⟲` | `@` |
| branch / merge tee | `├─` | `+-` |
| branch corner | `╮ ` | `\ ` |
| merge corner | `╯ ` | `/ ` |
| arc branch corner | `╮ ` | `\ ` |
| arc branch tee (co-source) | `┬─` | `+-` |
| arc land corner | `╯ ` | `/ ` |
| arc crossing | `┼─` | `+-` |
| horizontal / arc land bridge | `──` | `--` |
| connector co-branch tee | `┬─` | `+-` |
| connector co-merge tee | `┴─` | `+-` |
| data column arrow | `→` | `->` |
| empty baseline source | `∅` | `-` |

Implementation: `UNICODE_PALETTE` / `ASCII_PALETTE` in
`migration-graph-tree-render.ts`; Tier-2 shared symbols come from
`migration-list-data-column.ts`.

### ASCII fallback

Default glyph mode is **Unicode** when `glyphMode` is omitted (tests and internal
callers). The CLI resolves mode through `TerminalUI.resolveGlyphMode`:

- **`--ascii`** forces ASCII (pipe-friendly, CI snapshots).
- Otherwise **`detectGlyphMode({ isTTY, env })`** — ASCII when stdout is not a TTY or
the locale is not UTF-8; Unicode on a UTF-8 TTY.

`--ascii` and `--no-color` are orthogonal: ASCII mode may still color hashes and refs
when color is enabled.

## Relationship to the other views

| Command | Rows | Gutter |
|---|---|---|
| `migration list` | migrations | none (flat) |
| `migration list --graph` | migrations (+ `o` at convergences) | forward spine only |
| `migration graph` (default) | dagre node graph | full topology via GraphViz |
| `migration graph --tree` | **nodes + migrations** | forward spine + routed back-arcs |

`--tree` does **not** use `graph-render.ts` / dagre. It reuses the same
`MigrationGraph` input as the default renderer but a separate row → grid → text
pipeline. `--json` and `--dot` are unaffected.

## Worked cases

Synthetic gallery tests in
`cli/test/utils/formatters/migration-graph-tree-render.test.ts` pin Unicode and ASCII
goldens for: linear chain, detached contract, ref/db/contract overlays, diamond,
three-way fan, skip-rollback with routed arcs, adjacent rollback, self-edge, and a
multi-topology composite.

Example (linear chain, Unicode):

```
○ a94b7b4
│↑ add_posts ef9de27 → a94b7b4
○ ef9de27
│↑ init ∅ → ef9de27
○ ∅
```

The same fixture in ASCII (`--ascii` / `glyphMode: 'ascii'`):

```
* a94b7b4
|^ add_posts ef9de27 -> a94b7b4
* ef9de27
|^ init - -> ef9de27
* -
```

## Out of scope

- **Demo fixture loading** — golden tests use synthetic graphs only; fixture-backed
snapshots are a separate follow-on.
- **Default dagre renderer** — `graph-render.ts`, `graph-migration-mapper.ts`, and
`--dot` output are unchanged by `--tree` / `--ascii`.
- **`migration list --graph`** — separate formatter and layout; see the Tier-2
reference doc.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
import { migrationGraphToRenderInput } from '../utils/formatters/graph-migration-mapper';
import { graphRenderer } from '../utils/formatters/graph-render';
import { buildMigrationGraphLayout } from '../utils/formatters/migration-graph-layout';
import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
import { renderMigrationGraphTree } from '../utils/formatters/migration-graph-tree-render';
import { formatStyledHeader } from '../utils/formatters/styled';
import type { CommonCommandOptions } from '../utils/global-flags';
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
Expand All @@ -24,6 +27,8 @@ import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
interface MigrationGraphOptions extends CommonCommandOptions {
readonly config?: string;
readonly dot?: boolean;
readonly tree?: boolean;
readonly ascii?: boolean;
}

export interface MigrationGraphResult {
Expand Down Expand Up @@ -85,14 +90,18 @@ export function createMigrationGraphCommand(): Command {
setCommandDescriptions(
command,
'Show the migration graph topology',
'Renders the migration graph as an ASCII tree. Offline — does not\n' +
'consult the database. Use --json for machine-readable output or\n' +
'--dot for Graphviz DOT format.',
'Renders the migration graph topology. Offline — does not consult\n' +
'the database. Use --tree for the condensed annotated tree\n' +
'(--ascii swaps box-drawing for pipe-friendly ASCII glyphs),\n' +
'--json for machine-readable output, or --dot for Graphviz DOT\n' +
'format.',
);
setCommandExamples(command, [
'prisma-next migration graph',
'prisma-next migration graph --json',
'prisma-next migration graph --dot',
'prisma-next migration graph --tree',
'prisma-next migration graph --tree --ascii',
]);
setCommandSeeAlso(command, [
{ verb: 'migration status', oneLiner: 'Show migration path and pending status' },
Expand All @@ -103,6 +112,8 @@ export function createMigrationGraphCommand(): Command {
addGlobalOptions(command)
.option('--config <path>', 'Path to prisma-next.config.ts')
.option('--dot', 'Output in Graphviz DOT format')
.option('--tree', 'Experimental condensed annotated tree renderer')
.option('--ascii', 'Use ASCII glyphs for --tree (pipe-friendly)')
.action(async (options: MigrationGraphOptions) => {
const flags = parseGlobalFlagsOrExit(options);
const ui = createTerminalUI(flags);
Expand Down Expand Up @@ -134,22 +145,52 @@ export function createMigrationGraphCommand(): Command {
JSON.stringify({ ok: true, nodes, edges, summary: graphResult.summary }, null, 2),
);
} else if (!flags.quiet) {
const renderInput = migrationGraphToRenderInput({
graph: graphResult.graph,
mode: 'offline',
markerHash: undefined,
contractHash: graphResult.contractHash ?? EMPTY_CONTRACT_HASH,
refs: graphResult.refs,
activeRefHash: undefined,
activeRefName: undefined,
edgeStatuses: [],
});
const graphOutput = graphRenderer.render(renderInput.graph, {
...renderInput.options,
colorize: flags.color !== false,
});
ui.log(graphOutput);
ui.log(`\n${graphResult.summary}`);
if (options.tree) {
const refsByHash = new Map<string, string[]>();
for (const ref of graphResult.refs) {
const existing = refsByHash.get(ref.hash);
refsByHash.set(ref.hash, existing ? [...existing, ref.name] : [ref.name]);
}
const rowModel = buildMigrationGraphRows(graphResult.graph, {
...(graphResult.contractHash !== null
? { contractHash: graphResult.contractHash }
: {}),
});
const layout = buildMigrationGraphLayout(rowModel);
const activeRef = graphResult.refs.find((ref) => ref.active);
const treeOutput = renderMigrationGraphTree(layout, {
refsByHash,
...(graphResult.contractHash !== null
? { contractHash: graphResult.contractHash }
: {}),
...(activeRef !== undefined ? { activeRefName: activeRef.name } : {}),
colorize: flags.color !== false,
glyphMode: ui.resolveGlyphMode(options.ascii === true),
});
// Emit the rendered tree to stdout like `migration list --graph`,
// not through clack's `log.message` rail: the graph is the command's
// result (and its own box-drawing is the only vertical structure it
// should carry), not a status line that needs the prompt gutter.
ui.output(treeOutput);
ui.output(`\n${graphResult.summary}`);
} else {
const renderInput = migrationGraphToRenderInput({
graph: graphResult.graph,
mode: 'offline',
markerHash: undefined,
contractHash: graphResult.contractHash ?? EMPTY_CONTRACT_HASH,
refs: graphResult.refs,
activeRefHash: undefined,
activeRefName: undefined,
edgeStatuses: [],
});
const graphOutput = graphRenderer.render(renderInput.graph, {
...renderInput.options,
colorize: flags.color !== false,
});
ui.log(graphOutput);
ui.log(`\n${graphResult.summary}`);
}
}
});
process.exit(exitCode);
Expand Down
Loading
Loading