opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8

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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * WAM Context Graph — Internal structured representation of task relationships.
3
+ *
4
+ * Represents tasks, outputs, evidence, decisions, constraints, and their typed
5
+ * relationships. Storage-independent, in-memory implementation for now.
6
+ *
7
+ * Invariant: A task depends on an output from another task, not on the entire
8
+ * context of that task.
9
+ */
10
+
11
+ // -- Types --
12
+
13
+ /**
14
+ * @typedef {string} ContextNodeType
15
+ * @typedef {string} ContextEdgeType
16
+ */
17
+
18
+ /** @type {ContextNodeType[]} */
19
+ export const NODE_TYPES = [
20
+ "task", "subtask", "requirement", "claim", "action",
21
+ "observation", "evidence", "decision", "constraint", "artifact", "output"
22
+ ];
23
+
24
+ /** @type {ContextEdgeType[]} */
25
+ export const EDGE_TYPES = [
26
+ "requires_completion", "requires_output", "requires_evidence",
27
+ "requires_decision", "depends_on_artifact", "produces",
28
+ "supports", "contradicts", "invalidates", "related_to"
29
+ ];
30
+
31
+ /**
32
+ * @typedef {Object} ContextNode
33
+ * @property {string} id
34
+ * @property {ContextNodeType} type
35
+ * @property {string} content
36
+ * @property {string} [taskId]
37
+ * @property {boolean} verified
38
+ * @property {number} createdAt
39
+ * @property {number} updatedAt
40
+ * @property {Record<string, unknown>} [metadata]
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} ContextEdge
45
+ * @property {string} from
46
+ * @property {string} to
47
+ * @property {ContextEdgeType} type
48
+ * @property {number} [weight]
49
+ */
50
+
51
+ // -- Validation --
52
+
53
+ function validateNode(node) {
54
+ if (!node || typeof node !== "object") throw new Error("Node must be an object");
55
+ if (!node.id || typeof node.id !== "string") throw new Error("Node id required");
56
+ if (!NODE_TYPES.includes(node.type)) throw new Error(`Invalid node type: ${node.type}`);
57
+ if (typeof node.content !== "string") throw new Error("Node content required");
58
+ return true;
59
+ }
60
+
61
+ function validateEdge(edge) {
62
+ if (!edge || typeof edge !== "object") throw new Error("Edge must be an object");
63
+ if (!edge.from || typeof edge.from !== "string") throw new Error("Edge from required");
64
+ if (!edge.to || typeof edge.to !== "string") throw new Error("Edge to required");
65
+ if (!EDGE_TYPES.includes(edge.type)) throw new Error(`Invalid edge type: ${edge.type}`);
66
+ return true;
67
+ }
68
+
69
+ // -- ContextGraph --
70
+
71
+ export class ContextGraph {
72
+ constructor() {
73
+ /** @type {Map<string, ContextNode>} */
74
+ this.nodes = new Map();
75
+ /** @type {Map<string, ContextEdge[]>} */
76
+ this.edgesFrom = new Map();
77
+ /** @type {Map<string, ContextEdge[]>} */
78
+ this.edgesTo = new Map();
79
+ }
80
+
81
+ // -- Node operations --
82
+
83
+ /**
84
+ * Add a node to the graph.
85
+ * @param {ContextNode} node
86
+ * @returns {ContextNode}
87
+ */
88
+ addNode(node) {
89
+ validateNode(node);
90
+ const now = Date.now();
91
+ const existing = this.nodes.get(node.id);
92
+ const entry = {
93
+ ...node,
94
+ createdAt: existing?.createdAt || now,
95
+ updatedAt: now,
96
+ };
97
+ this.nodes.set(node.id, entry);
98
+ return entry;
99
+ }
100
+
101
+ /**
102
+ * Get a node by id.
103
+ * @param {string} id
104
+ * @returns {ContextNode | undefined}
105
+ */
106
+ getNode(id) {
107
+ return this.nodes.get(id);
108
+ }
109
+
110
+ /**
111
+ * Check if a node exists.
112
+ * @param {string} id
113
+ * @returns {boolean}
114
+ */
115
+ hasNode(id) {
116
+ return this.nodes.has(id);
117
+ }
118
+
119
+ /**
120
+ * Get all nodes, optionally filtered by type.
121
+ * @param {ContextNodeType} [type]
122
+ * @returns {ContextNode[]}
123
+ */
124
+ getNodes(type) {
125
+ const all = [...this.nodes.values()];
126
+ return type ? all.filter((n) => n.type === type) : all;
127
+ }
128
+
129
+ /**
130
+ * Update a node's properties.
131
+ * @param {string} id
132
+ * @param {Partial<ContextNode>} patch
133
+ * @returns {ContextNode | null}
134
+ */
135
+ updateNode(id, patch) {
136
+ const node = this.nodes.get(id);
137
+ if (!node) return null;
138
+ const updated = { ...node, ...patch, id: node.id, updatedAt: Date.now() };
139
+ this.nodes.set(id, updated);
140
+ return updated;
141
+ }
142
+
143
+ /**
144
+ * Remove a node and all its edges.
145
+ * @param {string} id
146
+ * @returns {boolean}
147
+ */
148
+ removeNode(id) {
149
+ const existed = this.nodes.delete(id);
150
+ if (!existed) return false;
151
+
152
+ // Remove all edges involving this node
153
+ const outgoing = this.edgesFrom.get(id) || [];
154
+ for (const edge of outgoing) {
155
+ this._removeFromEdgesTo(edge);
156
+ }
157
+ this.edgesFrom.delete(id);
158
+
159
+ const incoming = this.edgesTo.get(id) || [];
160
+ for (const edge of incoming) {
161
+ this._removeFromEdgesFrom(edge);
162
+ }
163
+ this.edgesTo.delete(id);
164
+
165
+ return true;
166
+ }
167
+
168
+ // -- Edge operations --
169
+
170
+ /**
171
+ * Add an edge to the graph.
172
+ * @param {ContextEdge} edge
173
+ * @returns {ContextEdge}
174
+ */
175
+ addEdge(edge) {
176
+ validateEdge(edge);
177
+ if (!this.nodes.has(edge.from)) throw new Error(`Source node ${edge.from} not found`);
178
+ if (!this.nodes.has(edge.to)) throw new Error(`Target node ${edge.to} not found`);
179
+
180
+ const entry = { ...edge, weight: edge.weight ?? 1.0 };
181
+
182
+ // Check for duplicate
183
+ const fromEdges = this.edgesFrom.get(edge.from) || [];
184
+ const exists = fromEdges.some(
185
+ (e) => e.to === edge.to && e.type === edge.type
186
+ );
187
+ if (exists) return entry;
188
+
189
+ fromEdges.push(entry);
190
+ this.edgesFrom.set(edge.from, fromEdges);
191
+
192
+ const toEdges = this.edgesTo.get(edge.to) || [];
193
+ toEdges.push(entry);
194
+ this.edgesTo.set(edge.to, toEdges);
195
+
196
+ return entry;
197
+ }
198
+
199
+ /**
200
+ * Get all edges from a node.
201
+ * @param {string} nodeId
202
+ * @returns {ContextEdge[]}
203
+ */
204
+ getEdgesFrom(nodeId) {
205
+ return this.edgesFrom.get(nodeId) || [];
206
+ }
207
+
208
+ /**
209
+ * Get all edges to a node.
210
+ * @param {string} nodeId
211
+ * @returns {ContextEdge[]}
212
+ */
213
+ getEdgesTo(nodeId) {
214
+ return this.edgesTo.get(nodeId) || [];
215
+ }
216
+
217
+ /**
218
+ * Get edges of a specific type from a node.
219
+ * @param {string} nodeId
220
+ * @param {ContextEdgeType} type
221
+ * @returns {ContextEdge[]}
222
+ */
223
+ getEdgesFromByType(nodeId, type) {
224
+ return this.getEdgesFrom(nodeId).filter((e) => e.type === type);
225
+ }
226
+
227
+ /**
228
+ * Get edges of a specific type to a node.
229
+ * @param {string} nodeId
230
+ * @param {ContextEdgeType} type
231
+ * @returns {ContextEdge[]}
232
+ */
233
+ getEdgesToByType(nodeId, type) {
234
+ return this.getEdgesTo(nodeId).filter((e) => e.type === type);
235
+ }
236
+
237
+ // -- Traversal --
238
+
239
+ /**
240
+ * Get direct dependencies of a node (nodes it requires).
241
+ * @param {string} nodeId
242
+ * @returns {ContextNode[]}
243
+ */
244
+ getDependencies(nodeId) {
245
+ const depEdges = this.getEdgesFromByType(nodeId, "requires_output")
246
+ .concat(this.getEdgesFromByType(nodeId, "requires_completion"))
247
+ .concat(this.getEdgesFromByType(nodeId, "requires_evidence"))
248
+ .concat(this.getEdgesFromByType(nodeId, "requires_decision"))
249
+ .concat(this.getEdgesFromByType(nodeId, "depends_on_artifact"));
250
+
251
+ return depEdges
252
+ .map((e) => this.nodes.get(e.to))
253
+ .filter(Boolean);
254
+ }
255
+
256
+ /**
257
+ * Get upstream context (what this node depends on, transitively).
258
+ * @param {string} nodeId
259
+ * @param {number} [maxDepth=10]
260
+ * @returns {ContextNode[]}
261
+ */
262
+ getUpstream(nodeId, maxDepth = 10) {
263
+ const visited = new Set();
264
+ const result = [];
265
+
266
+ const traverse = (id, depth) => {
267
+ if (depth >= maxDepth || visited.has(id)) return;
268
+ visited.add(id);
269
+
270
+ const deps = this.getDependencies(id);
271
+ for (const dep of deps) {
272
+ if (!visited.has(dep.id)) {
273
+ result.push(dep);
274
+ traverse(dep.id, depth + 1);
275
+ }
276
+ }
277
+
278
+ // Also traverse through "produces" edges to find tasks that produce this node
279
+ const producers = this.getEdgesToByType(id, "produces");
280
+ for (const edge of producers) {
281
+ if (!visited.has(edge.from)) {
282
+ const producerNode = this.nodes.get(edge.from);
283
+ if (producerNode) {
284
+ result.push(producerNode);
285
+ traverse(edge.from, depth + 1);
286
+ }
287
+ }
288
+ }
289
+ };
290
+
291
+ traverse(nodeId, 0);
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * Get downstream context (what depends on this node, transitively).
297
+ * @param {string} nodeId
298
+ * @param {number} [maxDepth=10]
299
+ * @returns {ContextNode[]}
300
+ */
301
+ getDownstream(nodeId, maxDepth = 10) {
302
+ const visited = new Set();
303
+ const result = [];
304
+
305
+ const traverse = (id, depth) => {
306
+ if (depth > maxDepth || visited.has(id)) return;
307
+ visited.add(id);
308
+
309
+ const dependents = this.getEdgesTo(id)
310
+ .filter((e) => ["requires_output", "requires_completion", "depends_on_artifact"].includes(e.type));
311
+
312
+ for (const edge of dependents) {
313
+ if (!visited.has(edge.from)) {
314
+ const node = this.nodes.get(edge.from);
315
+ if (node) {
316
+ result.push(node);
317
+ traverse(edge.from, depth + 1);
318
+ }
319
+ }
320
+ }
321
+ };
322
+
323
+ traverse(nodeId, 0);
324
+ return result;
325
+ }
326
+
327
+ /**
328
+ * Get nodes that are invalidated by a given node.
329
+ * @param {string} nodeId
330
+ * @returns {ContextNode[]}
331
+ */
332
+ getInvalidatedBy(nodeId) {
333
+ return this.getEdgesFromByType(nodeId, "invalidates")
334
+ .map((e) => this.nodes.get(e.to))
335
+ .filter(Boolean);
336
+ }
337
+
338
+ /**
339
+ * Get nodes that contradict a given node.
340
+ * @param {string} nodeId
341
+ * @returns {ContextNode[]}
342
+ */
343
+ getContradicting(nodeId) {
344
+ const fromContradictions = this.getEdgesFromByType(nodeId, "contradicts")
345
+ .map((e) => this.nodes.get(e.to));
346
+ const toContradictions = this.getEdgesToByType(nodeId, "contradicts")
347
+ .map((e) => this.nodes.get(e.from));
348
+
349
+ return [...fromContradictions, ...toContradictions].filter(Boolean);
350
+ }
351
+
352
+ // -- Validation --
353
+
354
+ /**
355
+ * Validate graph consistency.
356
+ * @returns {{ valid: boolean, errors: string[] }}
357
+ */
358
+ validate() {
359
+ const errors = [];
360
+
361
+ // Check all edges reference existing nodes
362
+ for (const [fromId, edges] of this.edgesFrom) {
363
+ if (!this.nodes.has(fromId)) {
364
+ errors.push(`Edge source ${fromId} not in nodes`);
365
+ }
366
+ for (const edge of edges) {
367
+ if (!this.nodes.has(edge.to)) {
368
+ errors.push(`Edge target ${edge.to} not in nodes`);
369
+ }
370
+ }
371
+ }
372
+
373
+ // Check for cycles in dependency edges
374
+ const visited = new Set();
375
+ const inStack = new Set();
376
+
377
+ const hasCycle = (nodeId) => {
378
+ if (inStack.has(nodeId)) return true;
379
+ if (visited.has(nodeId)) return false;
380
+
381
+ visited.add(nodeId);
382
+ inStack.add(nodeId);
383
+
384
+ const deps = this.getEdgesFromByType(nodeId, "requires_output")
385
+ .concat(this.getEdgesFromByType(nodeId, "depends_on_artifact"));
386
+
387
+ for (const edge of deps) {
388
+ if (hasCycle(edge.to)) return true;
389
+ }
390
+
391
+ inStack.delete(nodeId);
392
+ return false;
393
+ };
394
+
395
+ for (const nodeId of this.nodes.keys()) {
396
+ if (hasCycle(nodeId)) {
397
+ errors.push(`Cycle detected involving node ${nodeId}`);
398
+ break;
399
+ }
400
+ }
401
+
402
+ return { valid: errors.length === 0, errors };
403
+ }
404
+
405
+ /**
406
+ * Get graph statistics.
407
+ * @returns {{ nodeCount: number, edgeCount: number, nodesByType: Record<string, number>, edgesByType: Record<string, number> }}
408
+ */
409
+ stats() {
410
+ const nodesByType = {};
411
+ for (const node of this.nodes.values()) {
412
+ nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
413
+ }
414
+
415
+ const edgesByType = {};
416
+ let edgeCount = 0;
417
+ for (const edges of this.edgesFrom.values()) {
418
+ edgeCount += edges.length;
419
+ for (const edge of edges) {
420
+ edgesByType[edge.type] = (edgesByType[edge.type] || 0) + 1;
421
+ }
422
+ }
423
+
424
+ return {
425
+ nodeCount: this.nodes.size,
426
+ edgeCount,
427
+ nodesByType,
428
+ edgesByType,
429
+ };
430
+ }
431
+
432
+ // -- Internal helpers --
433
+
434
+ _removeFromEdgesTo(edge) {
435
+ const toEdges = this.edgesTo.get(edge.to);
436
+ if (!toEdges) return;
437
+ const idx = toEdges.findIndex(
438
+ (e) => e.from === edge.from && e.type === edge.type
439
+ );
440
+ if (idx !== -1) toEdges.splice(idx, 1);
441
+ }
442
+
443
+ _removeFromEdgesFrom(edge) {
444
+ const fromEdges = this.edgesFrom.get(edge.from);
445
+ if (!fromEdges) return;
446
+ const idx = fromEdges.findIndex(
447
+ (e) => e.to === edge.to && e.type === edge.type
448
+ );
449
+ if (idx !== -1) fromEdges.splice(idx, 1);
450
+ }
451
+ }