@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.3

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.
@@ -28,6 +28,64 @@ export interface TopoIssue {
28
28
  // Validators
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
+ const WHITE = 0;
32
+ const GRAY = 1;
33
+ const BLACK = 2;
34
+
35
+ /** Build an adjacency list and initial color map from hikes. */
36
+ const buildFollowGraph = (
37
+ hikes: ReadonlyMap<string, AnyHike>
38
+ ): {
39
+ graph: Map<string, readonly string[]>;
40
+ color: Map<string, number>;
41
+ } => {
42
+ const graph = new Map<string, readonly string[]>();
43
+ for (const [id, h] of hikes) {
44
+ graph.set(id, h.follows);
45
+ }
46
+ const color = new Map<string, number>();
47
+ for (const id of graph.keys()) {
48
+ color.set(id, WHITE);
49
+ }
50
+ return { color, graph };
51
+ };
52
+
53
+ /** Detect multi-node cycles in the hike follow graph via DFS. */
54
+ const detectFollowCycles = (
55
+ hikes: ReadonlyMap<string, AnyHike>
56
+ ): TopoIssue[] => {
57
+ const issues: TopoIssue[] = [];
58
+ const { color, graph } = buildFollowGraph(hikes);
59
+
60
+ const dfs = (node: string, path: string[]): void => {
61
+ color.set(node, GRAY);
62
+ for (const next of graph.get(node) ?? []) {
63
+ if (!graph.has(next)) {
64
+ continue;
65
+ }
66
+ const c = color.get(next) ?? WHITE;
67
+ if (c === GRAY) {
68
+ const cycle = [...path.slice(path.indexOf(next)), next];
69
+ issues.push({
70
+ message: `Cycle detected: ${cycle.join(' → ')}`,
71
+ rule: 'follow-cycle',
72
+ trailId: next,
73
+ });
74
+ } else if (c === WHITE) {
75
+ dfs(next, [...path, next]);
76
+ }
77
+ }
78
+ color.set(node, BLACK);
79
+ };
80
+
81
+ for (const id of graph.keys()) {
82
+ if (color.get(id) === WHITE) {
83
+ dfs(id, [id]);
84
+ }
85
+ }
86
+ return issues;
87
+ };
88
+
31
89
  const checkFollows = (
32
90
  hikes: ReadonlyMap<string, AnyHike>,
33
91
  topo: Topo
@@ -50,6 +108,7 @@ const checkFollows = (
50
108
  }
51
109
  }
52
110
  }
111
+ issues.push(...detectFollowCycles(hikes));
53
112
  return issues;
54
113
  };
55
114
 
@@ -66,7 +125,7 @@ const checkOneExample = (
66
125
  ): TopoIssue[] => {
67
126
  const issues: TopoIssue[] = [];
68
127
  const result = validateInput(inputSchema as AnyTrail['input'], example.input);
69
- if (result.isErr() && example.error === undefined) {
128
+ if (result.isErr() && example.error !== 'ValidationError') {
70
129
  issues.push({
71
130
  message: `Example "${example.name}" input does not parse against schema`,
72
131
  rule: 'example-input-valid',