effect 4.0.0-beta.79 → 4.0.0-beta.80

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/src/Graph.ts CHANGED
@@ -1386,6 +1386,31 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
1386
1386
  }
1387
1387
  }
1388
1388
 
1389
+ /**
1390
+ * @internal
1391
+ */
1392
+ const rebuildAdjacency = <N, E, T extends Kind = "directed">(
1393
+ mutable: MutableGraph<N, E, T>
1394
+ ): void => {
1395
+ mutable.adjacency.clear()
1396
+ mutable.reverseAdjacency.clear()
1397
+
1398
+ for (const nodeIndex of mutable.nodes.keys()) {
1399
+ mutable.adjacency.set(nodeIndex, [])
1400
+ mutable.reverseAdjacency.set(nodeIndex, [])
1401
+ }
1402
+
1403
+ for (const [edgeIndex, edgeData] of mutable.edges) {
1404
+ mutable.adjacency.get(edgeData.source)!.push(edgeIndex)
1405
+ mutable.reverseAdjacency.get(edgeData.target)!.push(edgeIndex)
1406
+
1407
+ if (mutable.type === "undirected") {
1408
+ mutable.adjacency.get(edgeData.target)!.push(edgeIndex)
1409
+ mutable.reverseAdjacency.get(edgeData.source)!.push(edgeIndex)
1410
+ }
1411
+ }
1412
+ }
1413
+
1389
1414
  /**
1390
1415
  * Swaps source and target nodes for every edge in a mutable graph.
1391
1416
  *
@@ -1413,6 +1438,10 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
1413
1438
  export const reverse = <N, E, T extends Kind = "directed">(
1414
1439
  mutable: MutableGraph<N, E, T>
1415
1440
  ): void => {
1441
+ if (mutable.type === "undirected") {
1442
+ return
1443
+ }
1444
+
1416
1445
  // Reverse all edges by swapping source and target
1417
1446
  for (const [index, edgeData] of mutable.edges) {
1418
1447
  mutable.edges.set(
@@ -1425,22 +1454,7 @@ export const reverse = <N, E, T extends Kind = "directed">(
1425
1454
  )
1426
1455
  }
1427
1456
 
1428
- // Clear and rebuild adjacency lists with reversed directions
1429
- mutable.adjacency.clear()
1430
- mutable.reverseAdjacency.clear()
1431
-
1432
- // Rebuild adjacency lists with reversed directions
1433
- for (const [edgeIndex, edgeData] of mutable.edges) {
1434
- // Add to forward adjacency (source -> target)
1435
- const sourceEdges = mutable.adjacency.get(edgeData.source) || []
1436
- sourceEdges.push(edgeIndex)
1437
- mutable.adjacency.set(edgeData.source, sourceEdges)
1438
-
1439
- // Add to reverse adjacency (target <- source)
1440
- const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || []
1441
- targetEdges.push(edgeIndex)
1442
- mutable.reverseAdjacency.set(edgeData.target, targetEdges)
1443
- }
1457
+ rebuildAdjacency(mutable)
1444
1458
 
1445
1459
  // Invalidate cycle flag since edge directions changed
1446
1460
  mutable.acyclic = Option.none()
@@ -2131,8 +2145,11 @@ export const hasEdge: {
2131
2145
  // Check if any edge in the adjacency list connects to the target
2132
2146
  for (const edgeIndex of adjacencyList) {
2133
2147
  const edge = graph.edges.get(edgeIndex)
2134
- if (edge !== undefined && edge.target === target) {
2135
- return true
2148
+ if (edge !== undefined) {
2149
+ const neighbor = graph.type === "undirected" && edge.target === source ? edge.source : edge.target
2150
+ if (neighbor === target) {
2151
+ return true
2152
+ }
2136
2153
  }
2137
2154
  }
2138
2155
 
@@ -2169,6 +2186,31 @@ export const edgeCount = <N, E, T extends Kind = "directed">(
2169
2186
  graph: Graph<N, E, T> | MutableGraph<N, E, T>
2170
2187
  ): number => graph.edges.size
2171
2188
 
2189
+ const getDirectedNeighbors = <N, E>(
2190
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2191
+ nodeIndex: NodeIndex,
2192
+ direction: Direction
2193
+ ): Array<NodeIndex> => {
2194
+ const adjacencyMap = direction === "incoming"
2195
+ ? graph.reverseAdjacency
2196
+ : graph.adjacency
2197
+
2198
+ const adjacencyList = adjacencyMap.get(nodeIndex)
2199
+ if (adjacencyList === undefined) {
2200
+ return []
2201
+ }
2202
+
2203
+ const result: Array<NodeIndex> = []
2204
+ for (const edgeIndex of adjacencyList) {
2205
+ const edge = graph.edges.get(edgeIndex)
2206
+ if (edge !== undefined) {
2207
+ result.push(direction === "incoming" ? edge.source : edge.target)
2208
+ }
2209
+ }
2210
+
2211
+ return result
2212
+ }
2213
+
2172
2214
  /**
2173
2215
  * Returns the neighboring node indices for a node.
2174
2216
  *
@@ -2286,24 +2328,160 @@ export const neighbors: {
2286
2328
  return getUndirectedNeighbors(graph as any, nodeIndex)
2287
2329
  }
2288
2330
 
2289
- const adjacencyList = graph.adjacency.get(nodeIndex)
2290
- if (adjacencyList === undefined) {
2291
- return []
2292
- }
2331
+ return getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, "outgoing")
2332
+ })
2293
2333
 
2294
- const result: Array<NodeIndex> = []
2295
- for (const edgeIndex of adjacencyList) {
2296
- const edge = graph.edges.get(edgeIndex)
2297
- if (edge !== undefined) {
2298
- result.push(edge.target)
2299
- }
2334
+ /**
2335
+ * Returns the outgoing neighbor node indices for a node in a directed graph.
2336
+ *
2337
+ * **When to use**
2338
+ *
2339
+ * Use when you need the nodes reached by following outgoing edges from a node in
2340
+ * a directed graph.
2341
+ *
2342
+ * **Gotchas**
2343
+ *
2344
+ * Throws a `GraphError` when used with an undirected graph.
2345
+ *
2346
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2347
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2348
+ *
2349
+ * @category queries
2350
+ * @since 4.0.0
2351
+ */
2352
+ export const successors: {
2353
+ /**
2354
+ * Returns the outgoing neighbor node indices for a node in a directed graph.
2355
+ *
2356
+ * **When to use**
2357
+ *
2358
+ * Use when you need the nodes reached by following outgoing edges from a node in
2359
+ * a directed graph.
2360
+ *
2361
+ * **Gotchas**
2362
+ *
2363
+ * Throws a `GraphError` when used with an undirected graph.
2364
+ *
2365
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2366
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2367
+ *
2368
+ * @category queries
2369
+ * @since 4.0.0
2370
+ */
2371
+ (nodeIndex: NodeIndex): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
2372
+ /**
2373
+ * Returns the outgoing neighbor node indices for a node in a directed graph.
2374
+ *
2375
+ * **When to use**
2376
+ *
2377
+ * Use when you need the nodes reached by following outgoing edges from a node in
2378
+ * a directed graph.
2379
+ *
2380
+ * **Gotchas**
2381
+ *
2382
+ * Throws a `GraphError` when used with an undirected graph.
2383
+ *
2384
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2385
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2386
+ *
2387
+ * @category queries
2388
+ * @since 4.0.0
2389
+ */
2390
+ <N, E>(
2391
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2392
+ nodeIndex: NodeIndex
2393
+ ): Array<NodeIndex>
2394
+ } = dual(2, <N, E, T extends Kind = "directed">(
2395
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2396
+ nodeIndex: NodeIndex
2397
+ ): Array<NodeIndex> => {
2398
+ if (graph.type === "undirected") {
2399
+ throw new GraphError({ message: "Cannot get successors of undirected graph" })
2300
2400
  }
2401
+ return getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, "outgoing")
2402
+ })
2301
2403
 
