mcp-castor 2026.3.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 (42) hide show
  1. package/README.md +487 -0
  2. package/bin/castor.js +706 -0
  3. package/index.js +206 -0
  4. package/package.json +97 -0
  5. package/skills/canary-test-staging/SKILL.md +24 -0
  6. package/skills/evo-mutation-rollback/SKILL.md +29 -0
  7. package/skills/hypothesis-generation/SKILL.md +26 -0
  8. package/skills/traceback-condensing/SKILL.md +26 -0
  9. package/src/castor_runner.js +469 -0
  10. package/src/config.js +1204 -0
  11. package/src/env.js +10 -0
  12. package/src/evo_engine.js +214 -0
  13. package/src/harness/core/events.js +75 -0
  14. package/src/harness/core/kernel.js +209 -0
  15. package/src/harness/evo/evaluator.js +156 -0
  16. package/src/harness/evo/evo_operator.js +550 -0
  17. package/src/harness/evo/lineage_dag.js +383 -0
  18. package/src/harness/evo/trace_repair.js +173 -0
  19. package/src/harness/evo/watchdog.js +72 -0
  20. package/src/harness/loop_detector.js +135 -0
  21. package/src/harness/runner.js +1216 -0
  22. package/src/harness/services/ast_service.js +1813 -0
  23. package/src/harness/services/event_logger.js +275 -0
  24. package/src/harness/services/mcp_bridge.js +408 -0
  25. package/src/harness/services/provider_vllm.js +728 -0
  26. package/src/harness/services/sandbox_fs.js +1238 -0
  27. package/src/harness/services/searxng_lifecycle.js +254 -0
  28. package/src/harness/services/shell_executor.js +264 -0
  29. package/src/harness/services/shell_validator.js +506 -0
  30. package/src/harness/services/web_service.js +828 -0
  31. package/src/platform.js +344 -0
  32. package/src/repetition_detector.js +139 -0
  33. package/src/semaphore.js +373 -0
  34. package/src/server_lifecycle.js +781 -0
  35. package/src/skills.js +400 -0
  36. package/src/state_pruner.js +392 -0
  37. package/src/task_registry.js +1357 -0
  38. package/src/telemetry.js +638 -0
  39. package/src/tools.js +997 -0
  40. package/src/wsl_bridge.js +629 -0
  41. package/src/wsl_env.js +171 -0
  42. package/stream_proxy.js +453 -0
