@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,105 @@
1
+ import type { HarnessSpec, TaskNode, TaskEdge, ConditionNode, MergeNode } from "../spec/types.js";
2
+
3
+ export interface HarnessChain {
4
+ name: string;
5
+ stages: HarnessStage[];
6
+ }
7
+
8
+ export interface HarnessStage {
9
+ name: string;
10
+ spec: HarnessSpec;
11
+ inputMapping: Record<string, string>;
12
+ condition?: string;
13
+ }
14
+
15
+ export interface CompositionResult {
16
+ combinedSpec: HarnessSpec;
17
+ stageCount: number;
18
+ totalNodes: number;
19
+ estimatedDurationMs: number;
20
+ }
21
+
22
+ export interface HarnessComposer {
23
+ chain(stages: HarnessStage[]): CompositionResult;
24
+ parallel(harnesses: HarnessSpec[]): CompositionResult;
25
+ conditional(condition: string, trueHarness: HarnessSpec, falseHarness?: HarnessSpec): CompositionResult;
26
+ }
27
+
28
+ export function prefixNodeId(stageName: string, nodeId: string): string {
29
+ return `${stageName}:${nodeId}`;
30
+ }
31
+
32
+ export function prefixNode(stageName: string, node: TaskNode): TaskNode {
33
+ const prefixed: TaskNode = {
34
+ ...node,
35
+ id: prefixNodeId(stageName, node.id),
36
+ } as TaskNode;
37
+
38
+ if (node.kind === "condition") {
39
+ (prefixed as ConditionNode).thenNodeId = prefixNodeId(stageName, (node as ConditionNode).thenNodeId);
40
+ (prefixed as ConditionNode).elseNodeId = prefixNodeId(stageName, (node as ConditionNode).elseNodeId);
41
+ }
42
+
43
+ if (node.kind === "merge") {
44
+ (prefixed as MergeNode).waitFor = (node as MergeNode).waitFor.map((id: string) => prefixNodeId(stageName, id));
45
+ }
46
+
47
+ return prefixed;
48
+ }
49
+
50
+ export function prefixEdge(stageName: string, edge: TaskEdge): TaskEdge {
51
+ return {
52
+ from: prefixNodeId(stageName, edge.from),
53
+ to: prefixNodeId(stageName, edge.to),
54
+ };
55
+ }
56
+
57
+ export function prefixSpec(stageName: string, spec: HarnessSpec): HarnessSpec {
58
+ return {
59
+ name: spec.name,
60
+ graph: {
61
+ entryNodeId: prefixNodeId(stageName, spec.graph.entryNodeId),
62
+ nodes: spec.graph.nodes.map((node) => prefixNode(stageName, node)),
63
+ edges: spec.graph.edges.map((edge) => prefixEdge(stageName, edge)),
64
+ },
65
+ ...(spec.executionPolicy ? { executionPolicy: spec.executionPolicy } : {}),
66
+ ...(spec.humanPolicy ? { humanPolicy: spec.humanPolicy } : {}),
67
+ ...(spec.observabilityPolicy ? { observabilityPolicy: spec.observabilityPolicy } : {}),
68
+ };
69
+ }
70
+
71
+ export function findTerminalNodes(spec: HarnessSpec): string[] {
72
+ const nodeIds = new Set(spec.graph.nodes.map((n) => n.id));
73
+ const hasIncoming = new Set(spec.graph.edges.map((e) => e.to));
74
+
75
+ const terminals: string[] = [];
76
+ for (const node of spec.graph.nodes) {
77
+ if (node.kind === "condition") {
78
+ continue;
79
+ }
80
+ if (!hasIncoming.has(node.id) && node.id !== spec.graph.entryNodeId) {
81
+ continue;
82
+ }
83
+ const hasOutgoing = spec.graph.edges.some((e) => e.from === node.id);
84
+ if (!hasOutgoing && node.kind !== "merge") {
85
+ terminals.push(node.id);
86
+ }
87
+ }
88
+
89
+ if (terminals.length === 0) {
90
+ for (const node of spec.graph.nodes) {
91
+ if (node.kind !== "condition" && node.kind !== "merge") {
92
+ const hasOutgoing = spec.graph.edges.some((e) => e.from === node.id);
93
+ if (!hasOutgoing) {
94
+ terminals.push(node.id);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ if (terminals.length === 0 && spec.graph.nodes.length === 1) {
101
+ return [spec.graph.entryNodeId];
102
+ }
103
+
104
+ return terminals;
105
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ EnvironmentModel,
3
+ EnvironmentAnalysis,
4
+ ToolCapability,
5
+ Constraint,
6
+ } from "./types.js";
7
+
8
+ export function analyzeEnvironment(
9
+ model: EnvironmentModel,
10
+ requiredTools?: string[]
11
+ ): EnvironmentAnalysis {
12
+ const matchedTools: ToolCapability[] = [];
13
+ const missingTools: string[] = [];
14
+
15
+ if (requiredTools && requiredTools.length > 0) {
16
+ for (const toolName of requiredTools) {
17
+ const found = model.tools.find(t => t.name === toolName);
18
+ if (found && found.available) {
19
+ matchedTools.push(found);
20
+ } else {
21
+ missingTools.push(toolName);
22
+ }
23
+ }
24
+ }
25
+
26
+ const highRiskConstraints = model.constraints.filter(c => c.severity === "high");
27
+
28
+ const totalRequired = requiredTools?.length ?? 0;
29
+ let readinessScore = totalRequired > 0
30
+ ? Math.round((matchedTools.length / totalRequired) * 100)
31
+ : 100;
32
+
33
+ for (const constraint of highRiskConstraints) {
34
+ readinessScore = Math.max(0, readinessScore - 20);
35
+ }
36
+
37
+ const preparatorySteps: string[] = [];
38
+
39
+ for (const missing of missingTools) {
40
+ preparatorySteps.push(`Install missing tool: ${missing}`);
41
+ }
42
+
43
+ for (const constraint of model.constraints) {
44
+ if (constraint.severity === "high" || constraint.severity === "medium") {
45
+ preparatorySteps.push(`Address constraint: ${constraint.description}`);
46
+ }
47
+ }
48
+
49
+ return {
50
+ matchedTools,
51
+ missingTools,
52
+ highRiskConstraints,
53
+ readinessScore,
54
+ preparatorySteps,
55
+ };
56
+ }
@@ -0,0 +1,179 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { statfs } from "node:fs/promises";
4
+ import { connect } from "node:net";
5
+ import type {
6
+ EnvironmentModel,
7
+ ToolCapability,
8
+ Resource,
9
+ Constraint,
10
+ AuthState,
11
+ RepoState,
12
+ ExternalSystem,
13
+ DiscoveryOptions,
14
+ } from "./types.js";
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ const DEFAULT_TOOLS = ["bash", "git", "node", "npm", "python", "docker", "kubectl"];
19
+
20
+ async function probeTool(name: string): Promise<ToolCapability> {
21
+ try {
22
+ const { stdout } = await execAsync(`${name} --version`, { timeout: 5000 });
23
+ const version = stdout.trim().split("\n")[0].trim();
24
+ return { name, version, available: true };
25
+ } catch {
26
+ return { name, available: false };
27
+ }
28
+ }
29
+
30
+ async function probeDisk(): Promise<Resource> {
31
+ try {
32
+ const stat = await statfs("/");
33
+ const totalBytes = BigInt(stat.bsize) * BigInt(stat.blocks);
34
+ const freeBytes = BigInt(stat.bsize) * BigInt(stat.bfree);
35
+ const usedBytes = totalBytes - freeBytes;
36
+ const usagePct = ((Number(usedBytes) / Number(totalBytes)) * 100).toFixed(1);
37
+
38
+ return {
39
+ name: "disk",
40
+ type: "disk",
41
+ available: true,
42
+ limit: `${(Number(totalBytes) / 1e9).toFixed(1)}GB`,
43
+ usage: `${usagePct}%`,
44
+ };
45
+ } catch {
46
+ return { name: "disk", type: "disk", available: false };
47
+ }
48
+ }
49
+
50
+ async function probeRepo(repoPath: string): Promise<RepoState | undefined> {
51
+ try {
52
+ const { stdout: branch } = await execAsync("git branch --show-current", {
53
+ cwd: repoPath,
54
+ timeout: 5000,
55
+ });
56
+
57
+ const { stdout: status } = await execAsync("git status --porcelain", {
58
+ cwd: repoPath,
59
+ timeout: 5000,
60
+ });
61
+
62
+ const { stdout: remotesOut } = await execAsync("git remote", {
63
+ cwd: repoPath,
64
+ timeout: 5000,
65
+ });
66
+
67
+ const { stdout: tagsOut } = await execAsync("git tag --list", {
68
+ cwd: repoPath,
69
+ timeout: 5000,
70
+ });
71
+
72
+ return {
73
+ path: repoPath,
74
+ branch: branch.trim() || undefined,
75
+ hasUncommittedChanges: status.trim().length > 0,
76
+ remotes: remotesOut.trim().split("\n").filter(Boolean),
77
+ tags: tagsOut.trim().split("\n").filter(Boolean),
78
+ };
79
+ } catch {
80
+ return {
81
+ path: repoPath,
82
+ hasUncommittedChanges: false,
83
+ remotes: [],
84
+ tags: [],
85
+ };
86
+ }
87
+ }
88
+
89
+ async function probeExternalSystem(
90
+ host: string,
91
+ port: number,
92
+ timeoutMs: number
93
+ ): Promise<ExternalSystem> {
94
+ const start = Date.now();
95
+ return new Promise((resolve) => {
96
+ const socket = connect({ host, port, timeout: timeoutMs });
97
+
98
+ socket.on("connect", () => {
99
+ const latency = Date.now() - start;
100
+ socket.destroy();
101
+ resolve({ name: host, reachable: true, latencyMs: latency });
102
+ });
103
+
104
+ socket.on("error", (err) => {
105
+ socket.destroy();
106
+ resolve({ name: host, reachable: false, error: err.message });
107
+ });
108
+
109
+ socket.on("timeout", () => {
110
+ socket.destroy();
111
+ resolve({ name: host, reachable: false, error: "Connection timed out" });
112
+ });
113
+ });
114
+ }
115
+
116
+ function detectConstraints(tools: ToolCapability[]): Constraint[] {
117
+ const constraints: Constraint[] = [];
118
+
119
+ const gitTool = tools.find(t => t.name === "git");
120
+ if (gitTool && !gitTool.available) {
121
+ constraints.push({
122
+ type: "permission",
123
+ description: "git is not available — repo operations will fail",
124
+ severity: "high",
125
+ });
126
+ }
127
+
128
+ const dockerTool = tools.find(t => t.name === "docker");
129
+ if (dockerTool && !dockerTool.available) {
130
+ constraints.push({
131
+ type: "permission",
132
+ description: "docker is not available — container operations will fail",
133
+ severity: "medium",
134
+ });
135
+ }
136
+
137
+ return constraints;
138
+ }
139
+
140
+ function detectAuthState(): AuthState[] {
141
+ return [];
142
+ }
143
+
144
+ export async function discoverEnvironment(
145
+ repoPath?: string,
146
+ options?: DiscoveryOptions
147
+ ): Promise<EnvironmentModel> {
148
+ const toolsToProbe = options?.tools ?? DEFAULT_TOOLS;
149
+ const toolResults = await Promise.all(toolsToProbe.map(probeTool));
150
+
151
+ const diskResource = await probeDisk();
152
+
153
+ let repoState: RepoState | undefined;
154
+ if (repoPath) {
155
+ repoState = await probeRepo(repoPath);
156
+ }
157
+
158
+ const externalSystems: ExternalSystem[] = [];
159
+ if (options?.externalSystems && options.externalSystems.length > 0) {
160
+ const timeout = options.networkTimeoutMS ?? 3000;
161
+ const probes = options.externalSystems.map(async (host) => {
162
+ return probeExternalSystem(host, 443, timeout);
163
+ });
164
+ externalSystems.push(...(await Promise.all(probes)));
165
+ }
166
+
167
+ const constraints = detectConstraints(toolResults);
168
+ const authState = detectAuthState();
169
+
170
+ return {
171
+ tools: toolResults,
172
+ resources: [diskResource],
173
+ constraints,
174
+ authState,
175
+ repoState,
176
+ externalSystems,
177
+ discoveredAt: Date.now(),
178
+ };
179
+ }
@@ -0,0 +1,68 @@
1
+ export interface ToolCapability {
2
+ name: string;
3
+ version?: string;
4
+ available: boolean;
5
+ path?: string;
6
+ }
7
+
8
+ export interface Resource {
9
+ name: string;
10
+ type: "disk" | "memory" | "network" | "cpu";
11
+ available: boolean;
12
+ limit?: string;
13
+ usage?: string;
14
+ }
15
+
16
+ export interface Constraint {
17
+ type: "auth" | "network" | "permission" | "rate-limit";
18
+ description: string;
19
+ severity: "low" | "medium" | "high";
20
+ }
21
+
22
+ export interface AuthState {
23
+ system: string;
24
+ authenticated: boolean;
25
+ expiresAt?: number;
26
+ scopes?: string[];
27
+ }
28
+
29
+ export interface RepoState {
30
+ path: string;
31
+ branch?: string;
32
+ hasUncommittedChanges: boolean;
33
+ remotes: string[];
34
+ tags: string[];
35
+ }
36
+
37
+ export interface ExternalSystem {
38
+ name: string;
39
+ reachable: boolean;
40
+ latencyMs?: number;
41
+ error?: string;
42
+ }
43
+
44
+ export interface EnvironmentModel {
45
+ tools: ToolCapability[];
46
+ resources: Resource[];
47
+ constraints: Constraint[];
48
+ authState: AuthState[];
49
+ repoState?: RepoState;
50
+ externalSystems: ExternalSystem[];
51
+ discoveredAt: number;
52
+ }
53
+
54
+ export interface DiscoveryOptions {
55
+ tools?: string[];
56
+ checkAuth?: boolean;
57
+ checkNetwork?: boolean;
58
+ networkTimeoutMs?: number;
59
+ externalSystems?: string[];
60
+ }
61
+
62
+ export interface EnvironmentAnalysis {
63
+ matchedTools: ToolCapability[];
64
+ missingTools: string[];
65
+ highRiskConstraints: Constraint[];
66
+ readinessScore: number;
67
+ preparatorySteps: string[];
68
+ }
@@ -0,0 +1,134 @@
1
+ export interface ClassifierResult {
2
+ matched: boolean;
3
+ evidence: string[];
4
+ }
5
+
6
+ const AUTH_PATTERNS = [
7
+ { regex: /\b401\b/i, evidence: "HTTP 401" },
8
+ { regex: /\b403\b/i, evidence: "HTTP 403" },
9
+ { regex: /unauthorized/i, evidence: "unauthorized" },
10
+ { regex: /authentication required/i, evidence: "authentication required" },
11
+ { regex: /token expired/i, evidence: "token expired" },
12
+ { regex: /invalid credentials/i, evidence: "invalid credentials" },
13
+ { regex: /access denied/i, evidence: "access denied" },
14
+ { regex: /forbidden/i, evidence: "forbidden" },
15
+ ];
16
+
17
+ const TOOL_PATTERNS = [
18
+ { regex: /command not found/i, evidence: "command not found" },
19
+ { regex: /exit(?:ed)? with code (\d+)/i, evidence: "exit code" },
20
+ { regex: /no such file or directory/i, evidence: "no such file or directory" },
21
+ { regex: /stderr:/i, evidence: "stderr output" },
22
+ { regex: /ENOENT/i, evidence: "ENOENT" },
23
+ { regex: /not a valid/i, evidence: "not a valid" },
24
+ ];
25
+
26
+ const RESOURCE_PATTERNS = [
27
+ { regex: /no space left on device/i, evidence: "disk full" },
28
+ { regex: /out of memory/i, evidence: "out of memory" },
29
+ { regex: /heap.*memory/i, evidence: "heap memory" },
30
+ { regex: /rate limit/i, evidence: "rate limit" },
31
+ { regex: /too many requests/i, evidence: "too many requests" },
32
+ { regex: /cannot allocate memory/i, evidence: "cannot allocate memory" },
33
+ { regex: /resource.*exhaust/i, evidence: "resource exhausted" },
34
+ ];
35
+
36
+ const SEMANTIC_PATTERNS = [
37
+ { regex: /assertion failed/i, evidence: "assertion failed" },
38
+ { regex: /unexpected output/i, evidence: "unexpected output" },
39
+ { regex: /schema/i, evidence: "schema" },
40
+ { regex: /type mismatch/i, evidence: "type mismatch" },
41
+ { regex: /validation error/i, evidence: "validation error" },
42
+ { regex: /invalid (?:format|json|yaml|xml)/i, evidence: "invalid format" },
43
+ ];
44
+
45
+ const HUMAN_PATTERNS = [
46
+ { regex: /reject(?:ed)?/i, evidence: "rejected" },
47
+ { regex: /declined/i, evidence: "declined" },
48
+ { regex: /no response/i, evidence: "no response" },
49
+ { regex: /timed out waiting for human/i, evidence: "human timeout" },
50
+ { regex: /human.*timeout/i, evidence: "human timeout" },
51
+ { regex: /approval.*timeout/i, evidence: "approval timeout" },
52
+ ];
53
+
54
+ const ENVIRONMENT_DRIFT_PATTERNS = [
55
+ { regex: /version mismatch/i, evidence: "version mismatch" },
56
+ { regex: /cannot find module/i, evidence: "missing dependency" },
57
+ { regex: /module.*not found/i, evidence: "missing dependency" },
58
+ { regex: /config(?:uration)? changed/i, evidence: "config changed" },
59
+ { regex: /incompatible.*version/i, evidence: "incompatible version" },
60
+ { regex: /required.*version/i, evidence: "required version" },
61
+ { regex: /dependency.*missing/i, evidence: "missing dependency" },
62
+ ];
63
+
64
+ const NETWORK_PATTERNS = [
65
+ { regex: /(?:connection|request) timed out/i, evidence: "timeout" },
66
+ { regex: /ECONNREFUSED/i, evidence: "connection refused" },
67
+ { regex: /ENOTFOUND/i, evidence: "DNS" },
68
+ { regex: /getaddrinfo/i, evidence: "DNS" },
69
+ { regex: /network.*unreachable/i, evidence: "network unreachable" },
70
+ { regex: /socket hang up/i, evidence: "socket hang up" },
71
+ { regex: /ECONNRESET/i, evidence: "connection reset" },
72
+ { regex: /ETIMEDOUT/i, evidence: "ETIMEDOUT" },
73
+ { regex: /connect.*refused/i, evidence: "connection refused" },
74
+ ];
75
+
76
+ function getErrorMessage(error: unknown): string {
77
+ if (error instanceof Error) {
78
+ return error.message;
79
+ }
80
+ if (typeof error === "string") {
81
+ return error;
82
+ }
83
+ return String(error);
84
+ }
85
+
86
+ function matchPatterns(
87
+ message: string,
88
+ patterns: Array<{ regex: RegExp; evidence: string }>,
89
+ ): ClassifierResult {
90
+ const evidence: string[] = [];
91
+ for (const pattern of patterns) {
92
+ if (pattern.regex.test(message)) {
93
+ if (pattern.regex.source.includes("(\\d+)")) {
94
+ const match = message.match(pattern.regex);
95
+ if (match) {
96
+ evidence.push(`${pattern.evidence} ${match[1]}`);
97
+ } else {
98
+ evidence.push(pattern.evidence);
99
+ }
100
+ } else {
101
+ evidence.push(pattern.evidence);
102
+ }
103
+ }
104
+ }
105
+ return { matched: evidence.length > 0, evidence };
106
+ }
107
+
108
+ export function classifyAuthFailure(error: unknown): ClassifierResult {
109
+ return matchPatterns(getErrorMessage(error), AUTH_PATTERNS);
110
+ }
111
+
112
+ export function classifyToolFailure(error: unknown): ClassifierResult {
113
+ return matchPatterns(getErrorMessage(error), TOOL_PATTERNS);
114
+ }
115
+
116
+ export function classifyResourceFailure(error: unknown): ClassifierResult {
117
+ return matchPatterns(getErrorMessage(error), RESOURCE_PATTERNS);
118
+ }
119
+
120
+ export function classifySemanticFailure(error: unknown): ClassifierResult {
121
+ return matchPatterns(getErrorMessage(error), SEMANTIC_PATTERNS);
122
+ }
123
+
124
+ export function classifyHumanFailure(error: unknown): ClassifierResult {
125
+ return matchPatterns(getErrorMessage(error), HUMAN_PATTERNS);
126
+ }
127
+
128
+ export function classifyEnvironmentDriftFailure(error: unknown): ClassifierResult {
129
+ return matchPatterns(getErrorMessage(error), ENVIRONMENT_DRIFT_PATTERNS);
130
+ }
131
+
132
+ export function classifyNetworkFailure(error: unknown): ClassifierResult {
133
+ return matchPatterns(getErrorMessage(error), NETWORK_PATTERNS);
134
+ }