@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.
- package/README.md +707 -0
- package/docs/agent-wrangling.png +0 -0
- package/package.json +26 -0
- package/src/capabilities/matcher.ts +25 -0
- package/src/capabilities/registry.ts +103 -0
- package/src/capabilities/types.ts +15 -0
- package/src/cir/lower.ts +253 -0
- package/src/cir/optimize.ts +251 -0
- package/src/cir/types.ts +131 -0
- package/src/cir/validate.ts +265 -0
- package/src/compiler/compile.ts +601 -0
- package/src/compiler/feedback.ts +471 -0
- package/src/compiler/runtime-helpers.ts +455 -0
- package/src/composition/chain.ts +58 -0
- package/src/composition/conditional.ts +76 -0
- package/src/composition/parallel.ts +75 -0
- package/src/composition/types.ts +105 -0
- package/src/environment/analyzer.ts +56 -0
- package/src/environment/discovery.ts +179 -0
- package/src/environment/types.ts +68 -0
- package/src/failures/classifiers.ts +134 -0
- package/src/failures/generator.ts +421 -0
- package/src/failures/map-reference-failures.ts +23 -0
- package/src/failures/ontology.ts +210 -0
- package/src/failures/recovery.ts +214 -0
- package/src/failures/types.ts +14 -0
- package/src/index.ts +67 -0
- package/src/memory/advisor.ts +132 -0
- package/src/memory/extractor.ts +166 -0
- package/src/memory/store.ts +107 -0
- package/src/memory/types.ts +53 -0
- package/src/metaharness/engine.ts +256 -0
- package/src/metaharness/predictor.ts +168 -0
- package/src/metaharness/types.ts +40 -0
- package/src/mutation/derive.ts +308 -0
- package/src/mutation/diff.ts +52 -0
- package/src/mutation/engine.ts +256 -0
- package/src/mutation/types.ts +84 -0
- package/src/pi/command-input.ts +209 -0
- package/src/pi/commands.ts +351 -0
- package/src/pi/extension.ts +16 -0
- package/src/planner/synthesize.ts +83 -0
- package/src/planner/template-rules.ts +183 -0
- package/src/planner/types.ts +42 -0
- package/src/reference/catalog.ts +128 -0
- package/src/reference/patch-validation-strategies.ts +170 -0
- package/src/reference/patch-validation.ts +174 -0
- package/src/reference/pr-review-merge.ts +155 -0
- package/src/reference/strategies.ts +126 -0
- package/src/reference/types.ts +33 -0
- package/src/replanner/risk-rules.ts +161 -0
- package/src/replanner/runtime.ts +308 -0
- package/src/replanner/synthesize.ts +619 -0
- package/src/replanner/types.ts +73 -0
- package/src/spec/schema.ts +254 -0
- package/src/spec/types.ts +319 -0
- package/src/spec/validate.ts +296 -0
- package/src/state/snapshots.ts +43 -0
- package/src/state/types.ts +12 -0
- package/src/synthesis/graph-builder.ts +267 -0
- package/src/synthesis/harness-builder.ts +113 -0
- package/src/synthesis/intent-ir.ts +63 -0
- package/src/synthesis/policy-builder.ts +320 -0
- package/src/synthesis/risk-analyzer.ts +182 -0
- package/src/synthesis/skill-parser.ts +441 -0
- package/src/verification/engine.ts +230 -0
- package/src/versioning/file-store.ts +103 -0
- package/src/versioning/history.ts +43 -0
- package/src/versioning/store.ts +16 -0
- package/src/versioning/types.ts +31 -0
- package/test/capabilities/matcher.test.ts +67 -0
- package/test/capabilities/registry.test.ts +136 -0
- package/test/capabilities/synthesis.test.ts +264 -0
- package/test/cir/lower.test.ts +417 -0
- package/test/cir/optimize.test.ts +266 -0
- package/test/cir/validate.test.ts +368 -0
- package/test/compiler/adaptive-runtime.test.ts +157 -0
- package/test/compiler/compile.test.ts +1198 -0
- package/test/compiler/feedback.test.ts +784 -0
- package/test/compiler/guardrails.test.ts +191 -0
- package/test/compiler/trace.test.ts +404 -0
- package/test/composition/chain.test.ts +328 -0
- package/test/composition/conditional.test.ts +241 -0
- package/test/composition/parallel.test.ts +215 -0
- package/test/environment/analyzer.test.ts +204 -0
- package/test/environment/discovery.test.ts +149 -0
- package/test/failures/classifiers.test.ts +287 -0
- package/test/failures/generator.test.ts +203 -0
- package/test/failures/ontology.test.ts +439 -0
- package/test/failures/recovery.test.ts +300 -0
- package/test/helpers/createFixtureRepo.ts +84 -0
- package/test/helpers/createPatchValidationFixture.ts +144 -0
- package/test/helpers/runCompiledWorkflow.ts +208 -0
- package/test/memory/advisor.test.ts +332 -0
- package/test/memory/extractor.test.ts +295 -0
- package/test/memory/store.test.ts +244 -0
- package/test/metaharness/engine.test.ts +575 -0
- package/test/metaharness/predictor.test.ts +436 -0
- package/test/mutation/derive-failure.test.ts +209 -0
- package/test/mutation/engine.test.ts +622 -0
- package/test/package-smoke.test.ts +29 -0
- package/test/pi/command-input.test.ts +153 -0
- package/test/pi/commands.test.ts +623 -0
- package/test/planner/classify-template.test.ts +32 -0
- package/test/planner/synthesize.test.ts +901 -0
- package/test/reference/PatchValidation.failures.test.ts +137 -0
- package/test/reference/PatchValidation.test.ts +326 -0
- package/test/reference/PrReviewMerge.failures.test.ts +121 -0
- package/test/reference/PrReviewMerge.test.ts +55 -0
- package/test/reference/catalog-open.test.ts +70 -0
- package/test/replanner/runtime.test.ts +207 -0
- package/test/replanner/synthesize.test.ts +303 -0
- package/test/spec/validate.test.ts +1056 -0
- package/test/state/snapshots.test.ts +264 -0
- package/test/synthesis/custom-workflow.test.ts +264 -0
- package/test/synthesis/graph-builder.test.ts +370 -0
- package/test/synthesis/harness-builder.test.ts +128 -0
- package/test/synthesis/policy-builder.test.ts +149 -0
- package/test/synthesis/risk-analyzer.test.ts +230 -0
- package/test/synthesis/skill-parser.test.ts +796 -0
- package/test/verification/engine.test.ts +509 -0
- package/test/versioning/history.test.ts +144 -0
- package/test/versioning/store.test.ts +254 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mutateHarness } from "../../src/mutation/engine.js";
|
|
3
|
+
import { diffSpecs } from "../../src/mutation/diff.js";
|
|
4
|
+
import { deriveMutationsFromTrace } from "../../src/mutation/derive.js";
|
|
5
|
+
import type {
|
|
6
|
+
HarnessMutation,
|
|
7
|
+
MutationPolicy,
|
|
8
|
+
SpecDiff,
|
|
9
|
+
} from "../../src/mutation/types.js";
|
|
10
|
+
import type { HarnessSpec, TaskNode, TaskEdge } from "../../src/spec/types.js";
|
|
11
|
+
import type { HarnessExecutionTrace } from "../../src/versioning/types.js";
|
|
12
|
+
import type { ExecutionTraceEntry } from "../../src/compiler/runtime-helpers.js";
|
|
13
|
+
|
|
14
|
+
function makeSpec(overrides?: Partial<HarnessSpec>): HarnessSpec {
|
|
15
|
+
return {
|
|
16
|
+
name: "test-harness",
|
|
17
|
+
graph: {
|
|
18
|
+
entryNodeId: "node-a",
|
|
19
|
+
nodes: [
|
|
20
|
+
{
|
|
21
|
+
id: "node-a",
|
|
22
|
+
label: "Node A",
|
|
23
|
+
kind: "tool",
|
|
24
|
+
tool: "bash",
|
|
25
|
+
args: ["echo hello"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "node-b",
|
|
29
|
+
label: "Node B",
|
|
30
|
+
kind: "tool",
|
|
31
|
+
tool: "bash",
|
|
32
|
+
args: ["echo world"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "node-c",
|
|
36
|
+
label: "Node C",
|
|
37
|
+
kind: "llm",
|
|
38
|
+
provider: "openai",
|
|
39
|
+
model: "gpt-4",
|
|
40
|
+
prompt: "Summarize results",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
edges: [
|
|
44
|
+
{ from: "node-a", to: "node-b" },
|
|
45
|
+
{ from: "node-b", to: "node-c" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
humanPolicy: {
|
|
49
|
+
defaultTimeout: 300,
|
|
50
|
+
allowAsync: false,
|
|
51
|
+
},
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeTrace(
|
|
57
|
+
entries: ExecutionTraceEntry[],
|
|
58
|
+
overrides?: Partial<HarnessExecutionTrace>,
|
|
59
|
+
): HarnessExecutionTrace {
|
|
60
|
+
return {
|
|
61
|
+
entries,
|
|
62
|
+
totalDurationMs: 1000,
|
|
63
|
+
nodeCount: entries.length,
|
|
64
|
+
failureCount: entries.filter((e) => e.phase === "failure").length,
|
|
65
|
+
startTimeMs: Date.now() - 1000,
|
|
66
|
+
endTimeMs: Date.now(),
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("mutation/engine", () => {
|
|
72
|
+
describe("mutateHarness", () => {
|
|
73
|
+
describe("add-node", () => {
|
|
74
|
+
it("should insert a new node and its edges", () => {
|
|
75
|
+
const spec = makeSpec();
|
|
76
|
+
const newNode: TaskNode = {
|
|
77
|
+
id: "node-d",
|
|
78
|
+
label: "Node D",
|
|
79
|
+
kind: "tool",
|
|
80
|
+
tool: "bash",
|
|
81
|
+
args: ["echo added"],
|
|
82
|
+
};
|
|
83
|
+
const mutations: HarnessMutation[] = [
|
|
84
|
+
{
|
|
85
|
+
type: "add-node",
|
|
86
|
+
params: {
|
|
87
|
+
node: newNode,
|
|
88
|
+
edges: [
|
|
89
|
+
{ from: "node-b", to: "node-d" },
|
|
90
|
+
{ from: "node-d", to: "node-c" },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const result = mutateHarness(spec, mutations);
|
|
97
|
+
|
|
98
|
+
expect(result.spec.graph.nodes).toHaveLength(4);
|
|
99
|
+
expect(result.spec.graph.nodes.find((n) => n.id === "node-d")).toEqual(newNode);
|
|
100
|
+
expect(result.spec.graph.edges).toHaveLength(4);
|
|
101
|
+
expect(result.spec.graph.edges).toContainEqual({ from: "node-b", to: "node-d" });
|
|
102
|
+
expect(result.spec.graph.edges).toContainEqual({ from: "node-d", to: "node-c" });
|
|
103
|
+
expect(result.diff.addedNodes).toContain("node-d");
|
|
104
|
+
expect(result.diff.addedEdges).toContainEqual({ from: "node-b", to: "node-d" });
|
|
105
|
+
expect(result.diff.addedEdges).toContainEqual({ from: "node-d", to: "node-c" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should add node without edges", () => {
|
|
109
|
+
const spec = makeSpec();
|
|
110
|
+
const newNode: TaskNode = {
|
|
111
|
+
id: "isolated",
|
|
112
|
+
kind: "tool",
|
|
113
|
+
tool: "bash",
|
|
114
|
+
args: ["echo isolated"],
|
|
115
|
+
};
|
|
116
|
+
const mutations: HarnessMutation[] = [
|
|
117
|
+
{ type: "add-node", params: { node: newNode } },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const result = mutateHarness(spec, mutations);
|
|
121
|
+
|
|
122
|
+
expect(result.spec.graph.nodes).toHaveLength(4);
|
|
123
|
+
expect(result.spec.graph.edges).toHaveLength(2);
|
|
124
|
+
expect(result.diff.addedNodes).toContain("isolated");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should throw if node ID already exists", () => {
|
|
128
|
+
const spec = makeSpec();
|
|
129
|
+
const duplicate: TaskNode = {
|
|
130
|
+
id: "node-a",
|
|
131
|
+
kind: "tool",
|
|
132
|
+
tool: "bash",
|
|
133
|
+
args: ["echo dup"],
|
|
134
|
+
};
|
|
135
|
+
const mutations: HarnessMutation[] = [
|
|
136
|
+
{ type: "add-node", params: { node: duplicate } },
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("already exists");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("remove-node", () => {
|
|
144
|
+
it("should remove a node and its edges", () => {
|
|
145
|
+
const spec = makeSpec();
|
|
146
|
+
const mutations: HarnessMutation[] = [
|
|
147
|
+
{ type: "remove-node", params: { nodeId: "node-b" } },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const result = mutateHarness(spec, mutations);
|
|
151
|
+
|
|
152
|
+
expect(result.spec.graph.nodes).toHaveLength(2);
|
|
153
|
+
expect(result.spec.graph.nodes.find((n) => n.id === "node-b")).toBeUndefined();
|
|
154
|
+
expect(result.spec.graph.edges).toHaveLength(1);
|
|
155
|
+
expect(result.spec.graph.edges).toContainEqual({ from: "node-a", to: "node-c" });
|
|
156
|
+
expect(result.diff.removedNodes).toContain("node-b");
|
|
157
|
+
expect(result.diff.removedEdges).toContainEqual({ from: "node-a", to: "node-b" });
|
|
158
|
+
expect(result.diff.removedEdges).toContainEqual({ from: "node-b", to: "node-c" });
|
|
159
|
+
expect(result.diff.addedEdges).toContainEqual({ from: "node-a", to: "node-c" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should update entryNodeId if removing the entry node", () => {
|
|
163
|
+
const spec = makeSpec();
|
|
164
|
+
const mutations: HarnessMutation[] = [
|
|
165
|
+
{ type: "remove-node", params: { nodeId: "node-a" } },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const result = mutateHarness(spec, mutations);
|
|
169
|
+
|
|
170
|
+
expect(result.spec.graph.entryNodeId).toBe("node-b");
|
|
171
|
+
expect(result.spec.graph.nodes).toHaveLength(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should throw if node does not exist", () => {
|
|
175
|
+
const spec = makeSpec();
|
|
176
|
+
const mutations: HarnessMutation[] = [
|
|
177
|
+
{ type: "remove-node", params: { nodeId: "nonexistent" } },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("not found");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should remove outgoing edges only when node has no incoming", () => {
|
|
184
|
+
const spec = makeSpec({
|
|
185
|
+
graph: {
|
|
186
|
+
entryNodeId: "a",
|
|
187
|
+
nodes: [
|
|
188
|
+
{ id: "a", kind: "tool", tool: "bash", args: ["a"] },
|
|
189
|
+
{ id: "b", kind: "tool", tool: "bash", args: ["b"] },
|
|
190
|
+
{ id: "c", kind: "tool", tool: "bash", args: ["c"] },
|
|
191
|
+
],
|
|
192
|
+
edges: [
|
|
193
|
+
{ from: "a", to: "b" },
|
|
194
|
+
{ from: "b", to: "c" },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const mutations: HarnessMutation[] = [
|
|
199
|
+
{ type: "remove-node", params: { nodeId: "a" } },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const result = mutateHarness(spec, mutations);
|
|
203
|
+
|
|
204
|
+
expect(result.spec.graph.nodes).toHaveLength(2);
|
|
205
|
+
expect(result.spec.graph.edges).toHaveLength(1);
|
|
206
|
+
expect(result.spec.graph.edges).toContainEqual({ from: "b", to: "c" });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("modify-node", () => {
|
|
211
|
+
it("should change node properties", () => {
|
|
212
|
+
const spec = makeSpec();
|
|
213
|
+
const mutations: HarnessMutation[] = [
|
|
214
|
+
{
|
|
215
|
+
type: "modify-node",
|
|
216
|
+
params: {
|
|
217
|
+
nodeId: "node-c",
|
|
218
|
+
changes: {
|
|
219
|
+
prompt: "Updated prompt",
|
|
220
|
+
model: "gpt-4o",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const result = mutateHarness(spec, mutations);
|
|
227
|
+
|
|
228
|
+
const modified = result.spec.graph.nodes.find(
|
|
229
|
+
(n) => n.id === "node-c",
|
|
230
|
+
) as Extract<TaskNode, { kind: "llm" }>;
|
|
231
|
+
expect(modified.prompt).toBe("Updated prompt");
|
|
232
|
+
expect(modified.model).toBe("gpt-4o");
|
|
233
|
+
expect(result.diff.modifiedNodes).toContain("node-c");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should throw if target node not found", () => {
|
|
237
|
+
const spec = makeSpec();
|
|
238
|
+
const mutations: HarnessMutation[] = [
|
|
239
|
+
{
|
|
240
|
+
type: "modify-node",
|
|
241
|
+
params: { nodeId: "nonexistent", changes: { label: "x" } },
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("not found");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should not allow changing node id or kind", () => {
|
|
249
|
+
const spec = makeSpec();
|
|
250
|
+
const mutations: HarnessMutation[] = [
|
|
251
|
+
{
|
|
252
|
+
type: "modify-node",
|
|
253
|
+
params: {
|
|
254
|
+
nodeId: "node-a",
|
|
255
|
+
changes: { id: "new-id", kind: "llm" },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const result = mutateHarness(spec, mutations);
|
|
261
|
+
const node = result.spec.graph.nodes.find((n) => n.id === "node-a");
|
|
262
|
+
expect(node?.kind).toBe("tool");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("add-edge", () => {
|
|
267
|
+
it("should add a transition between two nodes", () => {
|
|
268
|
+
const spec = makeSpec();
|
|
269
|
+
const mutations: HarnessMutation[] = [
|
|
270
|
+
{
|
|
271
|
+
type: "add-edge",
|
|
272
|
+
params: { edge: { from: "node-a", to: "node-c" } },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const result = mutateHarness(spec, mutations);
|
|
277
|
+
|
|
278
|
+
expect(result.spec.graph.edges).toHaveLength(3);
|
|
279
|
+
expect(result.spec.graph.edges).toContainEqual({ from: "node-a", to: "node-c" });
|
|
280
|
+
expect(result.diff.addedEdges).toContainEqual({ from: "node-a", to: "node-c" });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should throw if edge already exists", () => {
|
|
284
|
+
const spec = makeSpec();
|
|
285
|
+
const mutations: HarnessMutation[] = [
|
|
286
|
+
{
|
|
287
|
+
type: "add-edge",
|
|
288
|
+
params: { edge: { from: "node-a", to: "node-b" } },
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("already exists");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should throw if source node does not exist", () => {
|
|
296
|
+
const spec = makeSpec();
|
|
297
|
+
const mutations: HarnessMutation[] = [
|
|
298
|
+
{
|
|
299
|
+
type: "add-edge",
|
|
300
|
+
params: { edge: { from: "nonexistent", to: "node-b" } },
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("not found");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should throw if target node does not exist", () => {
|
|
308
|
+
const spec = makeSpec();
|
|
309
|
+
const mutations: HarnessMutation[] = [
|
|
310
|
+
{
|
|
311
|
+
type: "add-edge",
|
|
312
|
+
params: { edge: { from: "node-a", to: "nonexistent" } },
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("not found");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("toggle-approval", () => {
|
|
321
|
+
it("should flip approvalRequired on the spec", () => {
|
|
322
|
+
const spec = makeSpec();
|
|
323
|
+
const mutations: HarnessMutation[] = [
|
|
324
|
+
{ type: "toggle-approval", params: {} },
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const result = mutateHarness(spec, mutations);
|
|
328
|
+
|
|
329
|
+
expect(result.spec.humanPolicy).toBeDefined();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should set explicit approval value", () => {
|
|
333
|
+
const spec = makeSpec();
|
|
334
|
+
const mutations: HarnessMutation[] = [
|
|
335
|
+
{ type: "toggle-approval", params: { approvalRequired: true } },
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const result = mutateHarness(spec, mutations);
|
|
339
|
+
|
|
340
|
+
expect(result.spec.humanPolicy).toBeDefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should create humanPolicy if missing", () => {
|
|
344
|
+
const spec = makeSpec({ humanPolicy: undefined });
|
|
345
|
+
const mutations: HarnessMutation[] = [
|
|
346
|
+
{ type: "toggle-approval", params: { approvalRequired: true } },
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
const result = mutateHarness(spec, mutations);
|
|
350
|
+
|
|
351
|
+
expect(result.spec.humanPolicy).toBeDefined();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("add-verification", () => {
|
|
356
|
+
it("should add verification policy to a node", () => {
|
|
357
|
+
const spec = makeSpec();
|
|
358
|
+
const mutations: HarnessMutation[] = [
|
|
359
|
+
{
|
|
360
|
+
type: "add-verification",
|
|
361
|
+
params: {
|
|
362
|
+
nodeId: "node-b",
|
|
363
|
+
verificationPolicy: {
|
|
364
|
+
rules: [
|
|
365
|
+
{
|
|
366
|
+
kind: "tool",
|
|
367
|
+
checkNodeId: "verify-check",
|
|
368
|
+
onFail: "block",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const result = mutateHarness(spec, mutations);
|
|
377
|
+
|
|
378
|
+
const node = result.spec.graph.nodes.find((n) => n.id === "node-b");
|
|
379
|
+
expect(node?.verificationPolicy).toBeDefined();
|
|
380
|
+
expect(node?.verificationPolicy?.rules).toHaveLength(1);
|
|
381
|
+
expect(node?.verificationPolicy?.rules[0].kind).toBe("tool");
|
|
382
|
+
expect(result.diff.modifiedNodes).toContain("node-b");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should throw if target node not found", () => {
|
|
386
|
+
const spec = makeSpec();
|
|
387
|
+
const mutations: HarnessMutation[] = [
|
|
388
|
+
{
|
|
389
|
+
type: "add-verification",
|
|
390
|
+
params: {
|
|
391
|
+
nodeId: "nonexistent",
|
|
392
|
+
verificationPolicy: { rules: [] },
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
expect(() => mutateHarness(spec, mutations)).toThrow("not found");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("policy enforcement", () => {
|
|
402
|
+
it("should reject mutations not in allowedMutations", () => {
|
|
403
|
+
const spec = makeSpec();
|
|
404
|
+
const policy: MutationPolicy = {
|
|
405
|
+
allowedMutations: ["modify-node"],
|
|
406
|
+
maxMutations: 10,
|
|
407
|
+
};
|
|
408
|
+
const mutations: HarnessMutation[] = [
|
|
409
|
+
{ type: "add-node", params: { node: { id: "x", kind: "tool", tool: "bash", args: [] } } },
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
expect(() => mutateHarness(spec, mutations, policy)).toThrow("not allowed");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should reject when exceeding maxMutations", () => {
|
|
416
|
+
const spec = makeSpec();
|
|
417
|
+
const policy: MutationPolicy = {
|
|
418
|
+
allowedMutations: ["add-node", "modify-node", "add-edge", "remove-node", "toggle-approval", "add-verification"],
|
|
419
|
+
maxMutations: 1,
|
|
420
|
+
};
|
|
421
|
+
const mutations: HarnessMutation[] = [
|
|
422
|
+
{ type: "modify-node", params: { nodeId: "node-a", changes: { label: "X" } } },
|
|
423
|
+
{ type: "modify-node", params: { nodeId: "node-b", changes: { label: "Y" } } },
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
expect(() => mutateHarness(spec, mutations, policy)).toThrow("exceeds maximum");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("should allow mutations within policy limits", () => {
|
|
430
|
+
const spec = makeSpec();
|
|
431
|
+
const policy: MutationPolicy = {
|
|
432
|
+
allowedMutations: ["modify-node"],
|
|
433
|
+
maxMutations: 2,
|
|
434
|
+
};
|
|
435
|
+
const mutations: HarnessMutation[] = [
|
|
436
|
+
{ type: "modify-node", params: { nodeId: "node-a", changes: { label: "X" } } },
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const result = mutateHarness(spec, mutations, policy);
|
|
440
|
+
expect(result.mutations).toHaveLength(1);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("diffSpecs", () => {
|
|
446
|
+
it("should detect added nodes", () => {
|
|
447
|
+
const before = makeSpec();
|
|
448
|
+
const after = makeSpec({
|
|
449
|
+
graph: {
|
|
450
|
+
...makeSpec().graph,
|
|
451
|
+
nodes: [
|
|
452
|
+
...makeSpec().graph.nodes,
|
|
453
|
+
{ id: "node-d", kind: "tool", tool: "bash", args: ["echo d"] },
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const diff = diffSpecs(before, after);
|
|
459
|
+
|
|
460
|
+
expect(diff.addedNodes).toContain("node-d");
|
|
461
|
+
expect(diff.removedNodes).toHaveLength(0);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should detect removed nodes", () => {
|
|
465
|
+
const before = makeSpec();
|
|
466
|
+
const after = makeSpec({
|
|
467
|
+
graph: {
|
|
468
|
+
...makeSpec().graph,
|
|
469
|
+
nodes: makeSpec().graph.nodes.filter((n) => n.id !== "node-c"),
|
|
470
|
+
edges: makeSpec().graph.edges.filter((e) => e.to !== "node-c" && e.from !== "node-c"),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const diff = diffSpecs(before, after);
|
|
475
|
+
|
|
476
|
+
expect(diff.removedNodes).toContain("node-c");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should detect modified nodes", () => {
|
|
480
|
+
const before = makeSpec();
|
|
481
|
+
const modifiedNodes = [...makeSpec().graph.nodes];
|
|
482
|
+
modifiedNodes[2] = { ...modifiedNodes[2], label: "Changed" };
|
|
483
|
+
const after = makeSpec({
|
|
484
|
+
graph: { ...makeSpec().graph, nodes: modifiedNodes },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const diff = diffSpecs(before, after);
|
|
488
|
+
|
|
489
|
+
expect(diff.modifiedNodes).toContain("node-c");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should detect added edges", () => {
|
|
493
|
+
const before = makeSpec();
|
|
494
|
+
const after = makeSpec({
|
|
495
|
+
graph: {
|
|
496
|
+
...makeSpec().graph,
|
|
497
|
+
edges: [...makeSpec().graph.edges, { from: "node-a", to: "node-c" }],
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const diff = diffSpecs(before, after);
|
|
502
|
+
|
|
503
|
+
expect(diff.addedEdges).toContainEqual({ from: "node-a", to: "node-c" });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should detect removed edges", () => {
|
|
507
|
+
const before = makeSpec();
|
|
508
|
+
const after = makeSpec({
|
|
509
|
+
graph: {
|
|
510
|
+
...makeSpec().graph,
|
|
511
|
+
edges: makeSpec().graph.edges.filter(
|
|
512
|
+
(e) => !(e.from === "node-a" && e.to === "node-b"),
|
|
513
|
+
),
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const diff = diffSpecs(before, after);
|
|
518
|
+
|
|
519
|
+
expect(diff.removedEdges).toContainEqual({ from: "node-a", to: "node-b" });
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("should return empty diff for identical specs", () => {
|
|
523
|
+
const spec = makeSpec();
|
|
524
|
+
const diff = diffSpecs(spec, spec);
|
|
525
|
+
|
|
526
|
+
expect(diff.addedNodes).toHaveLength(0);
|
|
527
|
+
expect(diff.removedNodes).toHaveLength(0);
|
|
528
|
+
expect(diff.modifiedNodes).toHaveLength(0);
|
|
529
|
+
expect(diff.addedEdges).toHaveLength(0);
|
|
530
|
+
expect(diff.removedEdges).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("deriveMutationsFromTrace", () => {
|
|
535
|
+
it("should suggest add-verification after repeated failures on same node", () => {
|
|
536
|
+
const spec = makeSpec();
|
|
537
|
+
const entries: ExecutionTraceEntry[] = [
|
|
538
|
+
{ nodeId: "node-b", source: "task", phase: "enter" },
|
|
539
|
+
{ nodeId: "node-b", source: "task", phase: "failure", details: { message: "fail 1" } },
|
|
540
|
+
{ nodeId: "node-b", source: "task", phase: "enter" },
|
|
541
|
+
{ nodeId: "node-b", source: "task", phase: "failure", details: { message: "fail 2" } },
|
|
542
|
+
{ nodeId: "node-b", source: "task", phase: "enter" },
|
|
543
|
+
{ nodeId: "node-b", source: "task", phase: "failure", details: { message: "fail 3" } },
|
|
544
|
+
];
|
|
545
|
+
const trace = makeTrace(entries);
|
|
546
|
+
|
|
547
|
+
const mutations = deriveMutationsFromTrace(trace, spec);
|
|
548
|
+
|
|
549
|
+
expect(mutations.length).toBeGreaterThanOrEqual(1);
|
|
550
|
+
expect(mutations.some((m) => m.type === "add-verification")).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("should suggest add-node for retry when failures are transient", () => {
|
|
554
|
+
const spec = makeSpec();
|
|
555
|
+
const entries: ExecutionTraceEntry[] = [
|
|
556
|
+
{ nodeId: "node-a", source: "task", phase: "enter" },
|
|
557
|
+
{
|
|
558
|
+
nodeId: "node-a",
|
|
559
|
+
source: "task",
|
|
560
|
+
phase: "failure",
|
|
561
|
+
details: { category: "transient", message: "timeout" },
|
|
562
|
+
},
|
|
563
|
+
{ nodeId: "node-a", source: "task", phase: "enter" },
|
|
564
|
+
{
|
|
565
|
+
nodeId: "node-a",
|
|
566
|
+
source: "task",
|
|
567
|
+
phase: "failure",
|
|
568
|
+
details: { category: "transient", message: "timeout again" },
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
const trace = makeTrace(entries);
|
|
572
|
+
|
|
573
|
+
const mutations = deriveMutationsFromTrace(trace, spec);
|
|
574
|
+
|
|
575
|
+
expect(mutations.some((m) => m.type === "modify-node")).toBe(true);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("should not suggest mutations for successful traces", () => {
|
|
579
|
+
const spec = makeSpec();
|
|
580
|
+
const entries: ExecutionTraceEntry[] = [
|
|
581
|
+
{ nodeId: "node-a", source: "task", phase: "enter" },
|
|
582
|
+
{ nodeId: "node-a", source: "task", phase: "success" },
|
|
583
|
+
{ nodeId: "node-b", source: "task", phase: "enter" },
|
|
584
|
+
{ nodeId: "node-b", source: "task", phase: "success" },
|
|
585
|
+
];
|
|
586
|
+
const trace = makeTrace(entries);
|
|
587
|
+
|
|
588
|
+
const mutations = deriveMutationsFromTrace(trace, spec);
|
|
589
|
+
|
|
590
|
+
expect(mutations).toHaveLength(0);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("should suggest toggle-approval when high-risk nodes fail", () => {
|
|
594
|
+
const spec = makeSpec();
|
|
595
|
+
const entries: ExecutionTraceEntry[] = [
|
|
596
|
+
{ nodeId: "node-a", source: "task", phase: "enter" },
|
|
597
|
+
{ nodeId: "node-a", source: "task", phase: "success" },
|
|
598
|
+
{ nodeId: "node-b", source: "task", phase: "enter" },
|
|
599
|
+
{
|
|
600
|
+
nodeId: "node-b",
|
|
601
|
+
source: "task",
|
|
602
|
+
phase: "failure",
|
|
603
|
+
details: { message: "critical failure" },
|
|
604
|
+
},
|
|
605
|
+
];
|
|
606
|
+
const trace = makeTrace(entries, { failureCount: 1 });
|
|
607
|
+
|
|
608
|
+
const mutations = deriveMutationsFromTrace(trace, spec);
|
|
609
|
+
|
|
610
|
+
expect(mutations.some((m) => m.type === "toggle-approval")).toBe(true);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("should return empty for empty trace", () => {
|
|
614
|
+
const spec = makeSpec();
|
|
615
|
+
const trace = makeTrace([]);
|
|
616
|
+
|
|
617
|
+
const mutations = deriveMutationsFromTrace(trace, spec);
|
|
618
|
+
|
|
619
|
+
expect(mutations).toHaveLength(0);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import lassoExtension, {
|
|
3
|
+
validateHarnessSpec,
|
|
4
|
+
lowerHarnessSpecToCir,
|
|
5
|
+
compileHarnessSpec,
|
|
6
|
+
planWorkflowRequest,
|
|
7
|
+
replanWorkflowRequest,
|
|
8
|
+
parsePromptOrSkill,
|
|
9
|
+
buildTaskGraph,
|
|
10
|
+
analyzeRisks,
|
|
11
|
+
synthesizePolicy,
|
|
12
|
+
synthesizeHarness,
|
|
13
|
+
} from "../src/index.js";
|
|
14
|
+
|
|
15
|
+
describe("lasso package scaffold", () => {
|
|
16
|
+
it("exports the public entrypoints", () => {
|
|
17
|
+
expect(typeof validateHarnessSpec).toBe("function");
|
|
18
|
+
expect(typeof lowerHarnessSpecToCir).toBe("function");
|
|
19
|
+
expect(typeof compileHarnessSpec).toBe("function");
|
|
20
|
+
expect(typeof planWorkflowRequest).toBe("function");
|
|
21
|
+
expect(typeof replanWorkflowRequest).toBe("function");
|
|
22
|
+
expect(typeof parsePromptOrSkill).toBe("function");
|
|
23
|
+
expect(typeof buildTaskGraph).toBe("function");
|
|
24
|
+
expect(typeof analyzeRisks).toBe("function");
|
|
25
|
+
expect(typeof synthesizePolicy).toBe("function");
|
|
26
|
+
expect(typeof synthesizeHarness).toBe("function");
|
|
27
|
+
expect(typeof lassoExtension).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
});
|