@tscircuit/fanout-solver 0.0.55 → 0.0.57

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 CHANGED
@@ -306,6 +306,19 @@ To capture the inputs without solving, use `bun run generate:dataset31`.
306
306
  The optional `--dataset dataset31` flag is accepted for explicit CI invocation;
307
307
  other dataset selections are rejected.
308
308
 
309
+ To isolate a failure without changing pads or clearance rules, reduce a captured
310
+ input by bus or connection count. This is a diagnostic, not a benchmark score:
311
+
312
+ ```sh
313
+ bun scripts/debug-dataset31.ts --input benchmark-results/inputs/10-left-bottom-offset.json --buses DDR_ADDR_CTRL --connection-limit 7 --output /tmp/address-seven
314
+ bun scripts/debug-dataset31.ts --input benchmark-results/inputs/10-left-bottom-offset.json --buses DDR_ADDR_CTRL --output /tmp/address-eight
315
+ ```
316
+
317
+ Each run has a hard process deadline and saves JSON progress and an SVG preview.
318
+ Use `--dump-input <path> --capture-only` to save the exact reduced solver input
319
+ as a regression fixture. Retained buses keep their original targets and timing
320
+ limits; only removed connections and their differential-pair metadata are pruned.
321
+
309
322
  Each sample runs in an isolated process, with up to four concurrent processes
310
323
  locally and a **120-second hard timeout** by default. A synchronous solver hang,
311
324
  exception, or unsolved case does not prevent later samples from running.
@@ -1,5 +1,8 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
+ import { selectCompatibleCandidates } from "./select-compatible-candidates"
4
+ import { refineAdaptivePlaneReservationCore } from "./refine-adaptive-plane-reservations"
5
+ import { shouldUseAdaptiveDensePlaneRouting } from "./should-use-adaptive-dense-plane-routing"
3
6
  import { type GraphicsObject, mergeGraphics } from "graphics-debug"
4
7
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
5
8
  import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
@@ -1041,7 +1044,11 @@ export class FanoutSolver extends BaseSolver {
1041
1044
  escapeLayersByBusId: this.escapeLayersByBusId,
1042
1045
  preferOrderedCoordinatedWindingLayers:
1043
1046
  this.config.densePlaneReservationBusIds.length > 0 ||
1044
- this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0,
1047
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
1048
+ shouldUseAdaptiveDensePlaneRouting(
1049
+ this.preparedBuses,
1050
+ this.config.allowBlindAndBuriedVias,
1051
+ ),
1045
1052
  }),
1046
1053
  generatedAssignments,
1047
1054
  maxAssignments: this.config.maxLayerCombinations,
