@prisma-next/cli 0.11.0 → 0.12.0

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.
Files changed (196) hide show
  1. package/README.md +13 -9
  2. package/dist/cli.mjs +259 -12
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-oXO2WCPD.mjs → client-KgJorIvG.mjs} +72 -60
  5. package/dist/client-KgJorIvG.mjs.map +1 -0
  6. package/dist/{command-helpers-BSb0tRC8.mjs → command-helpers-Bbw1GbwL.mjs} +646 -46
  7. package/dist/command-helpers-Bbw1GbwL.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +32 -7
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -4
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +12 -10
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +41 -11
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +6 -2
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +75 -40
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +4 -3
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +1 -280
  33. package/dist/commands/migration-graph.d.mts +13 -2
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +2 -137
  36. package/dist/commands/migration-list.d.mts +64 -4
  37. package/dist/commands/migration-list.d.mts.map +1 -1
  38. package/dist/commands/migration-list.mjs +143 -56
  39. package/dist/commands/migration-list.mjs.map +1 -1
  40. package/dist/commands/migration-log.d.mts +10 -1
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +10 -15
  43. package/dist/commands/migration-log.mjs.map +1 -1
  44. package/dist/commands/migration-new.d.mts.map +1 -1
  45. package/dist/commands/migration-new.mjs +32 -38
  46. package/dist/commands/migration-new.mjs.map +1 -1
  47. package/dist/commands/migration-plan.d.mts +3 -2
  48. package/dist/commands/migration-plan.d.mts.map +1 -1
  49. package/dist/commands/migration-plan.mjs +1 -1
  50. package/dist/commands/migration-show.d.mts +4 -55
  51. package/dist/commands/migration-show.d.mts.map +1 -1
  52. package/dist/commands/migration-show.mjs +61 -153
  53. package/dist/commands/migration-show.mjs.map +1 -1
  54. package/dist/commands/migration-status.d.mts +12 -49
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +85 -81
  57. package/dist/commands/migration-status.mjs.map +1 -1
  58. package/dist/commands/ref.d.mts +1 -1
  59. package/dist/commands/ref.d.mts.map +1 -1
  60. package/dist/commands/ref.mjs +38 -10
  61. package/dist/commands/ref.mjs.map +1 -1
  62. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  63. package/dist/config-loader.d.mts.map +1 -1
  64. package/dist/contract-at-errors-BxP-TOMl.mjs +42 -0
  65. package/dist/contract-at-errors-BxP-TOMl.mjs.map +1 -0
  66. package/dist/{contract-emit-bcrpT-wD.mjs → contract-emit-D-4jrNve.mjs} +25 -10
  67. package/dist/contract-emit-D-4jrNve.mjs.map +1 -0
  68. package/dist/{contract-emit-r4y8Zhf1.mjs → contract-emit-DxcGl4Uq.mjs} +19 -14
  69. package/dist/contract-emit-DxcGl4Uq.mjs.map +1 -0
  70. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-a0V5Y_mL.mjs} +4 -25
  71. package/dist/contract-enrichment-a0V5Y_mL.mjs.map +1 -0
  72. package/dist/{contract-infer-BmySmqVT.mjs → contract-infer-D8uEbJuu.mjs} +4 -5
  73. package/dist/{contract-infer-BmySmqVT.mjs.map → contract-infer-D8uEbJuu.mjs.map} +1 -1
  74. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs +247 -0
  75. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs.map +1 -0
  76. package/dist/{db-verify-BClPs3ph.mjs → db-verify-v_vUKXTU.mjs} +5 -7
  77. package/dist/{db-verify-BClPs3ph.mjs.map → db-verify-v_vUKXTU.mjs.map} +1 -1
  78. package/dist/exports/control-api.d.mts +3 -3
  79. package/dist/exports/control-api.d.mts.map +1 -1
  80. package/dist/exports/control-api.mjs +3 -3
  81. package/dist/exports/index.d.mts.map +1 -1
  82. package/dist/exports/index.mjs +1 -1
  83. package/dist/exports/index.mjs.map +1 -1
  84. package/dist/exports/init-output.d.mts.map +1 -1
  85. package/dist/exports/init-output.mjs +1 -1
  86. package/dist/extension-pack-inputs-IDvjRCi3.mjs +62 -0
  87. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +1 -0
  88. package/dist/{framework-components-65gOHkHB.mjs → framework-components-fYXjz_in.mjs} +2 -2
  89. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-fYXjz_in.mjs.map} +1 -1
  90. package/dist/global-flags-DEHjV8_s.d.mts +34 -0
  91. package/dist/global-flags-DEHjV8_s.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-rFAqZujX.mjs} +2 -2
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-rFAqZujX.mjs.map} +1 -1
  94. package/dist/{init-BCJZPWE1.mjs → init-Cv9UzWL5.mjs} +20 -269
  95. package/dist/init-Cv9UzWL5.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-DSRbFoOL.mjs → inspect-live-schema-C6ohV_oQ.mjs} +4 -5
  97. package/dist/{inspect-live-schema-DSRbFoOL.mjs.map → inspect-live-schema-C6ohV_oQ.mjs.map} +1 -1
  98. package/dist/migration-check-BiBJoYYW.mjs +341 -0
  99. package/dist/migration-check-BiBJoYYW.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts.map +1 -1
  101. package/dist/migration-cli.mjs +4 -4
  102. package/dist/migration-cli.mjs.map +1 -1
  103. package/dist/{migration-command-scaffold-Bzd9La5c.mjs → migration-command-scaffold-CjvwO6at.mjs} +4 -5
  104. package/dist/{migration-command-scaffold-Bzd9La5c.mjs.map → migration-command-scaffold-CjvwO6at.mjs.map} +1 -1
  105. package/dist/migration-graph-D7DVUElV.mjs +1232 -0
  106. package/dist/migration-graph-D7DVUElV.mjs.map +1 -0
  107. package/dist/migration-list-styler-BRwF4-gy.mjs +399 -0
  108. package/dist/migration-list-styler-BRwF4-gy.mjs.map +1 -0
  109. package/dist/{migration-plan-CFwqw3Gk.mjs → migration-plan-9DJ7q7_z.mjs} +372 -133
  110. package/dist/migration-plan-9DJ7q7_z.mjs.map +1 -0
  111. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-D2FW63pr.d.mts} +1 -1
  112. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-D2FW63pr.d.mts.map} +1 -1
  113. package/dist/{migrations-CwZMa1Ck.mjs → migrations-Cv2jxNNK.mjs} +12 -13
  114. package/dist/migrations-Cv2jxNNK.mjs.map +1 -0
  115. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  116. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  117. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
  118. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
  119. package/dist/ref-advancement-DUZqsue6.mjs +50 -0
  120. package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
  121. package/dist/terminal-ui-5Y6mrg93.d.mts +133 -0
  122. package/dist/terminal-ui-5Y6mrg93.d.mts.map +1 -0
  123. package/dist/{types--CqjMdk0.d.mts → types-Dt_SfqFm.d.mts} +28 -28
  124. package/dist/types-Dt_SfqFm.d.mts.map +1 -0
  125. package/dist/{verify-Bom75OYI.mjs → verify-DCA9Sldu.mjs} +2 -2
  126. package/dist/{verify-Bom75OYI.mjs.map → verify-DCA9Sldu.mjs.map} +1 -1
  127. package/package.json +35 -24
  128. package/src/commands/contract-emit.ts +19 -7
  129. package/src/commands/contract-infer.ts +1 -1
  130. package/src/commands/db-init.ts +48 -2
  131. package/src/commands/db-sign.ts +9 -5
  132. package/src/commands/db-update.ts +54 -8
  133. package/src/commands/init/hygiene-gitattributes.ts +2 -2
  134. package/src/commands/init/index.ts +2 -1
  135. package/src/commands/init/templates/code-templates.ts +4 -2
  136. package/src/commands/init/templates/env.ts +13 -14
  137. package/src/commands/migrate.ts +125 -44
  138. package/src/commands/migration-check.ts +43 -83
  139. package/src/commands/migration-graph.ts +75 -60
  140. package/src/commands/migration-list.ts +220 -74
  141. package/src/commands/migration-log.ts +8 -14
  142. package/src/commands/migration-new.ts +44 -48
  143. package/src/commands/migration-plan.ts +412 -197
  144. package/src/commands/migration-show.ts +65 -284
  145. package/src/commands/migration-status.ts +127 -124
  146. package/src/commands/ref.ts +53 -8
  147. package/src/control-api/client.ts +0 -1
  148. package/src/control-api/contract-enrichment.ts +6 -42
  149. package/src/control-api/operations/{apply-aggregate.ts → apply.ts} +44 -75
  150. package/src/control-api/operations/contract-emit.ts +14 -6
  151. package/src/control-api/operations/{db-apply-aggregate.ts → db-apply.ts} +19 -19
  152. package/src/control-api/operations/db-init.ts +4 -4
  153. package/src/control-api/operations/db-update.ts +4 -4
  154. package/src/control-api/operations/db-verify.ts +15 -11
  155. package/src/control-api/operations/migration-apply.ts +56 -47
  156. package/src/control-api/types.ts +26 -27
  157. package/src/migration-cli.ts +4 -4
  158. package/src/utils/cli-errors.ts +234 -0
  159. package/src/utils/command-helpers.ts +9 -24
  160. package/src/utils/contract-at-errors.ts +96 -0
  161. package/src/utils/contract-space-aggregate-loader.ts +336 -117
  162. package/src/utils/formatters/migration-graph-layout.ts +1119 -0
  163. package/src/utils/formatters/migration-graph-rows.ts +336 -0
  164. package/src/utils/formatters/migration-graph-tree-render.ts +459 -0
  165. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  166. package/src/utils/formatters/migration-list-graph-topology.ts +368 -0
  167. package/src/utils/formatters/migration-list-render.ts +191 -0
  168. package/src/utils/formatters/migration-list-styler.ts +63 -0
  169. package/src/utils/formatters/migration-list-types.ts +21 -0
  170. package/src/utils/formatters/migrations.ts +37 -46
  171. package/src/utils/glyph-mode.ts +22 -0
  172. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  173. package/src/utils/plan-resolution.ts +258 -0
  174. package/src/utils/ref-advancement.ts +68 -0
  175. package/src/utils/terminal-ui.ts +42 -1
  176. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  177. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  178. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  179. package/dist/client-oXO2WCPD.mjs.map +0 -1
  180. package/dist/command-helpers-BSb0tRC8.mjs.map +0 -1
  181. package/dist/commands/migration-check.mjs.map +0 -1
  182. package/dist/commands/migration-graph.mjs.map +0 -1
  183. package/dist/contract-emit-bcrpT-wD.mjs.map +0 -1
  184. package/dist/contract-emit-r4y8Zhf1.mjs.map +0 -1
  185. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  186. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  187. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  188. package/dist/global-flags-CdE7M0d9.d.mts +0 -15
  189. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  190. package/dist/init-BCJZPWE1.mjs.map +0 -1
  191. package/dist/migration-plan-CFwqw3Gk.mjs.map +0 -1
  192. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  193. package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
  194. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  195. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  196. package/dist/types--CqjMdk0.d.mts.map +0 -1
