@mhingston5/lasso 0.1.0

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 (124) hide show
  1. package/README.md +707 -0
  2. package/docs/agent-wrangling.png +0 -0
  3. package/package.json +26 -0
  4. package/src/capabilities/matcher.ts +25 -0
  5. package/src/capabilities/registry.ts +103 -0
  6. package/src/capabilities/types.ts +15 -0
  7. package/src/cir/lower.ts +253 -0
  8. package/src/cir/optimize.ts +251 -0
  9. package/src/cir/types.ts +131 -0
  10. package/src/cir/validate.ts +265 -0
  11. package/src/compiler/compile.ts +601 -0
  12. package/src/compiler/feedback.ts +471 -0
  13. package/src/compiler/runtime-helpers.ts +455 -0
  14. package/src/composition/chain.ts +58 -0
  15. package/src/composition/conditional.ts +76 -0
  16. package/src/composition/parallel.ts +75 -0
  17. package/src/composition/types.ts +105 -0
  18. package/src/environment/analyzer.ts +56 -0
  19. package/src/environment/discovery.ts +179 -0
  20. package/src/environment/types.ts +68 -0
  21. package/src/failures/classifiers.ts +134 -0
  22. package/src/failures/generator.ts +421 -0
  23. package/src/failures/map-reference-failures.ts +23 -0
  24. package/src/failures/ontology.ts +210 -0
  25. package/src/failures/recovery.ts +214 -0
  26. package/src/failures/types.ts +14 -0
  27. package/src/index.ts +67 -0
  28. package/src/memory/advisor.ts +132 -0
  29. package/src/memory/extractor.ts +166 -0
  30. package/src/memory/store.ts +107 -0
  31. package/src/memory/types.ts +53 -0
  32. package/src/metaharness/engine.ts +256 -0
  33. package/src/metaharness/predictor.ts +168 -0
  34. package/src/metaharness/types.ts +40 -0
  35. package/src/mutation/derive.ts +308 -0
  36. package/src/mutation/diff.ts +52 -0
  37. package/src/mutation/engine.ts +256 -0
  38. package/src/mutation/types.ts +84 -0
  39. package/src/pi/command-input.ts +209 -0
  40. package/src/pi/commands.ts +351 -0
  41. package/src/pi/extension.ts +16 -0
  42. package/src/planner/synthesize.ts +83 -0
  43. package/src/planner/template-rules.ts +183 -0
  44. package/src/planner/types.ts +42 -0
  45. package/src/reference/catalog.ts +128 -0
  46. package/src/reference/patch-validation-strategies.ts +170 -0
  47. package/src/reference/patch-validation.ts +174 -0
  48. package/src/reference/pr-review-merge.ts +155 -0
  49. package/src/reference/strategies.ts +126 -0
  50. package/src/reference/types.ts +33 -0
  51. package/src/replanner/risk-rules.ts +161 -0
  52. package/src/replanner/runtime.ts +308 -0
  53. package/src/replanner/synthesize.ts +619 -0
  54. package/src/replanner/types.ts +73 -0
  55. package/src/spec/schema.ts +254 -0
  56. package/src/spec/types.ts +319 -0
  57. package/src/spec/validate.ts +296 -0
  58. package/src/state/snapshots.ts +43 -0
  59. package/src/state/types.ts +12 -0
  60. package/src/synthesis/graph-builder.ts +267 -0
  61. package/src/synthesis/harness-builder.ts +113 -0
  62. package/src/synthesis/intent-ir.ts +63 -0
  63. package/src/synthesis/policy-builder.ts +320 -0
  64. package/src/synthesis/risk-analyzer.ts +182 -0
  65. package/src/synthesis/skill-parser.ts +441 -0
  66. package/src/verification/engine.ts +230 -0
  67. package/src/versioning/file-store.ts +103 -0
  68. package/src/versioning/history.ts +43 -0
  69. package/src/versioning/store.ts +16 -0
  70. package/src/versioning/types.ts +31 -0
  71. package/test/capabilities/matcher.test.ts +67 -0
  72. package/test/capabilities/registry.test.ts +136 -0
  73. package/test/capabilities/synthesis.test.ts +264 -0
  74. package/test/cir/lower.test.ts +417 -0
  75. package/test/cir/optimize.test.ts +266 -0
  76. package/test/cir/validate.test.ts +368 -0
  77. package/test/compiler/adaptive-runtime.test.ts +157 -0
  78. package/test/compiler/compile.test.ts +1198 -0
  79. package/test/compiler/feedback.test.ts +784 -0
  80. package/test/compiler/guardrails.test.ts +191 -0
  81. package/test/compiler/trace.test.ts +404 -0
  82. package/test/composition/chain.test.ts +328 -0
  83. package/test/composition/conditional.test.ts +241 -0
  84. package/test/composition/parallel.test.ts +215 -0
  85. package/test/environment/analyzer.test.ts +204 -0
  86. package/test/environment/discovery.test.ts +149 -0
  87. package/test/failures/classifiers.test.ts +287 -0
  88. package/test/failures/generator.test.ts +203 -0
  89. package/test/failures/ontology.test.ts +439 -0
  90. package/test/failures/recovery.test.ts +300 -0
  91. package/test/helpers/createFixtureRepo.ts +84 -0
  92. package/test/helpers/createPatchValidationFixture.ts +144 -0
  93. package/test/helpers/runCompiledWorkflow.ts +208 -0
  94. package/test/memory/advisor.test.ts +332 -0
  95. package/test/memory/extractor.test.ts +295 -0
  96. package/test/memory/store.test.ts +244 -0
  97. package/test/metaharness/engine.test.ts +575 -0
  98. package/test/metaharness/predictor.test.ts +436 -0
  99. package/test/mutation/derive-failure.test.ts +209 -0
  100. package/test/mutation/engine.test.ts +622 -0
  101. package/test/package-smoke.test.ts +29 -0
  102. package/test/pi/command-input.test.ts +153 -0
  103. package/test/pi/commands.test.ts +623 -0
  104. package/test/planner/classify-template.test.ts +32 -0
  105. package/test/planner/synthesize.test.ts +901 -0
  106. package/test/reference/PatchValidation.failures.test.ts +137 -0
  107. package/test/reference/PatchValidation.test.ts +326 -0
  108. package/test/reference/PrReviewMerge.failures.test.ts +121 -0
  109. package/test/reference/PrReviewMerge.test.ts +55 -0
  110. package/test/reference/catalog-open.test.ts +70 -0
  111. package/test/replanner/runtime.test.ts +207 -0
  112. package/test/replanner/synthesize.test.ts +303 -0
  113. package/test/spec/validate.test.ts +1056 -0
  114. package/test/state/snapshots.test.ts +264 -0
  115. package/test/synthesis/custom-workflow.test.ts +264 -0
  116. package/test/synthesis/graph-builder.test.ts +370 -0
  117. package/test/synthesis/harness-builder.test.ts +128 -0
  118. package/test/synthesis/policy-builder.test.ts +149 -0
  119. package/test/synthesis/risk-analyzer.test.ts +230 -0
  120. package/test/synthesis/skill-parser.test.ts +796 -0
  121. package/test/verification/engine.test.ts +509 -0
  122. package/test/versioning/history.test.ts +144 -0
  123. package/test/versioning/store.test.ts +254 -0
  124. package/vitest.config.ts +9 -0
