engramx 0.1.1 → 0.2.1

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.
@@ -1,6 +1,7 @@
1
1
  // src/core.ts
2
- import { join as join3, resolve as resolve2 } from "path";
3
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
2
+ import { join as join4, resolve as resolve2 } from "path";
3
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync } from "fs";
4
+ import { homedir } from "os";
4
5
 
5
6
  // src/graph/store.ts
6
7
  import initSqlJs from "sql.js";
@@ -164,7 +165,7 @@ var GraphStore = class _GraphStore {
164
165
  `SELECT n.*, COUNT(*) as degree
165
166
  FROM nodes n
166
167
  JOIN edges e ON e.source = n.id OR e.target = n.id
167
- WHERE n.kind NOT IN ('file', 'import', 'module')
168
+ WHERE n.kind NOT IN ('file', 'import', 'module', 'concept')
168
169
  GROUP BY n.id
169
170
  ORDER BY degree DESC
170
171
  LIMIT ?`
@@ -293,7 +294,34 @@ var GraphStore = class _GraphStore {
293
294
  }
294
295
  };
295
296
 
297
+ // src/graph/render-utils.ts
298
+ function sliceGraphemeSafe(s, max) {
299
+ if (max <= 0) return "";
300
+ if (s.length <= max) return s;
301
+ let cut = max;
302
+ const code = s.charCodeAt(cut - 1);
303
+ if (code >= 55296 && code <= 56319) cut--;
304
+ return s.slice(0, cut);
305
+ }
306
+ function truncateGraphemeSafe(s, max) {
307
+ if (max <= 0) return "";
308
+ if (s.length <= max) return s;
309
+ let cut = max - 1;
310
+ if (cut <= 0) return "";
311
+ const code = s.charCodeAt(cut - 1);
312
+ if (code >= 55296 && code <= 56319) cut--;
313
+ return s.slice(0, cut) + "\u2026";
314
+ }
315
+
296
316
  // src/graph/query.ts
317
+ var MISTAKE_SCORE_BOOST = 2.5;
318
+ var KEYWORD_SCORE_DOWNWEIGHT = 0.5;
319
+ var MAX_MISTAKE_LABEL_CHARS = 500;
320
+ function isHiddenKeyword(node) {
321
+ if (node.kind !== "concept") return false;
322
+ const meta = node.metadata;
323
+ return meta?.subkind === "keyword";
324
+ }
297
325
  var CHARS_PER_TOKEN = 4;
298
326
  function scoreNodes(store, terms) {
299
327
  const allNodes = store.getAllNodes();
@@ -306,7 +334,11 @@ function scoreNodes(store, terms) {
306
334
  if (label.includes(t)) score += 2;
307
335
  if (file.includes(t)) score += 1;
308
336
  }
309
- if (score > 0) scored.push({ score, node });
337
+ if (score > 0) {
338
+ if (node.kind === "mistake") score *= MISTAKE_SCORE_BOOST;
339
+ if (isHiddenKeyword(node)) score *= KEYWORD_SCORE_DOWNWEIGHT;
340
+ scored.push({ score, node });
341
+ }
310
342
  }
311
343
  return scored.sort((a, b) => b.score - a.score);
312
344
  }
