@prisma-next/cli 0.12.0-dev.6 → 0.12.0-dev.60
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/README.md +2 -2
- package/dist/cli.mjs +180 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-DQvxtihf.mjs} +194 -105
- package/dist/client-DQvxtihf.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-CxHSiwEg.mjs} +318 -25
- package/dist/command-helpers-CxHSiwEg.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +4 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +6 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +10 -7
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +37 -3
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +291 -12
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +55 -13
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +3 -2
- package/dist/commands/migration-graph.d.mts +17 -8
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +185 -2
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +26 -27
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +2 -190
- package/dist/commands/migration-log.d.mts +9 -19
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +4 -4
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +17 -21
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +24 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +42 -144
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +4 -4
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
- package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-gBvfOS8r.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-gBvfOS8r.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-By59Nmmn.mjs} +5 -5
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-By59Nmmn.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-OpMbysHj.mjs} +7 -7
- package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-OpMbysHj.mjs.map} +1 -1
- package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
- package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BkyyYGDf.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-BkyyYGDf.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-Bup14UkI.mjs} +63 -5
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-Bup14UkI.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-DbmfgeYc.mjs} +6 -6
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-DbmfgeYc.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +2 -2
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +1 -3
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-CmBpbvzV.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-CmBpbvzV.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-tidQpK21.mjs} +5 -58
- package/dist/init-tidQpK21.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-CUvD_9uF.mjs} +5 -5
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-CUvD_9uF.mjs.map} +1 -1
- package/dist/migration-check-BjNlXTGF.mjs +572 -0
- package/dist/migration-check-BjNlXTGF.mjs.map +1 -0
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DWlpBp98.mjs} +5 -5
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DWlpBp98.mjs.map} +1 -1
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs +2370 -0
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs.map +1 -0
- package/dist/migration-list-zP59uUBC.mjs +230 -0
- package/dist/migration-list-zP59uUBC.mjs.map +1 -0
- package/dist/migration-log-DoytJNuF.mjs +222 -0
- package/dist/migration-log-DoytJNuF.mjs.map +1 -0
- package/dist/migration-path-target-DjbhWi_5.mjs +38 -0
- package/dist/migration-path-target-DjbhWi_5.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CgCXpjYD.mjs} +6 -7
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CgCXpjYD.mjs.map} +1 -1
- package/dist/migration-status-ByptVtRZ.mjs +446 -0
- package/dist/migration-status-ByptVtRZ.mjs.map +1 -0
- package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
- package/dist/schemas-CeGMYFYX.d.mts +191 -0
- package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
- package/dist/schemas-KhXMzNA_.mjs +112 -0
- package/dist/schemas-KhXMzNA_.mjs.map +1 -0
- package/dist/telemetry-DVv3V0gj.mjs +122 -0
- package/dist/telemetry-DVv3V0gj.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
- package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
- package/dist/{types-Dt_SfqFm.d.mts → types-BepB6ydp.d.mts} +44 -31
- package/dist/types-BepB6ydp.d.mts.map +1 -0
- package/dist/{verify-DCA9Sldu.mjs → verify-CJpG9m7-.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-CJpG9m7-.mjs.map} +1 -1
- package/package.json +25 -22
- package/src/cli.ts +5 -0
- package/src/commands/db-update.ts +7 -1
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/json/schemas.ts +195 -0
- package/src/commands/migrate.ts +518 -8
- package/src/commands/migration-check.ts +469 -134
- package/src/commands/migration-graph.ts +164 -91
- package/src/commands/migration-list.ts +72 -39
- package/src/commands/migration-log.ts +52 -102
- package/src/commands/migration-show.ts +31 -66
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +457 -1067
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +20 -9
- package/src/control-api/operations/contract-emit.ts +2 -2
- package/src/control-api/operations/db-init.ts +3 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +51 -13
- package/src/control-api/operations/db-update.ts +4 -4
- package/src/control-api/operations/db-verify.ts +15 -5
- package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
- package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
- package/src/control-api/types.ts +46 -29
- package/src/utils/cli-errors.ts +70 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
- package/src/utils/formatters/migration-graph-layout.ts +227 -38
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +148 -0
- package/src/utils/formatters/migration-graph-tree-render.ts +959 -81
- package/src/utils/formatters/migration-list-data-column.ts +4 -91
- package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
- package/src/utils/formatters/migration-list-render.ts +135 -71
- package/src/utils/formatters/migration-list-styler.ts +46 -5
- package/src/utils/formatters/migration-list-types.ts +5 -21
- package/src/utils/formatters/migration-log-table.ts +205 -0
- package/src/utils/formatters/migrations.ts +33 -11
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/integrity-violation-to-check-failure.ts +28 -19
- package/src/utils/legend.ts +38 -0
- package/src/utils/migration-path-target.ts +60 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
- package/dist/commands/migration-list.mjs.map +0 -1
- package/dist/commands/migration-log.mjs.map +0 -1
- package/dist/commands/migration-status.mjs.map +0 -1
- package/dist/extension-pack-inputs-IDvjRCi3.mjs +0 -62
- package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +0 -1
- package/dist/graph-render-rFAqZujX.mjs +0 -1081
- package/dist/graph-render-rFAqZujX.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/migration-check-BiBJoYYW.mjs +0 -341
- package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs +0 -1232
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
- package/dist/migration-types-D2FW63pr.d.mts +0 -15
- package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
- package/dist/migrations-Cv2jxNNK.mjs +0 -228
- package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
- package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
- package/dist/types-Dt_SfqFm.d.mts.map +0 -1
- package/src/utils/formatters/graph-migration-mapper.ts +0 -235
- package/src/utils/formatters/graph-render.ts +0 -1323
- package/src/utils/formatters/graph-types.ts +0 -120
|
@@ -12,17 +12,24 @@ export type StructuralCell =
|
|
|
12
12
|
readonly arcTee?: boolean;
|
|
13
13
|
readonly arcLand?: boolean;
|
|
14
14
|
}
|
|
15
|
-
| { readonly kind: 'vertical-pass' }
|
|
16
|
-
| { readonly kind: 'horizontal-pass' }
|
|
17
|
-
| { readonly kind: 'branch-tee' }
|
|
18
|
-
| { readonly kind: 'branch-corner' }
|
|
19
|
-
| { readonly kind: 'merge-tee' }
|
|
20
|
-
| { readonly kind: 'merge-corner' }
|
|
21
|
-
| { readonly kind: 'arc-branch-corner' }
|
|
22
|
-
| { readonly kind: 'arc-branch-tee' }
|
|
23
|
-
| { readonly kind: 'arc-land-corner' }
|
|
24
|
-
| { readonly kind: 'arc-
|
|
25
|
-
| {
|
|
15
|
+
| { readonly kind: 'vertical-pass'; readonly migrationHash?: string }
|
|
16
|
+
| { readonly kind: 'horizontal-pass'; readonly migrationHash?: string }
|
|
17
|
+
| { readonly kind: 'branch-tee'; readonly migrationHash?: string }
|
|
18
|
+
| { readonly kind: 'branch-corner'; readonly migrationHash?: string }
|
|
19
|
+
| { readonly kind: 'merge-tee'; readonly migrationHash?: string }
|
|
20
|
+
| { readonly kind: 'merge-corner'; readonly migrationHash?: string }
|
|
21
|
+
| { readonly kind: 'arc-branch-corner'; readonly migrationHash?: string }
|
|
22
|
+
| { readonly kind: 'arc-branch-tee'; readonly migrationHash?: string }
|
|
23
|
+
| { readonly kind: 'arc-land-corner'; readonly migrationHash?: string }
|
|
24
|
+
| { readonly kind: 'arc-land-tee'; readonly migrationHash?: string }
|
|
25
|
+
| {
|
|
26
|
+
readonly kind: 'arc-crossing';
|
|
27
|
+
/** Hash of the edge whose vertical lane passes through this cell. */
|
|
28
|
+
readonly migrationHash?: string;
|
|
29
|
+
/** Hash of the arc edge that crosses over the vertical lane. */
|
|
30
|
+
readonly arcMigrationHash?: string;
|
|
31
|
+
}
|
|
32
|
+
| { readonly kind: 'arc-land-bridge'; readonly migrationHash?: string }
|
|
26
33
|
| {
|
|
27
34
|
readonly kind: 'edge-lane';
|
|
28
35
|
readonly migrationHash: string;
|
|
@@ -336,6 +343,15 @@ function refineAdjacency(
|
|
|
336
343
|
row.convergenceProducer ?? false,
|
|
337
344
|
divergenceBranchEdge,
|
|
338
345
|
);
|
|
346
|
+
// Reconstruct lane owners from the existing cells so the refined row
|
|
347
|
+
// preserves per-cell identity on its pass-through vertical-pass cells.
|
|
348
|
+
const existingLaneEdge = new Map<number, string>();
|
|
349
|
+
for (const lane of row.passThroughLanes ?? []) {
|
|
350
|
+
const cell = row.cells[lane];
|
|
351
|
+
if (cell !== undefined && 'migrationHash' in cell && cell.migrationHash !== undefined) {
|
|
352
|
+
existingLaneEdge.set(lane, cell.migrationHash);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
339
355
|
return {
|
|
340
356
|
...row,
|
|
341
357
|
cells: buildEdgeCells(
|
|
@@ -344,6 +360,7 @@ function refineAdjacency(
|
|
|
344
360
|
row.passThroughLanes ?? [],
|
|
345
361
|
adjacency,
|
|
346
362
|
row.cells.length,
|
|
363
|
+
existingLaneEdge,
|
|
347
364
|
),
|
|
348
365
|
};
|
|
349
366
|
});
|
|
@@ -376,24 +393,51 @@ function emptyCells(width: number): StructuralCell[] {
|
|
|
376
393
|
return Array.from({ length: width }, () => ({ kind: 'empty' as const }));
|
|
377
394
|
}
|
|
378
395
|
|
|
396
|
+
/** Returns `{ migrationHash: hash }` when hash is defined, otherwise `{}`. */
|
|
397
|
+
function hashProp(hash: string | undefined): { readonly migrationHash: string } | object {
|
|
398
|
+
return hash !== undefined ? { migrationHash: hash } : {};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Returns `{ arcMigrationHash: hash }` when hash is defined, otherwise `{}`. */
|
|
402
|
+
function arcHashProp(hash: string | undefined): { readonly arcMigrationHash: string } | object {
|
|
403
|
+
return hash !== undefined ? { arcMigrationHash: hash } : {};
|
|
404
|
+
}
|
|
405
|
+
|
|
379
406
|
function buildBranchConnectorCells(
|
|
380
407
|
startLane: number,
|
|
381
408
|
endLane: number,
|
|
409
|
+
fanTargetLanes: ReadonlySet<number>,
|
|
382
410
|
activeLanes: ReadonlySet<number>,
|
|
383
411
|
gridWidth: number,
|
|
412
|
+
/** Hash of the edge whose lane is at startLane (the source/trunk edge). */
|
|
413
|
+
trunkEdgeHash: string | undefined,
|
|
414
|
+
/** Hash of the fan edge for each fan-target lane. */
|
|
415
|
+
fanEdgeHashByLane: ReadonlyMap<number, string>,
|
|
416
|
+
/** Hash of the edge occupying each active pass-through lane. */
|
|
417
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
384
418
|
): StructuralCell[] {
|
|
385
419
|
const cells = emptyCells(gridWidth);
|
|
386
420
|
for (let lane = 0; lane < gridWidth; lane++) {
|
|
387
421
|
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
388
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
422
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
389
423
|
continue;
|
|
390
424
|
}
|
|
391
425
|
if (lane === startLane) {
|
|
392
|
-
cells[lane] = { kind: 'branch-tee' };
|
|
426
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(trunkEdgeHash) };
|
|
393
427
|
} else if (lane === endLane) {
|
|
394
|
-
cells[lane] = { kind: 'branch-corner' };
|
|
428
|
+
cells[lane] = { kind: 'branch-corner', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
395
429
|
} else if (lane > startLane && lane < endLane) {
|
|
396
|
-
|
|
430
|
+
if (fanTargetLanes.has(lane)) {
|
|
431
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
432
|
+
} else if (activeLanes.has(lane)) {
|
|
433
|
+
cells[lane] = {
|
|
434
|
+
kind: 'arc-crossing',
|
|
435
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
436
|
+
...arcHashProp(fanEdgeHashByLane.get(endLane)),
|
|
437
|
+
};
|
|
438
|
+
} else {
|
|
439
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
440
|
+
}
|
|
397
441
|
}
|
|
398
442
|
}
|
|
399
443
|
return cells;
|
|
@@ -402,21 +446,34 @@ function buildBranchConnectorCells(
|
|
|
402
446
|
function buildMergeConnectorCells(
|
|
403
447
|
startLane: number,
|
|
404
448
|
endLane: number,
|
|
449
|
+
fanTargetLanes: ReadonlySet<number>,
|
|
405
450
|
activeLanes: ReadonlySet<number>,
|
|
406
451
|
gridWidth: number,
|
|
452
|
+
/** Hash of the edge occupying each active lane (fan lanes + pass-throughs). */
|
|
453
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
407
454
|
): StructuralCell[] {
|
|
408
455
|
const cells = emptyCells(gridWidth);
|
|
409
456
|
for (let lane = 0; lane < gridWidth; lane++) {
|
|
410
457
|
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
411
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
458
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
412
459
|
continue;
|
|
413
460
|
}
|
|
414
461
|
if (lane === startLane) {
|
|
415
|
-
cells[lane] = { kind: 'merge-tee' };
|
|
462
|
+
cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
416
463
|
} else if (lane === endLane) {
|
|
417
|
-
cells[lane] = { kind: 'merge-corner' };
|
|
464
|
+
cells[lane] = { kind: 'merge-corner', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
418
465
|
} else if (lane > startLane && lane < endLane) {
|
|
419
|
-
|
|
466
|
+
if (fanTargetLanes.has(lane)) {
|
|
467
|
+
cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
468
|
+
} else if (activeLanes.has(lane)) {
|
|
469
|
+
cells[lane] = {
|
|
470
|
+
kind: 'arc-crossing',
|
|
471
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
472
|
+
...arcHashProp(laneEdgeByIndex.get(endLane)),
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
cells[lane] = { kind: 'horizontal-pass', ...hashProp(laneEdgeByIndex.get(startLane)) };
|
|
476
|
+
}
|
|
420
477
|
}
|
|
421
478
|
}
|
|
422
479
|
return cells;
|
|
@@ -427,11 +484,13 @@ function buildNodeCells(
|
|
|
427
484
|
nodeColumn: number,
|
|
428
485
|
activeLanes: readonly number[],
|
|
429
486
|
gridWidth: number,
|
|
487
|
+
/** Hash of the edge occupying each active pass-through lane. */
|
|
488
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
430
489
|
): StructuralCell[] {
|
|
431
490
|
const cells = emptyCells(gridWidth);
|
|
432
491
|
for (const lane of activeLanes) {
|
|
433
492
|
if (lane !== nodeColumn && lane < gridWidth) {
|
|
434
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
493
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
435
494
|
}
|
|
436
495
|
}
|
|
437
496
|
if (nodeColumn < gridWidth) {
|
|
@@ -446,10 +505,14 @@ function buildEdgeCells(
|
|
|
446
505
|
passThroughLanes: readonly number[],
|
|
447
506
|
adjacency: EdgeAdjacency,
|
|
448
507
|
gridWidth: number,
|
|
508
|
+
/** Hash of the edge occupying each active pass-through lane. */
|
|
509
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
449
510
|
): StructuralCell[] {
|
|
450
511
|
const cells = emptyCells(gridWidth);
|
|
451
512
|
for (const lane of passThroughLanes) {
|
|
452
|
-
if (lane < gridWidth)
|
|
513
|
+
if (lane < gridWidth) {
|
|
514
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
515
|
+
}
|
|
453
516
|
}
|
|
454
517
|
if (laneIndex < gridWidth) {
|
|
455
518
|
cells[laneIndex] = {
|
|
@@ -689,6 +752,17 @@ function applySkipRollbackRouting(
|
|
|
689
752
|
.map((other) => other.backLane);
|
|
690
753
|
const maxCoSourcedLane = Math.max(...coSourcedLanes);
|
|
691
754
|
|
|
755
|
+
// Back-lanes of arcs that converge on this same target node. They share the
|
|
756
|
+
// node's landing row, so each inner lane reads as a `┴` junction (the outer
|
|
757
|
+
// arcs' horizontal bridge passes through it on the way to the node) and only
|
|
758
|
+
// the outermost closes the corner with `╯`.
|
|
759
|
+
const coLandingLanes = routes
|
|
760
|
+
.filter((other) => other.edge.to === edge.to)
|
|
761
|
+
.map((other) => other.backLane);
|
|
762
|
+
const maxCoLandingLane = Math.max(...coLandingLanes);
|
|
763
|
+
|
|
764
|
+
const { migrationHash: arcHash } = edge;
|
|
765
|
+
|
|
692
766
|
const sourceRow = result[sourceRowIndex];
|
|
693
767
|
if (sourceRow !== undefined) {
|
|
694
768
|
const cells = sourceRow.cells;
|
|
@@ -697,7 +771,12 @@ function applySkipRollbackRouting(
|
|
|
697
771
|
cells[nodeCol] = { kind: 'node', contractHash, arcTee: true };
|
|
698
772
|
for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
|
|
699
773
|
if (coSourcedLanes.includes(lane)) {
|
|
700
|
-
|
|
774
|
+
// A co-sourced arc tees off at this lane; tag it with that arc's hash.
|
|
775
|
+
const coSourcedArc = routes.find((r) => r.backLane === lane && r.edge.from === edge.from);
|
|
776
|
+
cells[lane] = {
|
|
777
|
+
kind: 'arc-branch-tee',
|
|
778
|
+
...hashProp(coSourcedArc?.edge.migrationHash),
|
|
779
|
+
};
|
|
701
780
|
continue;
|
|
702
781
|
}
|
|
703
782
|
const existing = cells[lane];
|
|
@@ -710,14 +789,30 @@ function applySkipRollbackRouting(
|
|
|
710
789
|
occupied ||
|
|
711
790
|
routes.some(
|
|
712
791
|
(other) =>
|
|
713
|
-
other.edge.migrationHash !==
|
|
792
|
+
other.edge.migrationHash !== arcHash &&
|
|
714
793
|
other.backLane === lane &&
|
|
715
794
|
routeCrossesRow(other, sourceRowIndex, result),
|
|
716
795
|
);
|
|
717
|
-
|
|
796
|
+
if (crossed) {
|
|
797
|
+
// The vertical lane was already occupied; tag the crossing with the
|
|
798
|
+
// existing vertical owner's hash and the arc that crosses over it.
|
|
799
|
+
const verticalHash =
|
|
800
|
+
existing !== undefined && 'migrationHash' in existing
|
|
801
|
+
? existing.migrationHash
|
|
802
|
+
: undefined;
|
|
803
|
+
cells[lane] = {
|
|
804
|
+
kind: 'arc-crossing',
|
|
805
|
+
...hashProp(verticalHash),
|
|
806
|
+
arcMigrationHash: arcHash,
|
|
807
|
+
};
|
|
808
|
+
} else {
|
|
809
|
+
cells[lane] = { kind: 'horizontal-pass', migrationHash: arcHash };
|
|
810
|
+
}
|
|
718
811
|
}
|
|
719
812
|
cells[backLane] =
|
|
720
|
-
backLane < maxCoSourcedLane
|
|
813
|
+
backLane < maxCoSourcedLane
|
|
814
|
+
? { kind: 'arc-branch-tee', migrationHash: arcHash }
|
|
815
|
+
: { kind: 'arc-branch-corner', migrationHash: arcHash };
|
|
721
816
|
}
|
|
722
817
|
|
|
723
818
|
const edgeRow = result[edgeRowIndex];
|
|
@@ -726,10 +821,17 @@ function applySkipRollbackRouting(
|
|
|
726
821
|
// lane may already cross this row, and rebuilding would clobber it.
|
|
727
822
|
const cells = edgeRow.cells;
|
|
728
823
|
ensureCellWidth(cells, backLane + 1);
|
|
729
|
-
|
|
824
|
+
// The forward lane at nodeCol is now interrupted by this rollback; tag the
|
|
825
|
+
// vertical-pass with the edge that owns that forward lane.
|
|
826
|
+
const forwardLaneCell = cells[nodeCol];
|
|
827
|
+
const forwardLaneHash =
|
|
828
|
+
forwardLaneCell !== undefined && 'migrationHash' in forwardLaneCell
|
|
829
|
+
? forwardLaneCell.migrationHash
|
|
830
|
+
: undefined;
|
|
831
|
+
cells[nodeCol] = { kind: 'vertical-pass', ...hashProp(forwardLaneHash) };
|
|
730
832
|
cells[backLane] = {
|
|
731
833
|
kind: 'edge-lane',
|
|
732
|
-
migrationHash:
|
|
834
|
+
migrationHash: arcHash,
|
|
733
835
|
edgeKind: edge.kind,
|
|
734
836
|
ownsLabel: true,
|
|
735
837
|
adjacency: 'node-skipping-rollback',
|
|
@@ -750,12 +852,13 @@ function applySkipRollbackRouting(
|
|
|
750
852
|
const existing = cells[backLane];
|
|
751
853
|
if (
|
|
752
854
|
existing?.kind !== 'arc-land-corner' &&
|
|
855
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
753
856
|
existing?.kind !== 'arc-land-bridge' &&
|
|
754
857
|
existing?.kind !== 'arc-branch-corner' &&
|
|
755
858
|
existing?.kind !== 'arc-branch-tee' &&
|
|
756
859
|
existing?.kind !== 'arc-crossing'
|
|
757
860
|
) {
|
|
758
|
-
cells[backLane] = { kind: 'vertical-pass' };
|
|
861
|
+
cells[backLane] = { kind: 'vertical-pass', migrationHash: arcHash };
|
|
759
862
|
}
|
|
760
863
|
}
|
|
761
864
|
|
|
@@ -766,6 +869,14 @@ function applySkipRollbackRouting(
|
|
|
766
869
|
const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
767
870
|
cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
|
|
768
871
|
for (let lane = targetCol + 1; lane < backLane; lane += 1) {
|
|
872
|
+
// An inner converging arc's own landing junction: the outer arcs' bridge
|
|
873
|
+
// passes through it (`┴`) while its own vertical run closes here.
|
|
874
|
+
if (coLandingLanes.includes(lane)) {
|
|
875
|
+
// Tag the landing tee with the inner arc that closes here.
|
|
876
|
+
const innerArc = routes.find((r) => r.backLane === lane && r.edge.to === edge.to);
|
|
877
|
+
cells[lane] = { kind: 'arc-land-tee', ...hashProp(innerArc?.edge.migrationHash) };
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
769
880
|
// A bridged lane that carries another arc OR a forward vertical still
|
|
770
881
|
// active at this row must cross over it (`┼`) rather than overwrite it
|
|
771
882
|
// with a bare bridge (`──`).
|
|
@@ -774,18 +885,36 @@ function applySkipRollbackRouting(
|
|
|
774
885
|
existing !== undefined &&
|
|
775
886
|
existing.kind !== 'empty' &&
|
|
776
887
|
existing.kind !== 'horizontal-pass' &&
|
|
777
|
-
existing.kind !== 'arc-land-bridge'
|
|
888
|
+
existing.kind !== 'arc-land-bridge' &&
|
|
889
|
+
existing.kind !== 'arc-land-tee';
|
|
778
890
|
const crossed =
|
|
779
891
|
occupied ||
|
|
780
892
|
routes.some(
|
|
781
893
|
(other) =>
|
|
782
|
-
other.edge.migrationHash !==
|
|
894
|
+
other.edge.migrationHash !== arcHash &&
|
|
783
895
|
other.backLane === lane &&
|
|
784
896
|
routeCrossesRow(other, targetRowIndex, result),
|
|
785
897
|
);
|
|
786
|
-
|
|
898
|
+
if (crossed) {
|
|
899
|
+
const verticalHash =
|
|
900
|
+
existing !== undefined && 'migrationHash' in existing
|
|
901
|
+
? existing.migrationHash
|
|
902
|
+
: undefined;
|
|
903
|
+
cells[lane] = {
|
|
904
|
+
kind: 'arc-crossing',
|
|
905
|
+
...hashProp(verticalHash),
|
|
906
|
+
arcMigrationHash: arcHash,
|
|
907
|
+
};
|
|
908
|
+
} else {
|
|
909
|
+
cells[lane] = { kind: 'arc-land-bridge', migrationHash: arcHash };
|
|
910
|
+
}
|
|
787
911
|
}
|
|
788
|
-
|
|
912
|
+
// Inner converging arcs close as a landing tee so the outermost arc's
|
|
913
|
+
// bridge reads through to the node; only the outermost arc draws `╯`.
|
|
914
|
+
cells[backLane] =
|
|
915
|
+
backLane < maxCoLandingLane
|
|
916
|
+
? { kind: 'arc-land-tee', migrationHash: arcHash }
|
|
917
|
+
: { kind: 'arc-land-corner', migrationHash: arcHash };
|
|
789
918
|
for (const other of routes) {
|
|
790
919
|
if (other.backLane <= backLane) continue;
|
|
791
920
|
if (!routeCrossesRow(other, targetRowIndex, result)) continue;
|
|
@@ -793,10 +922,16 @@ function applySkipRollbackRouting(
|
|
|
793
922
|
const existing = cells[other.backLane];
|
|
794
923
|
if (
|
|
795
924
|
existing?.kind !== 'arc-land-corner' &&
|
|
925
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
796
926
|
existing?.kind !== 'arc-land-bridge' &&
|
|
797
927
|
existing?.kind !== 'node'
|
|
798
928
|
) {
|
|
799
|
-
|
|
929
|
+
// This is a pass-through from another arc still in flight; tag with
|
|
930
|
+
// that arc's hash.
|
|
931
|
+
cells[other.backLane] = {
|
|
932
|
+
kind: 'vertical-pass',
|
|
933
|
+
migrationHash: other.edge.migrationHash,
|
|
934
|
+
};
|
|
800
935
|
}
|
|
801
936
|
}
|
|
802
937
|
}
|
|
@@ -878,6 +1013,9 @@ function layoutComponent(
|
|
|
878
1013
|
const nodeColumn = new Map<string, number>();
|
|
879
1014
|
const edgeColumn = new Map<string, number>();
|
|
880
1015
|
const producerLaneByHash = new Map<string, number>();
|
|
1016
|
+
// Tracks which edge's migrationHash last occupied each lane, so pass-through
|
|
1017
|
+
// cells on node/edge/connector rows can carry per-cell identity.
|
|
1018
|
+
const laneEdgeByIndex = new Map<number, string>();
|
|
881
1019
|
let gridWidth = 1;
|
|
882
1020
|
|
|
883
1021
|
function ensureGridWidth(minWidth: number): void {
|
|
@@ -922,13 +1060,21 @@ function layoutComponent(
|
|
|
922
1060
|
const endLane = Math.max(...laneIndices);
|
|
923
1061
|
ensureGridWidth(endLane + 1);
|
|
924
1062
|
const activeLanes = new Set(activeLaneIndices());
|
|
1063
|
+
const fanTargetLanes = new Set(laneIndices);
|
|
925
1064
|
rows.push({
|
|
926
1065
|
kind: 'merge-connector',
|
|
927
1066
|
contractHash,
|
|
928
1067
|
startLane,
|
|
929
1068
|
endLane,
|
|
930
1069
|
branchCount: laneIndices.length,
|
|
931
|
-
cells: buildMergeConnectorCells(
|
|
1070
|
+
cells: buildMergeConnectorCells(
|
|
1071
|
+
startLane,
|
|
1072
|
+
endLane,
|
|
1073
|
+
fanTargetLanes,
|
|
1074
|
+
activeLanes,
|
|
1075
|
+
gridWidth,
|
|
1076
|
+
laneEdgeByIndex,
|
|
1077
|
+
),
|
|
932
1078
|
});
|
|
933
1079
|
for (const index of laneIndices) {
|
|
934
1080
|
if (index !== startLane) setLane(index, null);
|
|
@@ -941,16 +1087,32 @@ function layoutComponent(
|
|
|
941
1087
|
startLane: number,
|
|
942
1088
|
endLane: number,
|
|
943
1089
|
branchCount: number,
|
|
1090
|
+
fanTargetLanes: readonly number[],
|
|
1091
|
+
/** Hash of the first/representative edge for each fan lane (keyed by lane index). */
|
|
1092
|
+
fanEdgeHashByLane: ReadonlyMap<number, string>,
|
|
944
1093
|
): void {
|
|
945
1094
|
ensureGridWidth(endLane + 1);
|
|
946
1095
|
const activeLanes = new Set(activeLaneIndices());
|
|
1096
|
+
// Prefer the fanEdgeHashByLane entry for startLane (the downward fanout edge
|
|
1097
|
+
// leaving this node) over laneEdgeByIndex, which may still hold the hash of
|
|
1098
|
+
// the last skip-rollback emitted into that lane before the branch-connector.
|
|
1099
|
+
const trunkEdgeHash = fanEdgeHashByLane.get(startLane) ?? laneEdgeByIndex.get(startLane);
|
|
947
1100
|
rows.push({
|
|
948
1101
|
kind: 'branch-connector',
|
|
949
1102
|
contractHash,
|
|
950
1103
|
startLane,
|
|
951
1104
|
endLane,
|
|
952
1105
|
branchCount,
|
|
953
|
-
cells: buildBranchConnectorCells(
|
|
1106
|
+
cells: buildBranchConnectorCells(
|
|
1107
|
+
startLane,
|
|
1108
|
+
endLane,
|
|
1109
|
+
new Set(fanTargetLanes),
|
|
1110
|
+
activeLanes,
|
|
1111
|
+
gridWidth,
|
|
1112
|
+
trunkEdgeHash,
|
|
1113
|
+
fanEdgeHashByLane,
|
|
1114
|
+
laneEdgeByIndex,
|
|
1115
|
+
),
|
|
954
1116
|
});
|
|
955
1117
|
}
|
|
956
1118
|
|
|
@@ -963,11 +1125,14 @@ function layoutComponent(
|
|
|
963
1125
|
edge,
|
|
964
1126
|
laneIndex: lane,
|
|
965
1127
|
passThroughLanes: passThrough,
|
|
966
|
-
cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth),
|
|
1128
|
+
cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth, laneEdgeByIndex),
|
|
967
1129
|
};
|
|
968
1130
|
rows.push(convergenceProducer ? { ...row, convergenceProducer: true } : row);
|
|
969
1131
|
edgeColumn.set(edge.migrationHash, lane);
|
|
970
1132
|
if (convergenceProducer) producerLaneByHash.set(edge.migrationHash, lane);
|
|
1133
|
+
// Record this edge as the current occupant of its lane so subsequent rows
|
|
1134
|
+
// can tag their pass-through cells with the correct owner.
|
|
1135
|
+
laneEdgeByIndex.set(lane, edge.migrationHash);
|
|
971
1136
|
}
|
|
972
1137
|
|
|
973
1138
|
function emitNodeRow(contractHash: string, column: number): void {
|
|
@@ -976,7 +1141,7 @@ function layoutComponent(
|
|
|
976
1141
|
rows.push({
|
|
977
1142
|
kind: 'node',
|
|
978
1143
|
contractHash,
|
|
979
|
-
cells: buildNodeCells(contractHash, column, passThrough, gridWidth),
|
|
1144
|
+
cells: buildNodeCells(contractHash, column, passThrough, gridWidth, laneEdgeByIndex),
|
|
980
1145
|
});
|
|
981
1146
|
nodeColumn.set(contractHash, column);
|
|
982
1147
|
}
|
|
@@ -1046,7 +1211,31 @@ function layoutComponent(
|
|
|
1046
1211
|
|
|
1047
1212
|
if (groups.length >= 2) {
|
|
1048
1213
|
const endLane = Math.max(...laneForGroup);
|
|
1049
|
-
|
|
1214
|
+
// Map each fan lane to the representative edge (first in the group) so
|
|
1215
|
+
// the branch-connector cells can carry per-cell identity.
|
|
1216
|
+
const fanEdgeHashByLane = new Map<number, string>();
|
|
1217
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
1218
|
+
const group = groups[groupIndex];
|
|
1219
|
+
const lane = laneForGroup[groupIndex];
|
|
1220
|
+
if (group === undefined || lane === undefined) continue;
|
|
1221
|
+
const firstEdge = group.edges[0];
|
|
1222
|
+
if (firstEdge !== undefined) fanEdgeHashByLane.set(lane, firstEdge.migrationHash);
|
|
1223
|
+
}
|
|
1224
|
+
emitBranchConnector(node, column, endLane, groups.length, laneForGroup, fanEdgeHashByLane);
|
|
1225
|
+
|
|
1226
|
+
// Pre-populate laneEdgeByIndex for every fan lane (including lane 0 / trunk) with the
|
|
1227
|
+
// representative edge hash BEFORE emitting any edge rows. Without this, when groupIndex=0's
|
|
1228
|
+
// edge rows are emitted first, the pass-through cells for groupIndex≥1 lanes carry no hash
|
|
1229
|
+
// (laneEdgeByIndex has no entry yet for those lanes) and fall through to whatever annotation
|
|
1230
|
+
// the row's default override is — often the wrong colour.
|
|
1231
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
1232
|
+
const fanLane = laneForGroup[groupIndex];
|
|
1233
|
+
if (fanLane === undefined) continue;
|
|
1234
|
+
const fanHash = fanEdgeHashByLane.get(fanLane);
|
|
1235
|
+
if (fanHash !== undefined) {
|
|
1236
|
+
laneEdgeByIndex.set(fanLane, fanHash);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1050
1239
|
}
|
|
1051
1240
|
|
|
1052
1241
|
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
@@ -173,10 +173,19 @@ function compareNodesTipsFirst(a: string, b: string, rank: ReadonlyMap<string, n
|
|
|
173
173
|
* at the same rank — stable across edge-insertion order and correct under
|
|
174
174
|
* diamonds, cross-links, and rollbacks.
|
|
175
175
|
*/
|
|
176
|
+
function maxRank(rank: ReadonlyMap<string, number>): number {
|
|
177
|
+
let max = 0;
|
|
178
|
+
for (const value of rank.values()) {
|
|
179
|
+
if (value > max) max = value;
|
|
180
|
+
}
|
|
181
|
+
return max;
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
function layerNodesByLongestForwardPath(
|
|
177
185
|
componentNodes: ReadonlySet<string>,
|
|
178
186
|
topology: MigrationListGraphTopology,
|
|
179
187
|
graph: MigrationGraph,
|
|
188
|
+
contractHash: string | undefined,
|
|
180
189
|
): readonly string[] {
|
|
181
190
|
const forwardOut = new Map<string, string[]>();
|
|
182
191
|
|
|
@@ -224,6 +233,15 @@ function layerNodesByLongestForwardPath(
|
|
|
224
233
|
}
|
|
225
234
|
}
|
|
226
235
|
|
|
236
|
+
if (
|
|
237
|
+
contractHash !== undefined &&
|
|
238
|
+
contractHash !== EMPTY_CONTRACT_HASH &&
|
|
239
|
+
componentNodes.has(contractHash) &&
|
|
240
|
+
(forwardOut.get(contractHash) ?? []).length === 0
|
|
241
|
+
) {
|
|
242
|
+
rank.set(contractHash, maxRank(rank) + 1);
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
|
|
228
246
|
}
|
|
229
247
|
|
|
@@ -262,6 +280,99 @@ function detachedContractHash(
|
|
|
262
280
|
: undefined;
|
|
263
281
|
}
|
|
264
282
|
|
|
283
|
+
function isForwardLeaf(node: string, edges: readonly ClassifiedEdge[]): boolean {
|
|
284
|
+
return !edges.some((e) => e.kind === 'forward' && e.from === node && e.from !== e.to);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function forwardReachableFrom(
|
|
288
|
+
start: string,
|
|
289
|
+
forwardTo: ReadonlyMap<string, readonly string[]>,
|
|
290
|
+
): ReadonlySet<string> {
|
|
291
|
+
const reachable = new Set<string>([start]);
|
|
292
|
+
const queue = [start];
|
|
293
|
+
while (queue.length > 0) {
|
|
294
|
+
const node = queue.shift();
|
|
295
|
+
if (node === undefined) continue;
|
|
296
|
+
for (const next of forwardTo.get(node) ?? []) {
|
|
297
|
+
if (!reachable.has(next)) {
|
|
298
|
+
reachable.add(next);
|
|
299
|
+
queue.push(next);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return reachable;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildForwardToMap(edges: readonly ClassifiedEdge[]): Map<string, string[]> {
|
|
307
|
+
const forwardTo = new Map<string, string[]>();
|
|
308
|
+
for (const edge of edges) {
|
|
309
|
+
if (edge.kind !== 'forward' || edge.from === edge.to) continue;
|
|
310
|
+
const bucket = forwardTo.get(edge.from);
|
|
311
|
+
if (bucket) bucket.push(edge.to);
|
|
312
|
+
else forwardTo.set(edge.from, [edge.to]);
|
|
313
|
+
}
|
|
314
|
+
return forwardTo;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function sortEdgesForContractHashTrunk(
|
|
318
|
+
edges: ClassifiedEdge[],
|
|
319
|
+
contractHash: string | undefined,
|
|
320
|
+
): ClassifiedEdge[] {
|
|
321
|
+
if (
|
|
322
|
+
contractHash === undefined ||
|
|
323
|
+
contractHash === EMPTY_CONTRACT_HASH ||
|
|
324
|
+
!isForwardLeaf(contractHash, edges)
|
|
325
|
+
) {
|
|
326
|
+
return edges;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const preferredLeaf = contractHash;
|
|
330
|
+
const forwardTo = buildForwardToMap(edges);
|
|
331
|
+
const reachability = new Map<string, ReadonlySet<string>>();
|
|
332
|
+
function canReachContractHash(from: string): boolean {
|
|
333
|
+
let cached = reachability.get(from);
|
|
334
|
+
if (cached === undefined) {
|
|
335
|
+
cached = forwardReachableFrom(from, forwardTo);
|
|
336
|
+
reachability.set(from, cached);
|
|
337
|
+
}
|
|
338
|
+
return cached.has(preferredLeaf);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function trunkBias(edge: ClassifiedEdge): number {
|
|
342
|
+
if (edge.kind !== 'forward' || edge.from === edge.to) return 0;
|
|
343
|
+
if (edge.to === preferredLeaf) return 2;
|
|
344
|
+
if (canReachContractHash(edge.to)) return 1;
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return edges
|
|
349
|
+
.map((edge, index) => ({ edge, index, bias: trunkBias(edge) }))
|
|
350
|
+
.sort((a, b) => {
|
|
351
|
+
if (a.edge.from !== b.edge.from) return a.index - b.index;
|
|
352
|
+
if (a.bias !== b.bias) return b.bias - a.bias;
|
|
353
|
+
return a.index - b.index;
|
|
354
|
+
})
|
|
355
|
+
.map(({ edge }) => edge);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function rebuildEdgeLookupMaps(edges: readonly ClassifiedEdge[]): {
|
|
359
|
+
edgesByFrom: Map<string, ClassifiedEdge[]>;
|
|
360
|
+
edgesByTo: Map<string, ClassifiedEdge[]>;
|
|
361
|
+
} {
|
|
362
|
+
const edgesByFrom = new Map<string, ClassifiedEdge[]>();
|
|
363
|
+
const edgesByTo = new Map<string, ClassifiedEdge[]>();
|
|
364
|
+
for (const classified of edges) {
|
|
365
|
+
const fromBucket = edgesByFrom.get(classified.from);
|
|
366
|
+
if (fromBucket) fromBucket.push(classified);
|
|
367
|
+
else edgesByFrom.set(classified.from, [classified]);
|
|
368
|
+
|
|
369
|
+
const toBucket = edgesByTo.get(classified.to);
|
|
370
|
+
if (toBucket) toBucket.push(classified);
|
|
371
|
+
else edgesByTo.set(classified.to, [classified]);
|
|
372
|
+
}
|
|
373
|
+
return { edgesByFrom, edgesByTo };
|
|
374
|
+
}
|
|
375
|
+
|
|
265
376
|
export function buildMigrationGraphRows(
|
|
266
377
|
graph: MigrationGraph,
|
|
267
378
|
options: BuildMigrationGraphRowsOptions = {},
|
|
@@ -284,31 +395,23 @@ export function buildMigrationGraphRows(
|
|
|
284
395
|
|
|
285
396
|
// 2. Build classified edge list
|
|
286
397
|
const edges: ClassifiedEdge[] = [];
|
|
287
|
-
const edgesByFrom = new Map<string, ClassifiedEdge[]>();
|
|
288
|
-
const edgesByTo = new Map<string, ClassifiedEdge[]>();
|
|
289
398
|
|
|
290
399
|
for (const edgeList of graph.forwardChain.values()) {
|
|
291
400
|
for (const edge of edgeList) {
|
|
292
401
|
const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? 'forward';
|
|
293
|
-
|
|
402
|
+
edges.push({
|
|
294
403
|
migrationHash: edge.migrationHash,
|
|
295
404
|
from: edge.from,
|
|
296
405
|
to: edge.to,
|
|
297
406
|
dirName: edge.dirName,
|
|
298
407
|
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]);
|
|
408
|
+
});
|
|
309
409
|
}
|
|
310
410
|
}
|
|
311
411
|
|
|
412
|
+
const sortedEdges = sortEdgesForContractHashTrunk(edges, options.contractHash);
|
|
413
|
+
const { edgesByFrom, edgesByTo } = rebuildEdgeLookupMaps(sortedEdges);
|
|
414
|
+
|
|
312
415
|
// 3. Find weakly-connected components (ordered: EMPTY first, then lex)
|
|
313
416
|
const components = weaklyConnectedComponents(graph);
|
|
314
417
|
|
|
@@ -318,7 +421,12 @@ export function buildMigrationGraphRows(
|
|
|
318
421
|
if (i > 0) nodes.push(null);
|
|
319
422
|
const component = components[i];
|
|
320
423
|
if (component === undefined) continue;
|
|
321
|
-
const ordered = layerNodesByLongestForwardPath(
|
|
424
|
+
const ordered = layerNodesByLongestForwardPath(
|
|
425
|
+
component,
|
|
426
|
+
topology,
|
|
427
|
+
graph,
|
|
428
|
+
options.contractHash,
|
|
429
|
+
);
|
|
322
430
|
for (const node of ordered) {
|
|
323
431
|
nodes.push(node);
|
|
324
432
|
}
|
|
@@ -332,5 +440,10 @@ export function buildMigrationGraphRows(
|
|
|
332
440
|
nodes.unshift(detached);
|
|
333
441
|
}
|
|
334
442
|
|
|
335
|
-
return {
|
|
443
|
+
return {
|
|
444
|
+
nodes,
|
|
445
|
+
edges: sortedEdges,
|
|
446
|
+
edgesByFrom,
|
|
447
|
+
edgesByTo,
|
|
448
|
+
};
|
|
336
449
|
}
|