@@ -1389,6 +1396,8 @@ export class FanoutSolver extends BaseSolver {
1389
1396
  busesInRoutingOrder: readonly PreparedBus[]
1390
1397
  denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
1391
1398
  lengthMatchingStage?: "before-planes" | "after-planes"
1399
+ promotedPlaneReservationBusIds?: readonly string[]
1400
+ planeReservationRetryCount?: number
1392
1401
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1393
1402
  if (this.config.allowBlindAndBuriedVias) return null
1394
1403
  const usePadAlignedDenseRouting =
@@ -1404,6 +1413,8 @@ export class FanoutSolver extends BaseSolver {
1404
1413
  "plane-match:incremental-failed",
1405
1414
  "plane-route:alternate-candidate-counts",
1406
1415
  "plane-route:alternate-search",
1416
+ "plane-route:csp",
1417
+ "plane-reservation-core",
1407
1418
  "plane-route:promote-failed",
1408
1419
  "plane-route:alternate-choice",
1409
1420
  "plane-route:promote-zero-candidates",
@@ -1422,9 +1433,20 @@ export class FanoutSolver extends BaseSolver {
1422
1433
  const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
1423
1434
  (bus) => bus.termination.type === "boundary",
1424
1435
  )
1425
- const useConfiguredDensePlaneRouting =
1436
+ const configuredDensePlaneRouting =
1426
1437
  this.config.densePlaneReservationBusIds.length > 0 ||
1427
1438
  this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
1439
+ const useAdaptiveDensePlaneRouting =
1440
+ !configuredDensePlaneRouting &&
1441
+ shouldUseAdaptiveDensePlaneRouting(
1442
+ this.preparedBuses,
1443
+ this.config.allowBlindAndBuriedVias,
1444
+ )
1445
+ const useConfiguredDensePlaneRouting =
1446
+ configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
1447
+ const useAdaptiveJointPlaneSelection =
1448
+ useAdaptiveDensePlaneRouting &&
1449
+ (params.planeReservationRetryCount ?? 0) === 0
1428
1450
  const matchLengthsAfterPlanes =
1429
1451
  useConfiguredDensePlaneRouting &&
1430
1452
  params.lengthMatchingStage !== "before-planes"
@@ -1663,6 +1685,17 @@ export class FanoutSolver extends BaseSolver {
1663
1685
  : 8,
1664
1686
  )
1665
1687
  : planeBuses
1688
+ if (params.promotedPlaneReservationBusIds) {
1689
+ activeBoundaryReservationPlaneBuses = [
1690
+ ...new Set([
1691
+ ...activeBoundaryReservationPlaneBuses,
1692
+ ...planeBuses.filter((bus) =>
1693
+ params.promotedPlaneReservationBusIds!.includes(bus.busId),
1694
+ ),
1695
+ ]),
1696
+ ]
1697
+ }
1698
+ const unroutablePlaneBusIds = new Set<string>()
1666
1699
  debugDense(
1667
1700
  "start",
1668
1701
  boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
@@ -1719,7 +1752,7 @@ export class FanoutSolver extends BaseSolver {
1719
1752
  getCornerBandSide(bus.exitEdge, bus.preferredExit)
1720
1753
  ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1721
1754
  : getExitEdgeForDirection(bus.direction) !== bus.exitEdge
1722
- : bus.exitEdge === "top" && bus.connections.length >= 8
1755
+ : hasThreeWideBoundaryBuses && bus.connections.length >= 8
1723
1756
  ? false
1724
1757
  : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1725
1758
  ]),
@@ -2114,6 +2147,16 @@ export class FanoutSolver extends BaseSolver {
2114
2147
  return [
2115
2148
  {
2116
2149
  connectionName: connection.connection.name,
2150
+ sourceEscapeSegment: matchedPlans.some(
2151
+ (plan) => plan.connectionIndex === connection.connectionIndex,
2152
+ )
2153
+ ? undefined
2154
+ : {
2155
+ start: connection.sourcePoint,
2156
+ end: center,
2157
+ layer: connection.sourceLayer,
2158
+ width: this.config.traceWidth,
2159
+ },
2117
2160
  via: {
2118
2161
  center,
2119
2162
  diameter: this.config.viaDiameter,
@@ -2175,7 +2218,7 @@ export class FanoutSolver extends BaseSolver {
2175
2218
  fixedViaPointsByConnectionIndex,
2176
2219
  reservedVias: getReservedVias(bus),
2177
2220
  viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2178
- allowBoundarySideViaFallback: true,
2221
+ allowBoundarySideViaFallback: bus.connections.length === 1,
2179
2222
  preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
2180
2223
  adaptiveWindingRouteOrder,
2181
2224
  alignWindingGridToPads:
@@ -2316,7 +2359,21 @@ export class FanoutSolver extends BaseSolver {
2316
2359
  ))[0]
2317
2360
  if (busPlans) break
2318
2361
  }
2319
- if (!busPlans) fixedViaPointsByConnectionIndex = originalPoints
2362
+ if (!busPlans) {
2363
+ fixedViaPointsByConnectionIndex = originalPoints
2364
+ if (bus.connections.length === 2) {
2365
+ busPlans = (yield* routeAlternatives(
2366
+ {
2367
+ ...routeParams,
2368
+ fixedViaPointsByConnectionIndex: originalPoints,
2369
+ reservedVias: getReservedVias(bus),
2370
+ allowBoundarySideViaFallback: true,
2371
+ fixedViaFallbackRouteOrderAttempts: 1,
2372
+ },
2373
+ 1,
2374
+ ))[0]
2375
+ }
2376
+ }
2320
2377
  }
2321
2378
  if (busPlans && bus.maxLengthSkew !== undefined) {
2322
2379
  const lengths = busPlans.map((plan) => plan.length)
@@ -3074,6 +3131,79 @@ export class FanoutSolver extends BaseSolver {
3074
3131
  })),
3075
3132
  }),
3076
3133
  )
