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