package/src/env.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * src/env.js — Leaf module: host platform detection.
3
+ *
4
+ * Single source of truth for the platform check. Kept as a LEAF (no imports)
5
+ * so that any module — including config.js and wsl_env.js — can import it
6
+ * without creating a dependency cycle. Previously this constant lived in
7
+ * config.js, which forced platform.js to import config.js (and vice versa),
8
+ * the cycle that produced the TDZ ReferenceError on Linux CI.
9
+ */
10
+ export const IS_WINDOWS = process.platform === "win32";
@@ -0,0 +1,214 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import path from "node:path";
4
+ import { LineageDag } from "./harness/evo/lineage_dag.js";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ /**
9
+ * Maps a canonical status to the legacy `state` view's status names.
10
+ * Only a rejected candidate maps to "FAILED"; neutral/pending/unknown map to
11
+ * their respective names.
12
+ */
13
+ function mapStatusToLegacy(status) {
14
+ const s = typeof status === "string" ? status.trim().toLowerCase() : "";
15
+ if (s === "accepted") return "ACCEPTED";
16
+ if (s === "rejected") return "FAILED";
17
+ if (s === "pending") return "PENDING";
18
+ if (s === "neutral") return "NEUTRAL";
19
+ return "UNKNOWN";
20
+ }
21
+
22
+ /**
23
+ * Evo Lineage Engine.
24
+ *
25
+ * Delegates to the unified LineageDag engine to guarantee atomic persistence,
26
+ * single-writer synchronization, and graph-based candidate tracking.
27
+ *
28
+ * The DAG is a per-workspace singleton (LineageDag.forWorkspace), so this
29
+ * engine and the EvoOperator share one in-memory graph and one
30
+ * read-modify-write persist path.
31
+ */
32
+ export class EvoLineageEngine {
33
+ constructor(cwd) {
34
+ this.cwd = cwd;
35
+ this.evoDir = path.join(cwd, ".evo");
36
+ this.dag = LineageDag.forWorkspace(cwd);
37
+ }
38
+
39
+ async init() {
40
+ this.dag._load();
41
+ }
42
+
43
+ get state() {
44
+ // Compatibility view for legacy callers
45
+ const nodes = Array.from(this.dag.nodes.values());
46
+ const candidates = nodes
47
+ .filter((n) => n.id !== "baseline")
48
+ .map((n) => ({
49
+ candidateId: n.id,
50
+ parentCommit: n.parentId,
51
+ commitSha: n.metrics?.commitSha,
52
+ hypothesis: n.hypothesis,
53
+ filesModified: n.filesModified,
54
+ metricScore: n.metrics?.fitness ?? n.metrics?.score ?? null,
55
+ // Only a rejected candidate maps to "FAILED".
56
+ status: mapStatusToLegacy(n.metrics?.status),
57
+ timestamp: n.timestamp,
58
+ errorLog: n.metrics?.errorLog ?? null,
59
+ }));
60
+
61
+ const best = this.dag.getBestCandidate();
62
+ return {
63
+ initializedAt: nodes[0]?.timestamp || new Date().toISOString(),
64
+ bestCommit: best?.metrics?.commitSha || "baseline",
65
+ bestMetric: best?.metrics?.fitness ?? null,
66
+ targetMetricName: null,
67
+ higherIsBetter: true,
68
+ candidates,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Returns the current git commit SHA, or null when it cannot be determined
74
+ * (no git repo, git unavailable, or a non-zero exit). Callers must treat
75
+ * null as "unknown".
76
+ */
77
+ async getCurrentCommit() {
78
+ try {
79
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: this.cwd });
80
+ const sha = stdout.trim();
81
+ return sha || null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ async save() {
88
+ this.dag.persist();
89
+ }
90
+
91
+ /**
92
+ * Generates a grounded, hallucination-free lineage brief for the agent.
93
+ */
94
+ async getLineageBrief() {
95
+ await this.init();
96
+ const nodes = Array.from(this.dag.nodes.values()).filter((n) => n.id !== "baseline");
97
+ if (nodes.length === 0) {
98
+ return (
99
+ `Evo Lineage Status: Fresh baseline.\n` +
100
+ `No candidate variations evaluated yet.`
101
+ );
102
+ }
103
+
104
+ const recent = nodes.slice(-8);
105
+ const summaryLines = recent.map((n) => {
106
+ const statusIcon = n.metrics?.status === "accepted" ? "[OK]" : "[FAIL]";
107
+ const metricStr = n.metrics?.fitness !== null && n.metrics?.fitness !== undefined
108
+ ? `Metric: ${n.metrics.fitness}`
109
+ : "No metric";
110
+ const failSnippet = n.metrics?.errorLog ? ` | Error: ${n.metrics.errorLog.slice(0, 120).replace(/\n/g, " ")}` : "";
111
+ return `${statusIcon} [${n.id}] Hypothesis: "${n.hypothesis}" -> ${n.metrics?.status?.toUpperCase()} (${metricStr})${failSnippet}`;
112
+ });
113
+
114
+ const best = this.dag.getBestCandidate();
115
+ return (
116
+ `=== Evo Candidate Lineage History ===\n` +
117
+ `Active Head: ${this.dag.currentHeadId} (Best Score: ${best?.metrics?.fitness ?? "N/A"})\n` +
118
+ `Total Candidates Evaluated: ${nodes.length}\n` +
119
+ `Recent Trajectory (Last ${recent.length}):\n` +
120
+ summaryLines.join("\n") +
121
+ `\n============================================`
122
+ );
123
+ }
124
+
125
+ async getLineageContext() {
126
+ return this.getLineageBrief();
127
+ }
128
+
129
+ extractMetric(output, name) {
130
+ if (!output || !name) return null;
131
+ let value = null;
132
+ for (const line of String(output).split(/\r?\n/)) {
133
+ if (!line.toLowerCase().includes(String(name).toLowerCase())) continue;
134
+ const nums = line.match(/-?\d+(?:\.\d+)?/g);
135
+ if (nums && nums.length > 0) value = Number(nums[nums.length - 1]);
136
+ }
137
+ return value === null || Number.isNaN(value) ? null : value;
138
+ }
139
+
140
+ async recordCandidate({
141
+ hypothesis,
142
+ testCommand,
143
+ metricScore,
144
+ metricValue,
145
+ targetMetricName,
146
+ metricName,
147
+ higherIsBetter = true,
148
+ stdout,
149
+ stderr,
150
+ testStderr,
151
+ exitCode,
152
+ filesModified,
153
+ status: callerStatus,
154
+ } = {}) {
155
+ await this.init();
156
+ const metric = metricScore ?? metricValue ?? null;
157
+ const errText = stderr ?? testStderr ?? "";
158
+ const passed =
159
+ exitCode !== undefined ? exitCode === 0 : callerStatus !== "FAILED";
160
+ const candidateId = `cand_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
161
+ // getCurrentCommit() returns null when the commit is unknown (no repo /
162
+ // git unavailable); record that as a null parentage.
163
+ const currentCommit = await this.getCurrentCommit();
164
+
165
+ const best = this.dag.getBestCandidate();
166
+ const bestScore = best?.metrics?.fitness ?? null;
167
+
168
+ let isImprovement = false;
169
+ if (passed && metric !== null && metric !== undefined) {
170
+ if (bestScore === null) {
171
+ isImprovement = true;
172
+ } else if (higherIsBetter && metric > bestScore) {
173
+ isImprovement = true;
174
+ } else if (!higherIsBetter && metric < bestScore) {
175
+ isImprovement = true;
176
+ }
177
+ }
178
+
179
+ const status = isImprovement
180
+ ? "accepted"
181
+ : passed
182
+ ? "neutral"
183
+ : "rejected";
184
+
185
+ const node = this.dag.addCandidate({
186
+ id: candidateId,
187
+ parentId: this.dag.currentHeadId,
188
+ hypothesis,
189
+ filesModified: filesModified ?? [],
190
+ metrics: {
191
+ fitness: metric,
192
+ status,
193
+ commitSha: currentCommit,
194
+ errorLog: !passed ? (errText || stdout || "").slice(-800) : null,
195
+ },
196
+ });
197
+
198
+ if (isImprovement) {
199
+ this.dag.currentHeadId = candidateId;
200
+ this.dag.persist();
201
+ return {
202
+ status: "ACCEPTED",
203
+ candidateId,
204
+ message: `Candidate ${candidateId} improved metric to ${metric} (new active baseline).`,
205
+ };
206
+ } else {
207
+ return {
208
+ status: status.toUpperCase(),
209
+ candidateId,
210
+ message: `Candidate ${candidateId} evaluated (Status: ${status.toUpperCase()}). Working tree preserved for inspection.`,
211
+ };
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Castor Event Bus with Reversible Disposers
3
+ * Provides lifecycle event decoupling where listeners return cleanup disposers.
4
+ */
5
+
6
+ export class EventBus {
7
+ constructor() {
8
+ this._listeners = new Map();
9
+ }
10
+
11
+ /**
12
+ * Registers a listener for an event. Returns a disposer function that unbinds it cleanly.
13
+ * @param {string} event
14
+ * @param {Function} handler
15
+ * @returns {() => void} Disposer
16
+ */
17
+ on(event, handler) {
18
+ if (!this._listeners.has(event)) {
19
+ this._listeners.set(event, new Set());
20
+ }
21
+ const handlers = this._listeners.get(event);
22
+ handlers.add(handler);
23
+
24
+ // Reversible effect disposer
25
+ return () => {
26
+ handlers.delete(handler);
27
+ if (handlers.size === 0) {
28
+ this._listeners.delete(event);
29
+ }
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Registers a one-shot listener.
35
+ * @param {string} event
36
+ * @param {Function} handler
37
+ * @returns {() => void} Disposer
38
+ */
39
+ once(event, handler) {
40
+ const dispose = this.on(event, (...args) => {
41
+ dispose();
42
+ handler(...args);
43
+ });
44
+ return dispose;
45
+ }
46
+
47
+ /**
48
+ * Emits an event synchronously or asynchronously.
49
+ * @param {string} event
50
+ * @param {...any} args
51
+ * @returns {Promise<any[]>}
52
+ */
53
+ async emit(event, ...args) {
54
+ const handlers = this._listeners.get(event);
55
+ if (!handlers || handlers.size === 0) return [];
56
+ const results = [];
57
+ for (const h of Array.from(handlers)) {
58
+ try {
59
+ const res = h(...args);
60
+ results.push(res instanceof Promise ? await res : res);
61
+ } catch (err) {
62
+ console.error(`[EventBus] Error in handler for event '${event}':`, err);
63
+ results.push({ error: err, isError: true });
64
+ }
65
+ }
66
+ return results;
67
+ }
68
+
69
+ /**
70
+ * Clears all listeners.
71
+ */
72
+ clear() {
73
+ this._listeners.clear();
74
+ }
75
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Castor Microkernel Core
3
+ *
4
+ * Implements "Everything is a Plugin" with reversible effects:
5
+ * - Service container with dependency injection
6
+ * - Pluggable tool registry
7
+ * - Reversible disposers that unbind listeners and registrations on unmount
8
+ * - Context hierarchy supporting isolated child contexts
9
+ */
10
+
11
+ import { EventBus } from "./events.js";
12
+
13
+ export class Context {
14
+ /**
15
+ * @param {Context|null} parent
16
+ * @param {string} [name="root"]
17
+ */
18
+ constructor(parent = null, name = "root") {
19
+ this.name = name;
20
+ this.parent = parent;
21
+ this.root = parent ? (parent.root || parent) : this;
22
+ this.events = parent ? parent.events : new EventBus();
23
+ this.services = parent ? new Map(parent.services) : new Map();
24
+ this.tools = parent ? new Map(parent.tools) : new Map();
25
+ this._disposers = new Set();
26
+ this._plugins = new Map();
27
+ }
28
+
29
+ /**
30
+ * Registers a reusable service in the container.
31
+ * @param {string} name
32
+ * @param {any} service
33
+ * @returns {() => void} Disposer
34
+ */
35
+ provide(name, service) {
36
+ const target = this.root || this;
37
+ target.services.set(name, service);
38
+ target[name] = service;
39
+ this.services.set(name, service);
40
+ this[name] = service;
41
+
42
+ const dispose = () => {
43
+ if (this.services.get(name) === service) {
44
+ this.services.delete(name);
45
+ delete this[name];
46
+ }
47
+ if (target.services.get(name) === service) {
48
+ target.services.delete(name);
49
+ delete target[name];
50
+ }
51
+ };
52
+ this._disposers.add(dispose);
53
+ return dispose;
54
+ }
55
+
56
+ /**
57
+ * Retrieves a registered service.
58
+ * @param {string} name
59
+ */
60
+ get(name) {
61
+ if (this.services.has(name)) return this.services.get(name);
62
+ if (this.root && this.root.services.has(name)) return this.root.services.get(name);
63
+ return undefined;
64
+ }
65
+
66
+ /**
67
+ * Registers a tool available to the agent.
68
+ * @param {string} name
69
+ * @param {object} definition { description, parameters, execute }
70
+ * @returns {() => void} Disposer
71
+ */
72
+ registerTool(name, definition) {
73
+ if (!definition || typeof definition.execute !== "function") {
74
+ throw new Error(`Tool '${name}' must provide an execute(params, ctx) function`);
75
+ }
76
+ const target = this.root || this;
77
+ const toolEntry = {
78
+ name,
79
+ description: definition.description || "",
80
+ parameters: definition.parameters || {},
81
+ execute: definition.execute,
82
+ };
83
+ target.tools.set(name, toolEntry);
84
+ this.tools.set(name, toolEntry);
85
+
86
+ const dispose = () => {
87
+ this.tools.delete(name);
88
+ if (target.tools.get(name) === toolEntry) {
89
+ target.tools.delete(name);
90
+ }
91
+ };
92
+ this._disposers.add(dispose);
93
+ this.events.emit("tool:registered", toolEntry);
94
+ return dispose;
95
+ }
96
+
97
+ /**
98
+ * Lists all available tools formatted for model consumption (OpenAI / MCP schema).
99
+ * Sorts tools alphabetically by name and canonicalizes parameters (RFC 8785)
100
+ * to guarantee deterministic token serialization for vLLM RadixAttention KV-cache prefix hits.
101
+ */
102
+ listTools() {
103
+ const toolsMap = this.root ? this.root.tools : this.tools;
104
+ const sorted = Array.from(toolsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
105
+ return sorted.map((t) => ({
106
+ type: "function",
107
+ function: {
108
+ name: t.name,
109
+ description: t.description,
110
+ parameters: canonicalizeJson(t.parameters),
111
+ },
112
+ }));
113
+ }
114
+
115
+ /**
116
+ * Executes a registered tool by name with safety wrapping.
117
+ * @param {string} name
118
+ * @param {object} args
119
+ */
120
+ async executeTool(name, args = {}) {
121
+ const toolsMap = this.root ? this.root.tools : this.tools;
122
+ const tool = toolsMap.get(name);
123
+ if (!tool) {
124
+ throw new Error(`Tool '${name}' is not registered in the harness kernel`);
125
+ }
126
+ await this.events.emit("tool:before_execute", { name, args });
127
+ const t0 = Date.now();
128
+ try {
129
+ const result = await tool.execute(args, this);
130
+ const dt = Date.now() - t0;
131
+ await this.events.emit("tool:after_execute", { name, args, result, latencyMs: dt, isError: false });
132
+ return { isError: false, result, latencyMs: dt };
133
+ } catch (err) {
134
+ const dt = Date.now() - t0;
135
+ await this.events.emit("tool:after_execute", { name, args, error: err.message, latencyMs: dt, isError: true });
136
+ return { isError: true, error: err.message, latencyMs: dt };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Mounts a plugin into the context.
142
+ * @param {Function|object} plugin Plugin function (ctx, options) or { apply: (ctx, options) }
143
+ * @param {object} [options={}]
144
+ * @returns {() => void} Disposer to cleanly unmount the plugin and all its effects
145
+ */
146
+ plugin(plugin, options = {}) {
147
+ const childCtx = new Context(this, plugin.name || "plugin");
148
+ let resultDisposer = null;
149
+
150
+ if (typeof plugin === "function") {
151
+ resultDisposer = plugin(childCtx, options);
152
+ } else if (plugin && typeof plugin.apply === "function") {
153
+ resultDisposer = plugin.apply(childCtx, options);
154
+ } else {
155
+ throw new Error("Plugin must be a callable function or object with an apply() method");
156
+ }
157
+
158
+ const unmount = () => {
159
+ if (typeof resultDisposer === "function") {
160
+ try {
161
+ resultDisposer();
162
+ } catch {}
163
+ }
164
+ childCtx.dispose();
165
+ this._plugins.delete(plugin);
166
+ };
167
+
168
+ this._plugins.set(plugin, unmount);
169
+ this._disposers.add(unmount);
170
+ return unmount;
171
+ }
172
+
173
+ /**
174
+ * Disposes of the context and cleanly unwinds all registrations and listeners.
175
+ */
176
+ dispose() {
177
+ for (const d of Array.from(this._disposers)) {
178
+ try {
179
+ d();
180
+ } catch {}
181
+ }
182
+ this._disposers.clear();
183
+ this.services.clear();
184
+ this.tools.clear();
185
+ this._plugins.clear();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Canonicalizes an object or array by sorting all object keys lexicographically (RFC 8785).
191
+ * Ensures deterministic byte serialization across JavaScript Map iteration orders and JSON encodings.
192
+ * @param {any} obj
193
+ * @returns {any}
194
+ */
195
+ export function canonicalizeJson(obj) {
196
+ if (obj === null || typeof obj !== "object") {
197
+ return obj;
198
+ }
199
+ if (Array.isArray(obj)) {
200
+ return obj.map(canonicalizeJson);
201
+ }
202
+ const sorted = {};
203
+ const keys = Object.keys(obj).sort();
204
+ for (const key of keys) {
205
+ sorted[key] = canonicalizeJson(obj[key]);
206
+ }
207
+ return sorted;
208
+ }
209
+
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Closed-Loop Metric Evaluator (Evo Fitness Engine)
3
+ *
4
+ * Executes evaluation commands and parses feedback into a quantitative FitnessScore.
5
+ */
6
+
7
+ import { condenseTraceback } from "./trace_repair.js";
8
+
9
+ export class ClosedLoopEvaluator {
10
+ constructor(options = {}) {
11
+ this.shell = options.shell; // ShellExecutorService instance
12
+ }
13
+
14
+ /**
15
+ * Evaluates a command line and computes a structured FitnessScore.
16
+ *
17
+ * @param {object} params
18
+ * @param {string} params.command Test or benchmark command (e.g. "npm test", "pytest")
19
+ * @param {string} [params.cwd]
20
+ * @param {string} [params.primaryMetric] Name of primary metric ("throughput", "latency_ms", "tests_passed")
21
+ * @param {number} [params.timeout_ms]
22
+ * @returns {Promise<{
23
+ * passed: boolean,
24
+ * fitness: number,
25
+ * exitCode: number,
26
+ * metrics: object,
27
+ * failureDigest: object|null,
28
+ * stdout: string,
29
+ * stderr: string,
30
+ * latencyMs: number
31
+ * }>}
32
+ */
33
+ async evaluate({ command, cwd, primaryMetric = "tests_passed", timeout_ms = 120_000 }) {
34
+ if (!this.shell) {
35
+ throw new Error("ClosedLoopEvaluator requires a ShellExecutorService instance");
36
+ }
37
+
38
+ const execResult = await this.shell.execute({
39
+ command,
40
+ cwd,
41
+ timeout_ms,
42
+ });
43
+
44
+ const parsed = this.parseMetrics(execResult.stdout, execResult.stderr, execResult.exitCode);
45
+ const fitness = this.calculateFitness(parsed, primaryMetric, execResult.latencyMs);
46
+
47
+ const failureDigest = !execResult.exitCode || parsed.testsFailed === 0
48
+ ? null
49
+ : condenseTraceback({
50
+ stdout: execResult.stdout,
51
+ stderr: execResult.stderr,
52
+ exitCode: execResult.exitCode,
53
+ timedOut: execResult.timedOut,
54
+ workspaceRoot: cwd,
55
+ });
56
+
57
+ return {
58
+ passed: parsed.testsFailed === 0 && execResult.exitCode === 0,
59
+ fitness,
60
+ exitCode: execResult.exitCode,
61
+ metrics: parsed,
62
+ failureDigest,
63
+ stdout: execResult.stdout.slice(0, 8000),
64
+ stderr: execResult.stderr.slice(0, 8000),
65
+ latencyMs: execResult.latencyMs,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Parses test outputs and numeric metrics from stdout/stderr.
71
+ */
72
+ parseMetrics(stdout, stderr, exitCode) {
73
+ const combined = `${stdout}\n${stderr}`;
74
+ const metrics = {
75
+ exitCode,
76
+ testsTotal: 0,
77
+ testsPassed: 0,
78
+ testsFailed: exitCode === 0 ? 0 : 1,
79
+ throughput: null,
80
+ latencyMs: null,
81
+ opsPerSec: null,
82
+ memoryMb: null,
83
+ };
84
+
85
+ // Pytest match: "X passed, Y failed in Zs"
86
+ const pytestMatch = combined.match(/(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/i);
87
+ if (pytestMatch) {
88
+ metrics.testsPassed = parseInt(pytestMatch[1], 10);
89
+ metrics.testsFailed = pytestMatch[2] ? parseInt(pytestMatch[2], 10) : 0;
90
+ metrics.testsTotal = metrics.testsPassed + metrics.testsFailed;
91
+ }
92
+
93
+ // Vitest / Jest match: "Tests: X passed, Y failed, Z total"
94
+ const jestMatch = combined.match(/Tests:\s+(?:(\d+)\s+failed,?\s*)?(?:(\d+)\s+passed,?\s*)?(\d+)\s+total/i);
95
+ if (jestMatch) {
96
+ metrics.testsFailed = jestMatch[1] ? parseInt(jestMatch[1], 10) : 0;
97
+ metrics.testsPassed = jestMatch[2] ? parseInt(jestMatch[2], 10) : 0;
98
+ metrics.testsTotal = parseInt(jestMatch[3], 10);
99
+ }
100
+
101
+ // Go test match: "PASS" or "FAIL"
102
+ if (combined.includes("--- PASS:")) metrics.testsPassed = Math.max(1, metrics.testsPassed);
103
+ if (combined.includes("--- FAIL:")) metrics.testsFailed = Math.max(1, metrics.testsFailed);
104
+
105
+ // Throughput matches: "123.45 ops/sec" or "123.45 req/sec" or "123 tokens/s"
106
+ const opsMatch = combined.match(/([\d.]+)\s*(?:ops\/s(?:ec)?|req\/s(?:ec)?|tokens?\/s(?:ec)?)/i);
107
+ if (opsMatch) {
108
+ metrics.opsPerSec = parseFloat(opsMatch[1]);
109
+ metrics.throughput = metrics.opsPerSec;
110
+ }
111
+
112
+ // Benchmark Latency: "123.45 ms"
113
+ const latencyMatch = combined.match(/(?:latency|duration|time):\s*([\d.]+)\s*ms/i);
114
+ if (latencyMatch) {
115
+ metrics.latencyMs = parseFloat(latencyMatch[1]);
116
+ }
117
+
118
+ // Memory usage: "123 MB"
119
+ const memMatch = combined.match(/([\d.]+)\s*MB(?:\s+heap)?/i);
120
+ if (memMatch) {
121
+ metrics.memoryMb = parseFloat(memMatch[1]);
122
+ }
123
+
124
+ return metrics;
125
+ }
126
+
127
+ /**
128
+ * Calculates scalar fitness score.
129
+ */
130
+ calculateFitness(parsed, primaryMetric, runDurationMs) {
131
+ // Severe penalty for non-zero exit code or failed tests
132
+ if (parsed.exitCode !== 0 || parsed.testsFailed > 0) {
133
+ return -100 - parsed.testsFailed * 25;
134
+ }
135
+
136
+ // Zero-assertion baseline: a compiler pass or command without test framework assertions
137
+ // is a neutral syntax gate (score: 50.0), NOT a verified functional optimization (100.0).
138
+ let baseScore = 50;
139
+ if (parsed.testsTotal > 0) {
140
+ baseScore = (parsed.testsPassed / parsed.testsTotal) * 100;
141
+ }
142
+
143
+ // Metric bonus
144
+ if (primaryMetric === "throughput" && parsed.throughput) {
145
+ return Number((baseScore + parsed.throughput).toFixed(2));
146
+ }
147
+
148
+ if (primaryMetric === "latency_ms") {
149
+ const lat = parsed.latencyMs || runDurationMs;
150
+ // Lower latency yields higher fitness
151
+ return Number((baseScore + 1000 / Math.max(1, lat)).toFixed(2));
152
+ }
153
+
154
+ return Number(baseScore.toFixed(2));
155
+ }
156
+ }