3134
+ if (useAdaptiveJointPlaneSelection) {
3135
+ const acceptedPlans = [
3136
+ ...candidatePlans,
3137
+ ...feasibleAlternatePlanePlans,
3138
+ ]
3139
+ for (const planeBus of planeBuses) {
3140
+ if (
3141
+ matchedPlaneBuses.includes(planeBus) ||
3142
+ candidateSets.some((set) => set.planeBus === planeBus)
3143
+ )
3144
+ continue
3145
+ const candidates: IndependentPlaneRouteCandidate[] = []
3146
+ const sites = getComponentDogboneViaSiteCandidates(
3147
+ [planeBus],
3148
+ {
3149
+ viaDiameter: this.config.viaDiameter,
3150
+ viaHoleDiameter: this.config.viaHoleDiameter,
3151
+ traceWidth: this.config.traceWidth,
3152
+ clearance: this.config.clearance,
3153
+ blockingSegments: acceptedPlans.flatMap((plan) =>
3154
+ plan.segments.map((segment) => ({
3155
+ connectionIndex: plan.connectionIndex,
3156
+ segment,
3157
+ })),
3158
+ ),
3159
+ blockingVias: acceptedPlans.flatMap((plan) =>
3160
+ [
3161
+ plan.via,
3162
+ ...(plan.additionalVias ?? []),
3163
+ plan.planeEndpointVia,
3164
+ ]
3165
+ .filter((via) => via !== undefined)
3166
+ .map((via) => ({
3167
+ connectionIndex: plan.connectionIndex,
3168
+ center: via!.center,
3169
+ diameter: via!.diameter,
3170
+ spanLayers: via!.spanLayers,
3171
+ })),
3172
+ ),
3173
+ additionalObstacles: this.routingSrj.obstacles,
3174
+ preferPlaneCheckerboardSites: true,
3175
+ canShareCopper,
3176
+ },
3177
+ )
3178
+ for (const site of sites) {
3179
+ const plans = routeBus({
3180
+ srj: this.routingSrj,
3181
+ bus: planeBus,
3182
+ targetLayer:
3183
+ params.busLayerAssignments[planeBus.busId]!,
3184
+ acceptedPlans,
3185
+ layerNames: this.config.layerNames,
3186
+ traceWidth: this.config.traceWidth,
3187
+ viaDiameter: this.config.viaDiameter,
3188
+ viaHoleDiameter: this.config.viaHoleDiameter,
3189
+ clearance: this.config.clearance,
3190
+ compactBusTracks: this.config.compactBusTracks,
3191
+ allowBlindAndBuriedVias: false,
3192
+ allowSameNetMerges: this.config.allowSameNetMerges,
3193
+ fixedViaPointsByConnectionIndex: new Map([
3194
+ [site.connectionIndex, site.point],
3195
+ ]),
3196
+ })
3197
+ if (plans)
3198
+ candidates.push({
3199
+ key: `${planeBus.busId}:local:${candidates.length}`,
3200
+ planeBus,
3201
+ plans,
3202
+ })
3203
+ }
3204
+ candidateSets.push({ planeBus, candidates })
3205
+ }
3206
+ }
3077
3207
  debugDense(
3078
3208
  "plane-route:alternate-candidate-counts",
3079
3209
  candidateSets.map((candidateSet) => [
@@ -3081,6 +3211,10 @@ export class FanoutSolver extends BaseSolver {
3081
3211
  candidateSet.candidates.length,
3082
3212
  ]),
3083
3213
  )
3214
+ for (const candidateSet of candidateSets) {
3215
+ if (candidateSet.candidates.length === 0)
3216
+ unroutablePlaneBusIds.add(candidateSet.planeBus.busId)
3217
+ }
3084
3218
  const compatibilityByCandidatePair = new Map<string, boolean>()
3085
3219
  const candidatesAreCompatible = (
3086
3220
  first: IndependentPlaneRouteCandidate,
@@ -3207,10 +3341,82 @@ export class FanoutSolver extends BaseSolver {
3207
3341
  )
3208
3342
  return null
3209
3343
  }
3210
- const selectedCandidates = selectCompatiblePlaneRoutes(
3211
- candidateSets,
3212
- [],
3213
- )
3344
+ let selectedCandidates: IndependentPlaneRouteCandidate[] | null
3345
+ if (useAdaptiveDensePlaneRouting) {
3346
+ const getUniqueDomains = (sets: typeof candidateSets) =>
3347
+ sets.map((set) => [
3348
+ ...new Map(
3349
+ set.candidates.map((candidate) => [
3350
+ JSON.stringify(
3351
+ candidate.plans.map((plan) => [
3352
+ plan.segments,
3353
+ plan.via,
3354
+ plan.additionalVias,
3355
+ plan.planeEndpointSegments,
3356
+ plan.planeEndpointVia,
3357
+ ]),
3358
+ ),
3359
+ candidate,
3360
+ ]),
3361
+ ).values(),
3362
+ ])
3363
+ const runCandidateSelection = (
3364
+ sets: typeof candidateSets,
3365
+ maximumSearchStates = maximumAlternatePlaneSearchStates,
3366
+ ) =>
3367
+ selectCompatibleCandidates({
3368
+ candidateSets: getUniqueDomains(sets),
3369
+ maximumSearchStates,
3370
+ areCompatible: (first, second) =>
3371
+ fanoutPlansAreMutuallyClear({
3372
+ plans: [...first.plans, ...second.plans],
3373
+ srj: this.routingSrj,
3374
+ clearance: this.config.clearance,
3375
+ allowSameNetMerges: this.config.allowSameNetMerges,
3376
+ }),
3377
+ })
3378
+ const result = runCandidateSelection(candidateSets)
3379
+ debugDense(
3380
+ "plane-route:csp",
3381
+ result.selection ? "complete" : "failed",
3382
+ result.searchStates,
3383
+ result.emptyDomainIndices.map(
3384
+ (index) => candidateSets[index]!.planeBus.busId,
3385
+ ),
3386
+ )
3387
+ selectedCandidates = result.selection
3388
+ alternatePlaneSearchStates = result.searchStates
3389
+ if (!result.selection && useAdaptiveDensePlaneRouting) {
3390
+ const expanded = result.emptyDomainIndices
3391
+ .map((index) => candidateSets[index]!.planeBus.busId)
3392
+ .filter(
3393
+ (id) =>
3394
+ !orderedAlternatePlaneBuses.some(
3395
+ (bus) => bus.busId === id,
3396
+ ),
3397
+ )
3398
+ if (expanded.length > 0) {
3399
+ debugDense("plane-route:expand-domains", expanded)
3400
+ return matchViaPointsAroundPlans(
3401
+ candidatePlans,
3402
+ new Set([...promotedAlternatePlaneBusIds, ...expanded]),
3403
+ )
3404
+ }
3405
+ }
3406
+ if (!result.selection)
3407
+ for (const index of (params.planeReservationRetryCount ??
3408
+ 0) === 0
3409
+ ? result.conflictDomainIndices
3410
+ : result.emptyDomainIndices.slice(0, 1))
3411
+ unroutablePlaneBusIds.add(
3412
+ candidateSets[index]!.planeBus.busId,
3413
+ )
3414
+ } else {
3415
+ selectedCandidates = selectCompatiblePlaneRoutes(
3416
+ candidateSets,
3417
+ [],
3418
+ )
3419
+ }
3214
3420
  alternatePlanePlans = selectedCandidates
3215
3421
  ? [
3216
3422
  ...feasibleAlternatePlanePlans,
@@ -3236,6 +3442,18 @@ export class FanoutSolver extends BaseSolver {
3236
3442
  )
3237
3443
  if (!alternatePlanePlans) return null
3238
3444
  feasibleAlternatePlanePlans = alternatePlanePlans
3445
+ if (useAdaptiveJointPlaneSelection) {
3446
+ matchedPlaneBusesInRoutingOrder = []
3447
+ return new Map([
3448
+ ...fixedBoundaryViaPoints,
3449
+ ...alternatePlanePlans
3450
+ .filter((plan) => plan.via)
3451
+ .map(
3452
+ (plan) =>
3453
+ [plan.connectionIndex, plan.via!.center] as const,
3454
+ ),
3455
+ ])
3456
+ }
3239
3457
  }
3240
3458
  let planeBusIdsRoutedWithoutDogbones = new Set<string>()
3241
3459
  let allBlockingSegments = [...blockingSegments]
@@ -3685,6 +3903,39 @@ export class FanoutSolver extends BaseSolver {
3685
3903
  }
3686
3904
  }