2302
- return result
2404
+ /**
2405
+ * Returns the incoming neighbor node indices for a node in a directed graph.
2406
+ *
2407
+ * **When to use**
2408
+ *
2409
+ * Use when you need the nodes that reach a node by following incoming edges in a
2410
+ * directed graph.
2411
+ *
2412
+ * **Gotchas**
2413
+ *
2414
+ * Throws a `GraphError` when used with an undirected graph.
2415
+ *
2416
+ * @see {@link successors} for outgoing neighbors in a directed graph
2417
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2418
+ *
2419
+ * @category queries
2420
+ * @since 4.0.0
2421
+ */
2422
+ export const predecessors: {
2423
+ /**
2424
+ * Returns the incoming neighbor node indices for a node in a directed graph.
2425
+ *
2426
+ * **When to use**
2427
+ *
2428
+ * Use when you need the nodes that reach a node by following incoming edges in a
2429
+ * directed graph.
2430
+ *
2431
+ * **Gotchas**
2432
+ *
2433
+ * Throws a `GraphError` when used with an undirected graph.
2434
+ *
2435
+ * @see {@link successors} for outgoing neighbors in a directed graph
2436
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2437
+ *
2438
+ * @category queries
2439
+ * @since 4.0.0
2440
+ */
2441
+ (nodeIndex: NodeIndex): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
2442
+ /**
2443
+ * Returns the incoming neighbor node indices for a node in a directed graph.
2444
+ *
2445
+ * **When to use**
2446
+ *
2447
+ * Use when you need the nodes that reach a node by following incoming edges in a
2448
+ * directed graph.
2449
+ *
2450
+ * **Gotchas**
2451
+ *
2452
+ * Throws a `GraphError` when used with an undirected graph.
2453
+ *
2454
+ * @see {@link successors} for outgoing neighbors in a directed graph
2455
+ * @see {@link neighbors} for generic neighbor lookup across graph kinds
2456
+ *
2457
+ * @category queries
2458
+ * @since 4.0.0
2459
+ */
2460
+ <N, E>(
2461
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2462
+ nodeIndex: NodeIndex
2463
+ ): Array<NodeIndex>
2464
+ } = dual(2, <N, E, T extends Kind = "directed">(
2465
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2466
+ nodeIndex: NodeIndex
2467
+ ): Array<NodeIndex> => {
2468
+ if (graph.type === "undirected") {
2469
+ throw new GraphError({ message: "Cannot get predecessors of undirected graph" })
2470
+ }
2471
+ return getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, "incoming")
2303
2472
  })
