@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,131 @@
1
+ import type {
2
+ ExecutionPolicy,
3
+ FailureClassification,
4
+ HumanNode,
5
+ HumanPolicy,
6
+ LlmNode,
7
+ MergeNode,
8
+ ObservabilityPolicy,
9
+ RetryPolicy,
10
+ SubworkflowNode,
11
+ TaskNode,
12
+ ToolNode,
13
+ VerificationRule,
14
+ } from "../spec/types.js";
15
+
16
+ export interface CirWorkflow {
17
+ name: string;
18
+ entryNodeId: string;
19
+ nodes: CirNode[];
20
+ transitions: CirTransition[];
21
+ globalPolicies?: CirGlobalPolicies;
22
+ }
23
+
24
+ export interface CirGlobalPolicies {
25
+ execution?: ExecutionPolicy;
26
+ human?: HumanPolicy;
27
+ observability?: ObservabilityPolicy;
28
+ }
29
+
30
+ export type CirExecutionPolicy = Omit<ExecutionPolicy, "failureClassification">;
31
+
32
+ export interface CirSourceRef {
33
+ specNodeId: string;
34
+ specNodeKind: TaskNode["kind"];
35
+ specPath: string;
36
+ label?: string;
37
+ }
38
+
39
+ export interface CirTransitionSource {
40
+ kind: "graph-edge" | "condition-then" | "condition-else";
41
+ specPath: string;
42
+ specNodeId?: string;
43
+ }
44
+
45
+ export type CirTransitionWhen = "success" | "condition-true" | "condition-false";
46
+
47
+ export interface CirTransition {
48
+ from: string;
49
+ to: string;
50
+ when: CirTransitionWhen;
51
+ source: CirTransitionSource;
52
+ }
53
+
54
+ export type CirFailureRoutingHint = FailureClassification;
55
+
56
+ export interface CirVerificationHook {
57
+ kind: "tool" | "llm" | "expression";
58
+ checkNodeId: string;
59
+ onFail: VerificationRule["onFail"];
60
+ maxAttempts?: number;
61
+ }
62
+
63
+ export interface CirNodeBase<K extends TaskNode["kind"] = TaskNode["kind"]> {
64
+ id: string;
65
+ kind: K;
66
+ source: CirSourceRef;
67
+ execution?: CirExecutionPolicy;
68
+ retry?: RetryPolicy;
69
+ verification?: CirVerificationHook[];
70
+ failureRouting?: CirFailureRoutingHint[];
71
+ terminal?: boolean;
72
+ }
73
+
74
+ export interface CirToolNode extends CirNodeBase<"tool"> {
75
+ action: {
76
+ tool: ToolNode["tool"];
77
+ args: ToolNode["args"];
78
+ env?: ToolNode["env"];
79
+ cwd?: ToolNode["cwd"];
80
+ };
81
+ }
82
+
83
+ export interface CirLlmNode extends CirNodeBase<"llm"> {
84
+ action: {
85
+ provider: LlmNode["provider"];
86
+ model: LlmNode["model"];
87
+ prompt: LlmNode["prompt"];
88
+ system?: LlmNode["system"];
89
+ temperature?: LlmNode["temperature"];
90
+ maxTokens?: LlmNode["maxTokens"];
91
+ };
92
+ }
93
+
94
+ export interface CirHumanNode extends CirNodeBase<"human"> {
95
+ action: {
96
+ prompt: HumanNode["prompt"];
97
+ interactionType: HumanNode["interactionType"];
98
+ options?: HumanNode["options"];
99
+ timeout?: number;
100
+ };
101
+ }
102
+
103
+ export interface CirConditionNode extends CirNodeBase<"condition"> {
104
+ action: {
105
+ conditionExpr: string;
106
+ };
107
+ }
108
+
109
+ export interface CirMergeNode extends CirNodeBase<"merge"> {
110
+ action: {
111
+ join: {
112
+ waitFor: string[];
113
+ strategy: NonNullable<MergeNode["strategy"]>;
114
+ };
115
+ };
116
+ }
117
+
118
+ export interface CirSubworkflowNode extends CirNodeBase<"subworkflow"> {
119
+ action: {
120
+ specRef: SubworkflowNode["specRef"];
121
+ inputs?: Record<string, unknown>;
122
+ };
123
+ }
124
+
125
+ export type CirNode =
126
+ | CirToolNode
127
+ | CirLlmNode
128
+ | CirHumanNode
129
+ | CirConditionNode
130
+ | CirMergeNode
131
+ | CirSubworkflowNode;
@@ -0,0 +1,265 @@
1
+ import type { CirMergeNode, CirNode, CirTransition, CirWorkflow } from "./types.js";
2
+
3
+ export type CirValidationResult =
4
+ | { valid: true }
5
+ | { valid: false; errors: string[] };
6
+
7
+ const retryableKinds = new Set(["tool", "llm", "subworkflow"]);
8
+ const verifiableKinds = new Set(["tool", "llm", "human", "subworkflow"]);
9
+ const verifierKinds = new Set(["tool", "llm", "human", "subworkflow"]);
10
+
11
+ export function validateCirWorkflow(workflow: CirWorkflow): CirValidationResult {
12
+ const errors: string[] = [];
13
+ const nodeMap = new Map<string, CirNode>();
14
+ const outgoingTransitions = new Map<string, CirTransition[]>();
15
+ const incomingTransitions = new Map<string, CirTransition[]>();
16
+ const transitionKeys = new Set<string>();
17
+
18
+ for (const node of workflow.nodes) {
19
+ if (nodeMap.has(node.id)) {
20
+ errors.push(`Duplicate CIR node ID: ${node.id}`);
21
+ continue;
22
+ }
23
+
24
+ nodeMap.set(node.id, node);
25
+
26
+ if (!node.source.specNodeId || !node.source.specNodeKind || !node.source.specPath) {
27
+ errors.push(`CIR node ${node.id} is missing source metadata`);
28
+ }
29
+ }
30
+
31
+ if (!nodeMap.has(workflow.entryNodeId)) {
32
+ errors.push(`CIR entry node not found: ${workflow.entryNodeId}`);
33
+ }
34
+
35
+ for (const transition of workflow.transitions) {
36
+ if (!transition.source.kind || !transition.source.specPath) {
37
+ errors.push(`Transition ${transition.from} -> ${transition.to} is missing source metadata`);
38
+ }
39
+
40
+ if (!nodeMap.has(transition.from)) {
41
+ errors.push(`Transition references nonexistent source node: ${transition.from}`);
42
+ }
43
+
44
+ if (!nodeMap.has(transition.to)) {
45
+ errors.push(`Transition references nonexistent target node: ${transition.to}`);
46
+ }
47
+
48
+ const transitionKey = `${transition.from}:${transition.when}:${transition.to}`;
49
+ if (transitionKeys.has(transitionKey)) {
50
+ errors.push(`Duplicate CIR transition: ${transition.from} -[${transition.when}]-> ${transition.to}`);
51
+ } else {
52
+ transitionKeys.add(transitionKey);
53
+ }
54
+
55
+ const outgoing = outgoingTransitions.get(transition.from) ?? [];
56
+ outgoing.push(transition);
57
+ outgoingTransitions.set(transition.from, outgoing);
58
+
59
+ const incoming = incomingTransitions.get(transition.to) ?? [];
60
+ incoming.push(transition);
61
+ incomingTransitions.set(transition.to, incoming);
62
+ }
63
+
64
+ validateReachability(workflow, nodeMap, outgoingTransitions, errors);
65
+
66
+ for (const node of workflow.nodes) {
67
+ const outgoing = outgoingTransitions.get(node.id) ?? [];
68
+
69
+ if (node.retry && !retryableKinds.has(node.kind)) {
70
+ errors.push(`CIR node ${node.id} of kind "${node.kind}" cannot carry retry metadata`);
71
+ }
72
+
73
+ if (node.verification?.length && !verifiableKinds.has(node.kind)) {
74
+ errors.push(`CIR node ${node.id} of kind "${node.kind}" cannot carry verification hooks`);
75
+ }
76
+
77
+ if (node.terminal && outgoing.length > 0) {
78
+ errors.push(`Terminal CIR node ${node.id} cannot have outgoing transitions`);
79
+ }
80
+
81
+ validateVerificationHooks(node, nodeMap, errors);
82
+
83
+ switch (node.kind) {
84
+ case "condition": {
85
+ const successTransitions = outgoing.filter(transition => transition.when === "success");
86
+ const trueTransitions = outgoing.filter(transition => transition.when === "condition-true");
87
+ const falseTransitions = outgoing.filter(transition => transition.when === "condition-false");
88
+
89
+ if (node.terminal) {
90
+ errors.push(`Condition node ${node.id} cannot be terminal`);
91
+ }
92
+
93
+ if (successTransitions.length > 0) {
94
+ errors.push(`Condition node ${node.id} cannot have success transitions`);
95
+ }
96
+
97
+ if (trueTransitions.length !== 1) {
98
+ errors.push(`Condition node ${node.id} must have exactly one condition-true transition`);
99
+ }
100
+
101
+ if (falseTransitions.length !== 1) {
102
+ errors.push(`Condition node ${node.id} must have exactly one condition-false transition`);
103
+ }
104
+
105
+ if ([...trueTransitions, ...falseTransitions].some(transition => transition.to === node.id)) {
106
+ errors.push(`Condition node ${node.id} cannot branch to itself`);
107
+ }
108
+
109
+ break;
110
+ }
111
+ case "merge":
112
+ validateMergeNode(node, nodeMap, outgoing, incomingTransitions.get(node.id) ?? [], errors);
113
+ break;
114
+ default: {
115
+ const branchTransitions = outgoing.filter(transition => transition.when !== "success");
116
+ if (branchTransitions.length > 0) {
117
+ errors.push(`CIR node ${node.id} of kind "${node.kind}" cannot have conditional transitions`);
118
+ }
119
+
120
+ if (!node.terminal && outgoing.length === 0) {
121
+ errors.push(`Non-terminal CIR node ${node.id} has no outgoing transitions`);
122
+ }
123
+
124
+ break;
125
+ }
126
+ }
127
+ }
128
+
129
+ return errors.length === 0 ? { valid: true } : { valid: false, errors };
130
+ }
131
+
132
+ function validateReachability(
133
+ workflow: CirWorkflow,
134
+ nodeMap: Map<string, CirNode>,
135
+ outgoingTransitions: Map<string, CirTransition[]>,
136
+ errors: string[],
137
+ ): void {
138
+ if (!nodeMap.has(workflow.entryNodeId)) {
139
+ return;
140
+ }
141
+
142
+ // Collect verification nodes
143
+ const verificationNodes = new Set<string>();
144
+ for (const node of nodeMap.values()) {
145
+ if (node.verification) {
146
+ for (const hook of node.verification) {
147
+ verificationNodes.add(hook.checkNodeId);
148
+ // If it's a condition node used as expression verifier, mark its branches too
149
+ const verifierNode = nodeMap.get(hook.checkNodeId);
150
+ if (verifierNode?.kind === "condition") {
151
+ const condTransitions = outgoingTransitions.get(hook.checkNodeId) ?? [];
152
+ for (const trans of condTransitions) {
153
+ verificationNodes.add(trans.to);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ const reachableNodeIds = new Set<string>([workflow.entryNodeId]);
161
+ const queue = [workflow.entryNodeId];
162
+
163
+ while (queue.length > 0) {
164
+ const currentNodeId = queue.shift()!;
165
+ for (const transition of outgoingTransitions.get(currentNodeId) ?? []) {
166
+ if (!reachableNodeIds.has(transition.to)) {
167
+ reachableNodeIds.add(transition.to);
168
+ queue.push(transition.to);
169
+ }
170
+ }
171
+ }
172
+
173
+ for (const nodeId of nodeMap.keys()) {
174
+ if (!reachableNodeIds.has(nodeId) && !verificationNodes.has(nodeId)) {
175
+ errors.push(`Unreachable CIR node: ${nodeId}`);
176
+ }
177
+ }
178
+ }
179
+
180
+ function validateVerificationHooks(node: CirNode, nodeMap: Map<string, CirNode>, errors: string[]): void {
181
+ if (!node.verification) {
182
+ return;
183
+ }
184
+
185
+ for (const hook of node.verification) {
186
+ const verifier = nodeMap.get(hook.checkNodeId);
187
+ if (!verifier) {
188
+ errors.push(`Verification hook on ${node.id} references missing node: ${hook.checkNodeId}`);
189
+ continue;
190
+ }
191
+
192
+ if (hook.checkNodeId === node.id) {
193
+ errors.push(`Verification hook on ${node.id} cannot reference itself`);
194
+ }
195
+
196
+ // Validate kind matching
197
+ if (hook.kind === "tool" && verifier.kind !== "tool") {
198
+ errors.push(
199
+ `Verification hook on ${node.id} has kind "tool" but references node ${hook.checkNodeId} of kind "${verifier.kind}"`,
200
+ );
201
+ } else if (hook.kind === "llm" && verifier.kind !== "llm") {
202
+ errors.push(
203
+ `Verification hook on ${node.id} has kind "llm" but references node ${hook.checkNodeId} of kind "${verifier.kind}"`,
204
+ );
205
+ } else if (hook.kind === "expression" && verifier.kind !== "condition") {
206
+ errors.push(
207
+ `Verification hook on ${node.id} has kind "expression" but references node ${hook.checkNodeId} of kind "${verifier.kind}" (expected "condition")`,
208
+ );
209
+ }
210
+
211
+ // Keep existing verifier kind check for backwards compatibility
212
+ if (!verifierKinds.has(verifier.kind) && hook.kind !== "expression") {
213
+ errors.push(
214
+ `Verification hook on ${node.id} references node ${hook.checkNodeId} of kind "${verifier.kind}", which cannot act as a verifier`,
215
+ );
216
+ }
217
+ }
218
+ }
219
+
220
+ function validateMergeNode(
221
+ node: CirMergeNode,
222
+ nodeMap: Map<string, CirNode>,
223
+ outgoing: CirTransition[],
224
+ incoming: CirTransition[],
225
+ errors: string[],
226
+ ): void {
227
+ const waitFor = node.action.join.waitFor;
228
+ const waitForSet = new Set(waitFor);
229
+ const incomingSources = new Set(incoming.map(transition => transition.from));
230
+
231
+ for (const transition of incoming) {
232
+ if (transition.when !== "success") {
233
+ errors.push(`Merge node ${node.id} can only receive success transitions (received ${transition.when} from ${transition.from})`);
234
+ }
235
+ }
236
+
237
+ if (waitFor.length === 0) {
238
+ errors.push(`Merge node ${node.id} must wait for at least one node`);
239
+ }
240
+
241
+ if (waitForSet.size !== waitFor.length) {
242
+ errors.push(`Merge node ${node.id} has duplicate waitFor entries`);
243
+ }
244
+
245
+ for (const waitForNodeId of waitFor) {
246
+ if (!nodeMap.has(waitForNodeId)) {
247
+ errors.push(`Merge node ${node.id} references missing waitFor node: ${waitForNodeId}`);
248
+ continue;
249
+ }
250
+
251
+ if (!incomingSources.has(waitForNodeId)) {
252
+ errors.push(`Merge node ${node.id} waitFor node ${waitForNodeId} does not transition into the merge`);
253
+ }
254
+ }
255
+
256
+ for (const incomingSource of incomingSources) {
257
+ if (!waitForSet.has(incomingSource)) {
258
+ errors.push(`Merge node ${node.id} has incoming transition from ${incomingSource} that is not declared in waitFor`);
259
+ }
260
+ }
261
+
262
+ if (!node.terminal && outgoing.length === 0) {
263
+ errors.push(`Non-terminal CIR node ${node.id} has no outgoing transitions`);
264
+ }
265
+ }