3687
3905
 
3906
+ if (
3907
+ useAdaptiveDensePlaneRouting &&
3908
+ unroutablePlaneBusIds.size > 0 &&
3909
+ (params.planeReservationRetryCount ?? 0) < 5
3910
+ ) {
3911
+ const newlyPromoted = [...unroutablePlaneBusIds].filter(
3912
+ (id) =>
3913
+ !activeBoundaryReservationPlaneBuses.some((bus) => bus.busId === id),
3914
+ )
3915
+ const refinedPromotions =
3916
+ (params.planeReservationRetryCount ?? 0) === 0
3917
+ ? refineAdaptivePlaneReservationCore({
3918
+ candidateBusIds: newlyPromoted,
3919
+ activeBusIds: new Set(
3920
+ activeBoundaryReservationPlaneBuses.map((bus) => bus.busId),
3921
+ ),
3922
+ planeBuses,
3923
+ })
3924
+ : newlyPromoted
3925
+ if (refinedPromotions.length > 0) {
3926
+ debugDense("plane-reservation-core", refinedPromotions)
3927
+ debugDense("promote-reservations", refinedPromotions)
3928
+ return yield* this.routeDenseThroughAllMixedTerminationSteps({
3929
+ ...params,
3930
+ promotedPlaneReservationBusIds: [
3931
+ ...(params.promotedPlaneReservationBusIds ?? []),
3932
+ ...refinedPromotions,
3933
+ ],
3934
+ planeReservationRetryCount:
3935
+ (params.planeReservationRetryCount ?? 0) + 1,
3936
+ })
3937
+ }
3938
+ }
3688
3939
  if (matchLengthsAfterPlanes) {
3689
3940
  return yield* this.routeDenseThroughAllMixedTerminationSteps({
3690
3941
  ...params,
@@ -0,0 +1,26 @@
1
+ import type { FanoutDirection, Point2D } from "./types"
2
+
3
+ /** A bounded fallback for channels trapped by uniform outward dogbones. */
4
+ export function getDogboneSideVariants(
5
+ connections: readonly { connectionIndex: number; sourcePoint: Point2D }[],
6
+ direction: FanoutDirection,
7
+ ): number[][] {
8
+ const axis = direction === "up" || direction === "down" ? "y" : "x"
9
+ const variants = connections
10
+ .slice(0, 32)
11
+ .map(({ connectionIndex }) => [connectionIndex])
12
+ // Two pads in the same escape row can jointly close a winding corridor.
13
+ // Move both to the other half-pitch row, keeping every other site unchanged.
14
+ // Do not enumerate the exponential Cartesian product of all via sites.
15
+ for (let first = 0; first < connections.length; first++) {
16
+ for (let second = first + 1; second < connections.length; second++) {
17
+ if (variants.length >= 32) return variants
18
+ const a = connections[first]!
19
+ const b = connections[second]!
20
+ if (Math.abs(a.sourcePoint[axis] - b.sourcePoint[axis]) <= 1e-9) {
21
+ variants.push([a.connectionIndex, b.connectionIndex])
22
+ }
23
+ }
24
+ }
25
+ return variants
26
+ }
@@ -0,0 +1,94 @@
1
+ import type { PreparedBus } from "./types"
2
+
3
+ const EPSILON = 1e-9
4
+
5
+ /**
6
+ * A failed arc-consistency chain ends with the domain that removed the last
7
+ * supported candidate. When that plane pad has an adjacent peer farther into
8
+ * the component, reserving the peer keeps the outer pad available for a
9
+ * displaced route while protecting the same local dogbone corridor.
10
+ */
11
+ export function refineAdaptivePlaneReservationCore(params: {
12
+ candidateBusIds: readonly string[]
13
+ activeBusIds: ReadonlySet<string>
14
+ planeBuses: readonly PreparedBus[]
15
+ }): string[] {
16
+ const core = params.candidateBusIds.filter(
17
+ (busId) => !params.activeBusIds.has(busId),
18
+ )
19
+ if (core.length < 2) return core
20
+
21
+ const terminalIndex = core.length - 1
22
+ const terminal = params.planeBuses.find(
23
+ (bus) => bus.busId === core[terminalIndex],
24
+ )
25
+ const source = terminal?.connections[0]?.sourcePoint
26
+ if (
27
+ !terminal ||
28
+ !source ||
29
+ terminal.connections.length !== 1 ||
30
+ terminal.termination.type !== "plane"
31
+ )
32
+ return core
33
+ const terminalLayer = terminal.termination.layer
34
+
35
+ const perpendicularAxis =
36
+ terminal.direction === "up" || terminal.direction === "down" ? "x" : "y"
37
+ const parallelAxis = perpendicularAxis === "x" ? "y" : "x"
38
+ const componentCoordinates =
39
+ perpendicularAxis === "x" ? terminal.xCoordinates : terminal.yCoordinates
40
+ const pitch = perpendicularAxis === "x" ? terminal.pitchX : terminal.pitchY
41
+ if (
42
+ componentCoordinates.length === 0 ||
43
+ !Number.isFinite(pitch) ||
44
+ pitch <= EPSILON
45
+ ) {
46
+ return core
47
+ }
48
+
49
+ const componentCenter =
50
+ (Math.min(...componentCoordinates) + Math.max(...componentCoordinates)) / 2
51
+ const inwardSign = Math.sign(componentCenter - source[perpendicularAxis])
52
+ if (inwardSign === 0) return core
53
+
54
+ const coreIds = new Set(core)
55
+ const replacement = params.planeBuses
56
+ .filter((bus) => {
57
+ const candidateSource = bus.connections[0]?.sourcePoint
58
+ if (
59
+ bus === terminal ||
60
+ bus.componentId !== terminal.componentId ||
61
+ bus.termination.type !== "plane" ||
62
+ bus.termination.layer !== terminalLayer ||
63
+ bus.direction !== terminal.direction ||
64
+ bus.connections.length !== 1 ||
65
+ !candidateSource ||
66
+ params.activeBusIds.has(bus.busId) ||
67
+ coreIds.has(bus.busId) ||
68
+ Math.abs(candidateSource[parallelAxis] - source[parallelAxis]) > EPSILON
69
+ ) {
70
+ return false
71
+ }
72
+ const inwardDistance =
73
+ (candidateSource[perpendicularAxis] - source[perpendicularAxis]) *
74
+ inwardSign
75
+ return inwardDistance > EPSILON && inwardDistance <= pitch + EPSILON
76
+ })
77
+ .toSorted((first, second) => {
78
+ const firstSource = first.connections[0]!.sourcePoint
79
+ const secondSource = second.connections[0]!.sourcePoint
80
+ return (
81
+ Math.abs(firstSource[perpendicularAxis] - source[perpendicularAxis]) -
82
+ Math.abs(
83
+ secondSource[perpendicularAxis] - source[perpendicularAxis],
84
+ ) ||
85
+ first.connections[0]!.connectionIndex -
86
+ second.connections[0]!.connectionIndex
87
+ )
88
+ })[0]
89
+
90
+ if (!replacement) return core
91
+ return core.map((busId, index) =>
92
+ index === terminalIndex ? replacement.busId : busId,
93
+ )
94
+ }