@@ -321,6 +353,12 @@ function queryGraph(store, question, options = {}) {
321
353
  for (const n of startNodes) store.incrementQueryCount(n.id);
322
354
  const visited = new Set(startNodes.map((n) => n.id));
323
355
  const collectedEdges = [];
356
+ const shouldSkipEdgeFrom = (currentNodeId, edge) => {
357
+ if (edge.relation !== "triggered_by") return false;
358
+ const currentNode = store.getNode(currentNodeId);
359
+ if (!currentNode) return false;
360
+ return !isHiddenKeyword(currentNode);
361
+ };
324
362
  if (mode === "bfs") {
325
363
  let frontier = new Set(startNodes.map((n) => n.id));
326
364
  for (let d = 0; d < depth; d++) {
@@ -328,6 +366,7 @@ function queryGraph(store, question, options = {}) {
328
366
  for (const nid of frontier) {
329
367
  const neighbors = store.getNeighbors(nid);
330
368
  for (const { node, edge } of neighbors) {
369
+ if (shouldSkipEdgeFrom(nid, edge)) continue;
331
370
  if (!visited.has(node.id)) {
332
371
  nextFrontier.add(node.id);
333
372
  collectedEdges.push(edge);
@@ -344,6 +383,7 @@ function queryGraph(store, question, options = {}) {
344
383
  if (d > depth) continue;
345
384
  const neighbors = store.getNeighbors(id);
346
385
  for (const { node, edge } of neighbors) {
386
+ if (shouldSkipEdgeFrom(id, edge)) continue;
347
387
  if (!visited.has(node.id)) {
348
388
  visited.add(node.id);
349
389
  stack.push({ id: node.id, d: d + 1 });
@@ -420,12 +460,28 @@ function shortestPath(store, sourceTerm, targetTerm, maxHops = 8) {
420
460
  function renderSubgraph(nodes, edges, tokenBudget) {
421
461
  const charBudget = tokenBudget * CHARS_PER_TOKEN;
422
462
  const lines = [];
463
+ const mistakes2 = nodes.filter((n) => n.kind === "mistake");
464
+ const visible = nodes.filter(
465
+ (n) => n.kind !== "mistake" && !isHiddenKeyword(n)
466
+ );
467
+ const hiddenKeywordIds = new Set(
468
+ nodes.filter(isHiddenKeyword).map((n) => n.id)
469
+ );
470
+ if (mistakes2.length > 0) {
471
+ lines.push("\u26A0\uFE0F PAST MISTAKES (relevant to your query):");
472
+ for (const m of mistakes2) {
473
+ const label = truncateGraphemeSafe(m.label, MAX_MISTAKE_LABEL_CHARS);
474
+ const confNote = m.confidence === "EXTRACTED" ? "" : ` [confidence ${m.confidenceScore}]`;
475
+ lines.push(` - ${label} (from ${m.sourceFile})${confNote}`);
476
+ }
477
+ lines.push("");
478
+ }
423
479
  const degreeMap = /* @__PURE__ */ new Map();
424
480
  for (const e of edges) {
425
481
  degreeMap.set(e.source, (degreeMap.get(e.source) ?? 0) + 1);
426
482
  degreeMap.set(e.target, (degreeMap.get(e.target) ?? 0) + 1);
427
483
  }
428
- const sorted = [...nodes].sort(
484
+ const sorted = [...visible].sort(
429
485
  (a, b) => (degreeMap.get(b.id) ?? 0) - (degreeMap.get(a.id) ?? 0)
430
486
  );
431
487
  for (const n of sorted) {
@@ -433,7 +489,18 @@ function renderSubgraph(nodes, edges, tokenBudget) {
433
489
  `NODE ${n.label} [${n.kind}] src=${n.sourceFile} ${n.sourceLocation ?? ""}`
434
490
  );
435
491
  }
492
+ const skillConceptIds = new Set(
493
+ nodes.filter(
494
+ (n) => n.kind === "concept" && n.metadata?.subkind === "skill"
495
+ ).map((n) => n.id)
496
+ );
436
497
  for (const e of edges) {
498
+ if (hiddenKeywordIds.has(e.source) || hiddenKeywordIds.has(e.target)) {
499
+ continue;
500
+ }
501
+ if (e.relation === "similar_to" && skillConceptIds.has(e.source) && skillConceptIds.has(e.target)) {
502
+ continue;
503
+ }
437
504
  const srcNode = nodes.find((n) => n.id === e.source);
438
505
  const tgtNode = nodes.find((n) => n.id === e.target);
439
506
  if (srcNode && tgtNode) {
@@ -445,7 +512,7 @@ function renderSubgraph(nodes, edges, tokenBudget) {
445
512
  }
446
513
  let output = lines.join("\n");
447
514
  if (output.length > charBudget) {
448
- output = output.slice(0, charBudget) + `
515
+ output = sliceGraphemeSafe(output, charBudget) + `
449
516
  ... (truncated to ~${tokenBudget} token budget)`;
450
517
  }
451
518
  return output;
@@ -1031,34 +1098,358 @@ function learnFromSession(text, sourceLabel = "session") {
1031
1098
  return mineText(text, sourceLabel);
1032
1099
  }
1033
1100
 
1101
+ // src/miners/skills-miner.ts
1102
+ import {
1103
+ existsSync as existsSync4,
1104
+ readFileSync as readFileSync4,
1105
+ readdirSync as readdirSync3,
1106
+ realpathSync as realpathSync2,
1107
+ statSync
1108
+ } from "fs";
1109
+ import { basename as basename3, dirname as dirname2, join as join3 } from "path";
1110
+ function makeId4(...parts) {
1111
+ return parts.filter(Boolean).join("_").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase().slice(0, 120);
1112
+ }
1113
+ function parseFrontmatter(content) {
1114
+ if (!content.startsWith("---")) {
1115
+ return { data: {}, body: content, parseOk: false };
1116
+ }
1117
+ const closeMatch = content.slice(3).match(/\n---\s*(\n|$)/);
1118
+ if (!closeMatch || closeMatch.index === void 0) {
1119
+ return { data: {}, body: content, parseOk: false };
1120
+ }
1121
+ const yamlBlock = content.slice(3, 3 + closeMatch.index).trim();
1122
+ const bodyStart = 3 + closeMatch.index + closeMatch[0].length;
1123
+ const body = content.slice(bodyStart);
1124
+ try {
1125
+ const data = parseYaml(yamlBlock);
1126
+ return { data, body, parseOk: true };
1127
+ } catch {
1128
+ return { data: {}, body, parseOk: false };
1129
+ }
1130
+ }
1131
+ function parseYaml(block) {
1132
+ const data = {};
1133
+ const lines = block.split("\n");
1134
+ let i = 0;
1135
+ while (i < lines.length) {
1136
+ const line = lines[i];
1137
+ if (!line.trim() || line.trim().startsWith("#")) {
1138
+ i++;
1139
+ continue;
1140
+ }
1141
+ const topMatch = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
1142
+ if (!topMatch) {
1143
+ throw new Error(`YAML parse: unexpected line ${i}: ${line}`);
1144
+ }
1145
+ const [, key, rest] = topMatch;
1146
+ if (rest === ">" || rest === "|") {
1147
+ const style = rest;
1148
+ const collected = [];
1149
+ i++;
1150
+ while (i < lines.length) {
1151
+ const next = lines[i];
1152
+ if (next === "" || /^\s/.test(next)) {
1153
+ collected.push(next.replace(/^ {2}/, "").replace(/^\t/, ""));
1154
+ i++;
1155
+ } else {
1156
+ break;
1157
+ }
1158
+ }
1159
+ data[key] = style === ">" ? collected.filter((l) => l.trim()).join(" ").trim() : collected.join("\n").trim();
1160
+ } else if (rest === "") {
1161
+ const nested = {};
1162
+ i++;
1163
+ while (i < lines.length && /^\s+\S/.test(lines[i])) {
1164
+ const childMatch = lines[i].match(
1165
+ /^\s+([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/
1166
+ );
1167
+ if (childMatch) {
1168
+ nested[childMatch[1]] = stripQuotes(childMatch[2]);
1169
+ }
1170
+ i++;
1171
+ }
1172
+ data[key] = nested;
1173
+ } else {
1174
+ data[key] = stripQuotes(rest);
1175
+ i++;
1176
+ }
1177
+ }
1178
+ return data;
1179
+ }
1180
+ function stripQuotes(s) {
1181
+ const trimmed = s.trim();
1182
+ if (trimmed.length < 2) return trimmed;
1183
+ const first = trimmed[0];
1184
+ const last = trimmed[trimmed.length - 1];
1185
+ if (first === '"' && last === '"') {
1186
+ return trimmed.slice(1, -1).replace(
1187
+ /\\u([0-9a-fA-F]{4})/g,
1188
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
1189
+ ).replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
1190
+ }
1191
+ if (first === "'" && last === "'") {
1192
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1193
+ }
1194
+ return trimmed;
1195
+ }
1196
+ var QUOTED_PHRASE_RE = /[\u0022\u0027\u201C\u201D\u2018\u2019]([^\u0022\u0027\u201C\u201D\u2018\u2019\n]{4,100}?)[\u0022\u0027\u201C\u201D\u2018\u2019]/g;
1197
+ var USE_WHEN_RE = /\bUse when\s+(.+?)(?=\.\s+[A-Z]|[\n\r]|$)/g;
1198
+ function extractTriggers(text) {
1199
+ const triggers = /* @__PURE__ */ new Set();
1200
+ for (const m of text.matchAll(QUOTED_PHRASE_RE)) {
1201
+ const t = m[1]?.trim();
1202
+ if (t && t.length >= 4) triggers.add(t);
1203
+ }
1204
+ for (const m of text.matchAll(USE_WHEN_RE)) {
1205
+ const t = m[1]?.trim().replace(/[.,;]+$/, "");
1206
+ if (t && t.length > 0 && t.length < 120) triggers.add(t);
1207
+ }
1208
+ return [...triggers];
1209
+ }
1210
+ function extractRelatedSkills(body) {
1211
+ const sectionMatch = body.match(
1212
+ /##\s*Related Skills\s*\r?\n([\s\S]*?)(?=\r?\n##|\r?\n#[^#]|$)/i
1213
+ );
1214
+ if (!sectionMatch) return [];
1215
+ const section = sectionMatch[1];
1216
+ const names = [];
1217
+ for (const line of section.split(/\r?\n/)) {
1218
+ const m = line.match(/^[\s]*[-*+]\s+`?([a-z][a-z0-9-]*)`?/i);
1219
+ if (m) names.push(m[1].toLowerCase());
1220
+ }
1221
+ return [...new Set(names)];
1222
+ }
1223
+ function discoverSkillFiles(skillsDir) {
1224
+ if (!existsSync4(skillsDir)) return [];
1225
+ let entries;
1226
+ try {
1227
+ entries = readdirSync3(skillsDir, {
1228
+ withFileTypes: true,
1229
+ encoding: "utf-8"
1230
+ });
1231
+ } catch {
1232
+ return [];
1233
+ }
1234
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
1235
+ const results = [];
1236
+ for (const entry of sorted) {
1237
+ if (entry.name.startsWith(".")) continue;
1238
+ let isDir = entry.isDirectory();
1239
+ if (entry.isSymbolicLink()) {
1240
+ try {
1241
+ const resolved = realpathSync2(join3(skillsDir, entry.name));
1242
+ isDir = statSync(resolved).isDirectory();
1243
+ } catch {
1244
+ continue;
1245
+ }
1246
+ }
1247
+ if (!isDir) continue;
1248
+ const skillMdPath = join3(skillsDir, entry.name, "SKILL.md");
1249
+ if (existsSync4(skillMdPath)) {
1250
+ results.push(skillMdPath);
1251
+ }
1252
+ }
1253
+ return results;
1254
+ }
1255
+ function mineSkills(skillsDir) {
1256
+ const result = {
1257
+ nodes: [],
1258
+ edges: [],
1259
+ skillCount: 0,
1260
+ anomalies: []
1261
+ };
1262
+ const skillFiles = discoverSkillFiles(skillsDir);
1263
+ if (skillFiles.length === 0) return result;
1264
+ const now = Date.now();
1265
+ const keywordIds = /* @__PURE__ */ new Set();
1266
+ const skillIdByDirName = /* @__PURE__ */ new Map();
1267
+ const pendingRelated = [];
1268
+ for (const skillPath of skillFiles) {
1269
+ let content;
1270
+ try {
1271
+ content = readFileSync4(skillPath, "utf-8");
1272
+ } catch {
1273
+ continue;
1274
+ }
1275
+ const skillDirName = basename3(dirname2(skillPath));
1276
+ const { data, body, parseOk } = parseFrontmatter(content);
1277
+ const hasFrontmatter = parseOk && Object.keys(data).length > 0;
1278
+ let name;
1279
+ let description;
1280
+ let version;
1281
+ if (hasFrontmatter) {
1282
+ name = String(data.name ?? skillDirName);
1283
+ description = String(data.description ?? "");
1284
+ const meta = data.metadata;
1285
+ if (meta && typeof meta === "object" && "version" in meta) {
1286
+ version = String(meta.version);
1287
+ }
1288
+ } else {
1289
+ result.anomalies.push(skillPath);
1290
+ const headingMatch = content.match(/^#\s+(.+)$/m);
1291
+ name = headingMatch?.[1]?.trim() ?? skillDirName;
1292
+ const paragraphs = content.split(/\r?\n\s*\r?\n/).map((p) => p.trim()).filter((p) => p && !p.startsWith("#"));
1293
+ description = paragraphs[0] ?? "";
1294
+ }
1295
+ const skillId = makeId4("skill", skillDirName);
1296
+ skillIdByDirName.set(skillDirName.toLowerCase(), skillId);
1297
+ const sourceFileRel = `${skillDirName}/SKILL.md`;
1298
+ const sizeLines = content.split("\n").length;
1299
+ result.nodes.push({
1300
+ id: skillId,
1301
+ label: name,
1302
+ kind: "concept",
1303
+ sourceFile: sourceFileRel,
1304
+ sourceLocation: skillPath,
1305
+ confidence: "EXTRACTED",
1306
+ confidenceScore: 1,
1307
+ lastVerified: now,
1308
+ queryCount: 0,
1309
+ metadata: {
1310
+ miner: "skills",
1311
+ subkind: "skill",
1312
+ description: truncateGraphemeSafe(description, 500),
1313
+ sizeLines,
1314
+ hasFrontmatter,
1315
+ version,
1316
+ skillDir: skillDirName
1317
+ }
1318
+ });
1319
+ result.skillCount++;
1320
+ const triggers = extractTriggers(description + "\n\n" + body);
1321
+ for (const trigger of triggers) {
1322
+ const normalized = trigger.toLowerCase().trim();
1323
+ if (normalized.length === 0 || normalized.length > 120) continue;
1324
+ const keywordId = makeId4("keyword", normalized);
1325
+ if (!keywordIds.has(keywordId)) {
1326
+ result.nodes.push({
1327
+ id: keywordId,
1328
+ label: trigger,
1329
+ kind: "concept",
1330
+ sourceFile: sourceFileRel,
1331
+ sourceLocation: null,
1332
+ confidence: "EXTRACTED",
1333
+ confidenceScore: 1,
1334
+ lastVerified: now,
1335
+ queryCount: 0,
1336
+ metadata: { miner: "skills", subkind: "keyword" }
1337
+ });
1338
+ keywordIds.add(keywordId);
1339
+ }
1340
+ result.edges.push({
1341
+ source: keywordId,
1342
+ target: skillId,
1343
+ relation: "triggered_by",
1344
+ confidence: "EXTRACTED",
1345
+ confidenceScore: 1,
1346
+ sourceFile: sourceFileRel,
1347
+ sourceLocation: skillPath,
1348
+ lastVerified: now,
1349
+ metadata: { miner: "skills" }
1350
+ });
1351
+ }
1352
+ if (hasFrontmatter || body.length > 0) {
1353
+ const relatedNames = extractRelatedSkills(body);
1354
+ for (const relatedName of relatedNames) {
1355
+ pendingRelated.push({ fromId: skillId, toName: relatedName });
1356
+ }
1357
+ }
1358
+ }
1359
+ for (const { fromId, toName } of pendingRelated) {
1360
+ const toId = skillIdByDirName.get(toName.toLowerCase());
1361
+ if (toId && toId !== fromId) {
1362
+ result.edges.push({
1363
+ source: fromId,
1364
+ target: toId,
1365
+ relation: "similar_to",
1366
+ confidence: "INFERRED",
1367
+ confidenceScore: 0.8,
1368
+ sourceFile: "SKILL.md",
1369
+ sourceLocation: null,
1370
+ lastVerified: Date.now(),
1371
+ metadata: { miner: "skills", via: "related_skills_section" }
1372
+ });
1373
+ }
1374
+ }
1375
+ return result;
1376
+ }
1377
+
1034
1378
  // src/core.ts
1035
1379
  var ENGRAM_DIR = ".engram";
1036
1380
  var DB_FILE = "graph.db";
1381
+ var LOCK_FILE = "init.lock";
1382
+ var DEFAULT_SKILLS_DIR = join4(homedir(), ".claude", "skills");
1037
1383
  function getDbPath(projectRoot) {
1038
- return join3(projectRoot, ENGRAM_DIR, DB_FILE);
1384
+ return join4(projectRoot, ENGRAM_DIR, DB_FILE);
1039
1385
  }
1040
1386
  async function getStore(projectRoot) {
1041
1387
  return GraphStore.open(getDbPath(projectRoot));
1042
1388
  }
1043
- async function init(projectRoot) {
1389
+ async function init(projectRoot, options = {}) {
1044
1390
  const root = resolve2(projectRoot);
1045
1391
  const start = Date.now();
1046
- mkdirSync2(join3(root, ENGRAM_DIR), { recursive: true });
1047
- const { nodes, edges, fileCount, totalLines } = extractDirectory(root);
1048
- const gitResult = mineGitHistory(root);
1049
- const sessionResult = mineSessionHistory(root);
1050
- const allNodes = [...nodes, ...gitResult.nodes, ...sessionResult.nodes];
1051
- const allEdges = [...edges, ...gitResult.edges, ...sessionResult.edges];
1052
- const store = await getStore(root);
1392
+ mkdirSync2(join4(root, ENGRAM_DIR), { recursive: true });
1393
+ const lockPath = join4(root, ENGRAM_DIR, LOCK_FILE);
1394
+ try {
1395
+ writeFileSync2(lockPath, String(process.pid), { flag: "wx" });
1396
+ } catch (err) {
1397
+ if (err.code === "EEXIST") {
1398
+ throw new Error(
1399
+ `engram: another init is running on ${root} (lock: ${lockPath}). If no other process is active, delete the lock file manually.`
1400
+ );
1401
+ }
1402
+ throw err;
1403
+ }
1053
1404
  try {
1054
- store.clearAll();
1055
- store.bulkUpsert(allNodes, allEdges);
1056
- store.setStat("last_mined", String(Date.now()));
1057
- store.setStat("project_root", root);
1405
+ const { nodes, edges, fileCount, totalLines } = extractDirectory(root);
1406
+ const gitResult = mineGitHistory(root);
1407
+ const sessionResult = mineSessionHistory(root);
1408
+ let skillCount = 0;
1409
+ let skillNodes = [];
1410
+ let skillEdges = [];
1411
+ if (options.withSkills) {
1412
+ const skillsDir = typeof options.withSkills === "string" ? options.withSkills : DEFAULT_SKILLS_DIR;
1413
+ const skillsResult = mineSkills(skillsDir);
1414
+ skillCount = skillsResult.skillCount;
1415
+ skillNodes = skillsResult.nodes;
1416
+ skillEdges = skillsResult.edges;
1417
+ }
1418
+ const allNodes = [
1419
+ ...nodes,
1420
+ ...gitResult.nodes,
1421
+ ...sessionResult.nodes,
1422
+ ...skillNodes
1423
+ ];
1424
+ const allEdges = [
1425
+ ...edges,
1426
+ ...gitResult.edges,
1427
+ ...sessionResult.edges,
1428
+ ...skillEdges
1429
+ ];
1430
+ const store = await getStore(root);
1431
+ try {
1432
+ store.clearAll();
1433
+ store.bulkUpsert(allNodes, allEdges);
1434
+ store.setStat("last_mined", String(Date.now()));
1435
+ store.setStat("project_root", root);
1436
+ } finally {
1437
+ store.close();
1438
+ }
1439
+ return {
1440
+ nodes: allNodes.length,
1441
+ edges: allEdges.length,
1442
+ fileCount,
1443
+ totalLines,
1444
+ timeMs: Date.now() - start,
1445
+ skillCount
1446
+ };
1058
1447
  } finally {
1059
- store.close();
1448
+ try {
1449
+ unlinkSync(lockPath);
1450
+ } catch {
1451
+ }
1060
1452
  }
1061
- return { nodes: allNodes.length, edges: allEdges.length, fileCount, totalLines, timeMs: Date.now() - start };
1062
1453
  }
1063
1454
  async function query(projectRoot, question, options = {}) {
1064
1455
  const store = await getStore(projectRoot);
@@ -1110,6 +1501,28 @@ async function learn(projectRoot, text, sourceLabel = "manual") {
1110
1501
  }
1111
1502
  return { nodesAdded: nodes.length };
1112
1503
  }
1504
+ async function mistakes(projectRoot, options = {}) {
1505
+ const store = await getStore(projectRoot);
1506
+ try {
1507
+ let items = store.getAllNodes().filter((n) => n.kind === "mistake");
1508
+ if (options.sinceDays !== void 0) {
1509
+ const cutoff = Date.now() - options.sinceDays * 24 * 60 * 60 * 1e3;
1510
+ items = items.filter((m) => m.lastVerified >= cutoff);
1511
+ }
1512
+ items.sort((a, b) => b.lastVerified - a.lastVerified);
1513
+ const limit = options.limit ?? 20;
1514
+ return items.slice(0, limit).map((m) => ({
1515
+ id: m.id,
1516
+ label: m.label,
1517
+ confidence: m.confidence,
1518
+ confidenceScore: m.confidenceScore,
1519
+ sourceFile: m.sourceFile,
1520
+ lastVerified: m.lastVerified
1521
+ }));
1522
+ } finally {
1523
+ store.close();
1524
+ }
1525
+ }
1113
1526
  async function benchmark(projectRoot, questions) {
1114
1527
  const root = resolve2(projectRoot);
1115
1528
  const store = await getStore(root);
@@ -1121,8 +1534,8 @@ async function benchmark(projectRoot, questions) {
1121
1534
  if (node.sourceFile && !seenFiles.has(node.sourceFile)) {
1122
1535
  seenFiles.add(node.sourceFile);
1123
1536
  try {
1124
- const fullPath = join3(root, node.sourceFile);
1125
- if (existsSync4(fullPath)) fullCorpusChars += readFileSync4(fullPath, "utf-8").length;
1537
+ const fullPath = join4(root, node.sourceFile);
1538
+ if (existsSync5(fullPath)) fullCorpusChars += readFileSync5(fullPath, "utf-8").length;
1126
1539
  } catch {
1127
1540
  }
1128
1541
  }
@@ -1143,8 +1556,8 @@ async function benchmark(projectRoot, questions) {
1143
1556
  let relevantChars = 0;
1144
1557
  for (const f of matchedFiles) {
1145
1558
  try {
1146
- const fullPath = join3(root, f);
1147
- if (existsSync4(fullPath)) relevantChars += readFileSync4(fullPath, "utf-8").length;
1559
+ const fullPath = join4(root, f);
1560
+ if (existsSync5(fullPath)) relevantChars += readFileSync5(fullPath, "utf-8").length;
1148
1561
  } catch {
1149
1562
  }
1150
1563
  }
@@ -1174,11 +1587,15 @@ async function benchmark(projectRoot, questions) {
1174
1587
 
1175
1588
  export {
1176
1589
  GraphStore,
1590
+ sliceGraphemeSafe,
1591
+ truncateGraphemeSafe,
1592
+ MAX_MISTAKE_LABEL_CHARS,
1177
1593
  queryGraph,
1178
1594
  shortestPath,
1179
1595
  SUPPORTED_EXTENSIONS,
1180
1596
  extractFile,
1181
1597
  extractDirectory,
1598
+ mineSkills,
1182
1599
  getDbPath,
1183
1600
  getStore,
1184
1601
  init,
@@ -1187,5 +1604,6 @@ export {
1187
1604
  godNodes,
1188
1605
  stats,
1189
1606
  learn,
1607
+ mistakes,
1190
1608
  benchmark
1191
1609
  };