@@ -0,0 +1,336 @@
1
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
3
+ import {
4
+ classifyMigrationGraphTopology,
5
+ type MigrationEdgeKind,
6
+ type MigrationListGraphTopology,
7
+ } from './migration-list-graph-topology';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Public types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * A migration edge with its forward/rollback/self classification resolved.
15
+ * `from` and `to` are contract hashes (EMPTY_CONTRACT_HASH for the baseline).
16
+ */
17
+ export interface ClassifiedEdge {
18
+ readonly migrationHash: string;
19
+ readonly from: string;
20
+ readonly to: string;
21
+ readonly dirName: string;
22
+ readonly kind: MigrationEdgeKind;
23
+ }
24
+
25
+ /**
26
+ * The pure-data output of the row-model stage.
27
+ *
28
+ * `nodes` is the vertical ordering of contract nodes: index 0 is the topmost
29
+ * row (the tip), the last non-null entry is the bottommost root. `null`
30
+ * sentinels separate disjoint components (the blank row in the rendered
31
+ * output). Ordering within each component is deterministic: longest forward-
32
+ * path rank from forward roots (tips at rank max, roots at 0), with lex-
33
+ * ascending tie-break among same-rank siblings.
34
+ *
35
+ * `edges` carries every classified migration. `edgesByFrom` and `edgesByTo`
36
+ * are pre-built lookup maps for the column allocator.
37
+ */
38
+ export interface MigrationGraphRowModel {
39
+ readonly nodes: readonly (string | null)[];
40
+ readonly edges: readonly ClassifiedEdge[];
41
+ readonly edgesByFrom: ReadonlyMap<string, readonly ClassifiedEdge[]>;
42
+ readonly edgesByTo: ReadonlyMap<string, readonly ClassifiedEdge[]>;
43
+ }
44
+
45
+ export interface BuildMigrationGraphRowsOptions {
46
+ readonly contractHash?: string;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Weak connectivity — identify disjoint components
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Return the weakly-connected components of `graph` as an array of node sets,
55
+ * ordered so the component containing EMPTY_CONTRACT_HASH comes first (if
56
+ * present), with remaining components sorted by their lex-smallest node hash.
57
+ */
58
+ function weaklyConnectedComponents(graph: MigrationGraph): readonly ReadonlySet<string>[] {
59
+ const visited = new Set<string>();
60
+ const adjacency = new Map<string, string[]>();
61
+
62
+ function addAdjacent(a: string, b: string): void {
63
+ const aList = adjacency.get(a);
64
+ if (aList) aList.push(b);
65
+ else adjacency.set(a, [b]);
66
+ const bList = adjacency.get(b);
67
+ if (bList) bList.push(a);
68
+ else adjacency.set(b, [a]);
69
+ }
70
+
71
+ for (const edges of graph.forwardChain.values()) {
72
+ for (const edge of edges) {
73
+ if (edge.from !== edge.to) {
74
+ addAdjacent(edge.from, edge.to);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Ensure all nodes (including isolated self-loops) are reachable
80
+ for (const node of graph.nodes) {
81
+ if (!adjacency.has(node)) {
82
+ adjacency.set(node, []);
83
+ }
84
+ }
85
+
86
+ const components: Set<string>[] = [];
87
+
88
+ function bfsComponent(start: string): Set<string> {
89
+ const component = new Set<string>();
90
+ const queue = [start];
91
+ while (queue.length > 0) {
92
+ const node = queue.shift();
93
+ if (node === undefined || visited.has(node)) continue;
94
+ visited.add(node);
95
+ component.add(node);
96
+ for (const neighbor of adjacency.get(node) ?? []) {
97
+ if (!visited.has(neighbor)) {
98
+ queue.push(neighbor);
99
+ }
100
+ }
101
+ }
102
+ return component;
103
+ }
104
+
105
+ // Deterministic: visit nodes in a fixed order (EMPTY first, then lex)
106
+ const allNodes = [...graph.nodes].sort((a, b) => {
107
+ if (a === EMPTY_CONTRACT_HASH) return -1;
108
+ if (b === EMPTY_CONTRACT_HASH) return 1;
109
+ return a.localeCompare(b);
110
+ });
111
+
112
+ for (const node of allNodes) {
113
+ if (!visited.has(node)) {
114
+ components.push(bfsComponent(node));
115
+ }
116
+ }
117
+
118
+ // Order: EMPTY component first, others by lex-smallest node hash
119
+ components.sort((a, b) => {
120
+ const aHasEmpty = a.has(EMPTY_CONTRACT_HASH);
121
+ const bHasEmpty = b.has(EMPTY_CONTRACT_HASH);
122
+ if (aHasEmpty && !bHasEmpty) return -1;
123
+ if (!aHasEmpty && bHasEmpty) return 1;
124
+ const aMin = [...a].sort((x, y) => x.localeCompare(y))[0] ?? '';
125
+ const bMin = [...b].sort((x, y) => x.localeCompare(y))[0] ?? '';
126
+ return aMin.localeCompare(bMin);
127
+ });
128
+
129
+ return components;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Longest forward-path node ordering within a component
134
+ // ---------------------------------------------------------------------------
135
+
136
+ function forwardRootsInComponent(
137
+ componentNodes: ReadonlySet<string>,
138
+ topology: MigrationListGraphTopology,
139
+ ): readonly string[] {
140
+ const roots: string[] = [];
141
+ for (const node of componentNodes) {
142
+ if ((topology.forwardInDegree.get(node) ?? 0) === 0) {
143
+ roots.push(node);
144
+ }
145
+ }
146
+ roots.sort((a, b) => {
147
+ if (a === EMPTY_CONTRACT_HASH) return -1;
148
+ if (b === EMPTY_CONTRACT_HASH) return 1;
149
+ return a.localeCompare(b);
150
+ });
151
+ if (roots.length > 0) return roots;
152
+
153
+ return [...componentNodes].sort((a, b) => {
154
+ if (a === EMPTY_CONTRACT_HASH) return -1;
155
+ if (b === EMPTY_CONTRACT_HASH) return 1;
156
+ return a.localeCompare(b);
157
+ });
158
+ }
159
+
160
+ function compareNodesTipsFirst(a: string, b: string, rank: ReadonlyMap<string, number>): number {
161
+ const rankA = rank.get(a) ?? 0;
162
+ const rankB = rank.get(b) ?? 0;
163
+ if (rankA !== rankB) return rankB - rankA;
164
+ if (a === EMPTY_CONTRACT_HASH) return 1;
165
+ if (b === EMPTY_CONTRACT_HASH) return -1;
166
+ return a.localeCompare(b);
167
+ }
168
+
169
+ /**
170
+ * Layer nodes by longest forward-path rank from forward roots within the
171
+ * component. Rank 0 is the root (bottom row); the maximum rank is the tip
172
+ * (top row). Emits rank-descending with lex-ascending tie-break among siblings
173
+ * at the same rank — stable across edge-insertion order and correct under
174
+ * diamonds, cross-links, and rollbacks.
175
+ */
176
+ function layerNodesByLongestForwardPath(
177
+ componentNodes: ReadonlySet<string>,
178
+ topology: MigrationListGraphTopology,
179
+ graph: MigrationGraph,
180
+ ): readonly string[] {
181
+ const forwardOut = new Map<string, string[]>();
182
+
183
+ for (const node of componentNodes) {
184
+ forwardOut.set(node, []);
185
+ }
186
+
187
+ for (const edges of graph.forwardChain.values()) {
188
+ for (const edge of edges) {
189
+ if (!componentNodes.has(edge.from) || !componentNodes.has(edge.to)) continue;
190
+ if (edge.from === edge.to) continue;
191
+ if (topology.kindByMigrationHash.get(edge.migrationHash) !== 'forward') continue;
192
+ const bucket = forwardOut.get(edge.from);
193
+ if (bucket) bucket.push(edge.to);
194
+ }
195
+ }
196
+
197
+ const roots = forwardRootsInComponent(componentNodes, topology);
198
+ const rank = new Map<string, number>();
199
+ for (const root of roots) {
200
+ rank.set(root, 0);
201
+ }
202
+
203
+ const maxPasses = componentNodes.size;
204
+ for (let pass = 0; pass < maxPasses; pass++) {
205
+ let changed = false;
206
+ for (const node of componentNodes) {
207
+ const base = rank.get(node);
208
+ if (base === undefined) continue;
209
+ for (const to of forwardOut.get(node) ?? []) {
210
+ const next = base + 1;
211
+ const prev = rank.get(to) ?? -1;
212
+ if (next > prev) {
213
+ rank.set(to, next);
214
+ changed = true;
215
+ }
216
+ }
217
+ }
218
+ if (!changed) break;
219
+ }
220
+
221
+ for (const node of componentNodes) {
222
+ if (!rank.has(node)) {
223
+ rank.set(node, 0);
224
+ }
225
+ }
226
+
227
+ return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Public builder
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /**
235
+ * Build the row model from a tolerant `MigrationGraph`.
236
+ *
237
+ * The row model is the first pure-data stage of the `migration graph` render
238
+ * pipeline. It:
239
+ * - classifies every edge as `forward`, `rollback`, or `self`;
240
+ * - produces a deterministic vertical node ordering (tips at index 0, roots
241
+ * at the end) within each weakly-connected component;
242
+ * - separates disjoint components with `null` sentinels;
243
+ * - optionally prepends a detached current contract as its own single-node
244
+ * component when `contractHash` is not already in the graph.
245
+ *
246
+ * No columns, no lane allocation, no glyphs, no rendering.
247
+ */
248
+ /**
249
+ * Resolve the detached current contract, if any: a real contract (not the
250
+ * empty baseline) that no migration on disk produces, so it is absent from
251
+ * the graph. Such a contract renders as a floating node rather than
252
+ * decorating an existing one. Returns the hash when detached, else undefined.
253
+ */
254
+ function detachedContractHash(
255
+ graph: MigrationGraph,
256
+ contractHash: string | undefined,
257
+ ): string | undefined {
258
+ return contractHash !== undefined &&
259
+ contractHash !== EMPTY_CONTRACT_HASH &&
260
+ !graph.nodes.has(contractHash)
261
+ ? contractHash
262
+ : undefined;
263
+ }
264
+
265
+ export function buildMigrationGraphRows(
266
+ graph: MigrationGraph,
267
+ options: BuildMigrationGraphRowsOptions = {},
268
+ ): MigrationGraphRowModel {
269
+ const emptyModel: MigrationGraphRowModel = {
270
+ nodes: [],
271
+ edges: [],
272
+ edgesByFrom: new Map(),
273
+ edgesByTo: new Map(),
274
+ };
275
+
276
+ if (graph.nodes.size === 0) {
277
+ const detached = detachedContractHash(graph, options.contractHash);
278
+ return detached !== undefined ? { ...emptyModel, nodes: [detached] } : emptyModel;
279
+ }
280
+
281
+ // 1. Classify all edges (shared classifier: DFS plus a peel pass that demotes
282
+ // node-skipping rollbacks, so the forward subgraph is acyclic)
283
+ const topology = classifyMigrationGraphTopology(graph);
284
+
285
+ // 2. Build classified edge list
286
+ const edges: ClassifiedEdge[] = [];
287
+ const edgesByFrom = new Map<string, ClassifiedEdge[]>();
288
+ const edgesByTo = new Map<string, ClassifiedEdge[]>();
289
+
290
+ for (const edgeList of graph.forwardChain.values()) {
291
+ for (const edge of edgeList) {
292
+ const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? 'forward';
293
+ const classified: ClassifiedEdge = {
294
+ migrationHash: edge.migrationHash,
295
+ from: edge.from,
296
+ to: edge.to,
297
+ dirName: edge.dirName,
298
+ kind,
299
+ };
300
+ edges.push(classified);
301
+
302
+ const fromBucket = edgesByFrom.get(edge.from);
303
+ if (fromBucket) fromBucket.push(classified);
304
+ else edgesByFrom.set(edge.from, [classified]);
305
+
306
+ const toBucket = edgesByTo.get(edge.to);
307
+ if (toBucket) toBucket.push(classified);
308
+ else edgesByTo.set(edge.to, [classified]);
309
+ }
310
+ }
311
+
312
+ // 3. Find weakly-connected components (ordered: EMPTY first, then lex)
313
+ const components = weaklyConnectedComponents(graph);
314
+
315
+ // 4. Layer nodes by longest forward path per component, separate with null
316
+ const nodes: (string | null)[] = [];
317
+ for (let i = 0; i < components.length; i++) {
318
+ if (i > 0) nodes.push(null);
319
+ const component = components[i];
320
+ if (component === undefined) continue;
321
+ const ordered = layerNodesByLongestForwardPath(component, topology, graph);
322
+ for (const node of ordered) {
323
+ nodes.push(node);
324
+ }
325
+ }
326
+
327
+ const detached = detachedContractHash(graph, options.contractHash);
328
+ if (detached !== undefined) {
329
+ if (nodes.length > 0) {
330
+ nodes.unshift(null);
331
+ }
332
+ nodes.unshift(detached);
333
+ }
334
+
335
+ return { nodes, edges, edgesByFrom, edgesByTo };
336
+ }