TML-2746: condensed annotated-tree migration graph renderer (--tree)#658
Conversation
…2746) Settle the visual language for the redesigned `migration graph` command: a condensed, deterministic, annotated node-link diagram drawing contracts as nodes and migrations as edges, with complete back-edges, replacing the dagre layout and its golden-path root/tip selection. - mockups.md: the locked design of record — hand-drawn layouts across the full fixture set plus synthetic pathologicals, with the layout rules the renderer must implement (lane direction glyphs, adjacency-decides-connector, forward edges fold into the spine, non-adjacent rollbacks as teed-off arcs). - prototype/: the zero-build harness used to reach the design. Sits on the consolidated tolerant graph model (TML-2739). Signed-off-by: Will Madden <madden@prisma.io>
…Tier 3 design Reuse the `migration list --graph` `(refs)` decoration for the three "where am I" overlays on the node graph, instead of the old `migration graph` per-marker glyph tags (◆ db / ◇ contract / rotating ref colours): - Refs, the live DB marker, and the current emitted contract all decorate the node row as one trailing `(name, …)` parenthetical, with `db` and `contract` reserved names sharing the parens alongside user refs. - A detached current contract (changed but not yet planned) is a floating node carrying `(contract)` — no phantom dashed connector, since an edge here is a migration and there is none. Updates rule 2, adds rule 10, and adds a worked node-overlays section. Signed-off-by: Will Madden <madden@prisma.io>
…ned-root tags - Refs/(db)/(contract) on node rows now start at the from → to data column, not flush against the hash. - Remove the "(root: parent pruned)" in-diagram annotations from the disjoint- forest and dangling-parent examples; the missing ∅ root already says it. Signed-off-by: Will Madden <madden@prisma.io>
…ration-graph-tier-3-condensed-annotated-node
…ration-graph-tier-3-condensed-annotated-node
…learnings ledger Pins the implementation architecture (tolerant MigrationGraph → row model → grid → text → arcs → overlays → ASCII), scope (rewire `migration graph` only; leave dagre for `migration status` → TML-2748), and the six-dispatch plan. Signed-off-by: Will Madden <madden@prisma.io>
…raph-topology Extract the forward/rollback/self DFS classification into an internal classifyNormalizedEdges() function that accepts a normalized edge shape independent of the calling tier. classifyMigrationListGraphTopology (Tier-2) now normalizes MigrationListEntry[] and delegates to the shared core — existing behaviour is byte-identical. Add classifyMigrationGraphTopology(MigrationGraph) as a new export that normalizes the graph edge set and delegates to the same core, so Tier-2 and Tier-3 cannot diverge on forward/rollback/self classification. Signed-off-by: Will Madden <madden@prisma.io>
…ordering) New module migration-graph-rows.ts implements the first pure-data stage of the migration graph render pipeline: - buildMigrationGraphRows(graph: MigrationGraph): MigrationGraphRowModel Classifies every edge as forward/rollback/self (via the shared DFS in classifyMigrationGraphTopology) and produces a deterministic vertical node ordering: roots at the bottom, tips at the top, disjoint components stacked and separated by null sentinels. - ClassifiedEdge: migrationHash + from + to + dirName + kind — the resolved edge shape the column allocator will consume. - MigrationGraphRowModel: ordered nodes[], all edges[], and edgesByFrom / edgesByTo lookup maps. Node ordering algorithm: DFS post-order over forward edges from roots (EMPTY_CONTRACT_HASH first, then lex). Disjoint components are identified via weak connectivity and ordered EMPTY-first then by lex-smallest node hash. Pure cycles fall back to lex-sorted root seeding, matching the topology pass. 22 tests cover: linear, rollback, diamond, disjoint forest, cross-link, self-edge, pure cycle, classified-edge fields, lookup maps, and determinism. Signed-off-by: Will Madden <madden@prisma.io>
Allocate git-log-style lanes and connector rows from the D1 row model, classifying edges as adjacent vs node-skipping without emitting glyphs. Signed-off-by: Will Madden <madden@prisma.io>
Replace fixture-specific convergence heuristics with git-log-style lane assignment: preset branch columns/spine lanes at divergence, offset convergence fans by active pass-through lanes, emit all fan producers before visiting subtrees, and derive merges from lane-wanting state or recorded producer/child columns. Assert cross-link and kitchen-sink mockup layouts; place self-edge rows immediately above their node. Signed-off-by: Will Madden <madden@prisma.io>
Rewrite the cross-link and kitchen-sink layout tests to assert the exact geometry hand-derived from mockups.md (columns, per-edge lane + pass-through, connector spans, connector cell roles, and kitchen-sink row ordering) rather than the allocator's current output, and skip them until the allocator produces that geometry. This breaks the tautological-test pattern: the assertions are now the design-of-record, not a mirror of the code. cross-link: D moves to its own column (it is both a divergence child of A and a convergence producer into E), lane 2 closes at D, the C-spine no longer carries a spurious pass-through above E, and connector rows assert column-absolute cells with pass-throughs for active lanes outside their span. kitchen-sink: the longer branch must render contiguously before the shorter sibling (no braiding); asserts full node-row ordering plus the absence of lane-1 pass-throughs on long-branch edges. The eight faithful fixtures (linear, diamond, sequential-diamonds, 3-way fan, self-edge, disjoint, rollback marking, structural roles) keep running and pass; the two rewritten fixtures are skipped pending implementation. Signed-off-by: Will Madden <madden@prisma.io>
… cell Two corrections to the locked layout suite surfaced by review: - Convergence-producer adjacency was inverted. 'adjacent' is the producer whose source node is the nearest node row below the fan (the diamond's alice/bob reading). In cross-link, B (B→E's source) renders immediately below the fan and D (D→E's source) further down, so B→E is adjacent and D→E is node-skipping-forward — the reverse of what was asserted. - merge@A's `├───┘` has a dormant lane 1 between its producers (lanes 0 and 2). The role vocabulary had no horizontal-fill, so the bridging `─` could not be pinned; introduce a `horizontal-pass` role and assert cells[1], so an implementation cannot emit a false join there and pass. Both fixtures remain skipped pending implementation; suite stays 8 pass / 2 skip. Signed-off-by: Will Madden <madden@prisma.io>
Kitchen-sink layout now walks each tip up its spine until the next divergence, visits longer tips first, and uses asymmetric pass-through on unequal fan-outs so lane 0 runs alone until the short branch opens. Signed-off-by: Will Madden <madden@prisma.io>
…pshots Adds a test-only debug renderer that maps the structural cell roles back to the box-drawing glyphs from mockups.md, so each layout fixture carries an inline snapshot of the actual diagram next to its geometry assertions. The renderer immediately surfaced two model defects the structural assertions missed: - node rows whose lane opened late (kitchen-sink tip_short) were missing their node cell; fixed by ensuring grid width covers the node column before building its cells. - adjacent rollbacks leave their lane reserved, spilling the forward edge below into a phantom second lane (annotated FIXME on both rollback snapshots; deferred to rollback-lane-lifecycle work). Declares the horizontal-pass cell role referenced by the locked cross-link spec. Signed-off-by: Will Madden <madden@prisma.io>
…nectors Reserve a gap lane for cross-link divergences (B spine at 0, D at 2, B→E at 1), emit full-width branch/merge connector cells with vertical-pass and horizontal-pass, and align convergence-producer adjacency with the mockup. Signed-off-by: Will Madden <madden@prisma.io>
…failures Replaces the rollback fixtures' inline snapshots that pinned the known-wrong phantom-lane output with `it.fails` tests asserting the correct single-lane rendering (per mockups.md). The expected-vs-actual diff is now the spec for the rollback-lane-lifecycle fix; when that lands, `it.fails` flips red and forces conversion to `it`. Geometry assertions stay in their own passing test. Signed-off-by: Will Madden <madden@prisma.io>
Add mockups and it.fails tests for the divergence-heavy topologies the current lane allocator gets wrong: multi-edge (parallel migrations between one pair), wide-fan (pure N-way divergence), sub-branches (nested divergence reusing lanes), diamond-sub-branch (diamond with a leaf spur), and complex (divergence + diamond + spine + leaf tip). Each test pins the correct node columns, connector placement, and per-edge lane staircase from mockups.md via spacing-free geometry assertions; forward adjacency is intentionally not asserted (forward edges always render the same glyph). The tests fail today and flip to it() once the allocator generalises to the lane-join rule. Signed-off-by: Will Madden <madden@prisma.io>
…e-join The previous allocator grew a special-case zoo (cross-link presets, divergence child-lane heuristics, spine-branch indices, phantom-lane bookkeeping) and still mis-laid divergence-heavy shapes: wide fans, nested divergence, diamonds with leaf spurs, parallel multi-edges. Patching it further was untenable. Replace it with one rule for every topology, adapted from the git-graph lane algorithm. Nodes are emitted tips-first in DFS post-order (input-order child visitation, so a node lands below every branch that feeds it). Each lane "wants" a target contract; when that contract is reached, the lanes wanting it converge (merge connector). A node's forward producers are grouped by source: distinct sources fan (branch connector), parallel multi-edges to one source stack in a single lane. Rollbacks decorate their node's own column and never reserve a downward lane, so adjacent and node-skipping rollbacks both stay single-lane. All divergence-family, multi-edge, and rollback cases that were locked as expected-failures now pass and are flipped to live tests; cross-link, kitchen-sink, and the convergence fixtures keep their exact geometry. Net -228 lines with the special-casing gone. Signed-off-by: Will Madden <madden@prisma.io>
Renders the tier-3 migration graph as the condensed annotated tree from the locked mockups: contracts as `○ <hash>` rows, migrations as labelled `│↑`/`│↓`/`⟲` lane rows, branch/merge connectors, and `(refs, db, contract)` node overlays reusing the `migration list --graph` decoration. Wires it behind an experimental `--tree` flag; the default dagre path is unchanged. The data column reserves a trailing gap past the longest dirName so long migration names keep whitespace before `hash → hash` instead of colliding with the source hash. Signed-off-by: Will Madden <madden@prisma.io>
The tree renderer routed through `ui.log` (clack's `log.message`), which prefixes every line with the prompt-flow `│ ` rail — a vertical bar that runs parallel to, and visually competes with, the graph's own lane glyphs. No input is being solicited, so the rail is incidental styling from the wrong sink. Emit the rendered tree and its summary via `ui.output` to stdout instead, matching `migration list --graph`. The graph is the command's result, so it belongs on stdout and carries only its own box-drawing. Signed-off-by: Will Madden <madden@prisma.io>
When the working schema emits a contract hash no migration produces, prepend it as a single-node component at the top of the row model so the tree renderer can overlay (contract) without a phantom edge. Signed-off-by: Will Madden <madden@prisma.io>
…raph case Addresses review nits on the detached-contract floating node: the three-part "is this contract detached" guard was duplicated across the empty-graph and prepend paths — extract it into a single `detachedContractHash` helper that returns the narrowed hash. Add the missing test for an empty graph whose working schema already emits a detached contract (lone floating node, no edges). Signed-off-by: Will Madden <madden@prisma.io>
The fixture refs lived at `migration-fixtures/<topology>/refs/`, but the contract-space loader reads a space's refs from `migrations/<space>/refs/` (the app space being `<topology>/app/refs/`). A top-level `refs/` is deliberately ignored by the space enumerator, so every fixture's refs (prod, staging, …) silently failed to load and no `(refs)` overlay ever appeared in migration graph/list/status for the demo. Relocate each fixture's `refs/` under `app/refs/` so the refs load and the overlays render. Signed-off-by: Will Madden <madden@prisma.io>
A node immediately followed by a branch connector was placing its hash at the fanned-out branch width, while a convergence base placed its hash at its own single lane. That asymmetry indented branch apexes (e.g. a diamond top) one lane too far, so they failed to line up with linear nodes and the floating contract node above them. Use the node row width for both cases so an apex and a base sit in the same column. Signed-off-by: Will Madden <madden@prisma.io>
… mockups Match the design-of-record to the corrected renderer: a branch apex sits in its own lane (col 4 for a lane-0 apex), symmetric with a convergence base, rather than reserving the fanned-out branch width. Signed-off-by: Will Madden <madden@prisma.io>
Replace the deferred single-lane node-skipping rollback assertion with the routed-arc target from mockups.md (source tees ○─╮, back-lane, target lands ○◂╯), plus the two-overlapping-arc case with crossing ┼ and horizontal landing ○◂──╯. Both it.fails until the routing feature lands. Signed-off-by: Will Madden <madden@prisma.io>
Allocate back-lanes right of forward lanes with interval coloring for overlapping spans, tee from source nodes, land on targets with crossings and bridges, and emit matching glyphs in the tree renderer. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
The node-skipping-arc work changed the non-arc edge-label gutter test from `index > laneIndex` to `index !== laneIndex`. Arc graphs short-circuit on the wide label column before reaching this branch, so the change only affects non-arc rendering and is unrelated to routed arcs. Revert to the original condition; the full formatter suite stays green. Signed-off-by: Will Madden <madden@prisma.io>
Address the routed back-arc review: - Replace the three readonly-stripping `as` casts with a local MutableGridRow type so the routing pass holds StructuralCell[] directly (cast count back to the origin/main baseline). - Fix nested skip-rollback landings: emit `arc-crossing` (┼) instead of a bare `arc-land-bridge` (──) when the bridged lane still carries an active arc, so a nested arc lands as `○◂┼─╯`. Lock with a nested-span layout golden. - Support co-sourced skip rollbacks via an `arc-branch-tee` (┬) role: arcs teeing off the same node share a `○─┬─╮` junction. Fill the arc body from below the source tee (skipping the labelled edge row) and mutate the edge row in place so a co-sourced inner lane is never clobbered. Lock with a co-sourced golden. - Comment the wide-grid label-column / dirName-gap spacing constants. Signed-off-by: Will Madden <madden@prisma.io>
Mirror Tier-2 glyph-mode wiring: palette indirection in the tree renderer, --ascii on migration graph --tree, ASCII gallery goldens, and reference doc. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
Replace DFS post-order row layering with longest-path ranks on the forward subgraph so diamonds, cross-links, and rollbacks keep tips at the top. Fix topology classification: GRAY back-edges only for immediate DFS parents, plus a peel pass for node-skipping rollbacks that would otherwise stay forward. Signed-off-by: Will Madden <madden@prisma.io>
Routed node-skipping rollback arcs that span a divergence region were rendered with gaps: the tee/landing rows left the horizontal run between the node and the back-lane corner blank and never crossed live forward lanes, and the branch connector row dropped a pass-through back-arc lane whenever a later row widened the grid. Fill the horizontal connector on tee rows and cross any active lane (forward vertical or another arc) with a junction on both tee and landing rows; render connector rows from their actual cells and pad trailing columns so pass-through lanes survive a post-emit grid-width increase. Locks the behaviour with a comprehensive annotated-tree golden covering a diamond, a forward cross-link, and overlapping node-skipping rollbacks. Signed-off-by: Will Madden <madden@prisma.io>
- Exclude arc-land-bridge from the tee-row crossing check, mirroring the landing-row check, so a node that is both a skip-rollback source and another skip-rollback target on the same row is not over-crossed. - Document the peel-pass determinism guarantee on shouldPeelForwardEdge. - Correct the stale "same algorithm as Tier-2" classifier comment. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (31)
💤 Files with no reviewable changes (1)
✅ Files skipped from review due to trivial changes (27)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds a condensed-tree migration-graph renderer: shared topology classifier, deterministic row model, lane-based grid layout with rollback routing, Unicode/ASCII text renderer, CLI flags ChangesCondensed Annotated Tree Rendering
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
…ration-graph-tier-3-condensed-annotated-node
size-limit report 📦
|
The migration graph tree renderer dimmed the whole glyph pair for nodes serving as back-arc landings (○◂) or tees (○─), so their node marker looked dim while plain nodes (○) stayed bright. Edge arrows (↑/↓/⟲) were likewise dimmed along with their lane. Split the marker pair so the contract-node marker and the direction arrow route through the bright signal channel, while the arc connector, landing arrowhead, and vertical lane stay dim. No-color output is byte-identical since both channels are identity without color. Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/1-framework/3-tooling/cli/src/commands/migration-graph.ts (1)
121-148:⚠️ Potential issue | 🟠 Major | ⚡ Quick winLet
--treeoverride auto-JSON.
flags.jsonis auto-enabled on non-TTY stdout, soprisma-next migration graph --tree --ascii | catcurrently emits JSON instead of the explicit tree format.--treeis a user-selected renderer and should be handled before the auto-JSON branch, while still letting an explicit--jsonflag win.Suggested fix
- } else if (flags.json) { + } else if (flags.json && !options.tree) { const nodes = [...graphResult.graph.nodes]; const edges = [...graphResult.graph.migrationByHash.values()].map((e) => ({ dirName: e.dirName, from: e.from, to: e.to, migrationHash: e.migrationHash, })); ui.output( JSON.stringify({ ok: true, nodes, edges, summary: graphResult.summary }, null, 2), ); - } else if (!flags.quiet) { - if (options.tree) { + } else if (!flags.quiet) { + 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]); }Also applies to: 168-176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-framework/3-tooling/cli/src/commands/migration-graph.ts` around lines 121 - 148, Reorder the renderer checks inside the handleResult callback so the explicit render option --tree (options.tree) is evaluated before the auto-JSON branch (flags.json) — but still prefer JSON when the user explicitly passed --json (use the same explicit-flag detection used elsewhere in the CLI); keep the existing special-case for options.dot first (in the same handleResult callback where options.dot, flags.json, flags.quiet and options.tree are handled) and ensure flags.quiet logic remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-tree-render.ts`:
- Around line 226-231: The function abbreviateHash currently calls
abbreviateContractHash (which already truncates to MIGRATION_LIST_HASH_WIDTH)
then slices by hashLength, so callers passing hashLength >
MIGRATION_LIST_HASH_WIDTH get a misleading result; fix this by clamping
hashLength to the supported range before slicing — e.g. compute const maxLength
= Math.min(Math.max(1, hashLength), MIGRATION_LIST_HASH_WIDTH) and then return
abbreviated.slice(0, maxLength) (leave the EMPTY_CONTRACT_HASH check and
abbreviateContractHash call as-is), referencing abbreviateHash,
abbreviateContractHash, MIGRATION_LIST_HASH_WIDTH and EMPTY_CONTRACT_HASH.
---
Outside diff comments:
In `@packages/1-framework/3-tooling/cli/src/commands/migration-graph.ts`:
- Around line 121-148: Reorder the renderer checks inside the handleResult
callback so the explicit render option --tree (options.tree) is evaluated before
the auto-JSON branch (flags.json) — but still prefer JSON when the user
explicitly passed --json (use the same explicit-flag detection used elsewhere in
the CLI); keep the existing special-case for options.dot first (in the same
handleResult callback where options.dot, flags.json, flags.quiet and
options.tree are handled) and ensure flags.quiet logic remains unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 62f583e1-9e2c-4674-949c-a56652c3690d
⛔ Files ignored due to path filters (6)
projects/migration-graph-rendering/README.mdis excluded by!projects/**projects/migration-graph-rendering/learnings.mdis excluded by!projects/**projects/migration-graph-rendering/mockups.mdis excluded by!projects/**projects/migration-graph-rendering/prototype/gallery.mdis excluded by!projects/**projects/migration-graph-rendering/prototype/proto.mjsis excluded by!projects/**projects/migration-graph-rendering/spec.mdis excluded by!projects/**
📒 Files selected for processing (31)
docs/reference/migration-graph-rendering.mdexamples/prisma-next-demo/migration-fixtures/complex/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/complex/app/refs/staging.jsonexamples/prisma-next-demo/migration-fixtures/converging-branches/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/diamond-sub-branch/app/refs/experiment.jsonexamples/prisma-next-demo/migration-fixtures/diamond-sub-branch/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/diamond/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/kitchen-sink/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/linear/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/long-spine/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/long-spine/app/refs/staging.jsonexamples/prisma-next-demo/migration-fixtures/multi-branch/app/refs/feature.jsonexamples/prisma-next-demo/migration-fixtures/multi-branch/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/multi-branch/app/refs/staging.jsonexamples/prisma-next-demo/migration-fixtures/multi-rollback-branch/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/multi-rollback-branch/app/refs/staging.jsonexamples/prisma-next-demo/migration-fixtures/rollback-continue/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/sequential-diamonds/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/single-branch/app/refs/prod.jsonexamples/prisma-next-demo/migration-fixtures/single-branch/app/refs/staging.jsonexamples/prisma-next-demo/migration-fixtures/sub-branches/app/refs/experiment.jsonexamples/prisma-next-demo/migration-fixtures/sub-branches/app/refs/feature.jsonexamples/prisma-next-demo/migration-fixtures/sub-branches/app/refs/prod.jsonpackages/1-framework/3-tooling/cli/src/commands/migration-graph.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-layout.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-rows.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-tree-render.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-list-graph-topology.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-graph-layout.test.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-graph-rows.test.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-graph-tree-render.test.ts
The ∅ (empty contract) source glyph rendered unpadded in migration graph tree edge rows (∅ → hash), so its arrow sat one column left of the 7-char hash sources above and below it. Pad the empty source to the hash-column width, matching the flat migration list, so every → / -> lines up. Signed-off-by: Will Madden <madden@prisma.io>
Records a second slice in the migration-graph-rendering project: now that the Tier-3 tree renderer is compact and correct, the Tier-2 list --graph gutter is the redundant middle. Captures the removal design, grounded footprint, and a two-dispatch plan; multi-space graph visibility left as an open question. Signed-off-by: Will Madden <madden@prisma.io>
The graph ref overlay previously bolded the live-database `db` marker as a special case while every other ref rendered plain green. `db` is just another ref and does not warrant that treatment. Emphasize the `contract` marker instead: it is not a stored ref but the user's declared desired state and the implicit base/target for `plan` / `migrate`, so bold is principled rather than arbitrary. The active-ref bold is unchanged. Signed-off-by: Will Madden <madden@prisma.io>
Adds a deliberately comprehensive migration-graph fixture that exercises
every rendering shape the Tier-3 migration graph renderer handles:
- Linear spine (init → add_name → merge → add_bio → add_locale → hotfix)
- Diamond divergence/convergence (alice_phone + bob_avatar branches from
add_name, both converging at merge_alice/merge_bob)
- Forward cross-link (fast_forward: init → merge, skipping B/ALICE/BOB)
- Adjacent rollback (rollback_locale: D → C)
- Node-skipping rollbacks (rollback_users: D → B; rollback_alice: ALICE → A)
- Self-edge with data op (reapply_noop: E → E, verifies verified=true)
- Disjoint 2-cycle component ({experiment, revert_experiment} disconnected
from the ∅ root after deleting the temporary bridge migration)
- Refs overlay: prod → E (hotfix), staging → D (add_locale)
- Contract overlay: (contract) lands on E (hotfix), the emitted state
The contract is left emitted at state E so the (contract) overlay aligns
with the prod ref. The (db) overlay is not included as it requires a live
DB connection (offline mode only).
Schema: single `account` model gaining nullable columns (name, phone,
avatar, bio, locale) plus `verified Boolean @default(true)` in hotfix.
The disjoint component adds a `widget` model disconnected from ∅.
Signed-off-by: Will Madden <madden@prisma.io>
…densed-annotated-node
After merging main (TML-2751 symmetric domain plane), the showcase migration fixture contracts still used top-level `models`, which fails the postgres snapshot-read-shapes scan. Move models under domain.namespaces.__unbound__ without changing storage hashes. Signed-off-by: Will Madden <madden@prisma.io>
abbreviateHash routed through abbreviateContractHash first, which already capped output at MIGRATION_LIST_HASH_WIDTH and made hashLength > 7 a no-op. Slice the stripped digest directly so the option works. Signed-off-by: Will Madden <madden@prisma.io>
Linked issue
Refs TML-2746.
Follow-up: TML-2748 migrates
migration statusand the defaultmigration graphoutput onto this renderer and deletes the dagre dependency.At a glance
prisma-next migration graph --treedraws the whole contract/migration graph as a condensed, annotated tree — one migration per line carryingfrom → to, contract hashes as nodes, full routed back-arcs for rollbacks, and ref/DB/contract overlays:↑is a forward migration,↓a rollback,⟲a self-edge;○is a contract node,∅the empty/genesis contract. Tips are at the top, roots at the bottom (git log --graphdirection).--asciirenders the same graph with single-width ASCII glyphs for non-UTF terminals and CI.The old dagre node-graph remains the default
migration graphoutput;--treeis the opt-in successor this PR introduces.Decision
This PR ships a new Tier-3 renderer for
migration graph, exposed behind--tree(with--ascii), that replaces the sprawling dagre layout with a condensed annotated tree in the same visual language asmigration list --graph. It carries four substantive pieces:git log --graphlane-join algorithm, replacing an earlier imperative special-case allocator that did not survive the graph's edge cases.docs/reference/migration-graph-rendering.md) capturing the rendering contract, glyph palette, worked topologies, and the allocator state machine.How it fits together
migration-list-graph-topology.ts). The shared tolerant classifier — already used bymigration list --graph— is extended with a peel pass that demotes node-skipping rollbacks so the forward subgraph is provably acyclic. Every edge is forward / rollback / self; convergence and divergence are counted over the forward subgraph only.migration-graph-rows.ts). Edges become ordered rows. Vertical order is a longest-forward-path layering (tips first,∅last), and a detached current contract is surfaced as a floating node so the post-merge "your combined contract" state is always visible.migration-graph-layout.ts). A deterministic lane-join state machine assigns each row a lane and emits structural cells (nodes, vertical/horizontal passes, branch/merge tees and corners). Node-skipping rollbacks are routed as real back-arcs into their own back-lane, with crossing characters where they span live lanes.migration-graph-tree-render.ts). Structural cells become box-drawing glyphs against a Unicode or ASCII palette, with thefrom → todata column, ref/DB/contract overlays aligned to that column, and the empty contract drawn as the∅glyph itself.migration-graph.ts).--treeroutes the contract-space aggregate through the new pipeline and emits to stdout without the Clack rail;--asciiforces the ASCII palette.Reviewer notes
migration-graph-layout.ts(~1.1k lines). It's the lane allocator + routed-arc machinery. The load-bearing parts are the lane-join placement, the back-arc tee/landing rows, and the crossing detection (an active lane is any non-empty / non-horizontal-pass cell, plus any back-arc route that crosses the row). The comprehensive golden inmigration-graph-rows.test.tsis the best place to read the output end-to-end.--treealongside it; it does not removegraph-render.tsor the dagre dependency. Migrating the default and deleting dagre is TML-2748.migration-graph-layout.test.tskeeps a self-containedrenderLayoutdebug helper with its own glyph rendering (so its○ … ∅goldens intentionally differ from the production renderer's∅). It exists to make the structural-cell layout legible in tests; it is not the production path.projects/migration-graph-rendering/(spec, plan, mockups, prototype gallery, learnings) are left on disk for review. Close-out will migrate the durable design intodocs/and delete the rest.Testing performed
Run on final HEAD (
ae40aa903):pnpm vitest runon the four graph formatter suites — 81 passed (migration-graph-layout,migration-graph-rows,migration-graph-tree-render,migration-list-graph-topology).pnpm typecheck(cli package) — clean.pnpm biome checkon touched files — clean; bare-cast ratchet delta 0.version.test.ts,removed-verb-redirects.test.ts) that pass in isolation.Follow-ups
migration statusoff dagre, make the condensed tree the defaultmigration graphoutput, and delete the dagre dependency.Alternatives considered
migration list --graphvisual language instead.git log --graphlane-join algorithm was smaller and correct.○ … ∅(node glyph + hash label). Rejected: the crossed-circle∅is itself the node marker for the empty contract, so the row collapses to∅.Skill update
n/a — internal renderer + new experimental CLI flags. The new
--tree/--asciiflags are documented in themigration graphcommand's long description and indocs/reference/migration-graph-rendering.md; no user-facing skill file changes.Checklist
git commit -s) per the DCO.TML-NNNN: <sentence-case title>form.Summary by CodeRabbit
New Features
Documentation
Tests
Chores