@@ -0,0 +1,436 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { HarnessSpec } from "../../src/spec/types.js";
3
+ import type { EnvironmentModel } from "../../src/environment/types.js";
4
+ import type { FailureSignature } from "../../src/failures/ontology.js";
5
+ import { predictFailuresFromEnvironment } from "../../src/metaharness/predictor.js";
6
+
7
+ function makeMinimalSpec(overrides?: Partial<HarnessSpec>): HarnessSpec {
8
+ return {
9
+ name: "test-harness",
10
+ graph: {
11
+ entryNodeId: "node-a",
12
+ nodes: [
13
+ {
14
+ id: "node-a",
15
+ label: "Node A",
16
+ kind: "tool",
17
+ tool: "bash",
18
+ args: ["echo hello"],
19
+ },
20
+ ],
21
+ edges: [],
22
+ },
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ function makeMinimalEnv(overrides?: Partial<EnvironmentModel>): EnvironmentModel {
28
+ return {
29
+ tools: [
30
+ { name: "bash", version: "5.0", available: true },
31
+ { name: "git", version: "2.39", available: true },
32
+ { name: "node", version: "20.0", available: true },
33
+ ],
34
+ resources: [
35
+ { name: "disk", type: "disk", available: true, limit: "500GB", usage: "45%" },
36
+ ],
37
+ constraints: [],
38
+ authState: [],
39
+ externalSystems: [],
40
+ discoveredAt: Date.now(),
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ function makeFailureSignature(
46
+ cls: FailureSignature["class"],
47
+ overrides?: Partial<FailureSignature>,
48
+ ): FailureSignature {
49
+ return {
50
+ class: cls,
51
+ confidence: 0.9,
52
+ evidence: ["test evidence"],
53
+ suggestedRecovery: [],
54
+ retryable: false,
55
+ requiresHumanIntervention: false,
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ describe("predictFailuresFromEnvironment", () => {
61
+ describe("tool availability checks", () => {
62
+ it("returns no failures when all required tools are available", () => {
63
+ const spec = makeMinimalSpec();
64
+ const env = makeMinimalEnv();
65
+
66
+ const failures = predictFailuresFromEnvironment(spec, env);
67
+
68
+ expect(failures.length).toBe(0);
69
+ });
70
+
71
+ it("predicts tool failure when required tool is missing", () => {
72
+ const spec: HarnessSpec = {
73
+ name: "test-harness",
74
+ graph: {
75
+ entryNodeId: "node-a",
76
+ nodes: [
77
+ {
78
+ id: "node-a",
79
+ label: "Node A",
80
+ kind: "tool",
81
+ tool: "kubectl",
82
+ args: ["get pods"],
83
+ },
84
+ ],
85
+ edges: [],
86
+ },
87
+ };
88
+ const env = makeMinimalEnv();
89
+
90
+ const failures = predictFailuresFromEnvironment(spec, env);
91
+
92
+ expect(failures.length).toBeGreaterThan(0);
93
+ expect(failures.some(f => f.class === "tool")).toBe(true);
94
+ });
95
+
96
+ it("predicts failures for multiple nodes with missing tools", () => {
97
+ const spec: HarnessSpec = {
98
+ name: "test-harness",
99
+ graph: {
100
+ entryNodeId: "node-a",
101
+ nodes: [
102
+ {
103
+ id: "node-a",
104
+ label: "Node A",
105
+ kind: "tool",
106
+ tool: "kubectl",
107
+ args: ["get pods"],
108
+ },
109
+ {
110
+ id: "node-b",
111
+ label: "Node B",
112
+ kind: "tool",
113
+ tool: "docker",
114
+ args: ["ps"],
115
+ },
116
+ ],
117
+ edges: [{ from: "node-a", to: "node-b" }],
118
+ },
119
+ };
120
+ const env = makeMinimalEnv();
121
+
122
+ const failures = predictFailuresFromEnvironment(spec, env);
123
+
124
+ expect(failures.length).toBeGreaterThanOrEqual(2);
125
+ });
126
+
127
+ it("includes node ID in evidence for tool failures", () => {
128
+ const spec: HarnessSpec = {
129
+ name: "test-harness",
130
+ graph: {
131
+ entryNodeId: "node-a",
132
+ nodes: [
133
+ {
134
+ id: "deploy-step",
135
+ label: "Deploy",
136
+ kind: "tool",
137
+ tool: "kubectl",
138
+ args: ["apply -f deploy.yaml"],
139
+ },
140
+ ],
141
+ edges: [],
142
+ },
143
+ };
144
+ const env = makeMinimalEnv();
145
+
146
+ const failures = predictFailuresFromEnvironment(spec, env);
147
+
148
+ const toolFailure = failures.find(f => f.class === "tool");
149
+ expect(toolFailure).toBeDefined();
150
+ expect(toolFailure!.evidence.some(e => e.includes("deploy-step"))).toBe(true);
151
+ });
152
+ });
153
+
154
+ describe("constraint-based predictions", () => {
155
+ it("predicts failures from high-severity constraints", () => {
156
+ const spec = makeMinimalSpec();
157
+ const env = makeMinimalEnv({
158
+ constraints: [
159
+ {
160
+ type: "permission",
161
+ description: "No write access to /etc",
162
+ severity: "high",
163
+ },
164
+ ],
165
+ });
166
+
167
+ const failures = predictFailuresFromEnvironment(spec, env);
168
+
169
+ expect(failures.length).toBeGreaterThan(0);
170
+ });
171
+
172
+ it("includes constraint description in evidence", () => {
173
+ const spec = makeMinimalSpec();
174
+ const env = makeMinimalEnv({
175
+ constraints: [
176
+ {
177
+ type: "auth",
178
+ description: "GitHub API rate limited",
179
+ severity: "high",
180
+ },
181
+ ],
182
+ });
183
+
184
+ const failures = predictFailuresFromEnvironment(spec, env);
185
+
186
+ expect(failures.some(f =>
187
+ f.evidence.some(e => e.includes("GitHub API rate limited")),
188
+ )).toBe(true);
189
+ });
190
+
191
+ it("does not predict failures for low-severity constraints", () => {
192
+ const spec = makeMinimalSpec();
193
+ const env = makeMinimalEnv({
194
+ constraints: [
195
+ {
196
+ type: "network",
197
+ description: "High latency to remote API",
198
+ severity: "low",
199
+ },
200
+ ],
201
+ });
202
+
203
+ const failures = predictFailuresFromEnvironment(spec, env);
204
+
205
+ expect(failures.length).toBe(0);
206
+ });
207
+ });
208
+
209
+ describe("auth state predictions", () => {
210
+ it("predicts auth failure when authentication is missing", () => {
211
+ const spec: HarnessSpec = {
212
+ name: "test-harness",
213
+ graph: {
214
+ entryNodeId: "node-a",
215
+ nodes: [
216
+ {
217
+ id: "node-a",
218
+ label: "Push to remote",
219
+ kind: "tool",
220
+ tool: "git",
221
+ args: ["push origin main"],
222
+ },
223
+ ],
224
+ edges: [],
225
+ },
226
+ };
227
+ const env = makeMinimalEnv({
228
+ authState: [
229
+ { system: "github", authenticated: false },
230
+ ],
231
+ });
232
+
233
+ const failures = predictFailuresFromEnvironment(spec, env);
234
+
235
+ expect(failures.some(f => f.class === "auth")).toBe(true);
236
+ });
237
+
238
+ it("does not predict auth failure when authenticated", () => {
239
+ const spec = makeMinimalSpec();
240
+ const env = makeMinimalEnv({
241
+ authState: [
242
+ { system: "github", authenticated: true },
243
+ ],
244
+ });
245
+
246
+ const failures = predictFailuresFromEnvironment(spec, env);
247
+
248
+ expect(failures.some(f => f.class === "auth")).toBe(false);
249
+ });
250
+ });
251
+
252
+ describe("resource constraint predictions", () => {
253
+ it("predicts resource failure when disk is unavailable", () => {
254
+ const spec = makeMinimalSpec();
255
+ const env = makeMinimalEnv({
256
+ resources: [
257
+ { name: "disk", type: "disk", available: false },
258
+ ],
259
+ });
260
+
261
+ const failures = predictFailuresFromEnvironment(spec, env);
262
+
263
+ expect(failures.some(f => f.class === "resource")).toBe(true);
264
+ });
265
+
266
+ it("predicts resource failure when disk usage is critical", () => {
267
+ const spec = makeMinimalSpec();
268
+ const env = makeMinimalEnv({
269
+ resources: [
270
+ { name: "disk", type: "disk", available: true, usage: "95%" },
271
+ ],
272
+ });
273
+
274
+ const failures = predictFailuresFromEnvironment(spec, env);
275
+
276
+ expect(failures.some(f => f.class === "resource")).toBe(true);
277
+ });
278
+
279
+ it("does not predict resource failure for healthy disk", () => {
280
+ const spec = makeMinimalSpec();
281
+ const env = makeMinimalEnv({
282
+ resources: [
283
+ { name: "disk", type: "disk", available: true, usage: "45%" },
284
+ ],
285
+ });
286
+
287
+ const failures = predictFailuresFromEnvironment(spec, env);
288
+
289
+ expect(failures.some(f => f.class === "resource")).toBe(false);
290
+ });
291
+ });
292
+
293
+ describe("confidence scores", () => {
294
+ it("assigns high confidence for missing tools", () => {
295
+ const spec: HarnessSpec = {
296
+ name: "test-harness",
297
+ graph: {
298
+ entryNodeId: "node-a",
299
+ nodes: [
300
+ {
301
+ id: "node-a",
302
+ kind: "tool",
303
+ tool: "nonexistent-tool-xyz",
304
+ args: [],
305
+ },
306
+ ],
307
+ edges: [],
308
+ },
309
+ };
310
+ const env = makeMinimalEnv();
311
+
312
+ const failures = predictFailuresFromEnvironment(spec, env);
313
+
314
+ const toolFailure = failures.find(f => f.class === "tool");
315
+ expect(toolFailure).toBeDefined();
316
+ expect(toolFailure!.confidence).toBeGreaterThanOrEqual(0.7);
317
+ });
318
+
319
+ it("assigns confidence based on constraint severity", () => {
320
+ const spec = makeMinimalSpec();
321
+ const env = makeMinimalEnv({
322
+ constraints: [
323
+ { type: "permission", description: "No access", severity: "high" },
324
+ ],
325
+ });
326
+
327
+ const failures = predictFailuresFromEnvironment(spec, env);
328
+
329
+ expect(failures[0].confidence).toBeGreaterThanOrEqual(0.7);
330
+ });
331
+ });
332
+
333
+ describe("empty/edge cases", () => {
334
+ it("returns empty array for spec with no nodes", () => {
335
+ const spec: HarnessSpec = {
336
+ name: "empty",
337
+ graph: { entryNodeId: "", nodes: [], edges: [] },
338
+ };
339
+ const env = makeMinimalEnv();
340
+
341
+ const failures = predictFailuresFromEnvironment(spec, env);
342
+
343
+ expect(failures.length).toBe(0);
344
+ });
345
+
346
+ it("handles LLM nodes without tool checks", () => {
347
+ const spec: HarnessSpec = {
348
+ name: "llm-harness",
349
+ graph: {
350
+ entryNodeId: "node-a",
351
+ nodes: [
352
+ {
353
+ id: "node-a",
354
+ kind: "llm",
355
+ provider: "openai",
356
+ model: "gpt-4",
357
+ prompt: "Review this code",
358
+ },
359
+ ],
360
+ edges: [],
361
+ },
362
+ };
363
+ const env = makeMinimalEnv();
364
+
365
+ const failures = predictFailuresFromEnvironment(spec, env);
366
+
367
+ // LLM nodes don't require specific tools, so no tool failures expected
368
+ expect(failures.some(f => f.class === "tool")).toBe(false);
369
+ });
370
+
371
+ it("handles human nodes without tool checks", () => {
372
+ const spec: HarnessSpec = {
373
+ name: "human-harness",
374
+ graph: {
375
+ entryNodeId: "node-a",
376
+ nodes: [
377
+ {
378
+ id: "node-a",
379
+ kind: "human",
380
+ prompt: "Approve this change",
381
+ interactionType: "approval",
382
+ },
383
+ ],
384
+ edges: [],
385
+ },
386
+ };
387
+ const env = makeMinimalEnv();
388
+
389
+ const failures = predictFailuresFromEnvironment(spec, env);
390
+
391
+ expect(failures.some(f => f.class === "tool")).toBe(false);
392
+ });
393
+ });
394
+
395
+ describe("retryable flag", () => {
396
+ it("marks tool failures as non-retryable", () => {
397
+ const spec: HarnessSpec = {
398
+ name: "test-harness",
399
+ graph: {
400
+ entryNodeId: "node-a",
401
+ nodes: [
402
+ {
403
+ id: "node-a",
404
+ kind: "tool",
405
+ tool: "missing-tool",
406
+ args: [],
407
+ },
408
+ ],
409
+ edges: [],
410
+ },
411
+ };
412
+ const env = makeMinimalEnv();
413
+
414
+ const failures = predictFailuresFromEnvironment(spec, env);
415
+
416
+ const toolFailure = failures.find(f => f.class === "tool");
417
+ expect(toolFailure).toBeDefined();
418
+ expect(toolFailure!.retryable).toBe(false);
419
+ });
420
+
421
+ it("marks resource failures as retryable", () => {
422
+ const spec = makeMinimalSpec();
423
+ const env = makeMinimalEnv({
424
+ resources: [
425
+ { name: "disk", type: "disk", available: false },
426
+ ],
427
+ });
428
+
429
+ const failures = predictFailuresFromEnvironment(spec, env);
430
+
431
+ const resourceFailure = failures.find(f => f.class === "resource");
432
+ expect(resourceFailure).toBeDefined();
433
+ expect(resourceFailure!.retryable).toBe(true);
434
+ });
435
+ });
436
+ });
@@ -0,0 +1,209 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveMutationsFromFailure } from "../../src/mutation/derive.js";
3
+ import type { HarnessMutation } from "../../src/mutation/types.js";
4
+ import type { HarnessSpec } from "../../src/spec/types.js";
5
+ import type { FailureSignature, FailureContext } from "../../src/failures/ontology.js";
6
+
7
+ function makeSpec(overrides?: Partial<HarnessSpec>): HarnessSpec {
8
+ return {
9
+ name: "test-harness",
10
+ graph: {
11
+ entryNodeId: "node-a",
12
+ nodes: [
13
+ {
14
+ id: "node-a",
15
+ label: "Node A",
16
+ kind: "tool",
17
+ tool: "bash",
18
+ args: ["echo hello"],
19
+ },
20
+ {
21
+ id: "node-b",
22
+ label: "Node B",
23
+ kind: "tool",
24
+ tool: "bash",
25
+ args: ["echo world"],
26
+ },
27
+ ],
28
+ edges: [{ from: "node-a", to: "node-b" }],
29
+ },
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function makeSignature(
35
+ cls: FailureSignature["class"],
36
+ nodeId: string,
37
+ overrides?: Partial<FailureSignature>,
38
+ ): FailureSignature {
39
+ return {
40
+ class: cls,
41
+ confidence: 0.9,
42
+ evidence: ["test evidence"],
43
+ suggestedRecovery: [],
44
+ retryable: true,
45
+ requiresHumanIntervention: false,
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ describe("deriveMutationsFromFailure", () => {
51
+ describe("auth failure mutations", () => {
52
+ it("should suggest add-node for auth-check before failing node", () => {
53
+ const spec = makeSpec();
54
+ const signature = makeSignature("auth", "node-b");
55
+ const ctx: FailureContext = { nodeId: "node-b" };
56
+
57
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
58
+
59
+ expect(mutations.length).toBeGreaterThan(0);
60
+ expect(mutations.some(m => m.type === "add-node")).toBe(true);
61
+ });
62
+
63
+ it("should create auth-check node with appropriate tool", () => {
64
+ const spec = makeSpec();
65
+ const signature = makeSignature("auth", "node-b");
66
+ const ctx: FailureContext = { nodeId: "node-b" };
67
+
68
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
69
+
70
+ const addNodeMutation = mutations.find(m => m.type === "add-node");
71
+ expect(addNodeMutation).toBeDefined();
72
+ const params = addNodeMutation!.params as Record<string, unknown>;
73
+ expect(params.node).toBeDefined();
74
+ });
75
+ });
76
+
77
+ describe("tool failure mutations", () => {
78
+ it("should suggest add-node for tool-availability check", () => {
79
+ const spec = makeSpec();
80
+ const signature = makeSignature("tool", "node-a");
81
+ const ctx: FailureContext = { nodeId: "node-a" };
82
+
83
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
84
+
85
+ expect(mutations.length).toBeGreaterThan(0);
86
+ expect(mutations.some(m => m.type === "add-node")).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe("resource failure mutations", () => {
91
+ it("should suggest add-node for resource-provisioning check", () => {
92
+ const spec = makeSpec();
93
+ const signature = makeSignature("resource", "node-a");
94
+ const ctx: FailureContext = { nodeId: "node-a" };
95
+
96
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
97
+
98
+ expect(mutations.length).toBeGreaterThan(0);
99
+ expect(mutations.some(m => m.type === "add-node")).toBe(true);
100
+ });
101
+ });
102
+
103
+ describe("network failure mutations", () => {
104
+ it("should suggest modify-node to add retry with backoff", () => {
105
+ const spec = makeSpec();
106
+ const signature = makeSignature("network", "node-a", {
107
+ retryable: true,
108
+ });
109
+ const ctx: FailureContext = { nodeId: "node-a" };
110
+
111
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
112
+
113
+ expect(mutations.length).toBeGreaterThan(0);
114
+ expect(mutations.some(m => m.type === "modify-node")).toBe(true);
115
+ });
116
+
117
+ it("should add retry policy with exponential backoff", () => {
118
+ const spec = makeSpec();
119
+ const signature = makeSignature("network", "node-a", {
120
+ retryable: true,
121
+ });
122
+ const ctx: FailureContext = { nodeId: "node-a" };
123
+
124
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
125
+
126
+ const modifyMutation = mutations.find(m => m.type === "modify-node");
127
+ expect(modifyMutation).toBeDefined();
128
+ const params = modifyMutation!.params as Record<string, unknown>;
129
+ expect(params.changes).toBeDefined();
130
+ const changes = params.changes as Record<string, unknown>;
131
+ expect(changes.retryPolicy).toBeDefined();
132
+ });
133
+ });
134
+
135
+ describe("semantic failure mutations", () => {
136
+ it("should suggest add-verification for semantic failures", () => {
137
+ const spec = makeSpec();
138
+ const signature = makeSignature("semantic", "node-b");
139
+ const ctx: FailureContext = { nodeId: "node-b" };
140
+
141
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
142
+
143
+ expect(mutations.length).toBeGreaterThan(0);
144
+ expect(
145
+ mutations.some(m => m.type === "add-verification"),
146
+ ).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe("human failure mutations", () => {
151
+ it("should suggest toggle-approval for human failures", () => {
152
+ const spec = makeSpec();
153
+ const signature = makeSignature("human", "node-a", {
154
+ requiresHumanIntervention: true,
155
+ });
156
+ const ctx: FailureContext = { nodeId: "node-a" };
157
+
158
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
159
+
160
+ expect(mutations.length).toBeGreaterThan(0);
161
+ expect(
162
+ mutations.some(m => m.type === "toggle-approval"),
163
+ ).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe("environment-drift failure mutations", () => {
168
+ it("should suggest add-node for environment-check", () => {
169
+ const spec = makeSpec();
170
+ const signature = makeSignature("environment-drift", "node-a");
171
+ const ctx: FailureContext = { nodeId: "node-a" };
172
+
173
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
174
+
175
+ expect(mutations.length).toBeGreaterThan(0);
176
+ expect(mutations.some(m => m.type === "add-node")).toBe(true);
177
+ });
178
+ });
179
+
180
+ describe("unknown failure mutations", () => {
181
+ it("should suggest add-verification for unknown failures", () => {
182
+ const spec = makeSpec();
183
+ const signature = makeSignature("unknown", "node-a");
184
+ const ctx: FailureContext = { nodeId: "node-a" };
185
+
186
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
187
+
188
+ expect(mutations.length).toBeGreaterThan(0);
189
+ });
190
+ });
191
+
192
+ describe("edge creation for added nodes", () => {
193
+ it("should add edge from auth-check to failing node", () => {
194
+ const spec = makeSpec();
195
+ const signature = makeSignature("auth", "node-b");
196
+ const ctx: FailureContext = { nodeId: "node-b" };
197
+
198
+ const mutations = deriveMutationsFromFailure(signature, spec, ctx);
199
+
200
+ const addNodeMutation = mutations.find(m => m.type === "add-node");
201
+ if (addNodeMutation) {
202
+ const params = addNodeMutation.params as Record<string, unknown>;
203
+ expect(params.edges).toBeDefined();
204
+ const edges = params.edges as Array<{ from: string; to: string }>;
205
+ expect(edges.some(e => e.to === "node-b")).toBe(true);
206
+ }
207
+ });
208
+ });
209
+ });