@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,264 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { HarnessState } from "../../src/state/types.js";
3
+ import { createHarnessState, addFailure, recordNodeResult, updateMetrics, captureSnapshot } from "../../src/state/snapshots.js";
4
+ import type { FailureRecord } from "../../src/failures/types.js";
5
+
6
+ describe("State snapshots", () => {
7
+ describe("HarnessState type", () => {
8
+ it("should represent complete harness execution state", () => {
9
+ const state: HarnessState = {
10
+ inputs: { prNumber: 42 },
11
+ outputs: { patchApplied: true },
12
+ nodeResults: {
13
+ "fetch-pr": { data: "..." },
14
+ "apply-patch": { success: true },
15
+ },
16
+ failures: [
17
+ {
18
+ domainType: "pr-review",
19
+ rootCause: "tool_timeout",
20
+ nodeId: "apply-patch",
21
+ message: "Timeout on first attempt",
22
+ },
23
+ ],
24
+ metrics: {
25
+ retries: 1,
26
+ durationMs: 5000,
27
+ },
28
+ };
29
+
30
+ expect(state.inputs).toEqual({ prNumber: 42 });
31
+ expect(state.outputs).toEqual({ patchApplied: true });
32
+ expect(state.nodeResults["fetch-pr"]).toEqual({ data: "..." });
33
+ expect(state.failures).toHaveLength(1);
34
+ expect(state.metrics.retries).toBe(1);
35
+ expect(state.metrics.durationMs).toBe(5000);
36
+ });
37
+ });
38
+
39
+ describe("createHarnessState", () => {
40
+ it("should initialize empty harness state", () => {
41
+ const state = createHarnessState({ userId: 123 });
42
+
43
+ expect(state.inputs).toEqual({ userId: 123 });
44
+ expect(state.outputs).toEqual({});
45
+ expect(state.nodeResults).toEqual({});
46
+ expect(state.failures).toEqual([]);
47
+ expect(state.metrics.retries).toBe(0);
48
+ expect(state.metrics.durationMs).toBe(0);
49
+ });
50
+
51
+ it("should initialize with empty input", () => {
52
+ const state = createHarnessState(undefined);
53
+
54
+ expect(state.inputs).toEqual({});
55
+ expect(state.outputs).toEqual({});
56
+ expect(state.nodeResults).toEqual({});
57
+ expect(state.failures).toEqual([]);
58
+ expect(state.metrics.retries).toBe(0);
59
+ expect(state.metrics.durationMs).toBe(0);
60
+ });
61
+
62
+ it("should deeply clone nested input objects to prevent external mutation", () => {
63
+ const input = {
64
+ config: { debug: true, retries: 3 },
65
+ data: { items: [1, 2, 3] },
66
+ };
67
+ const state = createHarnessState(input);
68
+
69
+ // Mutate the original input object
70
+ (input.config as any).debug = false;
71
+ (input.config as any).retries = 999;
72
+ input.data.items.push(4);
73
+
74
+ // State should be unaffected
75
+ expect(state.inputs.config).toEqual({ debug: true, retries: 3 });
76
+ expect(state.inputs.data).toEqual({ items: [1, 2, 3] });
77
+ });
78
+ });
79
+
80
+ describe("addFailure", () => {
81
+ it("should append failure to state", () => {
82
+ const state = createHarnessState({ test: true });
83
+ const failure: FailureRecord = {
84
+ domainType: "test",
85
+ rootCause: "tool_timeout",
86
+ nodeId: "node-1",
87
+ message: "Timeout",
88
+ };
89
+
90
+ addFailure(state, failure);
91
+
92
+ expect(state.failures).toHaveLength(1);
93
+ expect(state.failures[0]).toEqual(failure);
94
+ });
95
+
96
+ it("should support multiple failures", () => {
97
+ const state = createHarnessState({});
98
+ const failure1: FailureRecord = {
99
+ domainType: "test",
100
+ rootCause: "tool_timeout",
101
+ message: "First",
102
+ };
103
+ const failure2: FailureRecord = {
104
+ domainType: "test",
105
+ rootCause: "rate_limited",
106
+ message: "Second",
107
+ };
108
+
109
+ addFailure(state, failure1);
110
+ addFailure(state, failure2);
111
+
112
+ expect(state.failures).toHaveLength(2);
113
+ expect(state.failures[0]).toEqual(failure1);
114
+ expect(state.failures[1]).toEqual(failure2);
115
+ });
116
+ });
117
+
118
+ describe("recordNodeResult", () => {
119
+ it("should record node execution result", () => {
120
+ const state = createHarnessState({});
121
+ const result = { status: "success", data: 42 };
122
+
123
+ recordNodeResult(state, "compute-node", result);
124
+
125
+ expect(state.nodeResults["compute-node"]).toEqual(result);
126
+ });
127
+
128
+ it("should overwrite previous result for same node", () => {
129
+ const state = createHarnessState({});
130
+
131
+ recordNodeResult(state, "retry-node", { attempt: 1, failed: true });
132
+ recordNodeResult(state, "retry-node", { attempt: 2, succeeded: true });
133
+
134
+ expect(state.nodeResults["retry-node"]).toEqual({ attempt: 2, succeeded: true });
135
+ });
136
+ });
137
+
138
+ describe("updateMetrics", () => {
139
+ it("should update retry count", () => {
140
+ const state = createHarnessState({});
141
+
142
+ updateMetrics(state, { retries: 3 });
143
+
144
+ expect(state.metrics.retries).toBe(3);
145
+ expect(state.metrics.durationMs).toBe(0);
146
+ });
147
+
148
+ it("should update duration", () => {
149
+ const state = createHarnessState({});
150
+
151
+ updateMetrics(state, { durationMs: 1234 });
152
+
153
+ expect(state.metrics.retries).toBe(0);
154
+ expect(state.metrics.durationMs).toBe(1234);
155
+ });
156
+
157
+ it("should update both metrics", () => {
158
+ const state = createHarnessState({});
159
+
160
+ updateMetrics(state, { retries: 2, durationMs: 5678 });
161
+
162
+ expect(state.metrics.retries).toBe(2);
163
+ expect(state.metrics.durationMs).toBe(5678);
164
+ });
165
+
166
+ it("should increment metrics when called multiple times", () => {
167
+ const state = createHarnessState({});
168
+
169
+ updateMetrics(state, { retries: 1 });
170
+ updateMetrics(state, { retries: 2 });
171
+
172
+ expect(state.metrics.retries).toBe(2);
173
+ });
174
+ });
175
+
176
+ describe("captureSnapshot", () => {
177
+ it("should return immutable snapshot of current state", () => {
178
+ const state = createHarnessState({ input: "test" });
179
+ recordNodeResult(state, "node-1", { value: 1 });
180
+ addFailure(state, {
181
+ domainType: "test",
182
+ rootCause: "unknown",
183
+ message: "Error",
184
+ });
185
+ updateMetrics(state, { retries: 1, durationMs: 100 });
186
+
187
+ const snapshot = captureSnapshot(state);
188
+
189
+ expect(snapshot.inputs).toEqual({ input: "test" });
190
+ expect(snapshot.nodeResults).toEqual({ "node-1": { value: 1 } });
191
+ expect(snapshot.failures).toHaveLength(1);
192
+ expect(snapshot.metrics.retries).toBe(1);
193
+ expect(snapshot.metrics.durationMs).toBe(100);
194
+ });
195
+
196
+ it("should be independent from original state", () => {
197
+ const state = createHarnessState({ x: 1 });
198
+ const snapshot = captureSnapshot(state);
199
+
200
+ recordNodeResult(state, "node-2", { value: 2 });
201
+ addFailure(state, {
202
+ domainType: "test",
203
+ rootCause: "unknown",
204
+ message: "New failure",
205
+ });
206
+
207
+ expect(snapshot.nodeResults).toEqual({});
208
+ expect(snapshot.failures).toHaveLength(0);
209
+ expect(state.nodeResults["node-2"]).toEqual({ value: 2 });
210
+ expect(state.failures).toHaveLength(1);
211
+ });
212
+
213
+ it("should deeply clone nested objects in inputs", () => {
214
+ const nestedInput = { config: { debug: true, retries: 3 } };
215
+ const state = createHarnessState(nestedInput);
216
+ const snapshot = captureSnapshot(state);
217
+
218
+ (state.inputs.config as any).debug = false;
219
+ (state.inputs.config as any).retries = 999;
220
+
221
+ expect(snapshot.inputs.config).toEqual({ debug: true, retries: 3 });
222
+ expect(state.inputs.config).toEqual({ debug: false, retries: 999 });
223
+ });
224
+
225
+ it("should deeply clone nested objects in outputs", () => {
226
+ const state = createHarnessState({});
227
+ state.outputs["node-1"] = { result: { status: "ok", data: [1, 2, 3] } };
228
+ const snapshot = captureSnapshot(state);
229
+
230
+ (state.outputs["node-1"] as any).result.status = "failed";
231
+ (state.outputs["node-1"] as any).result.data.push(4);
232
+
233
+ expect(snapshot.outputs["node-1"]).toEqual({ result: { status: "ok", data: [1, 2, 3] } });
234
+ expect(state.outputs["node-1"]).toEqual({ result: { status: "failed", data: [1, 2, 3, 4] } });
235
+ });
236
+
237
+ it("should deeply clone nested objects in nodeResults", () => {
238
+ const state = createHarnessState({});
239
+ recordNodeResult(state, "compute", { matrix: [[1, 2], [3, 4]] });
240
+ const snapshot = captureSnapshot(state);
241
+
242
+ ((state.nodeResults["compute"] as any).matrix[0] as number[]).push(999);
243
+
244
+ expect(snapshot.nodeResults["compute"]).toEqual({ matrix: [[1, 2], [3, 4]] });
245
+ expect(state.nodeResults["compute"]).toEqual({ matrix: [[1, 2, 999], [3, 4]] });
246
+ });
247
+
248
+ it("should deeply clone failure records", () => {
249
+ const state = createHarnessState({});
250
+ addFailure(state, {
251
+ domainType: "test",
252
+ rootCause: "unknown",
253
+ message: "Error",
254
+ nodeId: "node-1",
255
+ });
256
+ const snapshot = captureSnapshot(state);
257
+
258
+ state.failures[0]!.message = "Modified message";
259
+
260
+ expect(snapshot.failures[0]?.message).toBe("Error");
261
+ expect(state.failures[0]?.message).toBe("Modified message");
262
+ });
263
+ });
264
+ });
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parsePromptOrSkill } from "../../src/synthesis/skill-parser.js";
3
+ import { validateIntent } from "../../src/synthesis/intent-ir.js";
4
+ import type { IntentIR } from "../../src/synthesis/intent-ir.js";
5
+ import { buildTaskGraph } from "../../src/synthesis/graph-builder.js";
6
+ import { synthesizePolicy } from "../../src/synthesis/policy-builder.js";
7
+ import type { RiskModel } from "../../src/synthesis/risk-analyzer.js";
8
+
9
+ describe("custom workflow families", () => {
10
+ const lowRisks: RiskModel = {
11
+ overallRisk: "low",
12
+ approvalRequired: false,
13
+ candidateSourceKind: null,
14
+ verificationBreadth: "moderate",
15
+ stageRisks: new Map(),
16
+ mitigations: []
17
+ };
18
+
19
+ describe("validateIntent", () => {
20
+ it("should accept a custom family with steps", () => {
21
+ const intent: IntentIR = {
22
+ family: "deploy-staging",
23
+ goal: "Deploy to staging",
24
+ inputs: { repoPath: "/repo" },
25
+ requiredTools: ["git"],
26
+ humanCheckpoints: [],
27
+ verificationTargets: [],
28
+ steps: [
29
+ { id: "step-1", label: "Build", kind: "tool" },
30
+ { id: "step-2", label: "Deploy", kind: "tool" }
31
+ ]
32
+ };
33
+
34
+ const result = validateIntent(intent);
35
+ expect(result).toBeNull();
36
+ });
37
+
38
+ it("should accept a custom family without steps", () => {
39
+ const intent: IntentIR = {
40
+ family: "data-pipeline",
41
+ goal: "Run data pipeline",
42
+ inputs: {},
43
+ requiredTools: [],
44
+ humanCheckpoints: [],
45
+ verificationTargets: []
46
+ };
47
+
48
+ const result = validateIntent(intent);
49
+ expect(result).toBeNull();
50
+ });
51
+
52
+ it("should still accept patch-validation", () => {
53
+ const intent: IntentIR = {
54
+ family: "patch-validation",
55
+ goal: "Validate patch",
56
+ inputs: {},
57
+ requiredTools: ["git"],
58
+ humanCheckpoints: [],
59
+ verificationTargets: []
60
+ };
61
+
62
+ const result = validateIntent(intent);
63
+ expect(result).toBeNull();
64
+ });
65
+
66
+ it("should still accept pr-review-merge", () => {
67
+ const intent: IntentIR = {
68
+ family: "pr-review-merge",
69
+ goal: "Review PR",
70
+ inputs: {},
71
+ requiredTools: ["git"],
72
+ humanCheckpoints: [],
73
+ verificationTargets: []
74
+ };
75
+
76
+ const result = validateIntent(intent);
77
+ expect(result).toBeNull();
78
+ });
79
+ });
80
+
81
+ describe("buildTaskGraph", () => {
82
+ it("should create a graph from custom-family steps", () => {
83
+ const intent: IntentIR = {
84
+ family: "deploy-staging",
85
+ goal: "Deploy to staging",
86
+ inputs: { repoPath: "/repo", environment: "staging" },
87
+ requiredTools: ["git"],
88
+ humanCheckpoints: [],
89
+ verificationTargets: ["curl https://staging.example.com/health"],
90
+ steps: [
91
+ { id: "step-1", label: "Build artifacts", kind: "tool" },
92
+ { id: "step-2", label: "Deploy to staging", kind: "tool" },
93
+ { id: "step-3", label: "Verify health", kind: "condition" }
94
+ ]
95
+ };
96
+
97
+ const graph = buildTaskGraph(intent);
98
+
99
+ expect(graph.family).toBe("deploy-staging");
100
+ expect(graph.stages.find(s => s.id === "step-1")).toBeDefined();
101
+ expect(graph.stages.find(s => s.id === "step-2")).toBeDefined();
102
+ expect(graph.stages.find(s => s.id === "step-3")).toBeDefined();
103
+ expect(graph.stages.find(s => s.id === "verify-target-0")).toBeDefined();
104
+ });
105
+
106
+ it("should create a minimal single-node graph for custom family without steps", () => {
107
+ const intent: IntentIR = {
108
+ family: "data-pipeline",
109
+ goal: "Run pipeline",
110
+ inputs: { source: "s3://bucket/data" },
111
+ requiredTools: [],
112
+ humanCheckpoints: [],
113
+ verificationTargets: []
114
+ };
115
+
116
+ const graph = buildTaskGraph(intent);
117
+
118
+ expect(graph.family).toBe("data-pipeline");
119
+ expect(graph.stages).toHaveLength(0);
120
+ expect(graph.goal).toBe("Run pipeline");
121
+ });
122
+
123
+ it("should connect steps sequentially for custom families", () => {
124
+ const intent: IntentIR = {
125
+ family: "deploy-staging",
126
+ goal: "Deploy",
127
+ inputs: {},
128
+ requiredTools: [],
129
+ humanCheckpoints: [],
130
+ verificationTargets: [],
131
+ steps: [
132
+ { id: "step-1", label: "First", kind: "tool" },
133
+ { id: "step-2", label: "Second", kind: "tool" },
134
+ { id: "step-3", label: "Third", kind: "llm" }
135
+ ]
136
+ };
137
+
138
+ const graph = buildTaskGraph(intent);
139
+
140
+ expect(graph.stages.find(s => s.id === "step-1")!.dependencies).toEqual([]);
141
+ expect(graph.stages.find(s => s.id === "step-2")!.dependencies).toEqual(["step-1"]);
142
+ expect(graph.stages.find(s => s.id === "step-3")!.dependencies).toEqual(["step-2"]);
143
+ });
144
+ });
145
+
146
+ describe("synthesizePolicy", () => {
147
+ it("should produce a valid bundle for custom families with steps", () => {
148
+ const graph = buildTaskGraph({
149
+ family: "deploy-staging",
150
+ goal: "Deploy to staging",
151
+ inputs: { repoPath: "/repo" },
152
+ requiredTools: ["git"],
153
+ humanCheckpoints: [],
154
+ verificationTargets: [],
155
+ steps: [
156
+ { id: "step-1", label: "Build", kind: "tool" }
157
+ ]
158
+ });
159
+
160
+ const result = synthesizePolicy(graph, lowRisks);
161
+
162
+ expect(result.success).toBe(true);
163
+ if (result.success) {
164
+ expect(result.policy.workflow).toBe("deploy-staging");
165
+ expect(result.policy.bundle).toBeDefined();
166
+ expect(result.policy.rationale.length).toBeGreaterThan(0);
167
+ }
168
+ });
169
+
170
+ it("should produce a valid bundle for custom families without steps", () => {
171
+ const graph = buildTaskGraph({
172
+ family: "data-pipeline",
173
+ goal: "Run pipeline",
174
+ inputs: {},
175
+ requiredTools: [],
176
+ humanCheckpoints: [],
177
+ verificationTargets: []
178
+ });
179
+
180
+ const result = synthesizePolicy(graph, lowRisks);
181
+
182
+ expect(result.success).toBe(true);
183
+ if (result.success) {
184
+ expect(result.policy.workflow).toBe("data-pipeline");
185
+ expect(result.policy.bundle).toBeDefined();
186
+ }
187
+ });
188
+ });
189
+
190
+ describe("end-to-end: skill markdown with custom family", () => {
191
+ it("should compile a deploy-staging skill markdown end-to-end", () => {
192
+ const skillMd = `
193
+ # Deploy to Staging
194
+
195
+ workflow: deploy-staging
196
+
197
+ ## Inputs
198
+ - repoPath: /path/to/repo
199
+ - environment: staging
200
+
201
+ ## Steps
202
+ - [tool] Build artifacts
203
+ - [tool] Deploy to staging env
204
+ - [condition] Health check passed
205
+ - [human] Approve go-live
206
+
207
+ ## Verification
208
+ - curl https://staging.example.com/health
209
+ `;
210
+
211
+ const result = parsePromptOrSkill(skillMd);
212
+
213
+ expect(result).toHaveProperty("success", true);
214
+ if ("success" in result && result.success) {
215
+ expect(result.intent.family).toBe("deploy-staging");
216
+ expect(result.intent.steps).toHaveLength(4);
217
+ expect(result.intent.inputs.repoPath).toBe("/path/to/repo");
218
+ expect(result.intent.inputs.environment).toBe("staging");
219
+ expect(result.intent.verificationTargets).toContain("curl https://staging.example.com/health");
220
+
221
+ const graph = buildTaskGraph(result.intent);
222
+ expect(graph.family).toBe("deploy-staging");
223
+ expect(graph.stages.find(s => s.id === "step-1")).toBeDefined();
224
+ expect(graph.stages.find(s => s.id === "step-4")).toBeDefined();
225
+ expect(graph.stages.find(s => s.id === "verify-target-0")).toBeDefined();
226
+
227
+ const risks = {
228
+ overallRisk: "low" as const,
229
+ approvalRequired: false,
230
+ candidateSourceKind: null as null,
231
+ verificationBreadth: "moderate" as const,
232
+ stageRisks: new Map(),
233
+ mitigations: []
234
+ };
235
+ const policyResult = synthesizePolicy(graph, risks);
236
+ expect(policyResult.success).toBe(true);
237
+ }
238
+ });
239
+
240
+ it("should compile a custom family skill markdown with no steps", () => {
241
+ const skillMd = `
242
+ # Data Pipeline
243
+
244
+ workflow: data-pipeline
245
+
246
+ ## Inputs
247
+ - source: s3://bucket/data
248
+ `;
249
+
250
+ const result = parsePromptOrSkill(skillMd);
251
+
252
+ expect(result).toHaveProperty("success", true);
253
+ if ("success" in result && result.success) {
254
+ expect(result.intent.family).toBe("data-pipeline");
255
+
256
+ const graph = buildTaskGraph(result.intent);
257
+ expect(graph.family).toBe("data-pipeline");
258
+
259
+ const policyResult = synthesizePolicy(graph, lowRisks);
260
+ expect(policyResult.success).toBe(true);
261
+ }
262
+ });
263
+ });
264
+ });