@kairos-sdk/core 0.3.2 → 0.4.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.
@@ -397,17 +397,28 @@ var N8nValidator = class {
397
397
  this.checkRule21(workflow, issues);
398
398
  this.checkRule22(workflow, issues);
399
399
  this.checkRule23(workflow, issues);
400
+ if (Array.isArray(workflow.nodes)) {
401
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
402
+ for (const issue of issues) {
403
+ if (issue.nodeId && !issue.nodeType) {
404
+ const nt = nodeById.get(issue.nodeId);
405
+ if (nt) issue.nodeType = nt;
406
+ }
407
+ }
408
+ }
400
409
  const errors = issues.filter((i) => i.severity === "error");
401
410
  return { valid: errors.length === 0, issues };
402
411
  }
403
- err(issues, rule, message, nodeId) {
412
+ err(issues, rule, message, nodeId, nodeType) {
404
413
  const issue = { rule, severity: "error", message };
405
414
  if (nodeId !== void 0) issue.nodeId = nodeId;
415
+ if (nodeType !== void 0) issue.nodeType = nodeType;
406
416
  issues.push(issue);
407
417
  }
408
- warn(issues, rule, message, nodeId) {
418
+ warn(issues, rule, message, nodeId, nodeType) {
409
419
  const issue = { rule, severity: "warn", message };
410
420
  if (nodeId !== void 0) issue.nodeId = nodeId;
421
+ if (nodeType !== void 0) issue.nodeType = nodeType;
411
422
  issues.push(issue);
412
423
  }
413
424
  isTriggerNode(node) {
@@ -752,6 +763,11 @@ var N8nValidator = class {
752
763
  }
753
764
  };
754
765
 
766
+ // src/generation/prompt-builder.ts
767
+ import { readFileSync } from "fs";
768
+ import { join } from "path";
769
+ import { homedir } from "os";
770
+
755
771
  // src/generation/prompts/v1.ts
756
772
  var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
757
773
 
@@ -944,8 +960,34 @@ function scoreToMode(score) {
944
960
  return "scratch";
945
961
  }
946
962
 
947
- // src/generation/prompt-builder.ts
948
- var RULE_REMEDIES = {
963
+ // src/validation/rule-metadata.ts
964
+ var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
965
+ var RULE_PIPELINE_STAGES = {
966
+ 1: "node_generation",
967
+ 2: "node_generation",
968
+ 3: "node_generation",
969
+ 4: "node_generation",
970
+ 5: "node_generation",
971
+ 6: "node_generation",
972
+ 7: "node_generation",
973
+ 8: "node_generation",
974
+ 9: "connection_wiring",
975
+ 10: "connection_wiring",
976
+ 11: "connection_wiring",
977
+ 12: "workflow_structure",
978
+ 13: "node_generation",
979
+ 14: "workflow_structure",
980
+ 15: "node_generation",
981
+ 16: "node_generation",
982
+ 17: "credential_injection",
983
+ 18: "connection_wiring",
984
+ 19: "node_generation",
985
+ 20: "connection_wiring",
986
+ 21: "workflow_structure",
987
+ 22: "workflow_structure",
988
+ 23: "node_generation"
989
+ };
990
+ var RULE_MITIGATIONS = {
949
991
  1: "Provide a non-empty workflow name string",
950
992
  2: "Include at least one node in the nodes array",
951
993
  3: "Every node must have a unique UUID v4 string as its id field",
@@ -956,8 +998,10 @@ var RULE_REMEDIES = {
956
998
  8: "Every node must have a non-empty name string",
957
999
  9: "connections must be a plain object (use {} if no connections)",
958
1000
  10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1001
+ 11: "Every non-trigger node should have at least one incoming connection",
959
1002
  12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
960
- 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1003
+ 13: "workflow.settings must be a plain object if present",
1004
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
961
1005
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
962
1006
  16: "All node names must be unique within the workflow",
963
1007
  17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
@@ -965,9 +1009,17 @@ var RULE_REMEDIES = {
965
1009
  19: "Use known safe typeVersion values for each node type",
966
1010
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
967
1011
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
968
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1012
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1013
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
969
1014
  };
1015
+
1016
+ // src/generation/prompt-builder.ts
1017
+ var CRITICAL_SCORE_THRESHOLD = 0.15;
970
1018
  var PromptBuilder = class {
1019
+ patternsPath;
1020
+ constructor(patternsPath) {
1021
+ this.patternsPath = patternsPath ?? join(homedir(), ".kairos", "patterns.json");
1022
+ }
971
1023
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
972
1024
  const mode = this.resolveMode(matches);
973
1025
  const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
@@ -1053,20 +1105,114 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1053
1105
  }
1054
1106
  return blocks;
1055
1107
  }
1108
+ loadPatterns() {
1109
+ try {
1110
+ const raw = readFileSync(this.patternsPath, "utf-8");
1111
+ const analysis = JSON.parse(raw);
1112
+ const patterns = analysis.topFailureRules ?? [];
1113
+ return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
1114
+ } catch {
1115
+ return [];
1116
+ }
1117
+ }
1118
+ getWarnedRules() {
1119
+ return this.getActivePatterns().map((p) => p.rule);
1120
+ }
1121
+ getActivePatterns() {
1122
+ const MAX_WARNED = 10;
1123
+ const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1124
+ const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1125
+ const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1126
+ const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1127
+ return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1128
+ }
1056
1129
  buildFailureWarnings(matches, globalFailureRates) {
1130
+ const richPatterns = this.getActivePatterns();
1131
+ if (richPatterns.length > 0) {
1132
+ return this.buildStageGroupedWarnings(richPatterns, matches);
1133
+ }
1134
+ return this.buildLegacyWarnings(matches, globalFailureRates);
1135
+ }
1136
+ buildStageGroupedWarnings(patterns, matches) {
1137
+ const stageLabels = {
1138
+ credential_injection: "CREDENTIAL FORMATTING",
1139
+ connection_wiring: "CONNECTION WIRING",
1140
+ node_generation: "NODE GENERATION",
1141
+ workflow_structure: "WORKFLOW STRUCTURE"
1142
+ };
1143
+ const byStage = /* @__PURE__ */ new Map();
1144
+ for (const p of patterns) {
1145
+ const list = byStage.get(p.pipelineStage) ?? [];
1146
+ list.push(p);
1147
+ byStage.set(p.pipelineStage, list);
1148
+ }
1149
+ const sections = [];
1150
+ for (const [stage, stagePatterns] of byStage) {
1151
+ const label = stageLabels[stage] ?? stage;
1152
+ const byMitigation = /* @__PURE__ */ new Map();
1153
+ for (const p of stagePatterns) {
1154
+ const key = p.mitigation ?? `rule_${p.rule}`;
1155
+ const list = byMitigation.get(key) ?? [];
1156
+ list.push(p);
1157
+ byMitigation.set(key, list);
1158
+ }
1159
+ const lines = [];
1160
+ for (const group of byMitigation.values()) {
1161
+ if (group.length === 1) {
1162
+ const p = group[0];
1163
+ const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
1164
+ const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
1165
+ const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
1166
+ const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1167
+ const remedyStr = remedy ? `
1168
+ Fix: ${remedy}` : "";
1169
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1170
+ } else {
1171
+ const ruleNums = group.map((p) => p.rule).join(", ");
1172
+ const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
1173
+ const hasConfirmed = group.some((p) => p.state === "confirmed");
1174
+ const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
1175
+ const remedy = group[0].mitigation;
1176
+ const remedyStr = remedy ? `
1177
+ Fix: ${remedy}` : "";
1178
+ lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
1179
+ }
1180
+ }
1181
+ sections.push(`### ${label}
1182
+ ${lines.join("\n")}`);
1183
+ }
1184
+ for (const match of matches) {
1185
+ const fps = match.workflow.failurePatterns;
1186
+ if (!fps?.length) continue;
1187
+ const coveredRules = new Set(patterns.map((p) => p.rule));
1188
+ const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
1189
+ for (const fp of extra) {
1190
+ const remedy = RULE_MITIGATIONS[fp.rule];
1191
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1192
+ sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
1193
+ }
1194
+ }
1195
+ if (sections.length === 0) return null;
1196
+ return `## Known Failure Patterns \u2014 AVOID THESE
1197
+
1198
+ Grouped by generation stage. Fix these BEFORE outputting your response:
1199
+
1200
+ ${sections.join("\n\n")}`;
1201
+ }
1202
+ buildLegacyWarnings(matches, globalFailureRates) {
1057
1203
  const lines = [];
1058
1204
  for (const match of matches) {
1059
1205
  const patterns = match.workflow.failurePatterns;
1060
1206
  if (!patterns?.length) continue;
1061
1207
  for (const fp of patterns) {
1062
- const remedy = RULE_REMEDIES[fp.rule];
1208
+ const remedy = RULE_MITIGATIONS[fp.rule];
1063
1209
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1064
1210
  lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1065
1211
  }
1066
1212
  }
1067
1213
  const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1068
1214
  for (const rule of highFreqRules) {
1069
- const remedy = RULE_REMEDIES[rule.rule];
1215
+ const remedy = RULE_MITIGATIONS[rule.rule];
1070
1216
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1071
1217
  lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1072
1218
  }
@@ -1085,15 +1231,55 @@ Workflow name: "${request.name}"` : "";
1085
1231
  };
1086
1232
 
1087
1233
  // src/telemetry/reader.ts
1088
- import { readFile, readdir } from "fs/promises";
1089
- import { join } from "path";
1090
- import { homedir } from "os";
1234
+ import { homedir as homedir2 } from "os";
1235
+ import { join as join3 } from "path";
1236
+
1237
+ // src/telemetry/event-reader.ts
1238
+ import { readdir } from "fs/promises";
1239
+ import { createReadStream } from "fs";
1240
+ import { join as join2 } from "path";
1241
+ import { createInterface } from "readline";
1242
+ async function readTelemetryEvents(dir, days) {
1243
+ let files;
1244
+ try {
1245
+ files = await readdir(dir);
1246
+ } catch {
1247
+ return [];
1248
+ }
1249
+ const cutoff = /* @__PURE__ */ new Date();
1250
+ cutoff.setDate(cutoff.getDate() - days);
1251
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1252
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1253
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1254
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
1255
+ const events = [];
1256
+ for (const file of recentFiles) {
1257
+ const fileDate = file.replace(".jsonl", "");
1258
+ try {
1259
+ const rl = createInterface({
1260
+ input: createReadStream(join2(dir, file), "utf-8"),
1261
+ crlfDelay: Infinity
1262
+ });
1263
+ for await (const line of rl) {
1264
+ if (!line.trim()) continue;
1265
+ try {
1266
+ events.push({ ...JSON.parse(line), fileDate });
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ return events;
1274
+ }
1275
+
1276
+ // src/telemetry/reader.ts
1091
1277
  var TelemetryReader = class {
1092
1278
  dir;
1093
1279
  cache = null;
1094
1280
  cacheTime = 0;
1095
1281
  constructor(dir) {
1096
- this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
1282
+ this.dir = dir ?? join3(homedir2(), ".kairos", "telemetry");
1097
1283
  }
1098
1284
  async getFailureRates(days = 30) {
1099
1285
  const now = Date.now();
@@ -1102,9 +1288,10 @@ var TelemetryReader = class {
1102
1288
  }
1103
1289
  const events = await this.readRecentEvents(days);
1104
1290
  const buildSessions = new Set(
1105
- events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1291
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
1106
1292
  );
1107
- if (buildSessions.size === 0) return [];
1293
+ const MIN_BUILDS_FOR_RATES = 3;
1294
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1108
1295
  const ruleSessions = /* @__PURE__ */ new Map();
1109
1296
  for (const event of events) {
1110
1297
  if (event.eventType !== "generation_attempt") continue;
@@ -1142,32 +1329,425 @@ var TelemetryReader = class {
1142
1329
  return rates;
1143
1330
  }
1144
1331
  async readRecentEvents(days) {
1145
- let files;
1332
+ return readTelemetryEvents(this.dir, days);
1333
+ }
1334
+ };
1335
+
1336
+ // src/telemetry/pattern-analyzer.ts
1337
+ import { writeFile, readFile as fsReadFile, appendFile, mkdir, rename } from "fs/promises";
1338
+ import { join as join4 } from "path";
1339
+ import { homedir as homedir3 } from "os";
1340
+ var PATTERN_SCHEMA_VERSION = 2;
1341
+ var PatternAnalyzer = class _PatternAnalyzer {
1342
+ telemetryDir;
1343
+ outputDir;
1344
+ constructor(telemetryDir) {
1345
+ const defaultDir = join4(homedir3(), ".kairos", "telemetry");
1346
+ this.telemetryDir = telemetryDir ?? defaultDir;
1347
+ this.outputDir = telemetryDir ? join4(telemetryDir, "..") : join4(homedir3(), ".kairos");
1348
+ }
1349
+ async loadPreviousPatterns() {
1146
1350
  try {
1147
- files = await readdir(this.dir);
1351
+ const raw = await fsReadFile(join4(this.outputDir, "patterns.json"), "utf-8");
1352
+ const prev = JSON.parse(raw);
1353
+ const version = prev.schemaVersion ?? 0;
1354
+ const patterns = prev.topFailureRules ?? [];
1355
+ if (version === PATTERN_SCHEMA_VERSION) return patterns;
1356
+ return this.migratePatterns(patterns, version);
1148
1357
  } catch {
1149
1358
  return [];
1150
1359
  }
1151
- const cutoff = /* @__PURE__ */ new Date();
1152
- cutoff.setDate(cutoff.getDate() - days);
1153
- const cutoffStr = cutoff.toISOString().slice(0, 10);
1154
- const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1155
- const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1156
- const events = [];
1157
- for (const file of recentFiles) {
1158
- try {
1159
- const content = await readFile(join(this.dir, file), "utf-8");
1160
- for (const line of content.split("\n")) {
1161
- if (!line.trim()) continue;
1162
- try {
1163
- events.push(JSON.parse(line));
1164
- } catch {
1360
+ }
1361
+ migratePatterns(patterns, fromVersion) {
1362
+ let migrated = patterns;
1363
+ if (fromVersion < 1) {
1364
+ migrated = migrated.map((p) => ({
1365
+ ...p,
1366
+ compositeScore: p.compositeScore ?? 0,
1367
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1368
+ pipelineStage: p.pipelineStage ?? "node_generation"
1369
+ }));
1370
+ }
1371
+ if (fromVersion < 2) {
1372
+ migrated = migrated.map((p) => ({
1373
+ ...p,
1374
+ scoringFactors: {
1375
+ ...p.scoringFactors,
1376
+ stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1377
+ }
1378
+ }));
1379
+ }
1380
+ return migrated;
1381
+ }
1382
+ async analyze(days = 30) {
1383
+ const previousPatterns = await this.loadPreviousPatterns();
1384
+ const events = await this.readAllEvents(days);
1385
+ const starts = events.filter((e) => e.eventType === "build_start");
1386
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
1387
+ const passed = attempts.filter(
1388
+ (a) => a.data.validationPassed === true
1389
+ );
1390
+ const failed = attempts.filter(
1391
+ (a) => a.data.validationPassed === false
1392
+ );
1393
+ const ruleFailures = /* @__PURE__ */ new Map();
1394
+ const credentialFailures = /* @__PURE__ */ new Map();
1395
+ for (const a of failed) {
1396
+ const weight = this.recencyWeight(a.fileDate);
1397
+ const data = a.data;
1398
+ for (const issue of data.issues ?? []) {
1399
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
1400
+ entry.count++;
1401
+ entry.sessions.add(a.sessionId);
1402
+ entry.recencyWeights.push(weight);
1403
+ entry.allMessages.push(issue.message);
1404
+ ruleFailures.set(issue.rule, entry);
1405
+ if (issue.rule === 17) {
1406
+ const credPatterns = [
1407
+ /credential\s+"([^"]+)"/,
1408
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
1409
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
1410
+ ];
1411
+ let credType = "unknown";
1412
+ for (const re of credPatterns) {
1413
+ const m = issue.message.match(re);
1414
+ if (m?.[1]) {
1415
+ credType = m[1];
1416
+ break;
1417
+ }
1165
1418
  }
1419
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
1166
1420
  }
1167
- } catch {
1168
1421
  }
1169
1422
  }
1170
- return events;
1423
+ const failedByDate = /* @__PURE__ */ new Map();
1424
+ for (const a of failed) {
1425
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
1426
+ }
1427
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1428
+ const hasTrendData = sortedFailDates.length >= 3;
1429
+ let midDate = "";
1430
+ if (hasTrendData) {
1431
+ const halfTotal = failed.length / 2;
1432
+ let cumulative = 0;
1433
+ for (const [date, count] of sortedFailDates) {
1434
+ cumulative += count;
1435
+ if (cumulative >= halfTotal) {
1436
+ midDate = date;
1437
+ break;
1438
+ }
1439
+ }
1440
+ }
1441
+ const ruleTrends = /* @__PURE__ */ new Map();
1442
+ if (hasTrendData) {
1443
+ for (const a of failed) {
1444
+ const data = a.data;
1445
+ const isNewer = a.fileDate > midDate;
1446
+ for (const issue of data.issues ?? []) {
1447
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
1448
+ if (isNewer) entry.newer++;
1449
+ else entry.older++;
1450
+ ruleTrends.set(issue.rule, entry);
1451
+ }
1452
+ }
1453
+ }
1454
+ const sessions = /* @__PURE__ */ new Map();
1455
+ for (const a of attempts) {
1456
+ const list = sessions.get(a.sessionId) ?? [];
1457
+ list.push(a);
1458
+ sessions.set(a.sessionId, list);
1459
+ }
1460
+ let firstTryPass = 0;
1461
+ let correctionNeeded = 0;
1462
+ let singleAttemptFail = 0;
1463
+ for (const sessionAttempts of sessions.values()) {
1464
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
1465
+ const lastPassed = lastAttempt.data.validationPassed === true;
1466
+ if (sessionAttempts.length === 1 && lastPassed) {
1467
+ firstTryPass++;
1468
+ } else if (sessionAttempts.length > 1 && lastPassed) {
1469
+ correctionNeeded++;
1470
+ } else {
1471
+ singleAttemptFail++;
1472
+ }
1473
+ }
1474
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
1475
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
1476
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
1477
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
1478
+ const totalSessions = Math.max(sessions.size, 1);
1479
+ const stickinessCount = /* @__PURE__ */ new Map();
1480
+ for (const sessionAttempts of sessions.values()) {
1481
+ if (sessionAttempts.length < 2) continue;
1482
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
1483
+ const curr = sessionAttempts[i].data;
1484
+ const next = sessionAttempts[i + 1].data;
1485
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
1486
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
1487
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
1488
+ for (const rule of currRules) {
1489
+ if (nextRules.has(rule)) {
1490
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+ const CONFIRMED_THRESHOLD = 3;
1496
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
1497
+ const RESOLVED_TTL_DAYS = 90;
1498
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
1499
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
1500
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
1501
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
1502
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1503
+ const stickiness = stickinessCount.get(rule) ?? 0;
1504
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1505
+ return {
1506
+ rule,
1507
+ failureCount: entry.count,
1508
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
1509
+ compositeScore,
1510
+ scoringFactors: factors,
1511
+ state,
1512
+ trend: this.classifyTrend(t.older, t.newer),
1513
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
1514
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
1515
+ mitigation: RULE_MITIGATIONS[rule] ?? null
1516
+ };
1517
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
1518
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
1519
+ for (const p of activePatterns) {
1520
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
1521
+ if (prev?.state === "resolved") {
1522
+ p.trend = "worsening";
1523
+ p.regressed = true;
1524
+ }
1525
+ }
1526
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
1527
+ for (const a of failed) {
1528
+ const data = a.data;
1529
+ for (const issue of data.issues ?? []) {
1530
+ const existing = ruleLastFailureDate.get(issue.rule);
1531
+ if (!existing || a.fileDate > existing) {
1532
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
1533
+ }
1534
+ }
1535
+ }
1536
+ const newlyResolved = previousPatterns.filter((p) => {
1537
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
1538
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
1539
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
1540
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
1541
+ }).map((p) => ({
1542
+ ...p,
1543
+ state: "resolved",
1544
+ trend: "improving",
1545
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
1546
+ confidence: 0,
1547
+ compositeScore: 0,
1548
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1549
+ failureCount: 0,
1550
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
1551
+ }));
1552
+ const ttlCutoff = /* @__PURE__ */ new Date();
1553
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
1554
+ const ttlCutoffStr = ttlCutoff.toISOString();
1555
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
1556
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
1557
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
1558
+ const deduped = [
1559
+ ...newlyResolved,
1560
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
1561
+ ...pendingResolution
1562
+ ];
1563
+ const patterns = [...activePatterns, ...deduped];
1564
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
1565
+ const drift = this.detectDrift(patterns);
1566
+ const warnEffMap = /* @__PURE__ */ new Map();
1567
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
1568
+ for (const bc of buildCompletes) {
1569
+ const bcData = bc.data;
1570
+ const warned = bcData.warnedRules ?? [];
1571
+ if (warned.length === 0) continue;
1572
+ const sessionFailedRules = /* @__PURE__ */ new Set();
1573
+ const sessionAttempts = sessions.get(bc.sessionId) ?? [];
1574
+ for (const a of sessionAttempts) {
1575
+ const ad = a.data;
1576
+ if (ad.validationPassed === false) {
1577
+ for (const issue of ad.issues ?? []) {
1578
+ sessionFailedRules.add(issue.rule);
1579
+ }
1580
+ }
1581
+ }
1582
+ for (const rule of warned) {
1583
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
1584
+ entry.warned++;
1585
+ if (sessionFailedRules.has(rule)) entry.failed++;
1586
+ else entry.passed++;
1587
+ warnEffMap.set(rule, entry);
1588
+ }
1589
+ }
1590
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
1591
+ rule,
1592
+ timesWarned: e.warned,
1593
+ timesWarnedAndPassed: e.passed,
1594
+ timesWarnedAndFailed: e.failed,
1595
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
1596
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
1597
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
1598
+ for (const a of failed) {
1599
+ const data = a.data;
1600
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
1601
+ for (let i = 0; i < rules.length; i++) {
1602
+ for (let j = i + 1; j < rules.length; j++) {
1603
+ const key = `${rules[i]},${rules[j]}`;
1604
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
1605
+ }
1606
+ }
1607
+ }
1608
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
1609
+ const [a, b] = key.split(",").map(Number);
1610
+ return { rules: [a, b], count };
1611
+ }).sort((a, b) => b.count - a.count);
1612
+ const attemptDistribution = {};
1613
+ for (const sessionAttempts of sessions.values()) {
1614
+ const depth = sessionAttempts.length;
1615
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
1616
+ }
1617
+ return {
1618
+ schemaVersion: PATTERN_SCHEMA_VERSION,
1619
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1620
+ summary: {
1621
+ totalBuilds: starts.length,
1622
+ totalAttempts: attempts.length,
1623
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
1624
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
1625
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
1626
+ avgDurationMs: Math.round(avgDuration),
1627
+ totalTokensInput: totalInput,
1628
+ totalTokensOutput: totalOutput,
1629
+ attemptDistribution
1630
+ },
1631
+ topFailureRules: patterns,
1632
+ failingCredentialTypes: credTypes,
1633
+ drift,
1634
+ warningEffectiveness,
1635
+ ruleCoOccurrence
1636
+ };
1637
+ }
1638
+ async analyzeAndSave(days = 30) {
1639
+ const analysis = await this.analyze(days);
1640
+ await mkdir(this.outputDir, { recursive: true });
1641
+ const outputPath = join4(this.outputDir, "patterns.json");
1642
+ const tmpPath = `${outputPath}.tmp`;
1643
+ await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
1644
+ await rename(tmpPath, outputPath);
1645
+ const historySummary = {
1646
+ timestamp: analysis.generatedAt,
1647
+ totalBuilds: analysis.summary.totalBuilds,
1648
+ firstTryPassRate: analysis.summary.firstTryPassRate,
1649
+ correctionRate: analysis.summary.correctionRate,
1650
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
1651
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
1652
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
1653
+ };
1654
+ const historyPath = join4(this.outputDir, "pattern-history.jsonl");
1655
+ await appendFile(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
1656
+ return analysis;
1657
+ }
1658
+ async getHistory(limit = 20) {
1659
+ try {
1660
+ const raw = await fsReadFile(join4(this.outputDir, "pattern-history.jsonl"), "utf-8");
1661
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
1662
+ } catch {
1663
+ return [];
1664
+ }
1665
+ }
1666
+ static fromEnv() {
1667
+ const dir = process.env["KAIROS_TELEMETRY"];
1668
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
1669
+ }
1670
+ detectDrift(patterns) {
1671
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
1672
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
1673
+ const alerts = [];
1674
+ for (const p of patterns) {
1675
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
1676
+ alerts.push({
1677
+ type: "stale_pattern",
1678
+ rule: p.rule,
1679
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
1680
+ });
1681
+ }
1682
+ }
1683
+ for (const rule of VALIDATOR_RULES) {
1684
+ if (!(rule in RULE_MITIGATIONS)) {
1685
+ alerts.push({
1686
+ type: "missing_mitigation",
1687
+ rule,
1688
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
1689
+ });
1690
+ }
1691
+ if (!(rule in RULE_PIPELINE_STAGES)) {
1692
+ alerts.push({
1693
+ type: "missing_stage_mapping",
1694
+ rule,
1695
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
1696
+ });
1697
+ }
1698
+ }
1699
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
1700
+ return {
1701
+ healthy: alerts.length === 0,
1702
+ alerts,
1703
+ coveredRules,
1704
+ totalRules: VALIDATOR_RULES.length
1705
+ };
1706
+ }
1707
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
1708
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
1709
+ const stateWeight = stateWeights[state];
1710
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
1711
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
1712
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
1713
+ return {
1714
+ compositeScore,
1715
+ factors: {
1716
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
1717
+ impact: Math.round(impact * 1e3) / 1e3,
1718
+ recency: Math.round(avgRecency * 1e3) / 1e3,
1719
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
1720
+ }
1721
+ };
1722
+ }
1723
+ classifyTrend(older, newer) {
1724
+ const total = older + newer;
1725
+ if (total === 0) return "stable";
1726
+ if (older === 0) return "new";
1727
+ const newerRatio = newer / total;
1728
+ if (newerRatio >= 0.65) return "worsening";
1729
+ if (newerRatio <= 0.35) return "improving";
1730
+ return "stable";
1731
+ }
1732
+ deduplicateMessages(messages, maxCount = 3) {
1733
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
1734
+ const seen = /* @__PURE__ */ new Set();
1735
+ const unique = [];
1736
+ for (const msg of messages) {
1737
+ const key = normalize(msg);
1738
+ if (!seen.has(key) && unique.length < maxCount) {
1739
+ seen.add(key);
1740
+ unique.push(msg);
1741
+ }
1742
+ }
1743
+ return unique;
1744
+ }
1745
+ recencyWeight(fileDate, halfLifeDays = 30) {
1746
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
1747
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
1748
+ }
1749
+ async readAllEvents(days) {
1750
+ return readTelemetryEvents(this.telemetryDir, days);
1171
1751
  }
1172
1752
  };
1173
1753
 
@@ -1399,9 +1979,9 @@ function rerank(candidates, clusters) {
1399
1979
  }
1400
1980
 
1401
1981
  // src/library/file-library.ts
1402
- import { readFile as readFile2, writeFile, rename, mkdir } from "fs/promises";
1403
- import { join as join2 } from "path";
1404
- import { homedir as homedir2 } from "os";
1982
+ import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
1983
+ import { join as join5 } from "path";
1984
+ import { homedir as homedir4 } from "os";
1405
1985
 
1406
1986
  // src/utils/uuid.ts
1407
1987
  function generateUUID() {
@@ -1427,7 +2007,7 @@ var FileLibrary = class {
1427
2007
  initPromise = null;
1428
2008
  writeQueue = Promise.resolve();
1429
2009
  constructor(dir) {
1430
- this.dir = dir ?? join2(homedir2(), ".kairos", "library");
2010
+ this.dir = dir ?? join5(homedir4(), ".kairos", "library");
1431
2011
  }
1432
2012
  async initialize() {
1433
2013
  if (!this.initPromise) {
@@ -1436,10 +2016,10 @@ var FileLibrary = class {
1436
2016
  return this.initPromise;
1437
2017
  }
1438
2018
  async doInitialize() {
1439
- await mkdir(this.dir, { recursive: true });
1440
- const indexPath = join2(this.dir, "index.json");
2019
+ await mkdir2(this.dir, { recursive: true });
2020
+ const indexPath = join5(this.dir, "index.json");
1441
2021
  try {
1442
- const raw = await readFile2(indexPath, "utf-8");
2022
+ const raw = await readFile(indexPath, "utf-8");
1443
2023
  const parsed = JSON.parse(raw);
1444
2024
  if (!Array.isArray(parsed)) {
1445
2025
  this.workflows = [];
@@ -1569,10 +2149,10 @@ var FileLibrary = class {
1569
2149
  }
1570
2150
  persist() {
1571
2151
  this.writeQueue = this.writeQueue.then(async () => {
1572
- const indexPath = join2(this.dir, "index.json");
2152
+ const indexPath = join5(this.dir, "index.json");
1573
2153
  const tmpPath = `${indexPath}.tmp`;
1574
- await writeFile(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
1575
- await rename(tmpPath, indexPath);
2154
+ await writeFile2(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2155
+ await rename2(tmpPath, indexPath);
1576
2156
  });
1577
2157
  return this.writeQueue;
1578
2158
  }
@@ -1591,6 +2171,7 @@ export {
1591
2171
  scoreToMode,
1592
2172
  PromptBuilder,
1593
2173
  TelemetryReader,
2174
+ PatternAnalyzer,
1594
2175
  nullLogger,
1595
2176
  hybridScore,
1596
2177
  clusterWorkflows,
@@ -1599,4 +2180,4 @@ export {
1599
2180
  buildSearchCorpus,
1600
2181
  FileLibrary
1601
2182
  };
1602
- //# sourceMappingURL=chunk-RYGYNOR6.js.map
2183
+ //# sourceMappingURL=chunk-NJ6QZBIC.js.map