2304
2473
 
2305
2474
  /**
2306
- * Gets neighbors of a node in a specific direction for bidirectional traversal.
2475
+ * Gets directed neighbors of a node in a specific direction.
2476
+ *
2477
+ * **When to use**
2478
+ *
2479
+ * Use when maintaining existing code that already passes an explicit traversal
2480
+ * direction. New code should prefer `successors` or `predecessors`.
2481
+ *
2482
+ * **Gotchas**
2483
+ *
2484
+ * Throws a `GraphError` when used with an undirected graph.
2307
2485
  *
2308
2486
  * **Example** (Traversing directed neighbors)
2309
2487
  *
@@ -2326,12 +2504,24 @@ export const neighbors: {
2326
2504
  * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
2327
2505
  * ```
2328
2506
  *
2507
+ * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
2508
+ * @see {@link successors} for outgoing neighbors in a directed graph
2509
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2329
2510
  * @category queries
2330
2511
  * @since 3.18.0
2331
2512
  */
2332
2513
  export const neighborsDirected: {
2333
2514
  /**
2334
- * Gets neighbors of a node in a specific direction for bidirectional traversal.
2515
+ * Gets directed neighbors of a node in a specific direction.
2516
+ *
2517
+ * **When to use**
2518
+ *
2519
+ * Use when maintaining existing code that already passes an explicit traversal
2520
+ * direction. New code should prefer `successors` or `predecessors`.
2521
+ *
2522
+ * **Gotchas**
2523
+ *
2524
+ * Throws a `GraphError` when used with an undirected graph.
2335
2525
  *
2336
2526
  * **Example** (Traversing directed neighbors)
2337
2527
  *
@@ -2354,12 +2544,24 @@ export const neighborsDirected: {
2354
2544
  * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
2355
2545
  * ```
2356
2546
  *
2547
+ * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
2548
+ * @see {@link successors} for outgoing neighbors in a directed graph
2549
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2357
2550
  * @category queries
2358
2551
  * @since 3.18.0
2359
2552
  */
2360
- (nodeIndex: NodeIndex, direction: Direction): <N, E, T extends Kind = "directed">(graph: Graph<N, E, T> | MutableGraph<N, E, T>) => Array<NodeIndex>
2553
+ (nodeIndex: NodeIndex, direction: Direction): <N, E>(graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">) => Array<NodeIndex>
2361
2554
  /**
2362
- * Gets neighbors of a node in a specific direction for bidirectional traversal.
2555
+ * Gets directed neighbors of a node in a specific direction.
2556
+ *
2557
+ * **When to use**
2558
+ *
2559
+ * Use when maintaining existing code that already passes an explicit traversal
2560
+ * direction. New code should prefer `successors` or `predecessors`.
2561
+ *
2562
+ * **Gotchas**
2563
+ *
2564
+ * Throws a `GraphError` when used with an undirected graph.
2363
2565
  *
2364
2566
  * **Example** (Traversing directed neighbors)
2365
2567
  *
@@ -2382,11 +2584,14 @@ export const neighborsDirected: {
2382
2584
  * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
2383
2585
  * ```
2384
2586
  *
2587
+ * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
2588
+ * @see {@link successors} for outgoing neighbors in a directed graph
2589
+ * @see {@link predecessors} for incoming neighbors in a directed graph
2385
2590
  * @category queries
2386
2591
  * @since 3.18.0
2387
2592
  */
2388
- <N, E, T extends Kind = "directed">(
2389
- graph: Graph<N, E, T> | MutableGraph<N, E, T>,
2593
+ <N, E>(
2594
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
2390
2595
  nodeIndex: NodeIndex,
2391
2596
  direction: Direction
2392
2597
  ): Array<NodeIndex>
@@ -2395,28 +2600,10 @@ export const neighborsDirected: {
2395
2600
  nodeIndex: NodeIndex,
2396
2601
  direction: Direction
2397
2602
  ): Array<NodeIndex> => {
2398
- const adjacencyMap = direction === "incoming"
2399
- ? graph.reverseAdjacency
2400
- : graph.adjacency
2401
-
2402
- const adjacencyList = adjacencyMap.get(nodeIndex)
2403
- if (adjacencyList === undefined) {
2404
- return []
2405
- }
2406
-
2407
- const result: Array<NodeIndex> = []
2408
- for (const edgeIndex of adjacencyList) {
2409
- const edge = graph.edges.get(edgeIndex)
2410
- if (edge !== undefined) {
2411
- // For incoming direction, we want the source node instead of target
2412
- const neighborNode = direction === "incoming"
2413
- ? edge.source
2414
- : edge.target
2415
- result.push(neighborNode)
2416
- }
2603
+ if (graph.type === "undirected") {
2604
+ throw new GraphError({ message: "Cannot get directed neighbors of undirected graph" })
2417
2605
  }
2418
-
2419
- return result
2606
+ return getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, direction)
2420
2607
  })
2421
2608
 
2422
2609
  // =============================================================================
@@ -3603,7 +3790,11 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
3603
3790
  recursionStack.add(node)
3604
3791
 
3605
3792
  // Get neighbors for this node
3606
- const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing"))
3793
+ const nodeNeighbors = getDirectedNeighbors(
3794
+ graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
3795
+ node,
3796
+ "outgoing"
3797
+ )
3607
3798
  stack[stack.length - 1] = [node, nodeNeighbors, 0, false]
3608
3799
  continue
3609
3800
  }
@@ -3757,7 +3948,7 @@ const getTraversalNeighbors = <N, E, T extends Kind>(
3757
3948
  ): Array<NodeIndex> =>
3758
3949
  graph.type === "undirected"
3759
3950
  ? getUndirectedNeighbors(graph as any, nodeIndex)
3760
- : neighborsDirected(graph, nodeIndex, direction)
3951
+ : getDirectedNeighbors(graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">, nodeIndex, direction)
3761
3952
 
3762
3953
  const getTraversableNeighbor = <E, T extends Kind>(
3763
3954
  graph: Graph<unknown, E, T> | MutableGraph<unknown, E, T>,
@@ -3828,6 +4019,10 @@ export const connectedComponents = <N, E>(
3828
4019
  * Finds strongly connected components in a directed graph using Kosaraju's algorithm.
3829
4020
  * Each SCC is represented as an array of node indices.
3830
4021
  *
4022
+ * **Gotchas**
4023
+ *
4024
+ * Throws a `GraphError` when used with an undirected graph.
4025
+ *
3831
4026
  * **Example** (Finding strongly connected components)
3832
4027
  *
3833
4028
  * ```ts
@@ -3849,9 +4044,13 @@ export const connectedComponents = <N, E>(
3849
4044
  * @category algorithms
3850
4045
  * @since 3.18.0
3851
4046
  */
3852
- export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
3853
- graph: Graph<N, E, T> | MutableGraph<N, E, T>
4047
+ export const stronglyConnectedComponents = <N, E>(
4048
+ graph: Graph<N, E, "directed"> | MutableGraph<N, E, "directed">
3854
4049
  ): Array<Array<NodeIndex>> => {
4050
+ if ((graph as Graph<N, E, Kind> | MutableGraph<N, E, Kind>).type === "undirected") {
4051
+ throw new GraphError({ message: "Cannot find strongly connected components of undirected graph" })
4052
+ }
4053
+
3855
4054
  const visited = new Set<NodeIndex>()
3856
4055
  const finishOrder: Array<NodeIndex> = []
3857
4056
  // Iterate directly over node keys
@@ -3877,7 +4076,7 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
3877
4076
  }
3878
4077
 
3879
4078
  visited.add(node)
3880
- const nodeNeighborsList = neighbors(graph, node)
4079
+ const nodeNeighborsList = getDirectedNeighbors(graph, node, "outgoing")
3881
4080
  stack[stack.length - 1] = [node, nodeNeighborsList, 0, false]
3882
4081
  continue
3883
4082
  }
@@ -4010,6 +4209,22 @@ export interface DijkstraConfig<E> {
4010
4209
  cost: (edgeData: E) => number
4011
4210
  }
4012
4211
 
4212
+ const validateNonNegativeEdgeWeights = <N, E, T extends Kind = "directed">(
4213
+ graph: Graph<N, E, T> | MutableGraph<N, E, T>,
4214
+ cost: (edgeData: E) => number,
4215
+ algorithm: string
4216
+ ): Map<EdgeIndex, number> => {
4217
+ const edgeWeights = new Map<EdgeIndex, number>()
4218
+ for (const [edgeIndex, edgeData] of graph.edges) {
4219
+ const weight = cost(edgeData.data)
4220
+ if (weight < 0 || Number.isNaN(weight)) {
4221
+ throw new GraphError({ message: `${algorithm} requires non-negative edge weights` })
4222
+ }
4223
+ edgeWeights.set(edgeIndex, weight)
4224
+ }
4225
+ return edgeWeights
4226
+ }
4227
+
4013
4228
  /**
4014
4229
  * Finds the shortest path from the configured source node to the target node
4015
4230
  * using Dijkstra's algorithm.
@@ -4142,6 +4357,8 @@ export const dijkstra: {
4142
4357
  throw missingNode(config.target)
4143
4358
  }
4144
4359
 
4360
+ const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "Dijkstra's algorithm")
4361
+
4145
4362
  // Early return if source equals target
4146
4363
  if (config.source === config.target) {
4147
4364
  return Option.some({
@@ -4202,12 +4419,7 @@ export const dijkstra: {
4202
4419
  const edge = graph.edges.get(edgeIndex)
4203
4420
  if (edge !== undefined) {
4204
4421
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
4205
- const cost = config.cost(edge.data)
4206
-
4207
- // Validate non-negative weights
4208
- if (cost < 0) {
4209
- throw new GraphError({ message: "Dijkstra's algorithm requires non-negative edge weights" })
4210
- }
4422
+ const cost = edgeWeights.get(edgeIndex)!
4211
4423
 
4212
4424
  const newDistance = currentDistance + cost
4213
4425
  const neighborDistance = distances.get(neighbor)!
@@ -4538,7 +4750,7 @@ export interface AstarConfig<E, N> {
4538
4750
  * **Details**
4539
4751
  *
4540
4752
  * The edge-cost function must return non-negative weights, and the heuristic
4541
- * should be admissible to preserve shortest-path guarantees. Returns
4753
+ * should be consistent to preserve shortest-path guarantees. Returns
4542
4754
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4543
4755
  * when either endpoint is missing or a negative edge cost is encountered.
4544
4756
  *
@@ -4585,7 +4797,7 @@ export const astar: {
4585
4797
  * **Details**
4586
4798
  *
4587
4799
  * The edge-cost function must return non-negative weights, and the heuristic
4588
- * should be admissible to preserve shortest-path guarantees. Returns
4800
+ * should be consistent to preserve shortest-path guarantees. Returns
4589
4801
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4590
4802
  * when either endpoint is missing or a negative edge cost is encountered.
4591
4803
  *
@@ -4632,7 +4844,7 @@ export const astar: {
4632
4844
  * **Details**
4633
4845
  *
4634
4846
  * The edge-cost function must return non-negative weights, and the heuristic
4635
- * should be admissible to preserve shortest-path guarantees. Returns
4847
+ * should be consistent to preserve shortest-path guarantees. Returns
4636
4848
  * `Option.none()` when the target is not reachable, and throws a `GraphError`
4637
4849
  * when either endpoint is missing or a negative edge cost is encountered.
4638
4850
  *
@@ -4684,6 +4896,8 @@ export const astar: {
4684
4896
  throw missingNode(config.target)
4685
4897
  }
4686
4898
 
4899
+ const edgeWeights = validateNonNegativeEdgeWeights(graph, config.cost, "A* algorithm")
4900
+
4687
4901
  // Early return if source equals target
4688
4902
  if (config.source === config.target) {
4689
4903
  return Option.some({
@@ -4759,12 +4973,7 @@ export const astar: {
4759
4973
  const edge = graph.edges.get(edgeIndex)
4760
4974
  if (edge !== undefined) {
4761
4975
  const neighbor = getTraversableNeighbor(graph, currentNode, edge)
4762
- const weight = config.cost(edge.data)
4763
-
4764
- // Validate non-negative weights
4765
- if (weight < 0) {
4766
- throw new GraphError({ message: "A* algorithm requires non-negative edge weights" })
4767
- }
4976
+ const weight = edgeWeights.get(edgeIndex)!
4768
4977
 
4769
4978
  const tentativeGScore = currentGScore + weight
4770
4979
  const neighborGScore = gScore.get(neighbor)!
@@ -5697,14 +5906,17 @@ export const bfs: {
5697
5906
  *
5698
5907
  * **When to use**
5699
5908
  *
5700
- * Use to seed a topological sort with specific initial node indices instead of
5701
- * starting from every zero in-degree node.
5909
+ * Use to prioritize specific zero in-degree nodes in a topological sort.
5702
5910
  *
5703
5911
  * **Details**
5704
5912
  *
5705
- * `initials` optionally supplies the node indices used as initial queue
5706
- * entries. When omitted, topological sorting starts from all nodes with zero
5707
- * in-degree.
5913
+ * `initials` optionally supplies zero in-degree node indices used as
5914
+ * prioritized initial queue entries. Topological sorting still includes the
5915
+ * other zero in-degree nodes and produces a complete topological order.
5916
+ *
5917
+ * **Gotchas**
5918
+ *
5919
+ * Throws a `GraphError` when any initial node has incoming edges.
5708
5920
  *
5709
5921
  * @see {@link topo} for the iterator that consumes this configuration
5710
5922
  *
@@ -5882,6 +6094,7 @@ export const topo: {
5882
6094
  [Symbol.iterator]: () => {
5883
6095
  const inDegree = new Map<NodeIndex, number>()
5884
6096
  const remaining = new Set<NodeIndex>()
6097
+ const initialSet = new Set(initials)
5885
6098
  const queue = [...initials]
5886
6099
 
5887
6100
  // Initialize in-degree counts
@@ -5896,12 +6109,16 @@ export const topo: {
5896
6109
  inDegree.set(edgeData.target, currentInDegree + 1)
5897
6110
  }
5898
6111
 
5899
- // Add nodes with zero in-degree to queue if no initials provided
5900
- if (initials.length === 0) {
5901
- for (const [nodeIndex, degree] of inDegree) {
5902
- if (degree === 0) {
5903
- queue.push(nodeIndex)
5904
- }
6112
+ for (const nodeIndex of initials) {
6113
+ if (inDegree.get(nodeIndex)! !== 0) {
6114
+ throw new GraphError({ message: `Initial node ${nodeIndex} has incoming edges` })
6115
+ }
6116
+ }
6117
+
6118
+ // Add remaining zero in-degree nodes after prioritized initials.
6119
+ for (const [nodeIndex, degree] of inDegree) {
6120
+ if (degree === 0 && !initialSet.has(nodeIndex)) {
6121
+ queue.push(nodeIndex)
5905
6122
  }
5906
6123
  }
5907
6124
 
@@ -5913,7 +6130,11 @@ export const topo: {
5913
6130
  remaining.delete(current)
5914
6131
 
5915
6132
  // Process outgoing edges, reducing in-degree of targets
5916
- const neighbors = neighborsDirected(graph, current, "outgoing")
6133
+ const neighbors = getDirectedNeighbors(
6134
+ graph as Graph<N, E, "directed"> | MutableGraph<N, E, "directed">,
6135
+ current,
6136
+ "outgoing"
6137
+ )
5917
6138
  for (const neighbor of neighbors) {
5918
6139
  if (remaining.has(neighbor)) {
5919
6140
  const currentInDegree = inDegree.get(neighbor) || 0
package/src/Schema.ts CHANGED
@@ -13258,6 +13258,13 @@ export interface ToJsonSchemaOptions {
13258
13258
  * properties and synthesized check descriptions; it does not change the draft
13259
13259
  * target.
13260
13260
  *
13261
+ * **Gotchas**
13262
+ *
13263
+ * JSON Schema generation is best-effort. Some Effect schema semantics cannot
13264
+ * be represented exactly in JSON Schema, and importing an emitted JSON Schema
13265
+ * may produce an equivalent approximation rather than the original schema
13266
+ * shape.
13267
+ *
13261
13268
  * @category converting
13262
13269
  * @since 4.0.0
13263
13270
  */
@@ -2179,6 +2179,13 @@ export function toSchema<S extends Schema.Top = Schema.Top>(document: Document,
2179
2179
  * Use when you need to produce a standard JSON Schema document from a schema
2180
2180
  * representation `Document`.
2181
2181
  *
2182
+ * **Gotchas**
2183
+ *
2184
+ * JSON Schema generation is best-effort. Some Effect schema representation
2185
+ * semantics cannot be represented exactly in JSON Schema, and importing an
2186
+ * emitted JSON Schema may produce an equivalent approximation rather than the
2187
+ * original representation shape.
2188
+ *
2182
2189
  * **Example** (Generating JSON Schema)
2183
2190
  *
2184
2191
  * ```ts
@@ -2211,6 +2218,13 @@ export const toJsonSchemaDocument: (
2211
2218
  * Use when you need to export related schema representation documents together
2212
2219
  * so shared definitions stay in multi-document JSON Schema form.
2213
2220
  *
2221
+ * **Gotchas**
2222
+ *
2223
+ * JSON Schema generation is best-effort. Some Effect schema representation
2224
+ * semantics cannot be represented exactly in JSON Schema, and importing an
2225
+ * emitted JSON Schema may produce equivalent approximations rather than the
2226
+ * original representation shapes.
2227
+ *
2214
2228
  * @see {@link MultiDocument}
2215
2229
  * @see {@link toJsonSchemaDocument}
2216
2230
  * @see {@link fromJsonSchemaMultiDocument}
@@ -2962,6 +2976,11 @@ function toRuntimeRegExp(regExp: RegExp): string {
2962
2976
  *
2963
2977
  * **Gotchas**
2964
2978
  *
2979
+ * JSON Schema import is best-effort. Some JSON Schema constructs do not map
2980
+ * exactly to Effect schema representations, and importing a schema previously
2981
+ * emitted by `toJsonSchemaDocument` may produce an equivalent approximation
2982
+ * rather than the original representation shape.
2983
+ *
2965
2984
  * This throws if a `$ref` cannot be resolved within the document's definitions.
2966
2985
  * Circular `$ref`s are detected and cause an error.
2967
2986
  *
@@ -3002,6 +3021,11 @@ export function fromJsonSchemaDocument(document: JsonSchema.Document<"draft-2020
3002
3021
  *
3003
3022
  * **Gotchas**
3004
3023
  *
3024
+ * JSON Schema import is best-effort. Some JSON Schema constructs do not map
3025
+ * exactly to Effect schema representations, and importing schemas previously
3026
+ * emitted by `toJsonSchemaMultiDocument` may produce equivalent approximations
3027
+ * rather than the original representation shapes.
3028
+ *
3005
3029
  * This throws if a `$ref` cannot be resolved.
3006
3030
  *
3007
3031
  * @see {@link MultiDocument}
package/src/Stream.ts CHANGED
@@ -1416,7 +1416,7 @@ export const fromReadableStream = <A, E>(
1416
1416
  scope,
1417
1417
  options.releaseLockOnEnd
1418
1418
  ? Effect.sync(() => reader.releaseLock())
1419
- : Effect.promise(() => reader.cancel())
1419
+ : Effect.promise(() => reader.cancel().catch(constVoid))
1420
1420
  )
1421
1421
  return Effect.flatMap(
1422
1422
  Effect.tryPromise({
@@ -362,8 +362,9 @@ export function toJsonSchemaMultiDocument(
362
362
  switch (schema._tag) {
363
363
  case "Any":
364
364
  case "Unknown":
365
- case "ObjectKeyword":
366
365
  return {}
366
+ case "ObjectKeyword":
367
+ return { anyOf: [{ type: "object" }, { type: "array" }] }
367
368
  case "Void":
368
369
  case "Undefined":
369
370
  return { type: "null" }