@prisma-next/cli 0.12.0-dev.19 → 0.12.0-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/commands/migration-graph.mjs +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/{migration-graph-Cm3oee8C.mjs → migration-graph-DKl_IYsF.mjs} +72 -26
- package/dist/migration-graph-DKl_IYsF.mjs.map +1 -0
- package/dist/{migration-list-styler-BRwF4-gy.mjs → migration-list-styler-COQbZmXk.mjs} +61 -46
- package/dist/migration-list-styler-COQbZmXk.mjs.map +1 -0
- package/package.json +18 -18
- package/src/utils/formatters/migration-graph-layout.ts +24 -2
- package/src/utils/formatters/migration-graph-tree-render.ts +76 -22
- package/src/utils/formatters/migration-list-graph-topology.ts +67 -83
- package/dist/migration-graph-Cm3oee8C.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
|
@@ -7,43 +7,50 @@ function compareDirNameDesc(a, b) {
|
|
|
7
7
|
function bumpDegree(map, key) {
|
|
8
8
|
map.set(key, (map.get(key) ?? 0) + 1);
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function compareNodesRootFirst(a, b) {
|
|
11
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
12
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
13
|
+
return a.localeCompare(b);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Shortest-path distance of each node from the forward roots, over the given
|
|
17
|
+
* candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);
|
|
18
|
+
* a rooted component therefore distances every node by how many forward steps
|
|
19
|
+
* it sits from a root. A component with no root (a pure cycle) is seeded from
|
|
20
|
+
* its single lexically-smallest node so the cycle still gets a stable layering.
|
|
21
|
+
*
|
|
22
|
+
* Crucially this is *shortest* path, not longest: a backward (rollback) edge
|
|
23
|
+
* `deep → shallow` never offers a shorter route to the already-shallower
|
|
24
|
+
* target, so it is inert here. Distances are thus stable whether or not the
|
|
25
|
+
* rollbacks are still in the candidate set — which is what lets the peel below
|
|
26
|
+
* tell a genuine back-edge (target strictly shallower than source) apart from a
|
|
27
|
+
* forward edge that merely happens to share the back-edge's cycle.
|
|
28
|
+
*/
|
|
29
|
+
function forwardDistances(nodes, candidates) {
|
|
11
30
|
const inDegree = /* @__PURE__ */ new Map();
|
|
12
31
|
for (const node of nodes) inDegree.set(node, 0);
|
|
13
32
|
for (const edge of candidates) bumpDegree(inDegree, edge.to);
|
|
14
|
-
const roots = [];
|
|
15
|
-
|
|
16
|
-
roots.sort((
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return a.localeCompare(b);
|
|
20
|
-
});
|
|
21
|
-
if (roots.length > 0) return roots;
|
|
22
|
-
return [...nodes].sort((a, b) => {
|
|
23
|
-
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
24
|
-
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
25
|
-
return a.localeCompare(b);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
function longestPathDepths(nodes, candidates) {
|
|
29
|
-
const depth = /* @__PURE__ */ new Map();
|
|
30
|
-
for (const root of forwardRootsForDepth(nodes, candidates)) depth.set(root, 0);
|
|
33
|
+
const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);
|
|
34
|
+
roots.sort(compareNodesRootFirst);
|
|
35
|
+
const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);
|
|
36
|
+
const dist = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const seed of seeds) dist.set(seed, 0);
|
|
31
38
|
const maxPasses = nodes.size;
|
|
32
39
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
33
40
|
let changed = false;
|
|
34
41
|
for (const edge of candidates) {
|
|
35
|
-
const base =
|
|
42
|
+
const base = dist.get(edge.from);
|
|
36
43
|
if (base === void 0) continue;
|
|
37
44
|
const next = base + 1;
|
|
38
|
-
if (next
|
|
39
|
-
|
|
45
|
+
if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {
|
|
46
|
+
dist.set(edge.to, next);
|
|
40
47
|
changed = true;
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
if (!changed) break;
|
|
44
51
|
}
|
|
45
|
-
for (const node of nodes) if (!
|
|
46
|
-
return
|
|
52
|
+
for (const node of nodes) if (!dist.has(node)) dist.set(node, 0);
|
|
53
|
+
return dist;
|
|
47
54
|
}
|
|
48
55
|
function canReachForward(start, goal, candidates) {
|
|
49
56
|
if (start === goal) return true;
|
|
@@ -68,27 +75,35 @@ function canReachForward(start, goal, candidates) {
|
|
|
68
75
|
}
|
|
69
76
|
return false;
|
|
70
77
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`
|
|
80
|
+
* is a rollback exactly when both hold:
|
|
81
|
+
* 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over
|
|
82
|
+
* the other forward edges, so the edge closes a cycle; and
|
|
83
|
+
* 2. `to` is strictly shallower than `from` (smaller forward distance) — the
|
|
84
|
+
* edge points back toward the root rather than advancing history.
|
|
85
|
+
*
|
|
86
|
+
* Condition 2 is the discriminator: in a cycle created by a rollback every edge
|
|
87
|
+
* satisfies condition 1, but only the rollback itself runs deep → shallow. The
|
|
88
|
+
* forward chain edges run shallow → deep and are never peeled, however many
|
|
89
|
+
* rollbacks converge on the same target. Tight back-edges whose source and
|
|
90
|
+
* target sit at the same distance (mutual two-node cycles) are already resolved
|
|
91
|
+
* by the DFS immediate-parent rule, so they never reach this pass. One edge is
|
|
92
|
+
* peeled per iteration (dirName-descending tie-break) and distances/reachability
|
|
93
|
+
* are recomputed, making the outcome independent of edge input order.
|
|
94
|
+
*/
|
|
95
|
+
function peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf) {
|
|
86
96
|
let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === "forward");
|
|
87
97
|
while (candidates.length > 0) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
const dist = forwardDistances(nodes, candidates);
|
|
99
|
+
const backEdges = candidates.filter((edge) => {
|
|
100
|
+
if ((dist.get(edge.to) ?? 0) >= (dist.get(edge.from) ?? 0)) return false;
|
|
101
|
+
const without = candidates.filter((candidate) => candidate !== edge);
|
|
102
|
+
return canReachForward(edge.to, edge.from, without);
|
|
103
|
+
});
|
|
104
|
+
if (backEdges.length === 0) break;
|
|
105
|
+
backEdges.sort(compareDirNameDesc);
|
|
106
|
+
const rollback = backEdges[0];
|
|
92
107
|
if (rollback === void 0) break;
|
|
93
108
|
kindByMigrationHash.set(rollback.hash, "rollback");
|
|
94
109
|
candidates = candidates.filter((edge) => edge !== rollback);
|
|
@@ -97,8 +112,8 @@ function peelNonMarginalForwardEdges(nodes, kindByMigrationHash, nonSelf) {
|
|
|
97
112
|
/**
|
|
98
113
|
* DFS with dirName-descending traversal. A GRAY target is a rollback only when it
|
|
99
114
|
* is the immediate DFS parent of the source — cross-links to other GRAY nodes
|
|
100
|
-
* stay forward. A follow-up peel pass
|
|
101
|
-
*
|
|
115
|
+
* stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is
|
|
116
|
+
* a forward-ancestor of the source and sits strictly shallower than it).
|
|
102
117
|
*/
|
|
103
118
|
function classifyNormalizedEdges(edges) {
|
|
104
119
|
const nodes = /* @__PURE__ */ new Set();
|
|
@@ -175,7 +190,7 @@ function classifyNormalizedEdges(edges) {
|
|
|
175
190
|
const remainingWhite = [...nodes].filter((node) => color.get(node) === WHITE);
|
|
176
191
|
remainingWhite.sort((a, b) => a.localeCompare(b));
|
|
177
192
|
for (const root of remainingWhite) runDfsFrom(root);
|
|
178
|
-
|
|
193
|
+
peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);
|
|
179
194
|
const forwardInDegree = /* @__PURE__ */ new Map();
|
|
180
195
|
const forwardOutDegree = /* @__PURE__ */ new Map();
|
|
181
196
|
for (const edge of edges) {
|
|
@@ -396,4 +411,4 @@ function createAnsiMigrationListStyler(opts) {
|
|
|
396
411
|
//#endregion
|
|
397
412
|
export { migrationListEmptySource as a, renderMigrationListWithStyle as i, createAnsiMigrationListStyler as n, migrationListForwardArrow as o, buildMigrationListTopologyBySpace as r, classifyMigrationGraphTopology as s, CONTRACT_MARKER_NAME as t };
|
|
398
413
|
|
|
399
|
-
//# sourceMappingURL=migration-list-styler-
|
|
414
|
+
//# sourceMappingURL=migration-list-styler-COQbZmXk.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-list-styler-COQbZmXk.mjs","names":[],"sources":["../src/utils/formatters/migration-list-graph-topology.ts","../src/utils/formatters/migration-list-data-column.ts","../src/utils/formatters/migration-list-render.ts","../src/utils/formatters/migration-list-styler.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';\nimport type { MigrationGraph } from '@prisma-next/migration-tools/graph';\nimport type { MigrationListEntry } from './migration-list-types';\n\nexport type MigrationEdgeKind = 'forward' | 'rollback' | 'self';\n\nexport interface MigrationListGraphTopology {\n readonly kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>;\n readonly forwardInDegree: ReadonlyMap<string, number>;\n readonly forwardOutDegree: ReadonlyMap<string, number>;\n}\n\n// ---------------------------------------------------------------------------\n// Shared classifier — operates on a normalized edge shape common to both\n// MigrationListEntry (Tier-2) and MigrationEdge / MigrationGraph (Tier-3).\n// ---------------------------------------------------------------------------\n\ninterface NormalizedEdge {\n readonly hash: string;\n readonly from: string;\n readonly to: string;\n readonly dirName: string;\n}\n\nfunction compareDirNameDesc(a: NormalizedEdge, b: NormalizedEdge): number {\n return b.dirName.localeCompare(a.dirName);\n}\n\nfunction bumpDegree(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1);\n}\n\nfunction compareNodesRootFirst(a: string, b: string): number {\n if (a === EMPTY_CONTRACT_HASH) return -1;\n if (b === EMPTY_CONTRACT_HASH) return 1;\n return a.localeCompare(b);\n}\n\n/**\n * Shortest-path distance of each node from the forward roots, over the given\n * candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);\n * a rooted component therefore distances every node by how many forward steps\n * it sits from a root. A component with no root (a pure cycle) is seeded from\n * its single lexically-smallest node so the cycle still gets a stable layering.\n *\n * Crucially this is *shortest* path, not longest: a backward (rollback) edge\n * `deep → shallow` never offers a shorter route to the already-shallower\n * target, so it is inert here. Distances are thus stable whether or not the\n * rollbacks are still in the candidate set — which is what lets the peel below\n * tell a genuine back-edge (target strictly shallower than source) apart from a\n * forward edge that merely happens to share the back-edge's cycle.\n */\nfunction forwardDistances(\n nodes: ReadonlySet<string>,\n candidates: readonly NormalizedEdge[],\n): Map<string, number> {\n const inDegree = new Map<string, number>();\n for (const node of nodes) {\n inDegree.set(node, 0);\n }\n for (const edge of candidates) {\n bumpDegree(inDegree, edge.to);\n }\n\n const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);\n roots.sort(compareNodesRootFirst);\n const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);\n\n const dist = new Map<string, number>();\n for (const seed of seeds) {\n dist.set(seed, 0);\n }\n\n const maxPasses = nodes.size;\n for (let pass = 0; pass < maxPasses; pass++) {\n let changed = false;\n for (const edge of candidates) {\n const base = dist.get(edge.from);\n if (base === undefined) continue;\n const next = base + 1;\n if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {\n dist.set(edge.to, next);\n changed = true;\n }\n }\n if (!changed) break;\n }\n\n for (const node of nodes) {\n if (!dist.has(node)) {\n dist.set(node, 0);\n }\n }\n\n return dist;\n}\n\nfunction canReachForward(\n start: string,\n goal: string,\n candidates: readonly NormalizedEdge[],\n): boolean {\n if (start === goal) return true;\n\n const outgoing = new Map<string, string[]>();\n for (const edge of candidates) {\n const bucket = outgoing.get(edge.from);\n if (bucket) bucket.push(edge.to);\n else outgoing.set(edge.from, [edge.to]);\n }\n\n const visited = new Set<string>([start]);\n const queue = [start];\n while (queue.length > 0) {\n const node = queue.shift();\n if (node === undefined) continue;\n for (const next of outgoing.get(node) ?? []) {\n if (next === goal) return true;\n if (!visited.has(next)) {\n visited.add(next);\n queue.push(next);\n }\n }\n }\n\n return false;\n}\n\n/**\n * Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`\n * is a rollback exactly when both hold:\n * 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over\n * the other forward edges, so the edge closes a cycle; and\n * 2. `to` is strictly shallower than `from` (smaller forward distance) — the\n * edge points back toward the root rather than advancing history.\n *\n * Condition 2 is the discriminator: in a cycle created by a rollback every edge\n * satisfies condition 1, but only the rollback itself runs deep → shallow. The\n * forward chain edges run shallow → deep and are never peeled, however many\n * rollbacks converge on the same target. Tight back-edges whose source and\n * target sit at the same distance (mutual two-node cycles) are already resolved\n * by the DFS immediate-parent rule, so they never reach this pass. One edge is\n * peeled per iteration (dirName-descending tie-break) and distances/reachability\n * are recomputed, making the outcome independent of edge input order.\n */\nfunction peelNodeSkippingRollbacks(\n nodes: ReadonlySet<string>,\n kindByMigrationHash: Map<string, MigrationEdgeKind>,\n nonSelf: readonly NormalizedEdge[],\n): void {\n let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === 'forward');\n\n while (candidates.length > 0) {\n const dist = forwardDistances(nodes, candidates);\n const backEdges = candidates.filter((edge) => {\n const toDist = dist.get(edge.to) ?? 0;\n const fromDist = dist.get(edge.from) ?? 0;\n if (toDist >= fromDist) return false;\n const without = candidates.filter((candidate) => candidate !== edge);\n return canReachForward(edge.to, edge.from, without);\n });\n if (backEdges.length === 0) break;\n\n backEdges.sort(compareDirNameDesc);\n const rollback = backEdges[0];\n if (rollback === undefined) break;\n\n kindByMigrationHash.set(rollback.hash, 'rollback');\n candidates = candidates.filter((edge) => edge !== rollback);\n }\n}\n\n/**\n * DFS with dirName-descending traversal. A GRAY target is a rollback only when it\n * is the immediate DFS parent of the source — cross-links to other GRAY nodes\n * stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is\n * a forward-ancestor of the source and sits strictly shallower than it).\n */\nfunction classifyNormalizedEdges(edges: readonly NormalizedEdge[]): MigrationListGraphTopology {\n const nodes = new Set<string>();\n const kindByMigrationHash = new Map<string, MigrationEdgeKind>();\n const outgoingByFrom = new Map<string, NormalizedEdge[]>();\n const nonSelf: NormalizedEdge[] = [];\n\n for (const edge of edges) {\n nodes.add(edge.from);\n nodes.add(edge.to);\n\n if (edge.from === edge.to) {\n kindByMigrationHash.set(edge.hash, 'self');\n continue;\n }\n\n nonSelf.push(edge);\n const bucket = outgoingByFrom.get(edge.from);\n if (bucket) bucket.push(edge);\n else outgoingByFrom.set(edge.from, [edge]);\n }\n\n for (const bucket of outgoingByFrom.values()) {\n bucket.sort(compareDirNameDesc);\n }\n\n const nonSelfInDegree = new Map<string, number>();\n for (const node of nodes) {\n nonSelfInDegree.set(node, 0);\n }\n for (const bucket of outgoingByFrom.values()) {\n for (const edge of bucket) {\n bumpDegree(nonSelfInDegree, edge.to);\n }\n }\n\n const dfsRoots: string[] = [];\n for (const node of nodes) {\n if ((nonSelfInDegree.get(node) ?? 0) === 0) {\n dfsRoots.push(node);\n }\n }\n dfsRoots.sort((a, b) => {\n if (a === EMPTY_CONTRACT_HASH) return -1;\n if (b === EMPTY_CONTRACT_HASH) return 1;\n return a.localeCompare(b);\n });\n if (dfsRoots.length === 0) {\n dfsRoots.push(...[...nodes].sort((a, b) => a.localeCompare(b)));\n }\n\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n const color = new Map<string, number>();\n const dfsParent = new Map<string, string | undefined>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n interface Frame {\n node: string;\n outgoing: readonly NormalizedEdge[];\n index: number;\n }\n const stack: Frame[] = [];\n\n function isImmediateDfsParent(ancestor: string, node: string): boolean {\n return dfsParent.get(node) === ancestor;\n }\n\n function pushFrame(node: string, parent: string | undefined): void {\n color.set(node, GRAY);\n dfsParent.set(node, parent);\n stack.push({ node, outgoing: outgoingByFrom.get(node) ?? [], index: 0 });\n }\n\n function runDfsFrom(root: string): void {\n if (color.get(root) !== WHITE) return;\n pushFrame(root, undefined);\n\n while (stack.length > 0) {\n const frame = stack[stack.length - 1];\n if (frame === undefined) break;\n if (frame.index >= frame.outgoing.length) {\n color.set(frame.node, BLACK);\n stack.pop();\n continue;\n }\n\n const edge = frame.outgoing[frame.index];\n frame.index += 1;\n if (edge === undefined) continue;\n\n const v = edge.to;\n const vColor = color.get(v);\n if (vColor === GRAY && isImmediateDfsParent(v, frame.node)) {\n kindByMigrationHash.set(edge.hash, 'rollback');\n } else {\n kindByMigrationHash.set(edge.hash, 'forward');\n if (vColor === WHITE) {\n pushFrame(v, frame.node);\n }\n }\n }\n }\n\n for (const root of dfsRoots) {\n runDfsFrom(root);\n }\n const remainingWhite = [...nodes].filter((node) => color.get(node) === WHITE);\n remainingWhite.sort((a, b) => a.localeCompare(b));\n for (const root of remainingWhite) {\n runDfsFrom(root);\n }\n\n peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);\n\n const forwardInDegree = new Map<string, number>();\n const forwardOutDegree = new Map<string, number>();\n\n for (const edge of edges) {\n if (kindByMigrationHash.get(edge.hash) !== 'forward') continue;\n bumpDegree(forwardOutDegree, edge.from);\n bumpDegree(forwardInDegree, edge.to);\n }\n\n return {\n kindByMigrationHash,\n forwardInDegree,\n forwardOutDegree,\n };\n}\n\nfunction canonicalFrom(from: string | null): string {\n return from ?? EMPTY_CONTRACT_HASH;\n}\n\n/**\n * Classify forward/rollback/self for a Tier-2 `MigrationListEntry[]` edge set.\n * Returns the kind of each migration plus the forward in/out degree of each\n * contract node. This is the established Tier-2 surface; its behaviour is\n * unchanged — only its implementation now delegates to the shared classifier.\n */\nexport function classifyMigrationListGraphTopology(\n entries: readonly MigrationListEntry[],\n): MigrationListGraphTopology {\n const normalized: NormalizedEdge[] = entries.map((entry) => ({\n hash: entry.migrationHash,\n from: canonicalFrom(entry.from),\n to: entry.to,\n dirName: entry.dirName,\n }));\n return classifyNormalizedEdges(normalized);\n}\n\n/**\n * Classify forward/rollback/self for a `MigrationGraph` edge set (Tier-3).\n * Delegates to the same shared classifier as `classifyMigrationListGraphTopology`\n * so both tiers agree on forward/rollback/self without duplicating logic.\n */\nexport function classifyMigrationGraphTopology(graph: MigrationGraph): MigrationListGraphTopology {\n const normalized: NormalizedEdge[] = [];\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n normalized.push({\n hash: edge.migrationHash,\n from: edge.from,\n to: edge.to,\n dirName: edge.dirName,\n });\n }\n }\n return classifyNormalizedEdges(normalized);\n}\n","import type { GlyphMode } from '../glyph-mode';\nimport type { MigrationEdgeKind } from './migration-list-graph-topology';\nimport type { MigrationListStyler } from './migration-list-render';\nimport type { MigrationListEntry } from './migration-list-types';\n\nexport const MIGRATION_LIST_HASH_WIDTH = 7;\nexport const MIGRATION_LIST_EMPTY_SOURCE = '∅';\nexport const MIGRATION_LIST_ASCII_EMPTY_SOURCE = '-';\nexport const MIGRATION_LIST_FORWARD_EDGE_GLYPH = '→';\nexport const MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH = '->';\nexport const MIGRATION_LIST_DECORATION_PREFIX = ' ';\n\nexport const MIGRATION_LIST_UNICODE_KIND_GLYPH: Record<MigrationEdgeKind, string> = {\n forward: '*',\n rollback: '↩',\n self: '⟲',\n};\n\nexport const MIGRATION_LIST_ASCII_KIND_GLYPH: Record<MigrationEdgeKind, string> = {\n forward: '*',\n rollback: '<',\n self: '~',\n};\n\nexport function migrationListKindGlyph(glyphMode: GlyphMode, edgeKind: MigrationEdgeKind): string {\n return glyphMode === 'ascii'\n ? MIGRATION_LIST_ASCII_KIND_GLYPH[edgeKind]\n : MIGRATION_LIST_UNICODE_KIND_GLYPH[edgeKind];\n}\n\nexport function migrationListForwardArrow(glyphMode: GlyphMode): string {\n return glyphMode === 'ascii'\n ? MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH\n : MIGRATION_LIST_FORWARD_EDGE_GLYPH;\n}\n\nexport function migrationListEmptySource(glyphMode: GlyphMode): string {\n return glyphMode === 'ascii' ? MIGRATION_LIST_ASCII_EMPTY_SOURCE : MIGRATION_LIST_EMPTY_SOURCE;\n}\n\nexport function abbreviateContractHash(hash: string): string {\n const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;\n return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);\n}\n\nexport function computeMigrationDirNameWidth(migrations: readonly MigrationListEntry[]): number {\n if (migrations.length === 0) return 0;\n return Math.max(...migrations.map((entry) => entry.dirName.length)) + 2;\n}\n\nfunction formatSourceColumn(\n from: string | null,\n style: MigrationListStyler,\n emptySource: string,\n): string {\n if (from === null) {\n return style.glyph(emptySource) + ' '.repeat(MIGRATION_LIST_HASH_WIDTH - emptySource.length);\n }\n return style.sourceHash(abbreviateContractHash(from));\n}\n\nexport function formatDecorations(\n providedInvariants: readonly string[],\n refs: readonly string[],\n style: MigrationListStyler,\n): string {\n const blocks: string[] = [];\n if (providedInvariants.length > 0) {\n blocks.push(style.invariants(providedInvariants));\n }\n if (refs.length > 0) {\n blocks.push(style.refs(refs));\n }\n if (blocks.length === 0) return '';\n return `${MIGRATION_LIST_DECORATION_PREFIX}${blocks.join(' ')}`;\n}\n\nexport interface MigrationDataColumnOptions {\n readonly dirNameWidth: number;\n readonly edgeKind: MigrationEdgeKind;\n readonly style: MigrationListStyler;\n readonly forwardArrow?: string;\n readonly emptySource?: string;\n}\n\nexport function formatMigrationDataColumn(\n migration: MigrationListEntry,\n options: MigrationDataColumnOptions,\n): string {\n const {\n dirNameWidth,\n edgeKind,\n style,\n forwardArrow = MIGRATION_LIST_FORWARD_EDGE_GLYPH,\n emptySource = MIGRATION_LIST_EMPTY_SOURCE,\n } = options;\n const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - migration.dirName.length));\n const dirName = `${style.dirName(migration.dirName)}${dirNamePadding}`;\n const decorations = formatDecorations(migration.providedInvariants, migration.refs, style);\n\n if (edgeKind === 'self') {\n const contractHash = migration.from ?? migration.to;\n const hash = style.sourceHash(abbreviateContractHash(contractHash));\n return `${dirName}${hash}${decorations}`;\n }\n\n const source = formatSourceColumn(migration.from, style, emptySource);\n const arrow = style.glyph(forwardArrow);\n const dest = style.destHash(abbreviateContractHash(migration.to));\n return `${dirName}${source} ${arrow} ${dest}${decorations}`;\n}\n\nexport function formatNodeLineDataColumn(contractHash: string, style: MigrationListStyler): string {\n return style.sourceHash(abbreviateContractHash(contractHash));\n}\n","import type { GlyphMode } from '../glyph-mode';\nimport {\n computeMigrationDirNameWidth,\n formatMigrationDataColumn,\n migrationListEmptySource,\n migrationListForwardArrow,\n migrationListKindGlyph,\n} from './migration-list-data-column';\nimport {\n classifyMigrationListGraphTopology,\n type MigrationEdgeKind,\n type MigrationListGraphTopology,\n} from './migration-list-graph-topology';\nimport type { MigrationListEntry, MigrationListResult } from './migration-list-types';\n\nexport type { GlyphMode } from '../glyph-mode';\nexport type { MigrationEdgeKind } from './migration-list-graph-topology';\nexport type {\n MigrationListEntry,\n MigrationListResult,\n MigrationSpaceListEntry,\n} from './migration-list-types';\n\n/**\n * Semantic styler for `migration list` output tokens. Token-typed so\n * the renderer composes presentation-neutral fragments and the styler\n * decides how each token kind is decorated (ANSI codes, plain text,\n * etc.). The renderer pads with raw spaces *outside* styled tokens so\n * visible column widths stay stable regardless of what the styler\n * emits — adding ANSI escape sequences never disturbs alignment.\n *\n * `invariants` and `refs` receive the underlying string arrays rather\n * than a pre-joined string so per-element styling (e.g. distinguishing\n * the live-DB `db` marker from user-named refs) is possible without\n * having to re-parse a joined block.\n */\nexport interface MigrationListStyler {\n kind(text: string): string;\n dirName(text: string): string;\n sourceHash(text: string): string;\n destHash(text: string): string;\n glyph(text: string): string;\n lane(text: string): string;\n invariants(ids: readonly string[]): string;\n refs(names: readonly string[]): string;\n spaceHeading(text: string): string;\n summary(text: string): string;\n emptyState(text: string): string;\n}\n\nexport const IDENTITY_MIGRATION_LIST_STYLER: MigrationListStyler = {\n kind: (text) => text,\n dirName: (text) => text,\n sourceHash: (text) => text,\n destHash: (text) => text,\n glyph: (text) => text,\n lane: (text) => text,\n invariants: (ids) => `{${ids.join(', ')}}`,\n refs: (names) => `(${names.join(', ')})`,\n spaceHeading: (text) => text,\n summary: (text) => text,\n emptyState: (text) => text,\n};\n\nfunction resolveEdgeKind(\n migrationHash: string,\n kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,\n): MigrationEdgeKind {\n return kindByMigrationHash.get(migrationHash) ?? 'forward';\n}\n\nfunction formatMigrationRow(\n migration: MigrationListEntry,\n dirNameWidth: number,\n edgeKind: MigrationEdgeKind,\n glyphMode: GlyphMode,\n style: MigrationListStyler,\n): string {\n const kindColumn = `${style.kind(migrationListKindGlyph(glyphMode, edgeKind))} `;\n const data = formatMigrationDataColumn(migration, {\n dirNameWidth,\n edgeKind,\n style,\n forwardArrow: migrationListForwardArrow(glyphMode),\n emptySource: migrationListEmptySource(glyphMode),\n });\n return `${kindColumn}${data}`;\n}\n\nfunction formatEmptyStateLine(spaceId: string, style: MigrationListStyler): string {\n return style.emptyState(`There are no migrations in migrations/${spaceId}/ yet`);\n}\n\nfunction renderSpaceBlock(\n spaceId: string,\n migrations: readonly MigrationListEntry[],\n multiSpace: boolean,\n glyphMode: GlyphMode,\n kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,\n style: MigrationListStyler,\n): readonly string[] {\n if (migrations.length === 0) {\n const emptyLine = formatEmptyStateLine(spaceId, style);\n if (!multiSpace) {\n return [emptyLine];\n }\n return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];\n }\n\n const dirNameWidth = computeMigrationDirNameWidth(migrations);\n const rows = migrations.map((entry) =>\n formatMigrationRow(\n entry,\n dirNameWidth,\n resolveEdgeKind(entry.migrationHash, kindByMigrationHash),\n glyphMode,\n style,\n ),\n );\n if (!multiSpace) {\n return rows;\n }\n return [style.spaceHeading(`${spaceId}:`), ...rows.map((row) => ` ${row}`)];\n}\n\nexport function buildMigrationListTopologyBySpace(\n result: MigrationListResult,\n): ReadonlyMap<string, MigrationListGraphTopology> {\n const topologyBySpaceId = new Map<string, MigrationListGraphTopology>();\n for (const space of result.spaces) {\n topologyBySpaceId.set(space.spaceId, classifyMigrationListGraphTopology(space.migrations));\n }\n return topologyBySpaceId;\n}\n\n/**\n * Compose the styled `migration list` output. The renderer is\n * presentation-neutral — every token passes through `style` before\n * landing in the output, so the same composition serves the pure-text\n * path ({@link renderMigrationList} via\n * {@link IDENTITY_MIGRATION_LIST_STYLER}) and the ANSI-styled CLI path\n * (via the ANSI styler the CLI shell wires up).\n */\nexport function renderMigrationListWithStyle(\n result: MigrationListResult,\n style: MigrationListStyler,\n glyphMode: GlyphMode = 'unicode',\n topologyBySpaceId: ReadonlyMap<\n string,\n MigrationListGraphTopology\n > = buildMigrationListTopologyBySpace(result),\n): string {\n const multiSpace = result.spaces.length > 1;\n const lines: string[] = [];\n\n for (let index = 0; index < result.spaces.length; index++) {\n const space = result.spaces[index]!;\n if (index > 0) {\n lines.push('');\n }\n const topology = topologyBySpaceId.get(space.spaceId);\n const kindByMigrationHash =\n topology?.kindByMigrationHash ??\n classifyMigrationListGraphTopology(space.migrations).kindByMigrationHash;\n lines.push(\n ...renderSpaceBlock(\n space.spaceId,\n space.migrations,\n multiSpace,\n glyphMode,\n kindByMigrationHash,\n style,\n ),\n );\n }\n\n const totalMigrations = result.spaces.reduce(\n (count, space) => count + space.migrations.length,\n 0,\n );\n if (totalMigrations > 0) {\n lines.push('');\n lines.push(style.summary(result.summary));\n }\n\n return lines.join('\\n');\n}\n\nexport function renderMigrationList(result: MigrationListResult): string {\n return renderMigrationListWithStyle(result, IDENTITY_MIGRATION_LIST_STYLER);\n}\n","import { bold, cyan, cyanBright, dim, green, yellow } from 'colorette';\nimport { IDENTITY_MIGRATION_LIST_STYLER, type MigrationListStyler } from './migration-list-render';\n\n/**\n * The current contract overlay marker. Unlike user refs, this names the user's\n * declared desired state — the implicit base/target for `plan` / `migrate` —\n * not a stored label. It is emphasized (bold) so it stands out from plain refs\n * (including the live-database `db` marker, which is just another ref).\n */\nexport const CONTRACT_MARKER_NAME = 'contract';\n\nfunction styleRefName(name: string): string {\n return name === CONTRACT_MARKER_NAME ? bold(green(name)) : green(name);\n}\n\n/**\n * Build a {@link MigrationListStyler} that decorates `migration list`\n * tokens with ANSI SGR codes. When `useColor` is `false` (non-TTY,\n * `--no-color`, `NO_COLOR=1`, piped output) the function returns the\n * shared identity styler so callers get plain text with zero ANSI\n * bytes — pipe-friendly by construction.\n *\n * Palette:\n *\n * - `dirName`: bold\n * - `sourceHash`: dim cyan\n * - `destHash`: bright cyan\n * - `kind` (`*` / `↩` / `⟲`): bright — the signal; lanes and arrows dim\n * - `glyph` (`→` / `⟲` / `∅`): dim\n * - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim\n * - `invariants` (`{...}`): yellow\n * - `refs` (`(...)`): green; the `contract` desired-state marker inside is\n * green-bold (the active ref is bolded separately by the tree styler)\n * - `spaceHeading` (`<spaceId>:`): bold\n * - `summary`: dim\n * - `emptyState`: dim\n */\nexport function createAnsiMigrationListStyler(opts: {\n readonly useColor: boolean;\n}): MigrationListStyler {\n if (!opts.useColor) {\n return IDENTITY_MIGRATION_LIST_STYLER;\n }\n return {\n // Kind glyphs stay bright in both flat and graph views; lanes carry the dim gutter.\n kind: (text) => text,\n dirName: (text) => bold(text),\n sourceHash: (text) => dim(cyan(text)),\n destHash: (text) => cyanBright(text),\n glyph: (text) => dim(text),\n lane: (text) => dim(text),\n invariants: (ids) => yellow(`{${ids.join(', ')}}`),\n refs: (names) => {\n const open = green('(');\n const close = green(')');\n const separator = green(', ');\n return open + names.map(styleRefName).join(separator) + close;\n },\n spaceHeading: (text) => bold(text),\n summary: (text) => dim(text),\n emptyState: (text) => dim(text),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/cli",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.20",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@clack/prompts": "^1.4.0",
|
|
12
12
|
"@dagrejs/dagre": "^3.0.0",
|
|
13
|
-
"@prisma-next/config": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
15
|
-
"@prisma-next/emitter": "0.12.0-dev.
|
|
16
|
-
"@prisma-next/errors": "0.12.0-dev.
|
|
17
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
18
|
-
"@prisma-next/migration-tools": "0.12.0-dev.
|
|
19
|
-
"@prisma-next/psl-printer": "0.12.0-dev.
|
|
20
|
-
"@prisma-next/cli-telemetry": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
13
|
+
"@prisma-next/config": "0.12.0-dev.20",
|
|
14
|
+
"@prisma-next/contract": "0.12.0-dev.20",
|
|
15
|
+
"@prisma-next/emitter": "0.12.0-dev.20",
|
|
16
|
+
"@prisma-next/errors": "0.12.0-dev.20",
|
|
17
|
+
"@prisma-next/framework-components": "0.12.0-dev.20",
|
|
18
|
+
"@prisma-next/migration-tools": "0.12.0-dev.20",
|
|
19
|
+
"@prisma-next/psl-printer": "0.12.0-dev.20",
|
|
20
|
+
"@prisma-next/cli-telemetry": "0.12.0-dev.20",
|
|
21
|
+
"@prisma-next/utils": "0.12.0-dev.20",
|
|
22
22
|
"arktype": "^2.2.0",
|
|
23
23
|
"c12": "^3.3.4",
|
|
24
24
|
"ci-info": "^4.3.1",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"wrap-ansi": "^10.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
39
|
-
"@prisma-next/sql-contract-emitter": "0.12.0-dev.
|
|
40
|
-
"@prisma-next/sql-contract-ts": "0.12.0-dev.
|
|
41
|
-
"@prisma-next/sql-operations": "0.12.0-dev.
|
|
42
|
-
"@prisma-next/sql-runtime": "0.12.0-dev.
|
|
43
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
44
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
45
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
38
|
+
"@prisma-next/sql-contract": "0.12.0-dev.20",
|
|
39
|
+
"@prisma-next/sql-contract-emitter": "0.12.0-dev.20",
|
|
40
|
+
"@prisma-next/sql-contract-ts": "0.12.0-dev.20",
|
|
41
|
+
"@prisma-next/sql-operations": "0.12.0-dev.20",
|
|
42
|
+
"@prisma-next/sql-runtime": "0.12.0-dev.20",
|
|
43
|
+
"@prisma-next/test-utils": "0.12.0-dev.20",
|
|
44
|
+
"@prisma-next/tsconfig": "0.12.0-dev.20",
|
|
45
|
+
"@prisma-next/tsdown": "0.12.0-dev.20",
|
|
46
46
|
"@types/node": "25.6.0",
|
|
47
47
|
"tsdown": "0.22.0",
|
|
48
48
|
"typescript": "5.9.3",
|
|
@@ -21,6 +21,7 @@ export type StructuralCell =
|
|
|
21
21
|
| { readonly kind: 'arc-branch-corner' }
|
|
22
22
|
| { readonly kind: 'arc-branch-tee' }
|
|
23
23
|
| { readonly kind: 'arc-land-corner' }
|
|
24
|
+
| { readonly kind: 'arc-land-tee' }
|
|
24
25
|
| { readonly kind: 'arc-crossing' }
|
|
25
26
|
| { readonly kind: 'arc-land-bridge' }
|
|
26
27
|
| {
|
|
@@ -703,6 +704,15 @@ function applySkipRollbackRouting(
|
|
|
703
704
|
.map((other) => other.backLane);
|
|
704
705
|
const maxCoSourcedLane = Math.max(...coSourcedLanes);
|
|
705
706
|
|
|
707
|
+
// Back-lanes of arcs that converge on this same target node. They share the
|
|
708
|
+
// node's landing row, so each inner lane reads as a `┴` junction (the outer
|
|
709
|
+
// arcs' horizontal bridge passes through it on the way to the node) and only
|
|
710
|
+
// the outermost closes the corner with `╯`.
|
|
711
|
+
const coLandingLanes = routes
|
|
712
|
+
.filter((other) => other.edge.to === edge.to)
|
|
713
|
+
.map((other) => other.backLane);
|
|
714
|
+
const maxCoLandingLane = Math.max(...coLandingLanes);
|
|
715
|
+
|
|
706
716
|
const sourceRow = result[sourceRowIndex];
|
|
707
717
|
if (sourceRow !== undefined) {
|
|
708
718
|
const cells = sourceRow.cells;
|
|
@@ -764,6 +774,7 @@ function applySkipRollbackRouting(
|
|
|
764
774
|
const existing = cells[backLane];
|
|
765
775
|
if (
|
|
766
776
|
existing?.kind !== 'arc-land-corner' &&
|
|
777
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
767
778
|
existing?.kind !== 'arc-land-bridge' &&
|
|
768
779
|
existing?.kind !== 'arc-branch-corner' &&
|
|
769
780
|
existing?.kind !== 'arc-branch-tee' &&
|
|
@@ -780,6 +791,12 @@ function applySkipRollbackRouting(
|
|
|
780
791
|
const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
781
792
|
cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
|
|
782
793
|
for (let lane = targetCol + 1; lane < backLane; lane += 1) {
|
|
794
|
+
// An inner converging arc's own landing junction: the outer arcs' bridge
|
|
795
|
+
// passes through it (`┴`) while its own vertical run closes here.
|
|
796
|
+
if (coLandingLanes.includes(lane)) {
|
|
797
|
+
cells[lane] = { kind: 'arc-land-tee' };
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
783
800
|
// A bridged lane that carries another arc OR a forward vertical still
|
|
784
801
|
// active at this row must cross over it (`┼`) rather than overwrite it
|
|
785
802
|
// with a bare bridge (`──`).
|
|
@@ -788,7 +805,8 @@ function applySkipRollbackRouting(
|
|
|
788
805
|
existing !== undefined &&
|
|
789
806
|
existing.kind !== 'empty' &&
|
|
790
807
|
existing.kind !== 'horizontal-pass' &&
|
|
791
|
-
existing.kind !== 'arc-land-bridge'
|
|
808
|
+
existing.kind !== 'arc-land-bridge' &&
|
|
809
|
+
existing.kind !== 'arc-land-tee';
|
|
792
810
|
const crossed =
|
|
793
811
|
occupied ||
|
|
794
812
|
routes.some(
|
|
@@ -799,7 +817,10 @@ function applySkipRollbackRouting(
|
|
|
799
817
|
);
|
|
800
818
|
cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'arc-land-bridge' };
|
|
801
819
|
}
|
|
802
|
-
|
|
820
|
+
// Inner converging arcs close as a landing tee so the outermost arc's
|
|
821
|
+
// bridge reads through to the node; only the outermost arc draws `╯`.
|
|
822
|
+
cells[backLane] =
|
|
823
|
+
backLane < maxCoLandingLane ? { kind: 'arc-land-tee' } : { kind: 'arc-land-corner' };
|
|
803
824
|
for (const other of routes) {
|
|
804
825
|
if (other.backLane <= backLane) continue;
|
|
805
826
|
if (!routeCrossesRow(other, targetRowIndex, result)) continue;
|
|
@@ -807,6 +828,7 @@ function applySkipRollbackRouting(
|
|
|
807
828
|
const existing = cells[other.backLane];
|
|
808
829
|
if (
|
|
809
830
|
existing?.kind !== 'arc-land-corner' &&
|
|
831
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
810
832
|
existing?.kind !== 'arc-land-bridge' &&
|
|
811
833
|
existing?.kind !== 'node'
|
|
812
834
|
) {
|
|
@@ -49,6 +49,7 @@ interface MigrationGraphTreeGlyphPalette {
|
|
|
49
49
|
readonly arcBranchCorner: string;
|
|
50
50
|
readonly arcBranchTee: string;
|
|
51
51
|
readonly arcLandCorner: string;
|
|
52
|
+
readonly arcLandTee: string;
|
|
52
53
|
readonly arcCrossing: string;
|
|
53
54
|
readonly arcLandBridge: string;
|
|
54
55
|
readonly horizontalPass: string;
|
|
@@ -72,6 +73,7 @@ const UNICODE_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
|
72
73
|
arcBranchCorner: '╮ ',
|
|
73
74
|
arcBranchTee: '┬─',
|
|
74
75
|
arcLandCorner: '╯ ',
|
|
76
|
+
arcLandTee: '┴─',
|
|
75
77
|
arcCrossing: '┼─',
|
|
76
78
|
arcLandBridge: '──',
|
|
77
79
|
horizontalPass: '──',
|
|
@@ -95,6 +97,7 @@ const ASCII_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
|
95
97
|
arcBranchCorner: '\\ ',
|
|
96
98
|
arcBranchTee: '+-',
|
|
97
99
|
arcLandCorner: '/ ',
|
|
100
|
+
arcLandTee: '+-',
|
|
98
101
|
arcCrossing: '+-',
|
|
99
102
|
arcLandBridge: '--',
|
|
100
103
|
horizontalPass: '--',
|
|
@@ -144,53 +147,93 @@ interface RowLaneColors {
|
|
|
144
147
|
readonly lane: readonly number[];
|
|
145
148
|
/** Colour column for a node arc-pair's connector half (`◂` / `─`). */
|
|
146
149
|
readonly connector: readonly number[];
|
|
150
|
+
/**
|
|
151
|
+
* Colour column for the trailing `─` of a landing tee (`┴─`). The junction
|
|
152
|
+
* (`lane`) keeps its own column; the dash leads into the next converging arc.
|
|
153
|
+
*/
|
|
154
|
+
readonly dash: readonly number[];
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
/**
|
|
150
158
|
* Resolve per-cell colour columns for a row. Scanning right-to-left lets each
|
|
151
|
-
* arc
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
159
|
+
* arc segment inherit the hue of the arc it leads into.
|
|
160
|
+
*
|
|
161
|
+
* On a converging-landing line (`○◂──────┴─┴─╯`), every horizontal dash segment
|
|
162
|
+
* takes the hue of the **nearest landing anchor** — the next `arc-land-tee` or
|
|
163
|
+
* `arc-land-corner` — to its right, i.e. the branch it leads into: the bridge
|
|
164
|
+
* run leads into the first converging arc, and each tee's trailing `─` leads
|
|
165
|
+
* into the next arc out. Tee/corner junction glyphs keep their own column hue.
|
|
166
|
+
* This mirrors the forward connector's `┬─` rule (see
|
|
167
|
+
* {@link resolveConnectorLaneColors}). A single (non-converging) landing has
|
|
168
|
+
* only the corner as an anchor, so its whole horizontal run reads as one hue.
|
|
169
|
+
*
|
|
170
|
+
* The source side (`○─`, `arc-branch-tee`, `arc-branch-corner`) and pure
|
|
171
|
+
* horizontal passes are unaffected: they track the nearest corner to the right
|
|
172
|
+
* (`arcCorner`), so a routed back-arc's source fan still reads as one hue. A
|
|
173
|
+
* crossing can only be one colour, so it takes the arc owning the horizontal
|
|
174
|
+
* run at this row; the crossed vertical lane is occluded at that one cell and
|
|
175
|
+
* reappears on the next row.
|
|
158
176
|
*/
|
|
159
177
|
function resolveRowLaneColors(cells: readonly StructuralCell[]): RowLaneColors {
|
|
160
178
|
const lane = new Array<number>(cells.length);
|
|
161
179
|
const connector = new Array<number>(cells.length);
|
|
180
|
+
const dash = new Array<number>(cells.length);
|
|
162
181
|
let arcCorner = NEUTRAL_LANE;
|
|
182
|
+
let landingAnchor = NEUTRAL_LANE;
|
|
163
183
|
for (let column = cells.length - 1; column >= 0; column--) {
|
|
164
184
|
const cell = cells[column];
|
|
165
|
-
connector[column] = arcCorner;
|
|
185
|
+
connector[column] = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
|
|
166
186
|
switch (cell?.kind) {
|
|
167
187
|
case 'arc-branch-corner':
|
|
188
|
+
arcCorner = column;
|
|
189
|
+
lane[column] = column;
|
|
190
|
+
dash[column] = column;
|
|
191
|
+
break;
|
|
168
192
|
case 'arc-land-corner':
|
|
169
193
|
arcCorner = column;
|
|
194
|
+
landingAnchor = column;
|
|
170
195
|
lane[column] = column;
|
|
196
|
+
dash[column] = column;
|
|
171
197
|
break;
|
|
198
|
+
// An inner co-sourced arc's own back-lane junction: its vertical run
|
|
199
|
+
// continues below in this column, so the whole `┬─` keeps its own column.
|
|
172
200
|
case 'arc-branch-tee':
|
|
173
|
-
// An inner co-sourced arc's own back-lane junction: its vertical run
|
|
174
|
-
// continues below in this column, so it keeps its own column hue.
|
|
175
201
|
lane[column] = column;
|
|
202
|
+
dash[column] = column;
|
|
203
|
+
break;
|
|
204
|
+
// The symmetric co-landing junction: the `┴` keeps its own column (its
|
|
205
|
+
// vertical run continues above), but the trailing `─` leads into the next
|
|
206
|
+
// converging arc — the nearest landing anchor still to its right.
|
|
207
|
+
case 'arc-land-tee':
|
|
208
|
+
lane[column] = column;
|
|
209
|
+
dash[column] = landingAnchor === NEUTRAL_LANE ? column : landingAnchor;
|
|
210
|
+
landingAnchor = column;
|
|
176
211
|
break;
|
|
177
212
|
case 'arc-crossing':
|
|
178
|
-
case 'arc-land-bridge':
|
|
179
|
-
|
|
213
|
+
case 'arc-land-bridge': {
|
|
214
|
+
const served = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
|
|
215
|
+
lane[column] = served;
|
|
216
|
+
dash[column] = served;
|
|
180
217
|
break;
|
|
218
|
+
}
|
|
181
219
|
case 'horizontal-pass':
|
|
182
220
|
lane[column] = arcCorner === NEUTRAL_LANE ? column : arcCorner;
|
|
221
|
+
dash[column] = lane[column] ?? column;
|
|
183
222
|
break;
|
|
184
223
|
case 'node':
|
|
185
224
|
lane[column] = column;
|
|
225
|
+
dash[column] = column;
|
|
186
226
|
arcCorner = NEUTRAL_LANE;
|
|
227
|
+
landingAnchor = NEUTRAL_LANE;
|
|
187
228
|
break;
|
|
188
229
|
default:
|
|
189
230
|
lane[column] = column;
|
|
231
|
+
dash[column] = column;
|
|
190
232
|
arcCorner = NEUTRAL_LANE;
|
|
233
|
+
landingAnchor = NEUTRAL_LANE;
|
|
191
234
|
}
|
|
192
235
|
}
|
|
193
|
-
return { lane, connector };
|
|
236
|
+
return { lane, connector, dash };
|
|
194
237
|
}
|
|
195
238
|
|
|
196
239
|
/**
|
|
@@ -215,10 +258,12 @@ interface ConnectorLaneColors {
|
|
|
215
258
|
* than tinting the dash with the left lane. The leading tee at `startLane` (the
|
|
216
259
|
* fork/merge origin) and pure horizontal segments inherit the nearest branch
|
|
217
260
|
* point to their right whole-cell, so the run into a branch — or collapsing
|
|
218
|
-
* into a merge corner — stays continuous.
|
|
219
|
-
*
|
|
261
|
+
* into a merge corner — stays continuous. An `arc-crossing` keeps its junction
|
|
262
|
+
* glyph at its own column but re-anchors `owner` like an intermediate tee so
|
|
263
|
+
* dashes on both sides lead into the nearest branch on their right. Pass-through
|
|
264
|
+
* verticals outside the run keep their own column (column 0 stays neutral).
|
|
220
265
|
*/
|
|
221
|
-
function resolveConnectorLaneColors(
|
|
266
|
+
export function resolveConnectorLaneColors(
|
|
222
267
|
cells: readonly StructuralCell[],
|
|
223
268
|
startLane: number,
|
|
224
269
|
): ConnectorLaneColors {
|
|
@@ -248,7 +293,8 @@ function resolveConnectorLaneColors(
|
|
|
248
293
|
break;
|
|
249
294
|
case 'arc-crossing':
|
|
250
295
|
glyph[column] = column;
|
|
251
|
-
dash[column] = column;
|
|
296
|
+
dash[column] = owner === NEUTRAL_LANE ? column : owner;
|
|
297
|
+
owner = column;
|
|
252
298
|
break;
|
|
253
299
|
case 'horizontal-pass': {
|
|
254
300
|
const served = owner === NEUTRAL_LANE ? column : owner;
|
|
@@ -381,6 +427,14 @@ function renderCellPair(
|
|
|
381
427
|
return lane(palette.arcBranchTee);
|
|
382
428
|
case 'arc-land-corner':
|
|
383
429
|
return lane(palette.arcLandCorner);
|
|
430
|
+
case 'arc-land-tee':
|
|
431
|
+
return renderConnectorTee(
|
|
432
|
+
palette.arcLandTee,
|
|
433
|
+
laneColumn,
|
|
434
|
+
colors.dash[column] ?? laneColumn,
|
|
435
|
+
colorize,
|
|
436
|
+
style,
|
|
437
|
+
);
|
|
384
438
|
case 'arc-crossing':
|
|
385
439
|
return lane(palette.arcLandBridge);
|
|
386
440
|
case 'arc-land-bridge':
|
|
@@ -759,10 +813,10 @@ export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptio
|
|
|
759
813
|
const sampleArrow = `${style.sourceHash('aaaaaa')} ${style.glyph(palette.forwardArrow)} ${style.destHash('bbbbbb')}`;
|
|
760
814
|
return [
|
|
761
815
|
'Legend:',
|
|
762
|
-
` ${style.kind(node)} contract ${style.kind(palette.edgeArrow.forward)} forward ${style.kind(palette.edgeArrow.rollback)} rollback`,
|
|
763
|
-
` ${style.kind(palette.edgeArrow.self)} migration without schema change`,
|
|
764
|
-
` ${style.
|
|
765
|
-
` ${style.refs(['refs'])} ${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`,
|
|
766
|
-
` ${sampleArrow} migration from contract aaaaaa to bbbbbb`,
|
|
816
|
+
` ${style.kind(node)} ${style.summary('contract')} ${style.kind(palette.edgeArrow.forward)} ${style.summary('forward')} ${style.kind(palette.edgeArrow.rollback)} ${style.summary('rollback')}`,
|
|
817
|
+
` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
|
|
818
|
+
` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
|
|
819
|
+
` ${style.refs(['refs'])} ${style.summary(`${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`)}`,
|
|
820
|
+
` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
|
|
767
821
|
].join('\n');
|
|
768
822
|
}
|