@prisma-next/cli 0.12.0-dev.52 → 0.12.0-dev.53
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 +7 -7
- package/dist/{client-BfNYTr8w.mjs → client-DIcitJdy.mjs} +95 -38
- package/dist/client-DIcitJdy.mjs.map +1 -0
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +2 -2
- package/dist/commands/db-schema.mjs +1 -1
- package/dist/commands/db-sign.mjs +1 -1
- package/dist/commands/db-update.mjs +2 -2
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +35 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +287 -6
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.mjs +2 -2
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +4 -2
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +1 -0
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-show.mjs +2 -2
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -2
- package/dist/{contract-infer-sUml0McE.mjs → contract-infer-BAdhYGQH.mjs} +2 -2
- package/dist/{contract-infer-sUml0McE.mjs.map → contract-infer-BAdhYGQH.mjs.map} +1 -1
- package/dist/{db-verify-DSZcaGQs.mjs → db-verify-CiUCDXnv.mjs} +2 -2
- package/dist/{db-verify-DSZcaGQs.mjs.map → db-verify-CiUCDXnv.mjs.map} +1 -1
- package/dist/exports/control-api.mjs +1 -1
- package/dist/{inspect-live-schema-B44OravN.mjs → inspect-live-schema-DegaqKFT.mjs} +2 -2
- package/dist/{inspect-live-schema-B44OravN.mjs.map → inspect-live-schema-DegaqKFT.mjs.map} +1 -1
- package/dist/{migration-check-BxWlQBOs.mjs → migration-check-B2ccCHe7.mjs} +3 -3
- package/dist/{migration-check-BxWlQBOs.mjs.map → migration-check-B2ccCHe7.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-BvVTTjzK.mjs → migration-command-scaffold-D6UeN71F.mjs} +2 -2
- package/dist/{migration-command-scaffold-BvVTTjzK.mjs.map → migration-command-scaffold-D6UeN71F.mjs.map} +1 -1
- package/dist/{migration-graph-space-render-CeNXh_Wy.mjs → migration-graph-space-render-B0HkTNj3.mjs} +488 -84
- package/dist/migration-graph-space-render-B0HkTNj3.mjs.map +1 -0
- package/dist/{migration-list-vJWFuXca.mjs → migration-list-mYmj2j33.mjs} +6 -4
- package/dist/migration-list-mYmj2j33.mjs.map +1 -0
- package/dist/{migration-log-DD01Jm_i.mjs → migration-log-Dzs18GU7.mjs} +3 -3
- package/dist/{migration-log-DD01Jm_i.mjs.map → migration-log-Dzs18GU7.mjs.map} +1 -1
- package/dist/{migration-path-target-UkxkgXnv.mjs → migration-path-target-DK-B7POa.mjs} +1 -1
- package/dist/{migration-path-target-UkxkgXnv.mjs.map → migration-path-target-DK-B7POa.mjs.map} +1 -1
- package/dist/{migration-status-jL5ajRrB.mjs → migration-status-BT9eCQsf.mjs} +8 -5
- package/dist/migration-status-BT9eCQsf.mjs.map +1 -0
- package/dist/{schemas-DJY2O09F.mjs → schemas-B4xeMrNt.mjs} +1 -1
- package/dist/{schemas-DJY2O09F.mjs.map → schemas-B4xeMrNt.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migrate.ts +512 -2
- package/src/commands/migration-graph.ts +2 -0
- package/src/commands/migration-list.ts +3 -0
- package/src/commands/migration-status.ts +4 -0
- package/src/control-api/operations/migrate.ts +149 -56
- package/src/utils/formatters/migration-graph-layout.ts +187 -42
- package/src/utils/formatters/migration-graph-space-render.ts +11 -1
- package/src/utils/formatters/migration-graph-tree-render.ts +609 -59
- package/src/utils/formatters/migration-list-render.ts +12 -0
- package/src/utils/formatters/migration-list-styler.ts +5 -7
- package/dist/client-BfNYTr8w.mjs.map +0 -1
- package/dist/migration-graph-space-render-CeNXh_Wy.mjs.map +0 -1
- package/dist/migration-list-vJWFuXca.mjs.map +0 -1
- package/dist/migration-status-jL5ajRrB.mjs.map +0 -1
|
@@ -156,58 +156,36 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
|
|
|
156
156
|
// The aggregate passed the integrity gate, so every member's head ref
|
|
157
157
|
// is resolved (the app's is synthesised from the live contract).
|
|
158
158
|
const headRef = requireHeadRef(member);
|
|
159
|
-
const
|
|
159
|
+
const memberTargetHash = isAppMember && refHash !== undefined ? refHash : headRef.hash;
|
|
160
|
+
const memberRefInvariants = isAppMember && refHash !== undefined ? refInvariants : undefined;
|
|
160
161
|
const liveMarker = markerRows.get(member.spaceId) ?? null;
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// still surfaces the member in `perSpace[]` as already-at-head;
|
|
170
|
-
// the runner is not invoked for these members because they have
|
|
171
|
-
// no authored ops and (for greenfield extensions) no marker to
|
|
172
|
-
// advance.
|
|
173
|
-
const liveHash = liveMarker?.storageHash;
|
|
174
|
-
if (
|
|
175
|
-
targetHash === liveHash ||
|
|
176
|
-
(liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)
|
|
177
|
-
) {
|
|
178
|
-
atHeadResolutions.set(
|
|
179
|
-
member.spaceId,
|
|
180
|
-
buildAtHeadResolution({
|
|
181
|
-
aggregateTargetId: aggregate.targetId,
|
|
182
|
-
member,
|
|
183
|
-
targetHash,
|
|
184
|
-
liveMarker,
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
return notOk(buildNeverPlannedFailure(member.spaceId, targetHash));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const targetInvariants =
|
|
193
|
-
isAppMember && refHash !== undefined && refInvariants !== undefined
|
|
194
|
-
? refInvariants
|
|
195
|
-
: headRef.invariants;
|
|
196
|
-
const targetMember: ContractSpaceMember =
|
|
197
|
-
targetHash === headRef.hash && targetInvariants === headRef.invariants
|
|
198
|
-
? member
|
|
199
|
-
: { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
|
|
200
|
-
|
|
201
|
-
const walked = graphWalkStrategy({
|
|
202
|
-
aggregateTargetId: aggregate.targetId,
|
|
203
|
-
member: targetMember,
|
|
204
|
-
currentMarker: liveMarker,
|
|
205
|
-
...(isAppMember && refName !== undefined ? { refName } : {}),
|
|
163
|
+
const outcome = planMemberPath({
|
|
164
|
+
member,
|
|
165
|
+
aggregate,
|
|
166
|
+
targetHash: memberTargetHash,
|
|
167
|
+
refInvariants: memberRefInvariants,
|
|
168
|
+
liveMarker,
|
|
169
|
+
...(isAppMember ? { refName } : {}),
|
|
206
170
|
});
|
|
207
|
-
|
|
208
|
-
|
|
171
|
+
|
|
172
|
+
if (outcome.kind === 'at-head') {
|
|
173
|
+
// Empty-graph member whose live marker already matches the target.
|
|
174
|
+
// Kept out of the runner schedule so we don't write spurious markers
|
|
175
|
+
// for greenfield extensions, but merged back into the success envelope
|
|
176
|
+
// so every loaded member is represented.
|
|
177
|
+
atHeadResolutions.set(member.spaceId, outcome.plan);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (outcome.kind === 'never-planned') {
|
|
181
|
+
return notOk(buildNeverPlannedFailure(outcome.spaceId, outcome.targetHash));
|
|
182
|
+
}
|
|
183
|
+
if (outcome.kind === 'unreachable') {
|
|
184
|
+
return notOk(
|
|
185
|
+
buildPathNotFoundFailure(outcome.spaceId, outcome.liveMarker, outcome.targetHash),
|
|
186
|
+
);
|
|
209
187
|
}
|
|
210
|
-
if (
|
|
188
|
+
if (outcome.kind === 'unsatisfiable') {
|
|
211
189
|
// Surface the canonical MIGRATION.NO_INVARIANT_PATH envelope
|
|
212
190
|
// (the error rendering pipeline maps it to meta.code +
|
|
213
191
|
// meta.required + meta.missing + meta.structuralPath that the
|
|
@@ -218,10 +196,12 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
|
|
|
218
196
|
// string here would leave the structural lookup with a hash that
|
|
219
197
|
// is never a graph node, producing an empty `structuralPath` and
|
|
220
198
|
// a less actionable diagnostic.
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
199
|
+
const structural = findPathWithDecision(
|
|
200
|
+
outcome.targetMember.graph(),
|
|
201
|
+
outcome.liveHash,
|
|
202
|
+
memberTargetHash,
|
|
203
|
+
{ required: new Set<string>() },
|
|
204
|
+
);
|
|
225
205
|
const structuralPath =
|
|
226
206
|
structural.kind === 'ok'
|
|
227
207
|
? structural.decision.selectedPath.map((edge) => ({
|
|
@@ -233,14 +213,14 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
|
|
|
233
213
|
}))
|
|
234
214
|
: [];
|
|
235
215
|
throw errorNoInvariantPath({
|
|
236
|
-
...(
|
|
237
|
-
required: targetInvariants,
|
|
238
|
-
missing:
|
|
216
|
+
...(outcome.refName !== undefined ? { refName: outcome.refName } : {}),
|
|
217
|
+
required: outcome.targetInvariants,
|
|
218
|
+
missing: outcome.missing,
|
|
239
219
|
structuralPath,
|
|
240
220
|
});
|
|
241
221
|
}
|
|
242
222
|
|
|
243
|
-
perSpacePlans.set(member.spaceId,
|
|
223
|
+
perSpacePlans.set(member.spaceId, outcome.plan);
|
|
244
224
|
}
|
|
245
225
|
|
|
246
226
|
const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
|
|
@@ -340,6 +320,119 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
|
|
|
340
320
|
);
|
|
341
321
|
}
|
|
342
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Outcome variants for one member's path computation.
|
|
325
|
+
*
|
|
326
|
+
* Callers switch on `kind` and map to their own error representation:
|
|
327
|
+
* `executeMigrate` throws / returns `notOk`; `executeMigrateShowCommand`
|
|
328
|
+
* returns a CLI structured error. The shared discriminant guarantees both
|
|
329
|
+
* paths feed `graphWalkStrategy` the same inputs.
|
|
330
|
+
*
|
|
331
|
+
* @internal Exported for `executeMigrateShowCommand` to call.
|
|
332
|
+
*/
|
|
333
|
+
export type MemberPathOutcome =
|
|
334
|
+
| { readonly kind: 'ok'; readonly plan: PerSpacePlan }
|
|
335
|
+
| { readonly kind: 'at-head'; readonly plan: PerSpacePlan }
|
|
336
|
+
| { readonly kind: 'never-planned'; readonly spaceId: string; readonly targetHash: string }
|
|
337
|
+
| {
|
|
338
|
+
readonly kind: 'unreachable';
|
|
339
|
+
readonly spaceId: string;
|
|
340
|
+
readonly liveMarker: ContractMarkerRecordLike | null;
|
|
341
|
+
readonly targetHash: string;
|
|
342
|
+
}
|
|
343
|
+
| {
|
|
344
|
+
readonly kind: 'unsatisfiable';
|
|
345
|
+
readonly spaceId: string;
|
|
346
|
+
readonly isAppMember: boolean;
|
|
347
|
+
readonly missing: readonly string[];
|
|
348
|
+
readonly targetInvariants: readonly string[];
|
|
349
|
+
readonly targetMember: ContractSpaceMember;
|
|
350
|
+
readonly liveHash: string;
|
|
351
|
+
readonly refName: string | undefined;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Compute the graph-walk path for one contract-space member.
|
|
356
|
+
*
|
|
357
|
+
* Encapsulates the invariant-correct input assembly that both
|
|
358
|
+
* `executeMigrate` and `executeMigrateShowCommand` must use:
|
|
359
|
+
* - `currentMarker` carries the full live marker including `invariants`
|
|
360
|
+
* (not a stripped `{ storageHash, invariants: [] }` shell).
|
|
361
|
+
* - `targetInvariants` uses the caller-supplied `refInvariants` when a
|
|
362
|
+
* `--to` ref was resolved (not always the file head ref's invariants).
|
|
363
|
+
*
|
|
364
|
+
* Both callers map the returned `MemberPathOutcome` to their own error
|
|
365
|
+
* representation; the path-compute logic is shared and identical.
|
|
366
|
+
*
|
|
367
|
+
* @internal Exported for `executeMigrateShowCommand`.
|
|
368
|
+
*/
|
|
369
|
+
export function planMemberPath({
|
|
370
|
+
member,
|
|
371
|
+
aggregate,
|
|
372
|
+
targetHash,
|
|
373
|
+
refInvariants,
|
|
374
|
+
liveMarker,
|
|
375
|
+
refName,
|
|
376
|
+
}: {
|
|
377
|
+
readonly member: ContractSpaceMember;
|
|
378
|
+
readonly aggregate: Pick<ContractSpaceAggregate, 'targetId' | 'app'>;
|
|
379
|
+
readonly targetHash: string;
|
|
380
|
+
readonly refInvariants: readonly string[] | undefined;
|
|
381
|
+
readonly liveMarker: ContractMarkerRecordLike | null;
|
|
382
|
+
readonly refName?: string;
|
|
383
|
+
}): MemberPathOutcome {
|
|
384
|
+
const isAppMember = member.spaceId === aggregate.app.spaceId;
|
|
385
|
+
const headRef = requireHeadRef(member);
|
|
386
|
+
|
|
387
|
+
if (member.graph().nodes.size === 0) {
|
|
388
|
+
const liveHash = liveMarker?.storageHash;
|
|
389
|
+
if (targetHash === liveHash || (liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)) {
|
|
390
|
+
return {
|
|
391
|
+
kind: 'at-head',
|
|
392
|
+
plan: buildAtHeadResolution({
|
|
393
|
+
aggregateTargetId: aggregate.targetId,
|
|
394
|
+
member,
|
|
395
|
+
targetHash,
|
|
396
|
+
liveMarker,
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { kind: 'never-planned', spaceId: member.spaceId, targetHash };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const targetInvariants =
|
|
404
|
+
isAppMember && refInvariants !== undefined ? refInvariants : headRef.invariants;
|
|
405
|
+
const targetMember: ContractSpaceMember =
|
|
406
|
+
targetHash === headRef.hash && targetInvariants === headRef.invariants
|
|
407
|
+
? member
|
|
408
|
+
: { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
|
|
409
|
+
|
|
410
|
+
const walked = graphWalkStrategy({
|
|
411
|
+
aggregateTargetId: aggregate.targetId,
|
|
412
|
+
member: targetMember,
|
|
413
|
+
currentMarker: liveMarker,
|
|
414
|
+
...(isAppMember && refName !== undefined ? { refName } : {}),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (walked.kind === 'unreachable') {
|
|
418
|
+
return { kind: 'unreachable', spaceId: member.spaceId, liveMarker, targetHash };
|
|
419
|
+
}
|
|
420
|
+
if (walked.kind === 'unsatisfiable') {
|
|
421
|
+
const liveHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
|
|
422
|
+
return {
|
|
423
|
+
kind: 'unsatisfiable',
|
|
424
|
+
spaceId: member.spaceId,
|
|
425
|
+
isAppMember,
|
|
426
|
+
missing: walked.missing,
|
|
427
|
+
targetInvariants,
|
|
428
|
+
targetMember,
|
|
429
|
+
liveHash,
|
|
430
|
+
refName,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return { kind: 'ok', plan: walked.result };
|
|
434
|
+
}
|
|
435
|
+
|
|
343
436
|
/**
|
|
344
437
|
* Build a zero-op {@link PerSpacePlan} for an empty-graph
|
|
345
438
|
* member whose live marker already matches the target. Lets the apply
|
|
@@ -12,18 +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-land-tee' }
|
|
25
|
-
| {
|
|
26
|
-
|
|
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 }
|
|
27
33
|
| {
|
|
28
34
|
readonly kind: 'edge-lane';
|
|
29
35
|
readonly migrationHash: string;
|
|
@@ -337,6 +343,15 @@ function refineAdjacency(
|
|
|
337
343
|
row.convergenceProducer ?? false,
|
|
338
344
|
divergenceBranchEdge,
|
|
339
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
|
+
}
|
|
340
355
|
return {
|
|
341
356
|
...row,
|
|
342
357
|
cells: buildEdgeCells(
|
|
@@ -345,6 +360,7 @@ function refineAdjacency(
|
|
|
345
360
|
row.passThroughLanes ?? [],
|
|
346
361
|
adjacency,
|
|
347
362
|
row.cells.length,
|
|
363
|
+
existingLaneEdge,
|
|
348
364
|
),
|
|
349
365
|
};
|
|
350
366
|
});
|
|
@@ -377,30 +393,50 @@ function emptyCells(width: number): StructuralCell[] {
|
|
|
377
393
|
return Array.from({ length: width }, () => ({ kind: 'empty' as const }));
|
|
378
394
|
}
|
|
379
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
|
+
|
|
380
406
|
function buildBranchConnectorCells(
|
|
381
407
|
startLane: number,
|
|
382
408
|
endLane: number,
|
|
383
409
|
fanTargetLanes: ReadonlySet<number>,
|
|
384
410
|
activeLanes: ReadonlySet<number>,
|
|
385
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>,
|
|
386
418
|
): StructuralCell[] {
|
|
387
419
|
const cells = emptyCells(gridWidth);
|
|
388
420
|
for (let lane = 0; lane < gridWidth; lane++) {
|
|
389
421
|
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
390
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
422
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
391
423
|
continue;
|
|
392
424
|
}
|
|
393
425
|
if (lane === startLane) {
|
|
394
|
-
cells[lane] = { kind: 'branch-tee' };
|
|
426
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(trunkEdgeHash) };
|
|
395
427
|
} else if (lane === endLane) {
|
|
396
|
-
cells[lane] = { kind: 'branch-corner' };
|
|
428
|
+
cells[lane] = { kind: 'branch-corner', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
397
429
|
} else if (lane > startLane && lane < endLane) {
|
|
398
430
|
if (fanTargetLanes.has(lane)) {
|
|
399
|
-
cells[lane] = { kind: 'branch-tee' };
|
|
431
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
400
432
|
} else if (activeLanes.has(lane)) {
|
|
401
|
-
cells[lane] = {
|
|
433
|
+
cells[lane] = {
|
|
434
|
+
kind: 'arc-crossing',
|
|
435
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
436
|
+
...arcHashProp(fanEdgeHashByLane.get(endLane)),
|
|
437
|
+
};
|
|
402
438
|
} else {
|
|
403
|
-
cells[lane] = { kind: 'branch-tee' };
|
|
439
|
+
cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
|
|
404
440
|
}
|
|
405
441
|
}
|
|
406
442
|
}
|
|
@@ -413,24 +449,30 @@ function buildMergeConnectorCells(
|
|
|
413
449
|
fanTargetLanes: ReadonlySet<number>,
|
|
414
450
|
activeLanes: ReadonlySet<number>,
|
|
415
451
|
gridWidth: number,
|
|
452
|
+
/** Hash of the edge occupying each active lane (fan lanes + pass-throughs). */
|
|
453
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
416
454
|
): StructuralCell[] {
|
|
417
455
|
const cells = emptyCells(gridWidth);
|
|
418
456
|
for (let lane = 0; lane < gridWidth; lane++) {
|
|
419
457
|
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
420
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
458
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
421
459
|
continue;
|
|
422
460
|
}
|
|
423
461
|
if (lane === startLane) {
|
|
424
|
-
cells[lane] = { kind: 'merge-tee' };
|
|
462
|
+
cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
425
463
|
} else if (lane === endLane) {
|
|
426
|
-
cells[lane] = { kind: 'merge-corner' };
|
|
464
|
+
cells[lane] = { kind: 'merge-corner', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
427
465
|
} else if (lane > startLane && lane < endLane) {
|
|
428
466
|
if (fanTargetLanes.has(lane)) {
|
|
429
|
-
cells[lane] = { kind: 'merge-tee' };
|
|
467
|
+
cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
430
468
|
} else if (activeLanes.has(lane)) {
|
|
431
|
-
cells[lane] = {
|
|
469
|
+
cells[lane] = {
|
|
470
|
+
kind: 'arc-crossing',
|
|
471
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
472
|
+
...arcHashProp(laneEdgeByIndex.get(endLane)),
|
|
473
|
+
};
|
|
432
474
|
} else {
|
|
433
|
-
cells[lane] = { kind: 'horizontal-pass' };
|
|
475
|
+
cells[lane] = { kind: 'horizontal-pass', ...hashProp(laneEdgeByIndex.get(startLane)) };
|
|
434
476
|
}
|
|
435
477
|
}
|
|
436
478
|
}
|
|
@@ -442,11 +484,13 @@ function buildNodeCells(
|
|
|
442
484
|
nodeColumn: number,
|
|
443
485
|
activeLanes: readonly number[],
|
|
444
486
|
gridWidth: number,
|
|
487
|
+
/** Hash of the edge occupying each active pass-through lane. */
|
|
488
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
445
489
|
): StructuralCell[] {
|
|
446
490
|
const cells = emptyCells(gridWidth);
|
|
447
491
|
for (const lane of activeLanes) {
|
|
448
492
|
if (lane !== nodeColumn && lane < gridWidth) {
|
|
449
|
-
cells[lane] = { kind: 'vertical-pass' };
|
|
493
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
450
494
|
}
|
|
451
495
|
}
|
|
452
496
|
if (nodeColumn < gridWidth) {
|
|
@@ -461,10 +505,14 @@ function buildEdgeCells(
|
|
|
461
505
|
passThroughLanes: readonly number[],
|
|
462
506
|
adjacency: EdgeAdjacency,
|
|
463
507
|
gridWidth: number,
|
|
508
|
+
/** Hash of the edge occupying each active pass-through lane. */
|
|
509
|
+
laneEdgeByIndex: ReadonlyMap<number, string>,
|
|
464
510
|
): StructuralCell[] {
|
|
465
511
|
const cells = emptyCells(gridWidth);
|
|
466
512
|
for (const lane of passThroughLanes) {
|
|
467
|
-
if (lane < gridWidth)
|
|
513
|
+
if (lane < gridWidth) {
|
|
514
|
+
cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
|
|
515
|
+
}
|
|
468
516
|
}
|
|
469
517
|
if (laneIndex < gridWidth) {
|
|
470
518
|
cells[laneIndex] = {
|
|
@@ -713,6 +761,8 @@ function applySkipRollbackRouting(
|
|
|
713
761
|
.map((other) => other.backLane);
|
|
714
762
|
const maxCoLandingLane = Math.max(...coLandingLanes);
|
|
715
763
|
|
|
764
|
+
const { migrationHash: arcHash } = edge;
|
|
765
|
+
|
|
716
766
|
const sourceRow = result[sourceRowIndex];
|
|
717
767
|
if (sourceRow !== undefined) {
|
|
718
768
|
const cells = sourceRow.cells;
|
|
@@ -721,7 +771,12 @@ function applySkipRollbackRouting(
|
|
|
721
771
|
cells[nodeCol] = { kind: 'node', contractHash, arcTee: true };
|
|
722
772
|
for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
|
|
723
773
|
if (coSourcedLanes.includes(lane)) {
|
|
724
|
-
|
|
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
|
+
};
|
|
725
780
|
continue;
|
|
726
781
|
}
|
|
727
782
|
const existing = cells[lane];
|
|
@@ -734,14 +789,30 @@ function applySkipRollbackRouting(
|
|
|
734
789
|
occupied ||
|
|
735
790
|
routes.some(
|
|
736
791
|
(other) =>
|
|
737
|
-
other.edge.migrationHash !==
|
|
792
|
+
other.edge.migrationHash !== arcHash &&
|
|
738
793
|
other.backLane === lane &&
|
|
739
794
|
routeCrossesRow(other, sourceRowIndex, result),
|
|
740
795
|
);
|
|
741
|
-
|
|
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
|
+
}
|
|
742
811
|
}
|
|
743
812
|
cells[backLane] =
|
|
744
|
-
backLane < maxCoSourcedLane
|
|
813
|
+
backLane < maxCoSourcedLane
|
|
814
|
+
? { kind: 'arc-branch-tee', migrationHash: arcHash }
|
|
815
|
+
: { kind: 'arc-branch-corner', migrationHash: arcHash };
|
|
745
816
|
}
|
|
746
817
|
|
|
747
818
|
const edgeRow = result[edgeRowIndex];
|
|
@@ -750,10 +821,17 @@ function applySkipRollbackRouting(
|
|
|
750
821
|
// lane may already cross this row, and rebuilding would clobber it.
|
|
751
822
|
const cells = edgeRow.cells;
|
|
752
823
|
ensureCellWidth(cells, backLane + 1);
|
|
753
|
-
|
|
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) };
|
|
754
832
|
cells[backLane] = {
|
|
755
833
|
kind: 'edge-lane',
|
|
756
|
-
migrationHash:
|
|
834
|
+
migrationHash: arcHash,
|
|
757
835
|
edgeKind: edge.kind,
|
|
758
836
|
ownsLabel: true,
|
|
759
837
|
adjacency: 'node-skipping-rollback',
|
|
@@ -780,7 +858,7 @@ function applySkipRollbackRouting(
|
|
|
780
858
|
existing?.kind !== 'arc-branch-tee' &&
|
|
781
859
|
existing?.kind !== 'arc-crossing'
|
|
782
860
|
) {
|
|
783
|
-
cells[backLane] = { kind: 'vertical-pass' };
|
|
861
|
+
cells[backLane] = { kind: 'vertical-pass', migrationHash: arcHash };
|
|
784
862
|
}
|
|
785
863
|
}
|
|
786
864
|
|
|
@@ -794,7 +872,9 @@ function applySkipRollbackRouting(
|
|
|
794
872
|
// An inner converging arc's own landing junction: the outer arcs' bridge
|
|
795
873
|
// passes through it (`┴`) while its own vertical run closes here.
|
|
796
874
|
if (coLandingLanes.includes(lane)) {
|
|
797
|
-
|
|
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) };
|
|
798
878
|
continue;
|
|
799
879
|
}
|
|
800
880
|
// A bridged lane that carries another arc OR a forward vertical still
|
|
@@ -811,16 +891,30 @@ function applySkipRollbackRouting(
|
|
|
811
891
|
occupied ||
|
|
812
892
|
routes.some(
|
|
813
893
|
(other) =>
|
|
814
|
-
other.edge.migrationHash !==
|
|
894
|
+
other.edge.migrationHash !== arcHash &&
|
|
815
895
|
other.backLane === lane &&
|
|
816
896
|
routeCrossesRow(other, targetRowIndex, result),
|
|
817
897
|
);
|
|
818
|
-
|
|
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
|
+
}
|
|
819
911
|
}
|
|
820
912
|
// Inner converging arcs close as a landing tee so the outermost arc's
|
|
821
913
|
// bridge reads through to the node; only the outermost arc draws `╯`.
|
|
822
914
|
cells[backLane] =
|
|
823
|
-
backLane < maxCoLandingLane
|
|
915
|
+
backLane < maxCoLandingLane
|
|
916
|
+
? { kind: 'arc-land-tee', migrationHash: arcHash }
|
|
917
|
+
: { kind: 'arc-land-corner', migrationHash: arcHash };
|
|
824
918
|
for (const other of routes) {
|
|
825
919
|
if (other.backLane <= backLane) continue;
|
|
826
920
|
if (!routeCrossesRow(other, targetRowIndex, result)) continue;
|
|
@@ -832,7 +926,12 @@ function applySkipRollbackRouting(
|
|
|
832
926
|
existing?.kind !== 'arc-land-bridge' &&
|
|
833
927
|
existing?.kind !== 'node'
|
|
834
928
|
) {
|
|
835
|
-
|
|
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
|
+
};
|
|
836
935
|
}
|
|
837
936
|
}
|
|
838
937
|
}
|
|
@@ -914,6 +1013,9 @@ function layoutComponent(
|
|
|
914
1013
|
const nodeColumn = new Map<string, number>();
|
|
915
1014
|
const edgeColumn = new Map<string, number>();
|
|
916
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>();
|
|
917
1019
|
let gridWidth = 1;
|
|
918
1020
|
|
|
919
1021
|
function ensureGridWidth(minWidth: number): void {
|
|
@@ -965,7 +1067,14 @@ function layoutComponent(
|
|
|
965
1067
|
startLane,
|
|
966
1068
|
endLane,
|
|
967
1069
|
branchCount: laneIndices.length,
|
|
968
|
-
cells: buildMergeConnectorCells(
|
|
1070
|
+
cells: buildMergeConnectorCells(
|
|
1071
|
+
startLane,
|
|
1072
|
+
endLane,
|
|
1073
|
+
fanTargetLanes,
|
|
1074
|
+
activeLanes,
|
|
1075
|
+
gridWidth,
|
|
1076
|
+
laneEdgeByIndex,
|
|
1077
|
+
),
|
|
969
1078
|
});
|
|
970
1079
|
for (const index of laneIndices) {
|
|
971
1080
|
if (index !== startLane) setLane(index, null);
|
|
@@ -979,9 +1088,15 @@ function layoutComponent(
|
|
|
979
1088
|
endLane: number,
|
|
980
1089
|
branchCount: number,
|
|
981
1090
|
fanTargetLanes: readonly number[],
|
|
1091
|
+
/** Hash of the first/representative edge for each fan lane (keyed by lane index). */
|
|
1092
|
+
fanEdgeHashByLane: ReadonlyMap<number, string>,
|
|
982
1093
|
): void {
|
|
983
1094
|
ensureGridWidth(endLane + 1);
|
|
984
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);
|
|
985
1100
|
rows.push({
|
|
986
1101
|
kind: 'branch-connector',
|
|
987
1102
|
contractHash,
|
|
@@ -994,6 +1109,9 @@ function layoutComponent(
|
|
|
994
1109
|
new Set(fanTargetLanes),
|
|
995
1110
|
activeLanes,
|
|
996
1111
|
gridWidth,
|
|
1112
|
+
trunkEdgeHash,
|
|
1113
|
+
fanEdgeHashByLane,
|
|
1114
|
+
laneEdgeByIndex,
|
|
997
1115
|
),
|
|
998
1116
|
});
|
|
999
1117
|
}
|
|
@@ -1007,11 +1125,14 @@ function layoutComponent(
|
|
|
1007
1125
|
edge,
|
|
1008
1126
|
laneIndex: lane,
|
|
1009
1127
|
passThroughLanes: passThrough,
|
|
1010
|
-
cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth),
|
|
1128
|
+
cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth, laneEdgeByIndex),
|
|
1011
1129
|
};
|
|
1012
1130
|
rows.push(convergenceProducer ? { ...row, convergenceProducer: true } : row);
|
|
1013
1131
|
edgeColumn.set(edge.migrationHash, lane);
|
|
1014
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);
|
|
1015
1136
|
}
|
|
1016
1137
|
|
|
1017
1138
|
function emitNodeRow(contractHash: string, column: number): void {
|
|
@@ -1020,7 +1141,7 @@ function layoutComponent(
|
|
|
1020
1141
|
rows.push({
|
|
1021
1142
|
kind: 'node',
|
|
1022
1143
|
contractHash,
|
|
1023
|
-
cells: buildNodeCells(contractHash, column, passThrough, gridWidth),
|
|
1144
|
+
cells: buildNodeCells(contractHash, column, passThrough, gridWidth, laneEdgeByIndex),
|
|
1024
1145
|
});
|
|
1025
1146
|
nodeColumn.set(contractHash, column);
|
|
1026
1147
|
}
|
|
@@ -1090,7 +1211,31 @@ function layoutComponent(
|
|
|
1090
1211
|
|
|
1091
1212
|
if (groups.length >= 2) {
|
|
1092
1213
|
const endLane = Math.max(...laneForGroup);
|
|
1093
|
-
|
|
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
|
+
}
|
|
1094
1239
|
}
|
|
1095
1240
|
|
|
1096
1241
|
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|