@harness-engineering/intelligence 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/dist/index.d.mts +901 -0
- package/dist/index.d.ts +901 -0
- package/dist/index.js +1588 -0
- package/dist/index.mjs +1520 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1588 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AnthropicAnalysisProvider: () => AnthropicAnalysisProvider,
|
|
34
|
+
ClaudeCliAnalysisProvider: () => ClaudeCliAnalysisProvider,
|
|
35
|
+
ExecutionOutcomeConnector: () => ExecutionOutcomeConnector,
|
|
36
|
+
GraphValidator: () => GraphValidator,
|
|
37
|
+
IntelligencePipeline: () => IntelligencePipeline,
|
|
38
|
+
OpenAICompatibleAnalysisProvider: () => OpenAICompatibleAnalysisProvider,
|
|
39
|
+
PeslSimulator: () => PeslSimulator,
|
|
40
|
+
buildSpecializationProfile: () => buildSpecializationProfile,
|
|
41
|
+
computeExpertiseLevel: () => computeExpertiseLevel,
|
|
42
|
+
computeHistoricalComplexity: () => computeHistoricalComplexity,
|
|
43
|
+
computePersonaEffectiveness: () => computePersonaEffectiveness,
|
|
44
|
+
computeSemanticComplexity: () => computeSemanticComplexity,
|
|
45
|
+
computeSpecialization: () => computeSpecialization,
|
|
46
|
+
computeStructuralComplexity: () => computeStructuralComplexity,
|
|
47
|
+
decayWeight: () => decayWeight,
|
|
48
|
+
detectBlindSpots: () => detectBlindSpots,
|
|
49
|
+
enrich: () => enrich,
|
|
50
|
+
githubToRawWorkItem: () => githubToRawWorkItem,
|
|
51
|
+
jiraToRawWorkItem: () => jiraToRawWorkItem,
|
|
52
|
+
linearToRawWorkItem: () => linearToRawWorkItem,
|
|
53
|
+
loadProfiles: () => loadProfiles,
|
|
54
|
+
manualToRawWorkItem: () => manualToRawWorkItem,
|
|
55
|
+
recommendPersona: () => recommendPersona,
|
|
56
|
+
refreshProfiles: () => refreshProfiles,
|
|
57
|
+
runGraphOnlyChecks: () => runGraphOnlyChecks,
|
|
58
|
+
runLlmSimulation: () => runLlmSimulation,
|
|
59
|
+
saveProfiles: () => saveProfiles,
|
|
60
|
+
scoreCML: () => score,
|
|
61
|
+
scoreToConcernSignals: () => scoreToConcernSignals,
|
|
62
|
+
temporalSuccessRate: () => temporalSuccessRate,
|
|
63
|
+
toRawWorkItem: () => toRawWorkItem,
|
|
64
|
+
weightedRecommendPersona: () => weightedRecommendPersona
|
|
65
|
+
});
|
|
66
|
+
module.exports = __toCommonJS(index_exports);
|
|
67
|
+
|
|
68
|
+
// src/adapter.ts
|
|
69
|
+
function toRawWorkItem(issue) {
|
|
70
|
+
return {
|
|
71
|
+
id: issue.id,
|
|
72
|
+
title: issue.title,
|
|
73
|
+
description: issue.description,
|
|
74
|
+
labels: issue.labels,
|
|
75
|
+
metadata: {
|
|
76
|
+
identifier: issue.identifier,
|
|
77
|
+
priority: issue.priority,
|
|
78
|
+
state: issue.state,
|
|
79
|
+
branchName: issue.branchName,
|
|
80
|
+
url: issue.url,
|
|
81
|
+
createdAt: issue.createdAt,
|
|
82
|
+
updatedAt: issue.updatedAt
|
|
83
|
+
},
|
|
84
|
+
linkedItems: issue.blockedBy.filter((b) => b.id != null).map((b) => b.id),
|
|
85
|
+
comments: [],
|
|
86
|
+
source: "roadmap"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/adapters/jira.ts
|
|
91
|
+
function jiraToRawWorkItem(issue) {
|
|
92
|
+
const linkedItems = [];
|
|
93
|
+
for (const link of issue.fields.issuelinks) {
|
|
94
|
+
if (link.inwardIssue) linkedItems.push(link.inwardIssue.id);
|
|
95
|
+
if (link.outwardIssue) linkedItems.push(link.outwardIssue.id);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
id: issue.id,
|
|
99
|
+
title: issue.fields.summary,
|
|
100
|
+
description: issue.fields.description,
|
|
101
|
+
labels: issue.fields.labels,
|
|
102
|
+
metadata: {
|
|
103
|
+
key: issue.key,
|
|
104
|
+
priority: issue.fields.priority,
|
|
105
|
+
status: issue.fields.status,
|
|
106
|
+
issuetype: issue.fields.issuetype,
|
|
107
|
+
created: issue.fields.created,
|
|
108
|
+
updated: issue.fields.updated
|
|
109
|
+
},
|
|
110
|
+
linkedItems,
|
|
111
|
+
comments: issue.fields.comment.comments.map((c) => c.body),
|
|
112
|
+
source: "jira"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/adapters/github.ts
|
|
117
|
+
function githubToRawWorkItem(issue) {
|
|
118
|
+
return {
|
|
119
|
+
id: String(issue.id),
|
|
120
|
+
title: issue.title,
|
|
121
|
+
description: issue.body,
|
|
122
|
+
labels: issue.labels.map((l) => l.name),
|
|
123
|
+
metadata: {
|
|
124
|
+
number: issue.number,
|
|
125
|
+
state: issue.state,
|
|
126
|
+
html_url: issue.html_url,
|
|
127
|
+
created_at: issue.created_at,
|
|
128
|
+
updated_at: issue.updated_at,
|
|
129
|
+
isPullRequest: issue.pull_request != null,
|
|
130
|
+
milestone: issue.milestone,
|
|
131
|
+
assignees: issue.assignees.map((a) => a.login)
|
|
132
|
+
},
|
|
133
|
+
linkedItems: issue.linked_issues.map(String),
|
|
134
|
+
comments: issue.comments_data.map((c) => c.body),
|
|
135
|
+
source: "github"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/adapters/linear.ts
|
|
140
|
+
function linearToRawWorkItem(issue) {
|
|
141
|
+
return {
|
|
142
|
+
id: issue.id,
|
|
143
|
+
title: issue.title,
|
|
144
|
+
description: issue.description,
|
|
145
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
146
|
+
metadata: {
|
|
147
|
+
identifier: issue.identifier,
|
|
148
|
+
priority: issue.priority,
|
|
149
|
+
state: issue.state,
|
|
150
|
+
url: issue.url,
|
|
151
|
+
branchName: issue.branchName,
|
|
152
|
+
createdAt: issue.createdAt,
|
|
153
|
+
updatedAt: issue.updatedAt
|
|
154
|
+
},
|
|
155
|
+
linkedItems: issue.relations.nodes.map((r) => r.relatedIssue.id),
|
|
156
|
+
comments: issue.comments.nodes.map((c) => c.body),
|
|
157
|
+
source: "linear"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/adapters/manual.ts
|
|
162
|
+
var import_node_crypto = require("crypto");
|
|
163
|
+
function manualToRawWorkItem(input) {
|
|
164
|
+
return {
|
|
165
|
+
id: `manual-${(0, import_node_crypto.randomUUID)()}`,
|
|
166
|
+
title: input.title,
|
|
167
|
+
description: input.description ?? null,
|
|
168
|
+
labels: input.labels ?? [],
|
|
169
|
+
metadata: {},
|
|
170
|
+
linkedItems: [],
|
|
171
|
+
comments: [],
|
|
172
|
+
source: "manual"
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/analysis-provider/anthropic.ts
|
|
177
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
178
|
+
|
|
179
|
+
// src/analysis-provider/schema.ts
|
|
180
|
+
function convertObject(def) {
|
|
181
|
+
const shape = def["shape"]();
|
|
182
|
+
const properties = {};
|
|
183
|
+
const required = [];
|
|
184
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
185
|
+
properties[key] = zodToJsonSchema(value);
|
|
186
|
+
const innerDef = value._def;
|
|
187
|
+
if (innerDef["typeName"] !== "ZodOptional") required.push(key);
|
|
188
|
+
}
|
|
189
|
+
const result = { type: "object", properties };
|
|
190
|
+
if (required.length > 0) result["required"] = required;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
function convertNullable(def) {
|
|
194
|
+
const inner = zodToJsonSchema(def["innerType"]);
|
|
195
|
+
if (typeof inner["type"] === "string") return { ...inner, type: [inner["type"], "null"] };
|
|
196
|
+
return { oneOf: [inner, { type: "null" }] };
|
|
197
|
+
}
|
|
198
|
+
var ZOD_CONVERTERS = {
|
|
199
|
+
ZodString: () => ({ type: "string" }),
|
|
200
|
+
ZodNumber: () => ({ type: "number" }),
|
|
201
|
+
ZodBoolean: () => ({ type: "boolean" }),
|
|
202
|
+
ZodLiteral: (def) => ({ type: typeof def["value"], const: def["value"] }),
|
|
203
|
+
ZodEnum: (def) => ({ type: "string", enum: def["values"] }),
|
|
204
|
+
ZodNativeEnum: (def) => ({ enum: Object.values(def["values"]) }),
|
|
205
|
+
ZodArray: (def) => ({ type: "array", items: zodToJsonSchema(def["type"]) }),
|
|
206
|
+
ZodObject: convertObject,
|
|
207
|
+
ZodOptional: (def) => zodToJsonSchema(def["innerType"]),
|
|
208
|
+
ZodNullable: convertNullable,
|
|
209
|
+
ZodDefault: (def) => zodToJsonSchema(def["innerType"]),
|
|
210
|
+
ZodRecord: (def) => ({
|
|
211
|
+
type: "object",
|
|
212
|
+
additionalProperties: zodToJsonSchema(def["valueType"])
|
|
213
|
+
}),
|
|
214
|
+
ZodUnion: (def) => ({ oneOf: def["options"].map(zodToJsonSchema) })
|
|
215
|
+
};
|
|
216
|
+
function zodToJsonSchema(schema) {
|
|
217
|
+
const def = schema._def;
|
|
218
|
+
const typeName = def["typeName"];
|
|
219
|
+
const converter = typeName ? ZOD_CONVERTERS[typeName] : void 0;
|
|
220
|
+
return converter ? converter(def) : {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/analysis-provider/anthropic.ts
|
|
224
|
+
var DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
225
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
226
|
+
var AnthropicAnalysisProvider = class {
|
|
227
|
+
client;
|
|
228
|
+
defaultModel;
|
|
229
|
+
constructor(options) {
|
|
230
|
+
this.client = new import_sdk.default({ apiKey: options.apiKey });
|
|
231
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
232
|
+
}
|
|
233
|
+
async analyze(request) {
|
|
234
|
+
const model = request.model ?? this.defaultModel;
|
|
235
|
+
const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
236
|
+
const jsonSchema = zodToJsonSchema(request.responseSchema);
|
|
237
|
+
const tool = {
|
|
238
|
+
name: "structured_output",
|
|
239
|
+
description: "Return the analysis result as structured JSON matching the required schema.",
|
|
240
|
+
input_schema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
...jsonSchema
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const systemParts = [];
|
|
246
|
+
if (request.systemPrompt) {
|
|
247
|
+
systemParts.push(request.systemPrompt);
|
|
248
|
+
}
|
|
249
|
+
systemParts.push(
|
|
250
|
+
'You MUST respond by calling the "structured_output" tool with your result. Do not return plain text.'
|
|
251
|
+
);
|
|
252
|
+
const startMs = performance.now();
|
|
253
|
+
const response = await this.client.messages.create({
|
|
254
|
+
model,
|
|
255
|
+
max_tokens: maxTokens,
|
|
256
|
+
system: systemParts.join("\n\n"),
|
|
257
|
+
tools: [tool],
|
|
258
|
+
tool_choice: { type: "tool", name: "structured_output" },
|
|
259
|
+
messages: [{ role: "user", content: request.prompt }]
|
|
260
|
+
});
|
|
261
|
+
const latencyMs = Math.round(performance.now() - startMs);
|
|
262
|
+
const toolUseBlock = response.content.find(
|
|
263
|
+
(block) => block.type === "tool_use"
|
|
264
|
+
);
|
|
265
|
+
if (!toolUseBlock) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Anthropic response did not contain a tool_use block. Stop reason: ${response.stop_reason}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const result = request.responseSchema.parse(toolUseBlock.input);
|
|
271
|
+
const tokenUsage = {
|
|
272
|
+
inputTokens: response.usage.input_tokens,
|
|
273
|
+
outputTokens: response.usage.output_tokens,
|
|
274
|
+
totalTokens: response.usage.input_tokens + response.usage.output_tokens
|
|
275
|
+
};
|
|
276
|
+
return { result, tokenUsage, model, latencyMs };
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// src/analysis-provider/openai-compatible.ts
|
|
281
|
+
var import_openai = __toESM(require("openai"));
|
|
282
|
+
var DEFAULT_MODEL2 = "deepseek-coder-v2";
|
|
283
|
+
var DEFAULT_MAX_TOKENS2 = 8192;
|
|
284
|
+
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
285
|
+
var OpenAICompatibleAnalysisProvider = class {
|
|
286
|
+
client;
|
|
287
|
+
defaultModel;
|
|
288
|
+
promptSuffix;
|
|
289
|
+
jsonMode;
|
|
290
|
+
constructor(options) {
|
|
291
|
+
this.client = new import_openai.default({
|
|
292
|
+
apiKey: options.apiKey,
|
|
293
|
+
baseURL: options.baseUrl,
|
|
294
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
295
|
+
});
|
|
296
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
|
|
297
|
+
this.promptSuffix = options.promptSuffix ?? null;
|
|
298
|
+
this.jsonMode = options.jsonMode ?? true;
|
|
299
|
+
}
|
|
300
|
+
async analyze(request) {
|
|
301
|
+
const model = request.model ?? this.defaultModel;
|
|
302
|
+
const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS2;
|
|
303
|
+
const jsonSchema = zodToJsonSchema(request.responseSchema);
|
|
304
|
+
const systemParts = [];
|
|
305
|
+
if (request.systemPrompt) systemParts.push(request.systemPrompt);
|
|
306
|
+
if (this.jsonMode) {
|
|
307
|
+
systemParts.push(
|
|
308
|
+
"Respond ONLY with the JSON object, no other text. Be concise \u2014 use short sentences in string fields and limit arrays to the most important items."
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
systemParts.push(
|
|
312
|
+
"You MUST respond with valid JSON matching this schema:\n" + JSON.stringify(jsonSchema, null, 2) + "\n\nRespond ONLY with the JSON object, no other text."
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const startMs = performance.now();
|
|
316
|
+
const responseFormat = this.jsonMode ? {
|
|
317
|
+
type: "json_schema",
|
|
318
|
+
json_schema: { name: "analysis_response", strict: true, schema: jsonSchema }
|
|
319
|
+
} : void 0;
|
|
320
|
+
const response = await this.client.chat.completions.create({
|
|
321
|
+
model,
|
|
322
|
+
max_tokens: maxTokens,
|
|
323
|
+
...responseFormat && { response_format: responseFormat },
|
|
324
|
+
messages: [
|
|
325
|
+
{ role: "system", content: systemParts.join("\n\n") },
|
|
326
|
+
{
|
|
327
|
+
role: "user",
|
|
328
|
+
content: this.promptSuffix ? `${request.prompt}
|
|
329
|
+
|
|
330
|
+
${this.promptSuffix}` : request.prompt
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
const latencyMs = Math.round(performance.now() - startMs);
|
|
335
|
+
const choice = response.choices[0];
|
|
336
|
+
const content = choice?.message?.content;
|
|
337
|
+
if (!content) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`OpenAI-compatible response did not contain content. Finish reason: ${choice?.finish_reason}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (choice.finish_reason === "length") {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Response truncated at max_tokens (${maxTokens}). Increase max_tokens or simplify the request.`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const parsed = JSON.parse(content);
|
|
348
|
+
const result = request.responseSchema.parse(parsed);
|
|
349
|
+
const usage = response.usage;
|
|
350
|
+
const tokenUsage = {
|
|
351
|
+
inputTokens: usage?.prompt_tokens ?? 0,
|
|
352
|
+
outputTokens: usage?.completion_tokens ?? 0,
|
|
353
|
+
totalTokens: usage?.total_tokens ?? 0
|
|
354
|
+
};
|
|
355
|
+
return { result, tokenUsage, model, latencyMs };
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/analysis-provider/claude-cli.ts
|
|
360
|
+
var import_node_child_process = require("child_process");
|
|
361
|
+
var DEFAULT_TIMEOUT_MS2 = 18e4;
|
|
362
|
+
var ClaudeCliAnalysisProvider = class {
|
|
363
|
+
command;
|
|
364
|
+
defaultModel;
|
|
365
|
+
timeoutMs;
|
|
366
|
+
constructor(options = {}) {
|
|
367
|
+
this.command = options.command ?? "claude";
|
|
368
|
+
this.defaultModel = options.defaultModel;
|
|
369
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
370
|
+
}
|
|
371
|
+
async analyze(request) {
|
|
372
|
+
const model = request.model ?? this.defaultModel;
|
|
373
|
+
const jsonSchema = zodToJsonSchema(request.responseSchema);
|
|
374
|
+
const prompt = request.systemPrompt ? `${request.systemPrompt}
|
|
375
|
+
|
|
376
|
+
${request.prompt}` : request.prompt;
|
|
377
|
+
const args = [
|
|
378
|
+
"--print",
|
|
379
|
+
"-p",
|
|
380
|
+
prompt,
|
|
381
|
+
"--output-format",
|
|
382
|
+
"json",
|
|
383
|
+
"--json-schema",
|
|
384
|
+
JSON.stringify({ type: "object", ...jsonSchema })
|
|
385
|
+
];
|
|
386
|
+
if (model) {
|
|
387
|
+
args.push("--model", model);
|
|
388
|
+
}
|
|
389
|
+
const startMs = performance.now();
|
|
390
|
+
const result = await this.runClaude(args);
|
|
391
|
+
const latencyMs = Math.round(performance.now() - startMs);
|
|
392
|
+
const parsed = request.responseSchema.parse(result.content);
|
|
393
|
+
return {
|
|
394
|
+
result: parsed,
|
|
395
|
+
tokenUsage: {
|
|
396
|
+
inputTokens: result.usage?.input_tokens ?? 0,
|
|
397
|
+
outputTokens: result.usage?.output_tokens ?? 0,
|
|
398
|
+
totalTokens: (result.usage?.input_tokens ?? 0) + (result.usage?.output_tokens ?? 0)
|
|
399
|
+
},
|
|
400
|
+
model: result.model ?? model ?? "claude",
|
|
401
|
+
latencyMs
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
runClaude(args) {
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
const child = (0, import_node_child_process.spawn)(this.command, args, {
|
|
407
|
+
env: process.env,
|
|
408
|
+
timeout: this.timeoutMs
|
|
409
|
+
});
|
|
410
|
+
let stdout = "";
|
|
411
|
+
let stderr = "";
|
|
412
|
+
child.stdout.on("data", (chunk) => {
|
|
413
|
+
stdout += chunk.toString();
|
|
414
|
+
});
|
|
415
|
+
child.stderr.on("data", (chunk) => {
|
|
416
|
+
stderr += chunk.toString();
|
|
417
|
+
});
|
|
418
|
+
child.on("error", (err) => {
|
|
419
|
+
reject(new Error(`Claude CLI failed to spawn: ${err.message}`));
|
|
420
|
+
});
|
|
421
|
+
child.on("exit", (code) => {
|
|
422
|
+
if (code !== 0) {
|
|
423
|
+
reject(
|
|
424
|
+
new Error(`Claude CLI exited with code ${code}: ${stderr.trim() || stdout.trim()}`)
|
|
425
|
+
);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const parsed = JSON.parse(stdout);
|
|
430
|
+
const content = parsed.result ?? parsed;
|
|
431
|
+
resolve({
|
|
432
|
+
content: typeof content === "string" ? JSON.parse(content) : content,
|
|
433
|
+
usage: parsed.usage,
|
|
434
|
+
model: parsed.model
|
|
435
|
+
});
|
|
436
|
+
} catch (err) {
|
|
437
|
+
reject(
|
|
438
|
+
new Error(
|
|
439
|
+
`Failed to parse Claude CLI output: ${err instanceof Error ? err.message : String(err)}`
|
|
440
|
+
)
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
child.stdin.end();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/sel/prompts.ts
|
|
450
|
+
var import_zod = require("zod");
|
|
451
|
+
var SEL_SYSTEM_PROMPT = `You are a spec enrichment agent. Your job is to analyze a work item (feature request, bug report, task, etc.) and produce structured output that captures the full engineering intent.
|
|
452
|
+
|
|
453
|
+
Analyze the provided work item and extract:
|
|
454
|
+
1. **intent** \u2014 a single sentence capturing the core engineering goal.
|
|
455
|
+
2. **summary** \u2014 a concise paragraph summarizing what needs to be done.
|
|
456
|
+
3. **affectedSystems** \u2014 an array of system/module names that will be touched (each as {name: string}).
|
|
457
|
+
4. **functionalRequirements** \u2014 concrete functional requirements implied by the work item.
|
|
458
|
+
5. **nonFunctionalRequirements** \u2014 performance, scalability, security, or reliability concerns.
|
|
459
|
+
6. **apiChanges** \u2014 any API surface changes (new endpoints, changed contracts, etc.).
|
|
460
|
+
7. **dbChanges** \u2014 any database schema or migration changes.
|
|
461
|
+
8. **integrationPoints** \u2014 external systems or services this work integrates with.
|
|
462
|
+
9. **assumptions** \u2014 assumptions you are making about the scope or context.
|
|
463
|
+
10. **unknowns** \u2014 things that are unclear and may need clarification.
|
|
464
|
+
11. **ambiguities** \u2014 parts of the spec that could be interpreted multiple ways.
|
|
465
|
+
12. **riskSignals** \u2014 potential risks (technical debt, breaking changes, security concerns, etc.).
|
|
466
|
+
13. **initialComplexityHints** \u2014 an object with two 0-1 scores:
|
|
467
|
+
- textualComplexity: how complex the requirement text itself is (0 = trivial, 1 = extremely dense).
|
|
468
|
+
- structuralComplexity: how many systems/layers are involved (0 = single module, 1 = cross-cutting).
|
|
469
|
+
|
|
470
|
+
Be thorough but concise. If information is missing or the description is empty, infer what you can from the title and labels, and note gaps in unknowns/ambiguities.
|
|
471
|
+
|
|
472
|
+
Return your analysis using the structured_output tool.`;
|
|
473
|
+
function buildUserPrompt(item) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
parts.push(`## Work Item: ${item.title}`);
|
|
476
|
+
parts.push(`**ID:** ${item.id}`);
|
|
477
|
+
parts.push(`**Source:** ${item.source}`);
|
|
478
|
+
if (item.description) {
|
|
479
|
+
parts.push(`
|
|
480
|
+
### Description
|
|
481
|
+
${item.description}`);
|
|
482
|
+
} else {
|
|
483
|
+
parts.push(`
|
|
484
|
+
### Description
|
|
485
|
+
_No description provided._`);
|
|
486
|
+
}
|
|
487
|
+
if (item.labels.length > 0) {
|
|
488
|
+
parts.push(`
|
|
489
|
+
### Labels
|
|
490
|
+
${item.labels.join(", ")}`);
|
|
491
|
+
}
|
|
492
|
+
if (item.linkedItems.length > 0) {
|
|
493
|
+
parts.push(`
|
|
494
|
+
### Linked Items
|
|
495
|
+
${item.linkedItems.join(", ")}`);
|
|
496
|
+
}
|
|
497
|
+
if (item.comments.length > 0) {
|
|
498
|
+
parts.push(`
|
|
499
|
+
### Comments
|
|
500
|
+
${item.comments.join("\n\n")}`);
|
|
501
|
+
}
|
|
502
|
+
const metaEntries = Object.entries(item.metadata).filter(
|
|
503
|
+
([, v]) => v !== void 0 && v !== null
|
|
504
|
+
);
|
|
505
|
+
if (metaEntries.length > 0) {
|
|
506
|
+
parts.push(
|
|
507
|
+
`
|
|
508
|
+
### Metadata
|
|
509
|
+
${metaEntries.map(([k, v]) => `- **${k}:** ${String(v)}`).join("\n")}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return parts.join("\n");
|
|
513
|
+
}
|
|
514
|
+
var selResponseSchema = import_zod.z.object({
|
|
515
|
+
intent: import_zod.z.string(),
|
|
516
|
+
summary: import_zod.z.string(),
|
|
517
|
+
affectedSystems: import_zod.z.array(import_zod.z.object({ name: import_zod.z.string() })),
|
|
518
|
+
functionalRequirements: import_zod.z.array(import_zod.z.string()),
|
|
519
|
+
nonFunctionalRequirements: import_zod.z.array(import_zod.z.string()),
|
|
520
|
+
apiChanges: import_zod.z.array(import_zod.z.string()),
|
|
521
|
+
dbChanges: import_zod.z.array(import_zod.z.string()),
|
|
522
|
+
integrationPoints: import_zod.z.array(import_zod.z.string()),
|
|
523
|
+
assumptions: import_zod.z.array(import_zod.z.string()),
|
|
524
|
+
unknowns: import_zod.z.array(import_zod.z.string()),
|
|
525
|
+
ambiguities: import_zod.z.array(import_zod.z.string()),
|
|
526
|
+
riskSignals: import_zod.z.array(import_zod.z.string()),
|
|
527
|
+
initialComplexityHints: import_zod.z.object({
|
|
528
|
+
textualComplexity: import_zod.z.number().min(0).max(1),
|
|
529
|
+
structuralComplexity: import_zod.z.number().min(0).max(1)
|
|
530
|
+
})
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/sel/enricher.ts
|
|
534
|
+
async function enrich(item, provider, graphValidator) {
|
|
535
|
+
const response = await provider.analyze({
|
|
536
|
+
prompt: buildUserPrompt(item),
|
|
537
|
+
systemPrompt: SEL_SYSTEM_PROMPT,
|
|
538
|
+
responseSchema: selResponseSchema
|
|
539
|
+
});
|
|
540
|
+
const llmResult = response.result;
|
|
541
|
+
const affectedSystems = graphValidator.validate(llmResult.affectedSystems);
|
|
542
|
+
return {
|
|
543
|
+
id: item.id,
|
|
544
|
+
title: item.title,
|
|
545
|
+
intent: llmResult.intent,
|
|
546
|
+
summary: llmResult.summary,
|
|
547
|
+
affectedSystems,
|
|
548
|
+
functionalRequirements: llmResult.functionalRequirements,
|
|
549
|
+
nonFunctionalRequirements: llmResult.nonFunctionalRequirements,
|
|
550
|
+
apiChanges: llmResult.apiChanges,
|
|
551
|
+
dbChanges: llmResult.dbChanges,
|
|
552
|
+
integrationPoints: llmResult.integrationPoints,
|
|
553
|
+
assumptions: llmResult.assumptions,
|
|
554
|
+
unknowns: llmResult.unknowns,
|
|
555
|
+
ambiguities: llmResult.ambiguities,
|
|
556
|
+
riskSignals: llmResult.riskSignals,
|
|
557
|
+
initialComplexityHints: llmResult.initialComplexityHints
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/sel/graph-validator.ts
|
|
562
|
+
var import_graph = require("@harness-engineering/graph");
|
|
563
|
+
var GraphValidator = class {
|
|
564
|
+
store;
|
|
565
|
+
cachedModuleNodes = null;
|
|
566
|
+
cachedFileNodes = null;
|
|
567
|
+
constructor(store) {
|
|
568
|
+
this.store = store;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Validate and enrich a list of affected system names against the graph.
|
|
572
|
+
*/
|
|
573
|
+
validate(systems) {
|
|
574
|
+
this.cachedModuleNodes = this.store.findNodes({ type: "module" });
|
|
575
|
+
this.cachedFileNodes = this.store.findNodes({ type: "file" });
|
|
576
|
+
const result = systems.map((system) => this.resolveSystem(system.name));
|
|
577
|
+
this.cachedModuleNodes = null;
|
|
578
|
+
this.cachedFileNodes = null;
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
resolveSystem(name) {
|
|
582
|
+
const moduleNodes = this.cachedModuleNodes ?? this.store.findNodes({ type: "module" });
|
|
583
|
+
const fileNodes = this.cachedFileNodes ?? this.store.findNodes({ type: "file" });
|
|
584
|
+
const candidates = [...moduleNodes, ...fileNodes];
|
|
585
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
586
|
+
let bestMatch = null;
|
|
587
|
+
for (const node of candidates) {
|
|
588
|
+
const normalizedNodeName = node.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
589
|
+
const score2 = this.fuzzyScore(normalizedName, normalizedNodeName);
|
|
590
|
+
if (score2 > 0 && (!bestMatch || score2 > bestMatch.score)) {
|
|
591
|
+
bestMatch = { id: node.id, name: node.name, score: score2 };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!bestMatch || bestMatch.score < 0.4) {
|
|
595
|
+
return {
|
|
596
|
+
name,
|
|
597
|
+
graphNodeId: null,
|
|
598
|
+
confidence: 0,
|
|
599
|
+
transitiveDeps: [],
|
|
600
|
+
testCoverage: 0,
|
|
601
|
+
owner: null
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const transitiveDeps = this.resolveTransitiveDeps(bestMatch.id);
|
|
605
|
+
const testCoverage = this.resolveTestCoverage(bestMatch.id);
|
|
606
|
+
const owner = this.resolveOwner(bestMatch.id);
|
|
607
|
+
return {
|
|
608
|
+
name,
|
|
609
|
+
graphNodeId: bestMatch.id,
|
|
610
|
+
confidence: bestMatch.score,
|
|
611
|
+
transitiveDeps,
|
|
612
|
+
testCoverage,
|
|
613
|
+
owner
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Simple fuzzy scoring: exact match = 1, contains = 0.7, substring overlap.
|
|
618
|
+
*/
|
|
619
|
+
fuzzyScore(query, candidate) {
|
|
620
|
+
if (query === candidate) return 1;
|
|
621
|
+
if (candidate.includes(query)) return 0.8;
|
|
622
|
+
if (query.includes(candidate)) return 0.7;
|
|
623
|
+
const shorter = query.length < candidate.length ? query : candidate;
|
|
624
|
+
const longer = query.length < candidate.length ? candidate : query;
|
|
625
|
+
let matches = 0;
|
|
626
|
+
for (let i = 0; i <= longer.length - shorter.length; i++) {
|
|
627
|
+
let current = 0;
|
|
628
|
+
for (let j = 0; j < shorter.length && i + j < longer.length; j++) {
|
|
629
|
+
if (shorter[j] === longer[i + j]) current++;
|
|
630
|
+
}
|
|
631
|
+
matches = Math.max(matches, current);
|
|
632
|
+
}
|
|
633
|
+
return shorter.length > 0 ? matches / longer.length : 0;
|
|
634
|
+
}
|
|
635
|
+
resolveTransitiveDeps(nodeId) {
|
|
636
|
+
try {
|
|
637
|
+
const simulator = new import_graph.CascadeSimulator(this.store);
|
|
638
|
+
const result = simulator.simulate(nodeId, {
|
|
639
|
+
maxDepth: 3,
|
|
640
|
+
probabilityFloor: 0.1
|
|
641
|
+
});
|
|
642
|
+
return result.flatSummary.map((n) => n.nodeId);
|
|
643
|
+
} catch {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
resolveTestCoverage(nodeId) {
|
|
648
|
+
const testedByEdges = this.store.getEdges({ from: nodeId, type: "tested_by" });
|
|
649
|
+
const verifiedByEdges = this.store.getEdges({ from: nodeId, type: "verified_by" });
|
|
650
|
+
return testedByEdges.length + verifiedByEdges.length;
|
|
651
|
+
}
|
|
652
|
+
resolveOwner(nodeId) {
|
|
653
|
+
const node = this.store.getNode(nodeId);
|
|
654
|
+
if (!node) return null;
|
|
655
|
+
const owner = node.metadata?.["owner"];
|
|
656
|
+
return typeof owner === "string" ? owner : null;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// src/cml/structural.ts
|
|
661
|
+
var import_graph2 = require("@harness-engineering/graph");
|
|
662
|
+
var NORMALIZATION_CEILING = 100;
|
|
663
|
+
function computeStructuralComplexity(spec, store) {
|
|
664
|
+
const emptyResult = {
|
|
665
|
+
score: 0,
|
|
666
|
+
blastRadius: { services: 0, modules: 0, filesEstimated: 0, testFilesAffected: 0 }
|
|
667
|
+
};
|
|
668
|
+
const systemsWithGraph = spec.affectedSystems.filter((s) => s.graphNodeId !== null);
|
|
669
|
+
if (systemsWithGraph.length === 0) {
|
|
670
|
+
return emptyResult;
|
|
671
|
+
}
|
|
672
|
+
const simulator = new import_graph2.CascadeSimulator(store);
|
|
673
|
+
let weightedTotal = 0;
|
|
674
|
+
const seenServices = /* @__PURE__ */ new Set();
|
|
675
|
+
const seenModules = /* @__PURE__ */ new Set();
|
|
676
|
+
let totalFiles = 0;
|
|
677
|
+
let totalTestFiles = 0;
|
|
678
|
+
for (const system of systemsWithGraph) {
|
|
679
|
+
let cascadeResult;
|
|
680
|
+
try {
|
|
681
|
+
cascadeResult = simulator.simulate(system.graphNodeId);
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
for (const node of cascadeResult.flatSummary) {
|
|
686
|
+
weightedTotal += node.cumulativeProbability;
|
|
687
|
+
}
|
|
688
|
+
const { categoryBreakdown } = cascadeResult.summary;
|
|
689
|
+
totalFiles += categoryBreakdown.code;
|
|
690
|
+
totalTestFiles += categoryBreakdown.tests;
|
|
691
|
+
for (const node of cascadeResult.flatSummary) {
|
|
692
|
+
if (node.type === "repository") {
|
|
693
|
+
seenServices.add(node.nodeId);
|
|
694
|
+
}
|
|
695
|
+
if (node.type === "module") {
|
|
696
|
+
seenModules.add(node.nodeId);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const score2 = Math.min(weightedTotal / NORMALIZATION_CEILING, 1);
|
|
701
|
+
return {
|
|
702
|
+
score: score2,
|
|
703
|
+
blastRadius: {
|
|
704
|
+
services: seenServices.size,
|
|
705
|
+
modules: seenModules.size,
|
|
706
|
+
filesEstimated: totalFiles,
|
|
707
|
+
testFilesAffected: totalTestFiles
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/cml/semantic.ts
|
|
713
|
+
var W_UNKNOWNS = 0.4;
|
|
714
|
+
var W_AMBIGUITIES = 0.35;
|
|
715
|
+
var W_RISK_SIGNALS = 0.25;
|
|
716
|
+
var DECAY = 0.3;
|
|
717
|
+
function computeSemanticComplexity(spec) {
|
|
718
|
+
const unknownsScore = 1 - Math.exp(-spec.unknowns.length * DECAY);
|
|
719
|
+
const ambiguitiesScore = 1 - Math.exp(-spec.ambiguities.length * DECAY);
|
|
720
|
+
const riskSignalsScore = 1 - Math.exp(-spec.riskSignals.length * DECAY);
|
|
721
|
+
const raw = unknownsScore * W_UNKNOWNS + ambiguitiesScore * W_AMBIGUITIES + riskSignalsScore * W_RISK_SIGNALS;
|
|
722
|
+
return Math.max(0, Math.min(1, raw));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/cml/historical.ts
|
|
726
|
+
var SMOOTHING = 2;
|
|
727
|
+
function countOutcomesForSystem(store, systemNodeId) {
|
|
728
|
+
const edges = store.getEdges({ to: systemNodeId, type: "outcome_of" });
|
|
729
|
+
let failures = 0;
|
|
730
|
+
let successes = 0;
|
|
731
|
+
for (const edge of edges) {
|
|
732
|
+
const outcomeNode = store.getNode(edge.from);
|
|
733
|
+
if (!outcomeNode || outcomeNode.type !== "execution_outcome") continue;
|
|
734
|
+
if (outcomeNode.metadata.result === "failure") {
|
|
735
|
+
failures++;
|
|
736
|
+
} else if (outcomeNode.metadata.result === "success") {
|
|
737
|
+
successes++;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return { failures, successes };
|
|
741
|
+
}
|
|
742
|
+
function computeFailureRate(counts) {
|
|
743
|
+
const total = counts.failures + counts.successes;
|
|
744
|
+
if (total === 0) return 0;
|
|
745
|
+
return counts.failures / (total + SMOOTHING);
|
|
746
|
+
}
|
|
747
|
+
function computeHistoricalComplexity(spec, store) {
|
|
748
|
+
const systemsWithGraph = spec.affectedSystems.filter((s) => s.graphNodeId !== null);
|
|
749
|
+
if (systemsWithGraph.length === 0) return 0;
|
|
750
|
+
let maxFailureRate = 0;
|
|
751
|
+
for (const system of systemsWithGraph) {
|
|
752
|
+
const counts = countOutcomesForSystem(store, system.graphNodeId);
|
|
753
|
+
const rate = computeFailureRate(counts);
|
|
754
|
+
if (rate > maxFailureRate) {
|
|
755
|
+
maxFailureRate = rate;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return Math.max(0, Math.min(1, maxFailureRate));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/cml/scorer.ts
|
|
762
|
+
var W_STRUCTURAL = 0.5;
|
|
763
|
+
var W_SEMANTIC = 0.35;
|
|
764
|
+
var W_HISTORICAL = 0.15;
|
|
765
|
+
function computeConfidence(structuralScore, semanticScore, historicalScore) {
|
|
766
|
+
const hasStructural = structuralScore > 0;
|
|
767
|
+
const hasSemantic = semanticScore > 0;
|
|
768
|
+
const hasHistorical = historicalScore > 0;
|
|
769
|
+
const dataSourceCount = (hasStructural ? 1 : 0) + (hasSemantic ? 1 : 0) + (hasHistorical ? 1 : 0);
|
|
770
|
+
if (dataSourceCount >= 2) return 0.8;
|
|
771
|
+
if (dataSourceCount === 1) return 0.5;
|
|
772
|
+
return 0.3;
|
|
773
|
+
}
|
|
774
|
+
function classifyRiskLevel(overall) {
|
|
775
|
+
if (overall >= 0.8) return "critical";
|
|
776
|
+
if (overall >= 0.6) return "high";
|
|
777
|
+
if (overall >= 0.3) return "medium";
|
|
778
|
+
return "low";
|
|
779
|
+
}
|
|
780
|
+
function determineRoute(riskLevel, semantic) {
|
|
781
|
+
if (riskLevel === "low") return "local";
|
|
782
|
+
if (riskLevel === "medium" && semantic < 0.5) return "local";
|
|
783
|
+
if (riskLevel === "high" || riskLevel === "critical") return "human";
|
|
784
|
+
return "simulation-required";
|
|
785
|
+
}
|
|
786
|
+
function score(spec, store) {
|
|
787
|
+
const structural = computeStructuralComplexity(spec, store);
|
|
788
|
+
const semantic = computeSemanticComplexity(spec);
|
|
789
|
+
const historical = computeHistoricalComplexity(spec, store);
|
|
790
|
+
const overall = structural.score * W_STRUCTURAL + semantic * W_SEMANTIC + historical * W_HISTORICAL;
|
|
791
|
+
const confidence = computeConfidence(structural.score, semantic, historical);
|
|
792
|
+
const riskLevel = classifyRiskLevel(overall);
|
|
793
|
+
const recommendedRoute = determineRoute(riskLevel, semantic);
|
|
794
|
+
const reasoning = [
|
|
795
|
+
`Structural complexity: ${structural.score.toFixed(2)} (${structural.blastRadius.filesEstimated} files, ${structural.blastRadius.services} services, ${structural.blastRadius.modules} modules affected)`,
|
|
796
|
+
`Semantic complexity: ${semantic.toFixed(2)} (${spec.unknowns.length} unknowns, ${spec.ambiguities.length} ambiguities, ${spec.riskSignals.length} risk signals)`,
|
|
797
|
+
`Historical complexity: ${historical.toFixed(2)} (from past execution outcomes)`,
|
|
798
|
+
`Overall: ${overall.toFixed(2)} \u2192 risk level "${riskLevel}", recommended route "${recommendedRoute}"`
|
|
799
|
+
];
|
|
800
|
+
return {
|
|
801
|
+
overall,
|
|
802
|
+
confidence,
|
|
803
|
+
riskLevel,
|
|
804
|
+
blastRadius: structural.blastRadius,
|
|
805
|
+
dimensions: { structural: structural.score, semantic, historical },
|
|
806
|
+
reasoning,
|
|
807
|
+
recommendedRoute
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/cml/signals.ts
|
|
812
|
+
function scoreToConcernSignals(score2) {
|
|
813
|
+
const signals = [];
|
|
814
|
+
if (score2.overall >= 0.7) {
|
|
815
|
+
signals.push({
|
|
816
|
+
name: "highComplexity",
|
|
817
|
+
reason: score2.reasoning.join("; ")
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (score2.blastRadius.filesEstimated > 20) {
|
|
821
|
+
signals.push({
|
|
822
|
+
name: "largeBlastRadius",
|
|
823
|
+
reason: `${score2.blastRadius.filesEstimated} files estimated to be affected`
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (score2.dimensions.semantic > 0.6) {
|
|
827
|
+
signals.push({
|
|
828
|
+
name: "highAmbiguity",
|
|
829
|
+
reason: "Significant unknowns or ambiguities in spec"
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
return signals;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/pesl/graph-checks.ts
|
|
836
|
+
var import_graph3 = require("@harness-engineering/graph");
|
|
837
|
+
var BASE_CONFIDENCE = 0.85;
|
|
838
|
+
var HOTSPOT_PENALTY = 0.05;
|
|
839
|
+
var TEST_GAP_PENALTY = 0.08;
|
|
840
|
+
var FAILURE_PENALTY = 0.1;
|
|
841
|
+
function runGraphOnlyChecks(spec, score2, store) {
|
|
842
|
+
const riskHotspots = [];
|
|
843
|
+
const predictedFailures = [];
|
|
844
|
+
const testGaps = [];
|
|
845
|
+
const recommendedChanges = [];
|
|
846
|
+
const systemsWithGraph = spec.affectedSystems.filter((s) => s.graphNodeId !== null);
|
|
847
|
+
const simulator = new import_graph3.CascadeSimulator(store);
|
|
848
|
+
for (const system of systemsWithGraph) {
|
|
849
|
+
let cascadeResult;
|
|
850
|
+
try {
|
|
851
|
+
cascadeResult = simulator.simulate(system.graphNodeId);
|
|
852
|
+
} catch {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
for (const nodeId of cascadeResult.summary.amplificationPoints) {
|
|
856
|
+
const node = store.getNode(nodeId);
|
|
857
|
+
riskHotspots.push(node ? `${node.name} (high fan-out)` : `${nodeId} (high fan-out)`);
|
|
858
|
+
}
|
|
859
|
+
const highRiskNodes = cascadeResult.flatSummary.filter((n) => n.cumulativeProbability >= 0.5);
|
|
860
|
+
if (highRiskNodes.length > 5) {
|
|
861
|
+
predictedFailures.push(
|
|
862
|
+
`${system.name}: cascade affects ${highRiskNodes.length} high-probability nodes`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const affectedNodes = cascadeResult.flatSummary.map((n) => store.getNode(n.nodeId)).filter((n) => n !== void 0);
|
|
866
|
+
const groups = (0, import_graph3.groupNodesByImpact)(affectedNodes);
|
|
867
|
+
if (groups.code.length > 0 && groups.tests.length === 0) {
|
|
868
|
+
recommendedChanges.push(`${system.name}: affected code has no test coverage in cascade path`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const system of spec.affectedSystems) {
|
|
872
|
+
if (system.testCoverage === 0) {
|
|
873
|
+
testGaps.push(`${system.name}: no test coverage detected`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (score2.blastRadius.filesEstimated > 10 && score2.blastRadius.testFilesAffected === 0) {
|
|
877
|
+
testGaps.push(
|
|
878
|
+
`Blast radius covers ${score2.blastRadius.filesEstimated} files but no test files are affected`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
let confidence = BASE_CONFIDENCE;
|
|
882
|
+
confidence -= riskHotspots.length * HOTSPOT_PENALTY;
|
|
883
|
+
confidence -= testGaps.length * TEST_GAP_PENALTY;
|
|
884
|
+
confidence -= predictedFailures.length * FAILURE_PENALTY;
|
|
885
|
+
confidence -= score2.overall * 0.2;
|
|
886
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
887
|
+
return {
|
|
888
|
+
simulatedPlan: [],
|
|
889
|
+
// Graph-only checks do not produce a plan
|
|
890
|
+
predictedFailures,
|
|
891
|
+
riskHotspots,
|
|
892
|
+
missingSteps: [],
|
|
893
|
+
// Graph-only checks do not produce missing steps
|
|
894
|
+
testGaps,
|
|
895
|
+
executionConfidence: confidence,
|
|
896
|
+
recommendedChanges,
|
|
897
|
+
abort: confidence < 0.3,
|
|
898
|
+
tier: "graph-only"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/pesl/prompts.ts
|
|
903
|
+
var import_zod2 = require("zod");
|
|
904
|
+
var PESL_SYSTEM_PROMPT = `You are a pre-execution simulation agent. Your job is to analyze an enriched specification and its complexity assessment, then simulate what would happen if an autonomous coding agent attempted to implement this change.
|
|
905
|
+
|
|
906
|
+
Your simulation should:
|
|
907
|
+
1. **Plan expansion** -- Break the spec into concrete implementation steps a coding agent would take.
|
|
908
|
+
2. **Dependency simulation** -- For each step, identify what files, modules, or services would be touched and what dependencies exist between steps.
|
|
909
|
+
3. **Failure injection** -- Predict likely failure modes: type errors, missing imports, test regressions, breaking API changes, race conditions, etc.
|
|
910
|
+
4. **Test projection** -- Identify what tests should exist but don't, and what existing tests are likely to break.
|
|
911
|
+
|
|
912
|
+
Be realistic and specific. Reference actual system names from the spec. Err on the side of flagging potential issues -- it is better to over-predict failures than to miss them.
|
|
913
|
+
|
|
914
|
+
Return your analysis using the structured_output tool.`;
|
|
915
|
+
var peslResponseSchema = import_zod2.z.object({
|
|
916
|
+
simulatedPlan: import_zod2.z.array(import_zod2.z.string()).describe("Ordered implementation steps the agent would take"),
|
|
917
|
+
predictedFailures: import_zod2.z.array(import_zod2.z.string()).describe("Likely failure modes during implementation"),
|
|
918
|
+
riskHotspots: import_zod2.z.array(import_zod2.z.string()).describe("Files or modules that are high-risk change points"),
|
|
919
|
+
missingSteps: import_zod2.z.array(import_zod2.z.string()).describe("Steps the agent might miss or overlook"),
|
|
920
|
+
testGaps: import_zod2.z.array(import_zod2.z.string()).describe("Tests that should exist but likely do not"),
|
|
921
|
+
recommendedChanges: import_zod2.z.array(import_zod2.z.string()).describe("Adjustments to improve success likelihood")
|
|
922
|
+
});
|
|
923
|
+
function appendSection(parts, heading, items) {
|
|
924
|
+
if (items.length === 0) return;
|
|
925
|
+
parts.push(`
|
|
926
|
+
### ${heading}`);
|
|
927
|
+
for (const item of items) {
|
|
928
|
+
parts.push(`- ${item}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function buildAffectedSystems(spec) {
|
|
932
|
+
const lines = [`
|
|
933
|
+
### Affected Systems`];
|
|
934
|
+
for (const system of spec.affectedSystems) {
|
|
935
|
+
const graphStatus = system.graphNodeId ? `(graph-resolved: ${system.graphNodeId})` : "(not in graph)";
|
|
936
|
+
lines.push(
|
|
937
|
+
`- **${system.name}** ${graphStatus} -- confidence: ${system.confidence}, deps: ${system.transitiveDeps.length}, test coverage: ${system.testCoverage}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return lines;
|
|
941
|
+
}
|
|
942
|
+
function buildComplexitySection(score2) {
|
|
943
|
+
const lines = [
|
|
944
|
+
`
|
|
945
|
+
### Complexity Assessment`,
|
|
946
|
+
`- **Overall:** ${score2.overall.toFixed(2)} (risk: ${score2.riskLevel})`,
|
|
947
|
+
`- **Blast radius:** ${score2.blastRadius.filesEstimated} files, ${score2.blastRadius.modules} modules, ${score2.blastRadius.services} services`,
|
|
948
|
+
`- **Structural:** ${score2.dimensions.structural.toFixed(2)}`,
|
|
949
|
+
`- **Semantic:** ${score2.dimensions.semantic.toFixed(2)}`
|
|
950
|
+
];
|
|
951
|
+
for (const reason of score2.reasoning) {
|
|
952
|
+
lines.push(`- ${reason}`);
|
|
953
|
+
}
|
|
954
|
+
return lines;
|
|
955
|
+
}
|
|
956
|
+
function buildPeslPrompt(spec, score2) {
|
|
957
|
+
const parts = [
|
|
958
|
+
`## Enriched Specification: ${spec.title}`,
|
|
959
|
+
`**Intent:** ${spec.intent}`,
|
|
960
|
+
`**Summary:** ${spec.summary}`
|
|
961
|
+
];
|
|
962
|
+
parts.push(...buildAffectedSystems(spec));
|
|
963
|
+
appendSection(parts, "Functional Requirements", spec.functionalRequirements);
|
|
964
|
+
appendSection(parts, "Non-Functional Requirements", spec.nonFunctionalRequirements);
|
|
965
|
+
appendSection(parts, "API Changes", spec.apiChanges);
|
|
966
|
+
appendSection(parts, "Database Changes", spec.dbChanges);
|
|
967
|
+
appendSection(parts, "Integration Points", spec.integrationPoints);
|
|
968
|
+
appendSection(parts, "Unknowns", spec.unknowns);
|
|
969
|
+
appendSection(parts, "Ambiguities", spec.ambiguities);
|
|
970
|
+
appendSection(parts, "Risk Signals", spec.riskSignals);
|
|
971
|
+
parts.push(...buildComplexitySection(score2));
|
|
972
|
+
parts.push(`
|
|
973
|
+
### Instructions`);
|
|
974
|
+
parts.push(
|
|
975
|
+
`Simulate implementation of this spec by an autonomous coding agent. Produce a step-by-step plan, predict failures, identify risk hotspots, flag missing steps, and project test gaps.`
|
|
976
|
+
);
|
|
977
|
+
return parts.join("\n");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/pesl/llm-simulation.ts
|
|
981
|
+
var BASE_CONFIDENCE2 = 0.75;
|
|
982
|
+
var FAILURE_PENALTY2 = 0.06;
|
|
983
|
+
var TEST_GAP_PENALTY2 = 0.05;
|
|
984
|
+
var MISSING_STEP_PENALTY = 0.04;
|
|
985
|
+
var COMPLEXITY_PENALTY_FACTOR = 0.15;
|
|
986
|
+
async function runLlmSimulation(spec, score2, store, provider, model) {
|
|
987
|
+
const graphResult = runGraphOnlyChecks(spec, score2, store);
|
|
988
|
+
const response = await provider.analyze({
|
|
989
|
+
prompt: buildPeslPrompt(spec, score2),
|
|
990
|
+
systemPrompt: PESL_SYSTEM_PROMPT,
|
|
991
|
+
responseSchema: peslResponseSchema,
|
|
992
|
+
...model !== void 0 && { model }
|
|
993
|
+
});
|
|
994
|
+
const llm = response.result;
|
|
995
|
+
const riskHotspots = dedup([...graphResult.riskHotspots, ...llm.riskHotspots]);
|
|
996
|
+
const predictedFailures = dedup([...graphResult.predictedFailures, ...llm.predictedFailures]);
|
|
997
|
+
const testGaps = dedup([...graphResult.testGaps, ...llm.testGaps]);
|
|
998
|
+
const recommendedChanges = dedup([...graphResult.recommendedChanges, ...llm.recommendedChanges]);
|
|
999
|
+
let confidence = BASE_CONFIDENCE2;
|
|
1000
|
+
confidence -= predictedFailures.length * FAILURE_PENALTY2;
|
|
1001
|
+
confidence -= testGaps.length * TEST_GAP_PENALTY2;
|
|
1002
|
+
confidence -= llm.missingSteps.length * MISSING_STEP_PENALTY;
|
|
1003
|
+
confidence -= score2.overall * COMPLEXITY_PENALTY_FACTOR;
|
|
1004
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
1005
|
+
return {
|
|
1006
|
+
simulatedPlan: llm.simulatedPlan,
|
|
1007
|
+
predictedFailures,
|
|
1008
|
+
riskHotspots,
|
|
1009
|
+
missingSteps: llm.missingSteps,
|
|
1010
|
+
testGaps,
|
|
1011
|
+
executionConfidence: confidence,
|
|
1012
|
+
recommendedChanges,
|
|
1013
|
+
abort: confidence < 0.3,
|
|
1014
|
+
tier: "full-simulation"
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function dedup(items) {
|
|
1018
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1019
|
+
const result = [];
|
|
1020
|
+
for (const item of items) {
|
|
1021
|
+
const key = item.toLowerCase();
|
|
1022
|
+
if (!seen.has(key)) {
|
|
1023
|
+
seen.add(key);
|
|
1024
|
+
result.push(item);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/pesl/simulator.ts
|
|
1031
|
+
var GRAPH_ONLY_TIERS = /* @__PURE__ */ new Set(["quick-fix", "diagnostic"]);
|
|
1032
|
+
var PeslSimulator = class {
|
|
1033
|
+
provider;
|
|
1034
|
+
store;
|
|
1035
|
+
options;
|
|
1036
|
+
constructor(provider, store, options = {}) {
|
|
1037
|
+
this.provider = provider;
|
|
1038
|
+
this.store = store;
|
|
1039
|
+
this.options = options;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Run pre-execution simulation for a spec.
|
|
1043
|
+
*
|
|
1044
|
+
* @param spec - Enriched spec from SEL
|
|
1045
|
+
* @param score - Complexity score from CML
|
|
1046
|
+
* @param tier - Scope tier of the issue
|
|
1047
|
+
* @returns SimulationResult with tier, confidence, and abort recommendation
|
|
1048
|
+
*/
|
|
1049
|
+
async simulate(spec, score2, tier) {
|
|
1050
|
+
const needsFullSimulation = score2.recommendedRoute === "simulation-required" || !GRAPH_ONLY_TIERS.has(tier);
|
|
1051
|
+
if (needsFullSimulation) {
|
|
1052
|
+
return runLlmSimulation(spec, score2, this.store, this.provider, this.options.model);
|
|
1053
|
+
}
|
|
1054
|
+
return runGraphOnlyChecks(spec, score2, this.store);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/outcome/connector.ts
|
|
1059
|
+
var ExecutionOutcomeConnector = class {
|
|
1060
|
+
constructor(store) {
|
|
1061
|
+
this.store = store;
|
|
1062
|
+
}
|
|
1063
|
+
store;
|
|
1064
|
+
ingest(outcome) {
|
|
1065
|
+
const errors = [];
|
|
1066
|
+
this.store.addNode({
|
|
1067
|
+
id: outcome.id,
|
|
1068
|
+
type: "execution_outcome",
|
|
1069
|
+
name: `${outcome.result}: ${outcome.identifier}`,
|
|
1070
|
+
metadata: {
|
|
1071
|
+
issueId: outcome.issueId,
|
|
1072
|
+
identifier: outcome.identifier,
|
|
1073
|
+
result: outcome.result,
|
|
1074
|
+
retryCount: outcome.retryCount,
|
|
1075
|
+
failureReasons: outcome.failureReasons,
|
|
1076
|
+
durationMs: outcome.durationMs,
|
|
1077
|
+
linkedSpecId: outcome.linkedSpecId,
|
|
1078
|
+
timestamp: outcome.timestamp,
|
|
1079
|
+
...outcome.agentPersona !== void 0 && { agentPersona: outcome.agentPersona },
|
|
1080
|
+
...outcome.taskType !== void 0 && { taskType: outcome.taskType }
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
let edgesAdded = 0;
|
|
1084
|
+
for (const systemNodeId of outcome.affectedSystemNodeIds) {
|
|
1085
|
+
const systemNode = this.store.getNode(systemNodeId);
|
|
1086
|
+
if (!systemNode) continue;
|
|
1087
|
+
this.store.addEdge({
|
|
1088
|
+
from: outcome.id,
|
|
1089
|
+
to: systemNodeId,
|
|
1090
|
+
type: "outcome_of"
|
|
1091
|
+
});
|
|
1092
|
+
edgesAdded++;
|
|
1093
|
+
}
|
|
1094
|
+
return { nodesAdded: 1, edgesAdded, errors };
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// src/pipeline.ts
|
|
1099
|
+
var IntelligencePipeline = class {
|
|
1100
|
+
provider;
|
|
1101
|
+
graphValidator;
|
|
1102
|
+
store;
|
|
1103
|
+
simulator;
|
|
1104
|
+
outcomeConnector;
|
|
1105
|
+
constructor(provider, store, options) {
|
|
1106
|
+
this.provider = provider;
|
|
1107
|
+
this.store = store;
|
|
1108
|
+
this.graphValidator = new GraphValidator(store);
|
|
1109
|
+
this.simulator = new PeslSimulator(provider, store, {
|
|
1110
|
+
...options?.peslModel !== void 0 && { model: options.peslModel }
|
|
1111
|
+
});
|
|
1112
|
+
this.outcomeConnector = new ExecutionOutcomeConnector(store);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Enrich a raw work item into an EnrichedSpec via LLM + graph validation.
|
|
1116
|
+
*/
|
|
1117
|
+
async enrich(item) {
|
|
1118
|
+
return enrich(item, this.provider, this.graphValidator);
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Score an enriched spec using graph-based structural + semantic analysis.
|
|
1122
|
+
* Synchronous — no LLM calls.
|
|
1123
|
+
*/
|
|
1124
|
+
score(spec) {
|
|
1125
|
+
return score(spec, this.store);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Run pre-execution simulation for a spec.
|
|
1129
|
+
*/
|
|
1130
|
+
async simulate(spec, score2, tier = "guided-change") {
|
|
1131
|
+
return this.simulator.simulate(spec, score2, tier);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Record an execution outcome in the knowledge graph.
|
|
1135
|
+
* Called by the orchestrator after a worker exits.
|
|
1136
|
+
*/
|
|
1137
|
+
recordOutcome(outcome) {
|
|
1138
|
+
return this.outcomeConnector.ingest(outcome);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Preprocess an issue through the intelligence pipeline.
|
|
1142
|
+
*
|
|
1143
|
+
* Behavior depends on which escalation tier the issue's scope falls into:
|
|
1144
|
+
* - `autoExecute`: returns immediately with null spec/score and empty signals
|
|
1145
|
+
* - `alwaysHuman`: runs SEL for enrichment context (human gets pre-analyzed view),
|
|
1146
|
+
* skips CML, returns empty signals (routing stays needs-human)
|
|
1147
|
+
* - `signalGated`: runs full SEL → CML → signals pipeline
|
|
1148
|
+
*/
|
|
1149
|
+
async preprocessIssue(issue, scopeTier, escalationConfig) {
|
|
1150
|
+
if (escalationConfig.autoExecute.includes(scopeTier)) {
|
|
1151
|
+
return { spec: null, score: null, signals: [] };
|
|
1152
|
+
}
|
|
1153
|
+
const workItem = toRawWorkItem(issue);
|
|
1154
|
+
const spec = await this.enrich(workItem);
|
|
1155
|
+
const complexityScore = this.score(spec);
|
|
1156
|
+
if (escalationConfig.alwaysHuman.includes(scopeTier)) {
|
|
1157
|
+
return { spec, score: complexityScore, signals: [] };
|
|
1158
|
+
}
|
|
1159
|
+
const signals = scoreToConcernSignals(complexityScore);
|
|
1160
|
+
return { spec, score: complexityScore, signals };
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
// src/effectiveness/scorer.ts
|
|
1165
|
+
var LAPLACE_ALPHA = 1;
|
|
1166
|
+
var NEUTRAL_PRIOR = 0.5;
|
|
1167
|
+
var DEFAULT_MIN_FAILURES = 2;
|
|
1168
|
+
var DEFAULT_MIN_FAILURE_RATE = 0.5;
|
|
1169
|
+
function bucket(map, persona, systemNodeId, result) {
|
|
1170
|
+
let perPersona = map.get(persona);
|
|
1171
|
+
if (!perPersona) {
|
|
1172
|
+
perPersona = /* @__PURE__ */ new Map();
|
|
1173
|
+
map.set(persona, perPersona);
|
|
1174
|
+
}
|
|
1175
|
+
let counts = perPersona.get(systemNodeId);
|
|
1176
|
+
if (!counts) {
|
|
1177
|
+
counts = { successes: 0, failures: 0 };
|
|
1178
|
+
perPersona.set(systemNodeId, counts);
|
|
1179
|
+
}
|
|
1180
|
+
if (result === "success") counts.successes += 1;
|
|
1181
|
+
else if (result === "failure") counts.failures += 1;
|
|
1182
|
+
}
|
|
1183
|
+
function gatherOutcomes(store) {
|
|
1184
|
+
const map = /* @__PURE__ */ new Map();
|
|
1185
|
+
const nodes = store.findNodes({ type: "execution_outcome" });
|
|
1186
|
+
for (const node of nodes) {
|
|
1187
|
+
const persona = node.metadata.agentPersona;
|
|
1188
|
+
if (typeof persona !== "string" || persona.length === 0) continue;
|
|
1189
|
+
const result = node.metadata.result;
|
|
1190
|
+
if (result !== "success" && result !== "failure") continue;
|
|
1191
|
+
const edges = store.getEdges({ from: node.id, type: "outcome_of" });
|
|
1192
|
+
for (const edge of edges) {
|
|
1193
|
+
bucket(map, persona, edge.to, result);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return map;
|
|
1197
|
+
}
|
|
1198
|
+
function smoothedSuccessRate(counts) {
|
|
1199
|
+
const total = counts.successes + counts.failures;
|
|
1200
|
+
return (counts.successes + LAPLACE_ALPHA) / (total + 2 * LAPLACE_ALPHA);
|
|
1201
|
+
}
|
|
1202
|
+
function computePersonaEffectiveness(store, opts) {
|
|
1203
|
+
const map = gatherOutcomes(store);
|
|
1204
|
+
const rows = [];
|
|
1205
|
+
for (const [persona, perSystem] of map) {
|
|
1206
|
+
if (opts?.persona !== void 0 && persona !== opts.persona) continue;
|
|
1207
|
+
for (const [systemNodeId, counts] of perSystem) {
|
|
1208
|
+
if (opts?.systemNodeId !== void 0 && systemNodeId !== opts.systemNodeId) continue;
|
|
1209
|
+
const sampleSize = counts.successes + counts.failures;
|
|
1210
|
+
if (sampleSize === 0) continue;
|
|
1211
|
+
rows.push({
|
|
1212
|
+
persona,
|
|
1213
|
+
systemNodeId,
|
|
1214
|
+
successes: counts.successes,
|
|
1215
|
+
failures: counts.failures,
|
|
1216
|
+
sampleSize,
|
|
1217
|
+
successRate: smoothedSuccessRate(counts)
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
rows.sort((a, b) => {
|
|
1222
|
+
const rateDiff = b.successRate - a.successRate;
|
|
1223
|
+
if (Math.abs(rateDiff) > 1e-10) return rateDiff;
|
|
1224
|
+
return b.sampleSize - a.sampleSize;
|
|
1225
|
+
});
|
|
1226
|
+
return rows;
|
|
1227
|
+
}
|
|
1228
|
+
function detectBlindSpots(store, opts) {
|
|
1229
|
+
const minFailures = opts?.minFailures ?? DEFAULT_MIN_FAILURES;
|
|
1230
|
+
const minFailureRate = opts?.minFailureRate ?? DEFAULT_MIN_FAILURE_RATE;
|
|
1231
|
+
const map = gatherOutcomes(store);
|
|
1232
|
+
const spots = [];
|
|
1233
|
+
for (const [persona, perSystem] of map) {
|
|
1234
|
+
if (opts?.persona !== void 0 && persona !== opts.persona) continue;
|
|
1235
|
+
for (const [systemNodeId, counts] of perSystem) {
|
|
1236
|
+
const total = counts.successes + counts.failures;
|
|
1237
|
+
if (total === 0) continue;
|
|
1238
|
+
const failureRate = counts.failures / total;
|
|
1239
|
+
if (counts.failures < minFailures) continue;
|
|
1240
|
+
if (failureRate < minFailureRate) continue;
|
|
1241
|
+
spots.push({
|
|
1242
|
+
persona,
|
|
1243
|
+
systemNodeId,
|
|
1244
|
+
successes: counts.successes,
|
|
1245
|
+
failures: counts.failures,
|
|
1246
|
+
failureRate
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
spots.sort((a, b) => {
|
|
1251
|
+
const rateDiff = b.failureRate - a.failureRate;
|
|
1252
|
+
if (Math.abs(rateDiff) > 1e-10) return rateDiff;
|
|
1253
|
+
return b.failures - a.failures;
|
|
1254
|
+
});
|
|
1255
|
+
return spots;
|
|
1256
|
+
}
|
|
1257
|
+
function recommendPersona(store, opts) {
|
|
1258
|
+
const { systemNodeIds } = opts;
|
|
1259
|
+
if (systemNodeIds.length === 0) return [];
|
|
1260
|
+
const map = gatherOutcomes(store);
|
|
1261
|
+
const candidates = opts.candidatePersonas ?? Array.from(map.keys());
|
|
1262
|
+
if (candidates.length === 0) return [];
|
|
1263
|
+
const minSamples = opts.minSamples ?? 0;
|
|
1264
|
+
const recommendations = [];
|
|
1265
|
+
for (const persona of candidates) {
|
|
1266
|
+
const perSystem = map.get(persona);
|
|
1267
|
+
let totalScore = 0;
|
|
1268
|
+
let covered = 0;
|
|
1269
|
+
let unknown = 0;
|
|
1270
|
+
let samples = 0;
|
|
1271
|
+
for (const systemNodeId of systemNodeIds) {
|
|
1272
|
+
const counts = perSystem?.get(systemNodeId);
|
|
1273
|
+
if (counts && counts.successes + counts.failures > 0) {
|
|
1274
|
+
totalScore += smoothedSuccessRate(counts);
|
|
1275
|
+
covered += 1;
|
|
1276
|
+
samples += counts.successes + counts.failures;
|
|
1277
|
+
} else {
|
|
1278
|
+
totalScore += NEUTRAL_PRIOR;
|
|
1279
|
+
unknown += 1;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (samples < minSamples) continue;
|
|
1283
|
+
recommendations.push({
|
|
1284
|
+
persona,
|
|
1285
|
+
score: totalScore / systemNodeIds.length,
|
|
1286
|
+
coveredSystems: covered,
|
|
1287
|
+
unknownSystems: unknown,
|
|
1288
|
+
totalSamples: samples
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
recommendations.sort((a, b) => {
|
|
1292
|
+
const scoreDiff = b.score - a.score;
|
|
1293
|
+
if (Math.abs(scoreDiff) > 1e-10) return scoreDiff;
|
|
1294
|
+
return b.totalSamples - a.totalSamples;
|
|
1295
|
+
});
|
|
1296
|
+
return recommendations;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/specialization/temporal.ts
|
|
1300
|
+
function decayWeight(ageDays, halfLifeDays) {
|
|
1301
|
+
const clamped = Math.max(0, ageDays);
|
|
1302
|
+
return Math.exp(-Math.LN2 / halfLifeDays * clamped);
|
|
1303
|
+
}
|
|
1304
|
+
function temporalSuccessRate(outcomes, config) {
|
|
1305
|
+
if (outcomes.length === 0) return 0.5;
|
|
1306
|
+
const refMs = config.referenceTime ? Date.parse(config.referenceTime) : Date.now();
|
|
1307
|
+
const msPerDay = 864e5;
|
|
1308
|
+
let weightedSuccesses = 0;
|
|
1309
|
+
let totalWeight = 0;
|
|
1310
|
+
for (const outcome of outcomes) {
|
|
1311
|
+
const ageMs = refMs - Date.parse(outcome.timestamp);
|
|
1312
|
+
const ageDays = ageMs / msPerDay;
|
|
1313
|
+
const weight = decayWeight(ageDays, config.halfLifeDays);
|
|
1314
|
+
if (outcome.result === "success") {
|
|
1315
|
+
weightedSuccesses += weight;
|
|
1316
|
+
}
|
|
1317
|
+
totalWeight += weight;
|
|
1318
|
+
}
|
|
1319
|
+
const smoothingWeight = totalWeight > 0 ? totalWeight / outcomes.length : 1;
|
|
1320
|
+
return (weightedSuccesses + smoothingWeight) / (totalWeight + 2 * smoothingWeight);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/specialization/scorer.ts
|
|
1324
|
+
var EXPERT_THRESHOLD = 30;
|
|
1325
|
+
var DEFAULT_HALF_LIFE = 30;
|
|
1326
|
+
var DEFAULT_MIN_SAMPLES = 1;
|
|
1327
|
+
var CONSISTENCY_WINDOW = 5;
|
|
1328
|
+
var W_TEMPORAL = 0.6;
|
|
1329
|
+
var W_CONSISTENCY = 0.25;
|
|
1330
|
+
var W_VOLUME = 0.15;
|
|
1331
|
+
function computeExpertiseLevel(sampleSize, successRate) {
|
|
1332
|
+
if (sampleSize < 5) return "novice";
|
|
1333
|
+
if (sampleSize < 15) return successRate >= 0.6 ? "competent" : "novice";
|
|
1334
|
+
if (sampleSize < 30) return successRate >= 0.7 ? "proficient" : "competent";
|
|
1335
|
+
return successRate >= 0.75 ? "expert" : "proficient";
|
|
1336
|
+
}
|
|
1337
|
+
function gatherOutcomesWithTaskType(store) {
|
|
1338
|
+
const records = [];
|
|
1339
|
+
const nodes = store.findNodes({ type: "execution_outcome" });
|
|
1340
|
+
for (const node of nodes) {
|
|
1341
|
+
const persona = node.metadata.agentPersona;
|
|
1342
|
+
if (typeof persona !== "string" || persona.length === 0) continue;
|
|
1343
|
+
const result = node.metadata.result;
|
|
1344
|
+
if (result !== "success" && result !== "failure") continue;
|
|
1345
|
+
const taskType = typeof node.metadata.taskType === "string" ? node.metadata.taskType : "*";
|
|
1346
|
+
const edges = store.getEdges({ from: node.id, type: "outcome_of" });
|
|
1347
|
+
const timestamp = typeof node.metadata.timestamp === "string" ? node.metadata.timestamp : "";
|
|
1348
|
+
for (const edge of edges) {
|
|
1349
|
+
records.push({ result, timestamp, persona, systemNodeId: edge.to, taskType });
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return records;
|
|
1353
|
+
}
|
|
1354
|
+
function computeConsistency(results) {
|
|
1355
|
+
if (results.length < CONSISTENCY_WINDOW) {
|
|
1356
|
+
const successes = results.filter((r) => r === "success").length;
|
|
1357
|
+
return results.length > 0 ? successes / results.length : 0.5;
|
|
1358
|
+
}
|
|
1359
|
+
const windowRates = [];
|
|
1360
|
+
for (let i = 0; i <= results.length - CONSISTENCY_WINDOW; i++) {
|
|
1361
|
+
const window = results.slice(i, i + CONSISTENCY_WINDOW);
|
|
1362
|
+
const rate = window.filter((r) => r === "success").length / CONSISTENCY_WINDOW;
|
|
1363
|
+
windowRates.push(rate);
|
|
1364
|
+
}
|
|
1365
|
+
if (windowRates.length <= 1) return windowRates[0] ?? 0.5;
|
|
1366
|
+
const mean = windowRates.reduce((a, b) => a + b, 0) / windowRates.length;
|
|
1367
|
+
const variance = windowRates.reduce((sum, r) => sum + (r - mean) ** 2, 0) / windowRates.length;
|
|
1368
|
+
const stddev = Math.sqrt(variance);
|
|
1369
|
+
return Math.max(0, Math.min(1, 1 - stddev / 0.5));
|
|
1370
|
+
}
|
|
1371
|
+
function computeVolumeBonus(sampleSize) {
|
|
1372
|
+
return Math.min(1, Math.log2(sampleSize + 1) / Math.log2(EXPERT_THRESHOLD + 1));
|
|
1373
|
+
}
|
|
1374
|
+
function bucketKey(persona, systemNodeId, taskType) {
|
|
1375
|
+
return `${persona}|${systemNodeId}|${taskType}`;
|
|
1376
|
+
}
|
|
1377
|
+
function computeSpecialization(store, opts) {
|
|
1378
|
+
const records = gatherOutcomesWithTaskType(store);
|
|
1379
|
+
if (records.length === 0) return [];
|
|
1380
|
+
const temporal = opts?.temporal ?? { halfLifeDays: DEFAULT_HALF_LIFE };
|
|
1381
|
+
const minSamples = opts?.minSamples ?? DEFAULT_MIN_SAMPLES;
|
|
1382
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1383
|
+
for (const record of records) {
|
|
1384
|
+
if (opts?.persona && record.persona !== opts.persona) continue;
|
|
1385
|
+
if (opts?.systemNodeId && record.systemNodeId !== opts.systemNodeId) continue;
|
|
1386
|
+
if (opts?.taskType && record.taskType !== opts.taskType) continue;
|
|
1387
|
+
const key = bucketKey(record.persona, record.systemNodeId, record.taskType);
|
|
1388
|
+
let arr = buckets.get(key);
|
|
1389
|
+
if (!arr) {
|
|
1390
|
+
arr = [];
|
|
1391
|
+
buckets.set(key, arr);
|
|
1392
|
+
}
|
|
1393
|
+
arr.push(record);
|
|
1394
|
+
}
|
|
1395
|
+
const entries = [];
|
|
1396
|
+
for (const [, records2] of buckets) {
|
|
1397
|
+
if (records2.length < minSamples) continue;
|
|
1398
|
+
const first = records2[0];
|
|
1399
|
+
const { persona, systemNodeId, taskType } = first;
|
|
1400
|
+
const sorted = [...records2].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
1401
|
+
const tsr = temporalSuccessRate(
|
|
1402
|
+
sorted.map((r) => ({ result: r.result, timestamp: r.timestamp })),
|
|
1403
|
+
temporal
|
|
1404
|
+
);
|
|
1405
|
+
const consistency = computeConsistency(sorted.map((r) => r.result));
|
|
1406
|
+
const volume = computeVolumeBonus(sorted.length);
|
|
1407
|
+
const composite = W_TEMPORAL * tsr + W_CONSISTENCY * consistency + W_VOLUME * volume;
|
|
1408
|
+
const lastOutcome = sorted[sorted.length - 1].timestamp;
|
|
1409
|
+
const rawSuccessRate = sorted.filter((r) => r.result === "success").length / sorted.length;
|
|
1410
|
+
entries.push({
|
|
1411
|
+
persona,
|
|
1412
|
+
systemNodeId,
|
|
1413
|
+
taskType,
|
|
1414
|
+
score: {
|
|
1415
|
+
temporalSuccessRate: tsr,
|
|
1416
|
+
consistencyScore: consistency,
|
|
1417
|
+
volumeBonus: volume,
|
|
1418
|
+
composite
|
|
1419
|
+
},
|
|
1420
|
+
expertiseLevel: computeExpertiseLevel(sorted.length, rawSuccessRate),
|
|
1421
|
+
sampleSize: sorted.length,
|
|
1422
|
+
lastOutcome
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
entries.sort((a, b) => b.score.composite - a.score.composite);
|
|
1426
|
+
return entries;
|
|
1427
|
+
}
|
|
1428
|
+
function buildSpecializationProfile(store, persona, opts) {
|
|
1429
|
+
const entries = computeSpecialization(store, { ...opts, persona });
|
|
1430
|
+
if (entries.length === 0) {
|
|
1431
|
+
return {
|
|
1432
|
+
persona,
|
|
1433
|
+
entries: [],
|
|
1434
|
+
strengths: [],
|
|
1435
|
+
weaknesses: [],
|
|
1436
|
+
overallLevel: "novice",
|
|
1437
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const weaknessSet = /* @__PURE__ */ new Set();
|
|
1441
|
+
const weaknesses = entries.filter((e) => e.score.temporalSuccessRate < 0.5).sort((a, b) => a.score.temporalSuccessRate - b.score.temporalSuccessRate).slice(0, 3);
|
|
1442
|
+
for (const w of weaknesses) {
|
|
1443
|
+
weaknessSet.add(`${w.systemNodeId}|${w.taskType}`);
|
|
1444
|
+
}
|
|
1445
|
+
const strengths = entries.filter((e) => !weaknessSet.has(`${e.systemNodeId}|${e.taskType}`)).slice(0, 3);
|
|
1446
|
+
const levelOrder = ["novice", "competent", "proficient", "expert"];
|
|
1447
|
+
const sortedLevels = entries.map((e) => levelOrder.indexOf(e.expertiseLevel)).sort((a, b) => a - b);
|
|
1448
|
+
const medianIdx = Math.floor(sortedLevels.length / 2);
|
|
1449
|
+
const medianValue = sortedLevels[medianIdx] ?? 0;
|
|
1450
|
+
const overallLevel = levelOrder[medianValue] ?? "novice";
|
|
1451
|
+
return {
|
|
1452
|
+
persona,
|
|
1453
|
+
entries,
|
|
1454
|
+
strengths,
|
|
1455
|
+
weaknesses,
|
|
1456
|
+
overallLevel,
|
|
1457
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function weightedRecommendPersona(store, opts) {
|
|
1461
|
+
const { systemNodeIds } = opts;
|
|
1462
|
+
if (systemNodeIds.length === 0) return [];
|
|
1463
|
+
const recOpts = { systemNodeIds };
|
|
1464
|
+
if (opts.candidatePersonas !== void 0) recOpts.candidatePersonas = opts.candidatePersonas;
|
|
1465
|
+
if (opts.minSamples !== void 0) recOpts.minSamples = opts.minSamples;
|
|
1466
|
+
const baseRecs = recommendPersona(store, recOpts);
|
|
1467
|
+
if (baseRecs.length === 0) return [];
|
|
1468
|
+
const temporal = opts.temporal ?? { halfLifeDays: DEFAULT_HALF_LIFE };
|
|
1469
|
+
const results = [];
|
|
1470
|
+
for (const rec of baseRecs) {
|
|
1471
|
+
const specOpts = { persona: rec.persona, temporal };
|
|
1472
|
+
if (opts.taskType !== void 0) specOpts.taskType = opts.taskType;
|
|
1473
|
+
const specEntries = computeSpecialization(store, specOpts);
|
|
1474
|
+
const relevant = specEntries.filter((e) => systemNodeIds.includes(e.systemNodeId));
|
|
1475
|
+
let multiplier;
|
|
1476
|
+
let expertiseLevel;
|
|
1477
|
+
let specializedSystems;
|
|
1478
|
+
if (relevant.length === 0) {
|
|
1479
|
+
multiplier = 1;
|
|
1480
|
+
expertiseLevel = "novice";
|
|
1481
|
+
specializedSystems = 0;
|
|
1482
|
+
} else {
|
|
1483
|
+
const meanComposite = relevant.reduce((sum, e) => sum + e.score.composite, 0) / relevant.length;
|
|
1484
|
+
multiplier = 0.5 + meanComposite;
|
|
1485
|
+
specializedSystems = relevant.length;
|
|
1486
|
+
const levelOrder = ["novice", "competent", "proficient", "expert"];
|
|
1487
|
+
const bestLevel = relevant.reduce((best, e) => {
|
|
1488
|
+
const idx = levelOrder.indexOf(e.expertiseLevel);
|
|
1489
|
+
return idx > levelOrder.indexOf(best) ? e.expertiseLevel : best;
|
|
1490
|
+
}, "novice");
|
|
1491
|
+
expertiseLevel = bestLevel;
|
|
1492
|
+
}
|
|
1493
|
+
results.push({
|
|
1494
|
+
persona: rec.persona,
|
|
1495
|
+
baseScore: rec.score,
|
|
1496
|
+
specializationMultiplier: multiplier,
|
|
1497
|
+
weightedScore: rec.score * multiplier,
|
|
1498
|
+
expertiseLevel,
|
|
1499
|
+
specializedSystems
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
results.sort((a, b) => b.weightedScore - a.weightedScore);
|
|
1503
|
+
return results;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/specialization/persistence.ts
|
|
1507
|
+
var fs = __toESM(require("fs"));
|
|
1508
|
+
var path = __toESM(require("path"));
|
|
1509
|
+
var PROFILE_FILE = "specialization-profiles.json";
|
|
1510
|
+
var HARNESS_DIR = ".harness";
|
|
1511
|
+
function profilePath(projectRoot) {
|
|
1512
|
+
return path.join(projectRoot, HARNESS_DIR, PROFILE_FILE);
|
|
1513
|
+
}
|
|
1514
|
+
function loadProfiles(projectRoot) {
|
|
1515
|
+
const filePath = profilePath(projectRoot);
|
|
1516
|
+
if (!fs.existsSync(filePath)) {
|
|
1517
|
+
return { profiles: {}, computedAt: (/* @__PURE__ */ new Date()).toISOString(), version: 1 };
|
|
1518
|
+
}
|
|
1519
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1520
|
+
const parsed = JSON.parse(raw);
|
|
1521
|
+
if (parsed.version !== 1 || typeof parsed.profiles !== "object" || parsed.profiles === null) {
|
|
1522
|
+
return { profiles: {}, computedAt: (/* @__PURE__ */ new Date()).toISOString(), version: 1 };
|
|
1523
|
+
}
|
|
1524
|
+
return parsed;
|
|
1525
|
+
}
|
|
1526
|
+
function saveProfiles(projectRoot, store) {
|
|
1527
|
+
const dirPath = path.join(projectRoot, HARNESS_DIR);
|
|
1528
|
+
if (!fs.existsSync(dirPath)) {
|
|
1529
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1530
|
+
}
|
|
1531
|
+
fs.writeFileSync(profilePath(projectRoot), JSON.stringify(store, null, 2), "utf-8");
|
|
1532
|
+
}
|
|
1533
|
+
function refreshProfiles(projectRoot, graphStore, opts) {
|
|
1534
|
+
const nodes = graphStore.findNodes({ type: "execution_outcome" });
|
|
1535
|
+
const personas = /* @__PURE__ */ new Set();
|
|
1536
|
+
for (const node of nodes) {
|
|
1537
|
+
const persona = node.metadata.agentPersona;
|
|
1538
|
+
if (typeof persona === "string" && persona.length > 0) {
|
|
1539
|
+
personas.add(persona);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const profiles = {};
|
|
1543
|
+
for (const persona of personas) {
|
|
1544
|
+
profiles[persona] = buildSpecializationProfile(graphStore, persona, opts);
|
|
1545
|
+
}
|
|
1546
|
+
const store = {
|
|
1547
|
+
profiles,
|
|
1548
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1549
|
+
version: 1
|
|
1550
|
+
};
|
|
1551
|
+
saveProfiles(projectRoot, store);
|
|
1552
|
+
return store;
|
|
1553
|
+
}
|
|
1554
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1555
|
+
0 && (module.exports = {
|
|
1556
|
+
AnthropicAnalysisProvider,
|
|
1557
|
+
ClaudeCliAnalysisProvider,
|
|
1558
|
+
ExecutionOutcomeConnector,
|
|
1559
|
+
GraphValidator,
|
|
1560
|
+
IntelligencePipeline,
|
|
1561
|
+
OpenAICompatibleAnalysisProvider,
|
|
1562
|
+
PeslSimulator,
|
|
1563
|
+
buildSpecializationProfile,
|
|
1564
|
+
computeExpertiseLevel,
|
|
1565
|
+
computeHistoricalComplexity,
|
|
1566
|
+
computePersonaEffectiveness,
|
|
1567
|
+
computeSemanticComplexity,
|
|
1568
|
+
computeSpecialization,
|
|
1569
|
+
computeStructuralComplexity,
|
|
1570
|
+
decayWeight,
|
|
1571
|
+
detectBlindSpots,
|
|
1572
|
+
enrich,
|
|
1573
|
+
githubToRawWorkItem,
|
|
1574
|
+
jiraToRawWorkItem,
|
|
1575
|
+
linearToRawWorkItem,
|
|
1576
|
+
loadProfiles,
|
|
1577
|
+
manualToRawWorkItem,
|
|
1578
|
+
recommendPersona,
|
|
1579
|
+
refreshProfiles,
|
|
1580
|
+
runGraphOnlyChecks,
|
|
1581
|
+
runLlmSimulation,
|
|
1582
|
+
saveProfiles,
|
|
1583
|
+
scoreCML,
|
|
1584
|
+
scoreToConcernSignals,
|
|
1585
|
+
temporalSuccessRate,
|
|
1586
|
+
toRawWorkItem,
|
|
1587
|
+
weightedRecommendPersona
|
|
1588
|
+
});
|