enigma-cli 1.59.1 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/commands/handoff.md +48 -0
  2. package/assets/mods/enigma-relay/.claude-plugin/plugin.json +1 -0
  3. package/assets/mods/enigma-relay/hooks/hooks.json +1 -0
  4. package/assets/mods/enigma-relay/hooks/register.ts +86 -0
  5. package/assets/mods/enigma-relay/tests/relay.test.ts +65 -0
  6. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  7. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  8. package/assets/skills/backend-policy/skill.json +1 -1
  9. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  10. package/assets/skills/code-review-policy/skill.json +1 -1
  11. package/assets/skills/core-engineering-policy/skill.json +1 -1
  12. package/assets/skills/database-expert/skill.json +1 -1
  13. package/assets/skills/debugging-policy/skill.json +1 -1
  14. package/assets/skills/dependency-policy/skill.json +1 -1
  15. package/assets/skills/email-policy/skill.json +1 -1
  16. package/assets/skills/frontend-design/skill.json +1 -1
  17. package/assets/skills/frontend-policy/skill.json +1 -1
  18. package/assets/skills/git-policy/skill.json +1 -1
  19. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  20. package/assets/skills/security-policy/skill.json +1 -1
  21. package/assets/skills/skill-creator/skill.json +1 -1
  22. package/assets/skills/task-completion-policy/skill.json +1 -1
  23. package/assets/skills/technical-writing-policy/skill.json +1 -1
  24. package/assets/skills/testing-policy/skill.json +1 -1
  25. package/assets/skills/validation-policy/skill.json +1 -1
  26. package/bin/checksums.json +4 -4
  27. package/dist/ci-watch.js +2 -0
  28. package/dist/codegraph-hook.js +576 -48
  29. package/dist/post-edit.js +576 -48
  30. package/package.json +1 -1
@@ -304,6 +304,8 @@ var init_config = __esm({
304
304
  gateTrivialLines: 20,
305
305
  subagentLimit: 4,
306
306
  compactWindow: 4e5,
307
+ relay: true,
308
+ relayAt: 3e5,
307
309
  planSessionLimit: 0,
308
310
  planWeeklyLimit: 0,
309
311
  planWeeklySonnetLimit: 0,
@@ -545,10 +547,222 @@ var init_codegraph_rank = __esm({
545
547
  }
546
548
  });
547
549
 
550
+ // src/codegraph-extract-docs.ts
551
+ function cap(text) {
552
+ const t = text.trim();
553
+ return t.length > MAX_SIGNATURE ? `${t.slice(0, MAX_SIGNATURE)}...` : t;
554
+ }
555
+ function indentOf(line) {
556
+ let n = 0;
557
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
558
+ return n;
559
+ }
560
+ function fencedLines(lines) {
561
+ const inside = new Uint8Array(lines.length);
562
+ let fence = null;
563
+ for (let i = 0; i < lines.length; i++) {
564
+ const m = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
565
+ if (fence) {
566
+ inside[i] = 1;
567
+ if (m && m[1][0] === fence[0] && m[1].length >= fence.length) fence = null;
568
+ continue;
569
+ }
570
+ if (m) {
571
+ fence = m[1];
572
+ inside[i] = 1;
573
+ }
574
+ }
575
+ return inside;
576
+ }
577
+ function markdownSymbols(lines) {
578
+ const fenced = fencedLines(lines);
579
+ const heads = [];
580
+ for (let i = 0; i < lines.length; i++) {
581
+ if (fenced[i]) continue;
582
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i]);
583
+ if (m && m[1].length <= MAX_HEADING_LEVEL) heads.push({ level: m[1].length, name: m[2].replace(/[`*_]/g, "").trim(), line: i + 1 });
584
+ }
585
+ return heads.filter((h) => h.name).map((h, idx) => {
586
+ const next = heads.slice(idx + 1).find((o) => o.level <= h.level);
587
+ return { name: h.name, kind: "section", line: h.line, endLine: next ? next.line - 1 : lines.length, signature: cap(lines[h.line - 1]) };
588
+ });
589
+ }
590
+ function sqlEnd(lines, start) {
591
+ let dollar = false;
592
+ for (let i = start; i < lines.length; i++) {
593
+ const parts = lines[i].replace(/--.*$/, "").split("$$");
594
+ for (let p = 0; p < parts.length; p++) {
595
+ if (p > 0) dollar = !dollar;
596
+ if (!dollar && parts[p].includes(";")) return i + 1;
597
+ }
598
+ }
599
+ return lines.length;
600
+ }
601
+ function sqlSymbols(lines) {
602
+ const rules = [
603
+ ["table", new RegExp(String.raw`^\s*create\s+(?:or\s+replace\s+)?(?:(?:global|local)\s+)?(?:temp(?:orary)?\s+|unlogged\s+)?(?:materialized\s+)?(?:table|view)\s+(?:if\s+not\s+exists\s+)?${SQL_NAME}`, "i")],
604
+ ["function", new RegExp(String.raw`^\s*create\s+(?:or\s+replace\s+)?(?:function|procedure)\s+${SQL_NAME}`, "i")],
605
+ ["type", new RegExp(String.raw`^\s*create\s+type\s+${SQL_NAME}`, "i")]
606
+ ];
607
+ const out = [];
608
+ for (let i = 0; i < lines.length; i++) {
609
+ for (const [kind, re] of rules) {
610
+ const m = re.exec(lines[i]);
611
+ if (!m) continue;
612
+ out.push({ name: m[1], kind, line: i + 1, endLine: sqlEnd(lines, i), signature: cap(lines[i].replace(/\(.*$/, "")) });
613
+ break;
614
+ }
615
+ }
616
+ return out;
617
+ }
618
+ function prismaSymbols(lines) {
619
+ const out = [];
620
+ for (let i = 0; i < lines.length; i++) {
621
+ const m = /^\s*(model|view|enum|type)\s+([A-Za-z_]\w*)\s*\{/.exec(lines[i]);
622
+ if (!m) continue;
623
+ let end = i;
624
+ while (end < lines.length - 1 && !/^\s*\}/.test(lines[end])) end++;
625
+ const kind = m[1] === "enum" ? "enum" : m[1] === "type" ? "type" : "table";
626
+ out.push({ name: m[2], kind, line: i + 1, endLine: end + 1, signature: cap(`${m[1]} ${m[2]}`) });
627
+ }
628
+ return out;
629
+ }
630
+ function yamlEnd(lines, start) {
631
+ const base = indentOf(lines[start]);
632
+ let end = start + 1;
633
+ for (let i = start + 1; i < lines.length; i++) {
634
+ const line = lines[i];
635
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
636
+ if (indentOf(line) <= base && !line.trimStart().startsWith("- ")) break;
637
+ end = i + 1;
638
+ }
639
+ return end;
640
+ }
641
+ function yamlSymbols(lines) {
642
+ const out = [];
643
+ const KEY = /^(\s*)([A-Za-z0-9_][\w.-]*)\s*:(?:\s|$)/;
644
+ for (let i = 0; i < lines.length; i++) {
645
+ const m = KEY.exec(lines[i]);
646
+ if (!m || m[1].length) continue;
647
+ const end = yamlEnd(lines, i);
648
+ out.push({ name: m[2], kind: "key", line: i + 1, endLine: end, signature: cap(lines[i]) });
649
+ if (!YAML_CONTAINERS.has(m[2])) continue;
650
+ let childIndent = -1;
651
+ for (let j = i + 1; j < end; j++) {
652
+ const c = KEY.exec(lines[j]);
653
+ if (!c || !c[1].length) continue;
654
+ if (childIndent === -1) childIndent = c[1].length;
655
+ if (c[1].length !== childIndent) continue;
656
+ out.push({ name: c[2], kind: "key", line: j + 1, endLine: yamlEnd(lines, j), signature: cap(`${m[2]}.${c[2]}`) });
657
+ }
658
+ }
659
+ return out;
660
+ }
661
+ function tomlSymbols(lines) {
662
+ const heads = [];
663
+ for (let i = 0; i < lines.length; i++) {
664
+ const m = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/.exec(lines[i]);
665
+ if (m) heads.push({ name: m[1], line: i + 1 });
666
+ }
667
+ return heads.map((h, idx) => ({
668
+ name: h.name,
669
+ kind: "key",
670
+ line: h.line,
671
+ endLine: idx + 1 < heads.length ? heads[idx + 1].line - 1 : lines.length,
672
+ signature: cap(lines[h.line - 1])
673
+ }));
674
+ }
675
+ function markdownLinks(lines) {
676
+ const fenced = fencedLines(lines);
677
+ const out = /* @__PURE__ */ new Set();
678
+ for (let i = 0; i < lines.length; i++) {
679
+ if (fenced[i]) continue;
680
+ const re = /\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
681
+ let m;
682
+ while (m = re.exec(lines[i])) {
683
+ const target = m[1].replace(/[#?].*$/, "");
684
+ if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue;
685
+ try {
686
+ out.add(decodeURI(target));
687
+ } catch {
688
+ out.add(target);
689
+ }
690
+ }
691
+ }
692
+ return [...out];
693
+ }
694
+ function markdownCodeMentions(content) {
695
+ const lines = content.split("\n");
696
+ const fenced = fencedLines(lines);
697
+ const out = [];
698
+ for (let i = 0; i < lines.length; i++) {
699
+ if (fenced[i]) continue;
700
+ const re = /`([A-Za-z_$][\w$]*)(?:\(\))?`/g;
701
+ let m;
702
+ while (m = re.exec(lines[i])) out.push({ name: m[1], line: i + 1 });
703
+ }
704
+ return out;
705
+ }
706
+ function extractDocFile(lang, content) {
707
+ const lines = content.split("\n");
708
+ const out = (() => {
709
+ switch (lang) {
710
+ case "markdown":
711
+ return { symbols: markdownSymbols(lines), imports: markdownLinks(lines) };
712
+ case "sql":
713
+ return { symbols: sqlSymbols(lines), imports: [] };
714
+ case "prisma":
715
+ return { symbols: prismaSymbols(lines), imports: [] };
716
+ case "yaml":
717
+ return { symbols: yamlSymbols(lines), imports: [] };
718
+ case "toml":
719
+ return { symbols: tomlSymbols(lines), imports: [] };
720
+ default:
721
+ return { symbols: [], imports: [] };
722
+ }
723
+ })();
724
+ for (const s of out.symbols) while (s.endLine > s.line && !(lines[s.endLine - 1] ?? "").trim()) s.endLine--;
725
+ return out;
726
+ }
727
+ var DOC_LANG_BY_EXT, DOC_LANGS, DOC_REFERENCE_LANGS, YAML_CONTAINERS, MAX_SIGNATURE, MAX_HEADING_LEVEL, SQL_NAME;
728
+ var init_codegraph_extract_docs = __esm({
729
+ "src/codegraph-extract-docs.ts"() {
730
+ "use strict";
731
+ DOC_LANG_BY_EXT = {
732
+ ".md": "markdown",
733
+ ".mdx": "markdown",
734
+ ".sql": "sql",
735
+ ".prisma": "prisma",
736
+ ".yaml": "yaml",
737
+ ".yml": "yaml",
738
+ ".toml": "toml"
739
+ };
740
+ DOC_LANGS = new Set(Object.values(DOC_LANG_BY_EXT));
741
+ DOC_REFERENCE_LANGS = /* @__PURE__ */ new Set(["sql", "prisma"]);
742
+ YAML_CONTAINERS = /* @__PURE__ */ new Set(["services", "jobs", "workflows", "volumes", "networks"]);
743
+ MAX_SIGNATURE = 200;
744
+ MAX_HEADING_LEVEL = 3;
745
+ SQL_NAME = String.raw`(?:[\w"\x60\[\]]+\.)?["\x60\[]?([A-Za-z_][\w$]*)["\x60\]]?`;
746
+ }
747
+ });
748
+
548
749
  // src/codegraph-extract.ts
549
750
  import { execFileSync as execFileSync2 } from "child_process";
550
751
  import { extname, join as join4, relative as relative2, resolve as resolve2 } from "path";
551
752
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
753
+ function isCodeLang(lang) {
754
+ return !DOC_LANGS.has(lang);
755
+ }
756
+ function isDocPath(path) {
757
+ return !!DOC_LANG_BY_EXT[extname(path).toLowerCase()];
758
+ }
759
+ function isForeignPath(rel) {
760
+ return FOREIGN_DOC_DIRS.test(rel);
761
+ }
762
+ function pickDocs(root, docs, room) {
763
+ const depth = (p) => relPath(root, p).split("/").length;
764
+ return docs.filter((p) => !FOREIGN_DOC_DIRS.test(relPath(root, p))).sort((a, b) => depth(a) - depth(b) || a.localeCompare(b)).slice(0, Math.max(0, Math.min(MAX_DOC_FILES, room)));
765
+ }
552
766
  function symRules(lang) {
553
767
  switch (lang) {
554
768
  case "ts":
@@ -713,7 +927,7 @@ function lineResolver(content) {
713
927
  return lo + 1;
714
928
  };
715
929
  }
716
- function indentOf(line) {
930
+ function indentOf2(line) {
717
931
  let n = 0;
718
932
  while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
719
933
  return n;
@@ -779,18 +993,18 @@ function braceEnd(lines, startIdx, lang) {
779
993
  return lines.length;
780
994
  }
781
995
  function indentEnd(lines, startIdx) {
782
- const base = indentOf(lines[startIdx]);
996
+ const base = indentOf2(lines[startIdx]);
783
997
  let end = startIdx + 1;
784
998
  for (let i = startIdx + 1; i < lines.length; i++) {
785
999
  const line = lines[i];
786
1000
  if (!line.trim()) continue;
787
- if (indentOf(line) <= base) break;
1001
+ if (indentOf2(line) <= base) break;
788
1002
  end = i + 1;
789
1003
  }
790
1004
  return end;
791
1005
  }
792
1006
  function clampToSiblings(symbols, lines) {
793
- const indentAt = (line) => indentOf(lines[line - 1] ?? "");
1007
+ const indentAt = (line) => indentOf2(lines[line - 1] ?? "");
794
1008
  for (let i = 0; i < symbols.length; i++) {
795
1009
  const s = symbols[i];
796
1010
  for (let j = i + 1; j < symbols.length; j++) {
@@ -803,14 +1017,15 @@ function clampToSiblings(symbols, lines) {
803
1017
  }
804
1018
  }
805
1019
  function signatureOf(line) {
806
- const MAX_SIGNATURE = 200;
1020
+ const MAX_SIGNATURE2 = 200;
807
1021
  let sig = line.trim();
808
1022
  const brace = sig.indexOf("{");
809
1023
  if (brace > 0) sig = sig.slice(0, brace).trim();
810
1024
  if (sig.endsWith(":")) sig = sig.slice(0, -1).trim();
811
- return sig.length > MAX_SIGNATURE ? `${sig.slice(0, MAX_SIGNATURE)}...` : sig;
1025
+ return sig.length > MAX_SIGNATURE2 ? `${sig.slice(0, MAX_SIGNATURE2)}...` : sig;
812
1026
  }
813
1027
  function extractFile(lang, content) {
1028
+ if (DOC_LANGS.has(lang)) return extractDocFile(lang, content);
814
1029
  const lineAt = lineResolver(content);
815
1030
  const lines = content.split("\n");
816
1031
  const braced = BRACE_LANGS.has(lang);
@@ -852,25 +1067,27 @@ function gitSourceFiles(root) {
852
1067
  const out = gitList(root, ["--cached", "--others", "--exclude-standard"]);
853
1068
  if (out === null) return null;
854
1069
  const nested = existsSync4(join4(root, ".gitmodules")) ? gitList(root, ["--recurse-submodules"]) : null;
855
- const files = [];
1070
+ const code = [];
1071
+ const docs = [];
856
1072
  const seen = /* @__PURE__ */ new Set();
857
1073
  for (const rel of [...out.split("\0"), ...(nested ?? "").split("\0")]) {
858
- if (files.length >= MAX_FILES) break;
1074
+ if (code.length >= MAX_FILES) break;
859
1075
  if (!rel || seen.has(rel)) continue;
860
1076
  seen.add(rel);
861
1077
  if (!LANG_BY_EXT[extname(rel).toLowerCase()]) continue;
862
1078
  if (rel.split("/").some((seg) => seg.startsWith(".") || IGNORE_DIRS.has(seg))) continue;
863
- files.push(join4(root, rel));
1079
+ (isDocPath(rel) ? docs : code).push(join4(root, rel));
864
1080
  }
865
- return files;
1081
+ return [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
866
1082
  }
867
1083
  function walk(root) {
868
1084
  const tracked = gitSourceFiles(root);
869
1085
  if (tracked && tracked.length) return { files: tracked, truncated: tracked.length >= MAX_FILES };
870
- const files = [];
1086
+ const code = [];
1087
+ const docs = [];
871
1088
  const queue = [root];
872
1089
  let visited = 0;
873
- for (let head = 0; head < queue.length && files.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
1090
+ for (let head = 0; head < queue.length && code.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
874
1091
  let entries;
875
1092
  try {
876
1093
  entries = readdirSync2(queue[head], { withFileTypes: true });
@@ -878,16 +1095,17 @@ function walk(root) {
878
1095
  continue;
879
1096
  }
880
1097
  for (const e of entries) {
881
- if (files.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
1098
+ if (code.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
882
1099
  if (e.isDirectory()) {
883
1100
  if (!e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) queue.push(join4(queue[head], e.name));
884
1101
  continue;
885
1102
  }
886
- if (!e.isFile()) continue;
887
- if (LANG_BY_EXT[extname(e.name).toLowerCase()]) files.push(join4(queue[head], e.name));
1103
+ if (!e.isFile() || !LANG_BY_EXT[extname(e.name).toLowerCase()]) continue;
1104
+ (isDocPath(e.name) ? docs : code).push(join4(queue[head], e.name));
888
1105
  }
889
1106
  }
890
- return { files, truncated: files.length >= MAX_FILES || visited >= MAX_ENTRIES };
1107
+ const ordered = [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
1108
+ return { files: ordered, truncated: code.length >= MAX_FILES || visited >= MAX_ENTRIES };
891
1109
  }
892
1110
  function relPath(root, abs) {
893
1111
  return relative2(root, abs).split("\\").join("/");
@@ -945,10 +1163,11 @@ function statSourceFiles(root) {
945
1163
  }
946
1164
  return out;
947
1165
  }
948
- var IGNORE_DIRS, LANG_BY_EXT, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
1166
+ var IGNORE_DIRS, CODE_LANG_BY_EXT, LANG_BY_EXT, FOREIGN_DOC_DIRS, MAX_DOC_FILES, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
949
1167
  var init_codegraph_extract = __esm({
950
1168
  "src/codegraph-extract.ts"() {
951
1169
  "use strict";
1170
+ init_codegraph_extract_docs();
952
1171
  IGNORE_DIRS = /* @__PURE__ */ new Set([
953
1172
  "node_modules",
954
1173
  ".git",
@@ -973,7 +1192,7 @@ var init_codegraph_extract = __esm({
973
1192
  ".gradle",
974
1193
  ".dart_tool"
975
1194
  ]);
976
- LANG_BY_EXT = {
1195
+ CODE_LANG_BY_EXT = {
977
1196
  ".ts": "ts",
978
1197
  ".tsx": "ts",
979
1198
  ".mts": "ts",
@@ -999,6 +1218,9 @@ var init_codegraph_extract = __esm({
999
1218
  ".hpp": "cpp",
1000
1219
  ".hh": "cpp"
1001
1220
  };
1221
+ LANG_BY_EXT = { ...CODE_LANG_BY_EXT, ...DOC_LANG_BY_EXT };
1222
+ FOREIGN_DOC_DIRS = /(?:^|\/)(?:references?|examples?|samples?|fixtures?|third[_-]party|vendor(?:ed)?)\//i;
1223
+ MAX_DOC_FILES = 1500;
1002
1224
  MAX_FILES = 8e3;
1003
1225
  MAX_FILE_BYTES = 512 * 1024;
1004
1226
  BRACE_LANGS = /* @__PURE__ */ new Set(["ts", "js", "go", "rust", "java", "csharp", "kotlin", "php", "c", "cpp"]);
@@ -1214,12 +1436,12 @@ function buildEdges(root, files, importEdges, resolveImport) {
1214
1436
  }
1215
1437
  const edges = [];
1216
1438
  const seen = /* @__PURE__ */ new Set();
1217
- const push = (from, to, rel) => {
1439
+ const push = (from, to, rel, inferred = false) => {
1218
1440
  if (edges.length >= MAX_EDGES || from === to) return;
1219
1441
  const key = `${from}|${to}|${rel}`;
1220
1442
  if (seen.has(key)) return;
1221
1443
  seen.add(key);
1222
- edges.push([from, to, rel]);
1444
+ edges.push(inferred ? [from, to, rel, 1] : [from, to, rel]);
1223
1445
  };
1224
1446
  for (const f of files) {
1225
1447
  for (const span of spansByFile.get(f.path) ?? []) push(f.path, span.id, "contains");
@@ -1248,12 +1470,14 @@ function buildEdges(root, files, importEdges, resolveImport) {
1248
1470
  const candidates = defsByName.get(name)?.filter((c) => imported?.has(c.path) && reachable(c.path));
1249
1471
  return candidates?.length === 1 ? candidates[0].id : null;
1250
1472
  };
1473
+ const extracted = (id) => id ? { id, inferred: false } : null;
1474
+ const inferred = (id) => id ? { id, inferred: true } : null;
1251
1475
  if (!f.bindings) {
1252
1476
  return (name) => {
1253
- if (local.has(name)) return local.get(name) ?? null;
1254
- if (imported?.size) return unique(name);
1477
+ if (local.has(name)) return extracted(local.get(name));
1478
+ if (imported?.size) return inferred(unique(name));
1255
1479
  const all = defsByName.get(name)?.filter((c) => reachable(c.path));
1256
- return all?.length === 1 ? all[0].id : null;
1480
+ return all?.length === 1 ? inferred(all[0].id) : null;
1257
1481
  };
1258
1482
  }
1259
1483
  const bound = new Set(f.bindings.names);
@@ -1265,10 +1489,10 @@ function buildEdges(root, files, importEdges, resolveImport) {
1265
1489
  return (name, memberOf) => {
1266
1490
  if (memberOf !== void 0) {
1267
1491
  const path = namespaces.get(memberOf);
1268
- return path ? idByPathName.get(`${path}#${name}`) ?? null : null;
1492
+ return path ? extracted(idByPathName.get(`${path}#${name}`)) : null;
1269
1493
  }
1270
- if (local.has(name)) return local.get(name) ?? null;
1271
- return bound.has(name) ? unique(name) : null;
1494
+ if (local.has(name)) return extracted(local.get(name));
1495
+ return bound.has(name) ? extracted(unique(name)) : null;
1272
1496
  };
1273
1497
  }
1274
1498
  const bodies = [];
@@ -1282,7 +1506,7 @@ function buildEdges(root, files, importEdges, resolveImport) {
1282
1506
  const spans = spansByFile.get(f.path) ?? [];
1283
1507
  const declLines = new Set(f.symbols.map((s) => s.line));
1284
1508
  const lines = content.split("\n");
1285
- indexBodies(bodies, f.path, spans, lines);
1509
+ indexBodies(bodies, f.path, spans, lines, DOC_LANGS.has(f.lang) ? MAX_DOC_BODY_TOKENS : MAX_BODY_TOKENS);
1286
1510
  const bind = binderFor(f);
1287
1511
  const strict = !!f.bindings;
1288
1512
  for (const s of f.symbols) {
@@ -1291,9 +1515,17 @@ function buildEdges(root, files, importEdges, resolveImport) {
1291
1515
  if (!self) continue;
1292
1516
  for (const [rel, name] of inheritanceTargets(s.signature)) {
1293
1517
  const target = bind(name);
1294
- if (target) push(self.id, target, rel);
1518
+ if (target) push(self.id, target.id, rel, target.inferred);
1295
1519
  }
1296
1520
  }
1521
+ if (f.lang === "markdown") {
1522
+ for (const { name, line } of markdownCodeMentions(content)) {
1523
+ if (name.length < MIN_DOC_MENTION) continue;
1524
+ const code = defsByName.get(name)?.filter((c) => !DOC_LANGS.has(langByPath.get(c.path) ?? ""));
1525
+ if (code?.length === 1) push(enclosingSymbol(spans, line) ?? f.path, code[0].id, "references", true);
1526
+ }
1527
+ }
1528
+ if (DOC_LANGS.has(f.lang) && !DOC_REFERENCE_LANGS.has(f.lang)) continue;
1297
1529
  for (let i = 0; i < lines.length; i++) {
1298
1530
  const lineNo = i + 1;
1299
1531
  if (declLines.has(lineNo)) continue;
@@ -1306,31 +1538,32 @@ function buildEdges(root, files, importEdges, resolveImport) {
1306
1538
  if (name.length < MIN_REF_NAME || !defsByName.has(name)) continue;
1307
1539
  const memberOf = strict ? /([A-Za-z_$][\w$]*)\s*\.\s*$/.exec(line.slice(0, m.index))?.[1] : void 0;
1308
1540
  const target = bind(name, memberOf);
1309
- if (!target || target === from) continue;
1541
+ if (!target || target.id === from) continue;
1310
1542
  const after = line.slice(m.index + name.length);
1311
- push(from, target, /^\s*\(/.test(after) ? "calls" : "references");
1543
+ const calls = !DOC_LANGS.has(f.lang) && /^\s*\(/.test(after);
1544
+ push(from, target.id, calls ? "calls" : "references", target.inferred);
1312
1545
  }
1313
1546
  }
1314
1547
  }
1315
1548
  return { edges, bodies };
1316
1549
  }
1317
- function indexBodies(out, path, spans, lines) {
1550
+ function indexBodies(out, path, spans, lines, limit) {
1318
1551
  const covered = new Uint8Array(lines.length + 1);
1319
1552
  for (const s of spans) {
1320
1553
  const text = lines.slice(s.line - 1, s.endLine).join("\n");
1321
1554
  for (let l = s.line; l <= Math.min(s.endLine, lines.length); l++) covered[l] = 1;
1322
- const bag = topTokens(text);
1555
+ const bag = topTokens(text, limit);
1323
1556
  if (bag.length) out.push([s.id, bag]);
1324
1557
  }
1325
1558
  const residual = [];
1326
1559
  for (let l = 1; l <= lines.length; l++) if (!covered[l]) residual.push(lines[l - 1]);
1327
- const fileBag = topTokens(residual.join("\n"));
1560
+ const fileBag = topTokens(residual.join("\n"), limit);
1328
1561
  if (fileBag.length) out.push([path, fileBag]);
1329
1562
  }
1330
- function topTokens(text) {
1563
+ function topTokens(text, limit) {
1331
1564
  const m = /* @__PURE__ */ new Map();
1332
1565
  for (const t of tokenize(text)) m.set(t, (m.get(t) ?? 0) + 1);
1333
- return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_BODY_TOKENS);
1566
+ return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit);
1334
1567
  }
1335
1568
  function isManagedPath(dir) {
1336
1569
  const managed = resolve3(join5(enigmaHome(), ".enigma"));
@@ -1548,17 +1781,18 @@ function codeGraphAvailable() {
1548
1781
  function codeGraphStatus() {
1549
1782
  return { enabled: readConfig().config.codeGraph, available: true, projects: listProjects().length };
1550
1783
  }
1551
- var GRAPH_VERSION, RESOLVE_EXTS, LANG_EXTS, LANG_INDEX, MIN_REF_NAME, MAX_EDGES, LANG_FAMILY, INDEX_NAMES, MODULE_ROOTS, MODULE_SEPARATOR, BARE_IS_PACKAGE, EXT_ORDER, INDEX_ORDER, MAX_BODY_TOKENS, ROOT_MARKERS, MAX_ROOT_WALK;
1784
+ var GRAPH_VERSION, RESOLVE_EXTS, LANG_EXTS, LANG_INDEX, MIN_REF_NAME, MIN_DOC_MENTION, MAX_EDGES, LANG_FAMILY, INDEX_NAMES, MODULE_ROOTS, MODULE_SEPARATOR, BARE_IS_PACKAGE, EXT_ORDER, INDEX_ORDER, MAX_BODY_TOKENS, MAX_DOC_BODY_TOKENS, ROOT_MARKERS, MAX_ROOT_WALK;
1552
1785
  var init_codegraph = __esm({
1553
1786
  "src/codegraph.ts"() {
1554
1787
  "use strict";
1555
1788
  init_util();
1556
1789
  init_config();
1557
1790
  init_codegraph_rank();
1791
+ init_codegraph_extract_docs();
1558
1792
  init_codegraph_extract();
1559
1793
  init_codegraph_extract();
1560
1794
  init_codegraph_types();
1561
- GRAPH_VERSION = 4;
1795
+ GRAPH_VERSION = 5;
1562
1796
  RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".kt"];
1563
1797
  LANG_EXTS = {
1564
1798
  ts: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
@@ -1574,6 +1808,7 @@ var init_codegraph = __esm({
1574
1808
  };
1575
1809
  LANG_INDEX = { python: ["__init__"], go: ["mod"], rust: ["mod"] };
1576
1810
  MIN_REF_NAME = 3;
1811
+ MIN_DOC_MENTION = 4;
1577
1812
  MAX_EDGES = 4e5;
1578
1813
  LANG_FAMILY = {
1579
1814
  ts: "js",
@@ -1590,6 +1825,7 @@ var init_codegraph = __esm({
1590
1825
  EXT_ORDER = orders(RESOLVE_EXTS, LANG_EXTS);
1591
1826
  INDEX_ORDER = orders(INDEX_NAMES, LANG_INDEX);
1592
1827
  MAX_BODY_TOKENS = 80;
1828
+ MAX_DOC_BODY_TOKENS = 24;
1593
1829
  ROOT_MARKERS = [".git", "package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml", "composer.json"];
1594
1830
  MAX_ROOT_WALK = 6;
1595
1831
  }
@@ -1822,11 +2058,11 @@ ${result.note}` : structural.fallthrough;
1822
2058
  }
1823
2059
  function adjacency(edges, direction) {
1824
2060
  const adj = /* @__PURE__ */ new Map();
1825
- for (const [source, target, rel] of edges) {
2061
+ for (const [source, target, rel, guess] of edges) {
1826
2062
  if (!WALK_RELATIONS.has(rel)) continue;
1827
2063
  const key = direction === "in" ? target : source;
1828
2064
  const other = direction === "in" ? source : target;
1829
- const entry = { other, relation: rel };
2065
+ const entry = { other, relation: rel, inferred: guess === 1 };
1830
2066
  const arr = adj.get(key);
1831
2067
  if (arr) arr.push(entry);
1832
2068
  else adj.set(key, [entry]);
@@ -1855,13 +2091,13 @@ function codeGraphTrace(symbol, opts = {}) {
1855
2091
  for (let d = 1; d <= depth && frontier.length; d++) {
1856
2092
  const next = [];
1857
2093
  for (const current of frontier) {
1858
- for (const { other, relation } of adj.get(current) ?? []) {
2094
+ for (const { other, relation, inferred } of adj.get(current) ?? []) {
1859
2095
  if (visited.has(other)) continue;
1860
2096
  visited.add(other);
1861
2097
  const node = loaded.byId.get(other);
1862
2098
  if (!node) continue;
1863
2099
  if (inPrefix !== void 0 && !underPrefix(node.path, inPrefix)) continue;
1864
- hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d });
2100
+ hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d, ...inferred ? { inferred } : {} });
1865
2101
  next.push(other);
1866
2102
  }
1867
2103
  }
@@ -1900,8 +2136,8 @@ function dirKey(path, depth) {
1900
2136
  const segments = path.split("/");
1901
2137
  return segments.slice(0, Math.min(depth, segments.length)).join("/");
1902
2138
  }
1903
- function topHubs(nodes, deg, cap) {
1904
- return nodes.map((n) => ({ name: n.name, kind: n.kind, path: n.path, line: n.line, inDegree: deg.get(n.id) ?? 0 })).filter((h) => h.inDegree > 0).sort((a, b) => b.inDegree - a.inDegree || a.name.localeCompare(b.name) || a.path.localeCompare(b.path)).slice(0, cap);
2139
+ function topHubs(nodes, deg, cap2) {
2140
+ return nodes.map((n) => ({ name: n.name, kind: n.kind, path: n.path, line: n.line, inDegree: deg.get(n.id) ?? 0 })).filter((h) => h.inDegree > 0).sort((a, b) => b.inDegree - a.inDegree || a.name.localeCompare(b.name) || a.path.localeCompare(b.path)).slice(0, cap2);
1905
2141
  }
1906
2142
  function codeGraphMap(opts = {}) {
1907
2143
  const loaded = load(opts);
@@ -2129,7 +2365,7 @@ ${r.note ?? empty}
2129
2365
  const lines = [head, ""];
2130
2366
  lines.push(`defined at: ${r.matched.map((m) => `${m.path}:${m.line}`).join(", ")}`, "");
2131
2367
  for (const h of r.hits) {
2132
- lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
2368
+ lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${h.inferred ? " (inferred)" : ""}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
2133
2369
  }
2134
2370
  lines.push("", `${r.hits.length} node${r.hits.length === 1 ? "" : "s"}.`);
2135
2371
  return `${lines.join("\n")}
@@ -2252,6 +2488,295 @@ var init_codegraph_format = __esm({
2252
2488
  }
2253
2489
  });
2254
2490
 
2491
+ // src/codegraph-cluster.ts
2492
+ function localMoves(level, totalWeight) {
2493
+ const { nodes, adj, selfLoops } = level;
2494
+ const degree = new Array(nodes).fill(0);
2495
+ for (let i = 0; i < nodes; i++) {
2496
+ let d = selfLoops[i] * 2;
2497
+ for (const w of adj[i].values()) d += w;
2498
+ degree[i] = d;
2499
+ }
2500
+ const comm = Array.from({ length: nodes }, (_, i) => i);
2501
+ const commDegree = degree.slice();
2502
+ const m2 = totalWeight * 2;
2503
+ for (let sweep = 0; sweep < MAX_SWEEPS; sweep++) {
2504
+ let moved = false;
2505
+ for (let i = 0; i < nodes; i++) {
2506
+ const current = comm[i];
2507
+ const toComm = /* @__PURE__ */ new Map();
2508
+ for (const [j, w] of adj[i]) toComm.set(comm[j], (toComm.get(comm[j]) ?? 0) + w);
2509
+ commDegree[current] -= degree[i];
2510
+ let best = current;
2511
+ let bestGain = (toComm.get(current) ?? 0) - commDegree[current] * degree[i] / m2;
2512
+ for (const [c, w] of [...toComm.entries()].sort((a, b) => a[0] - b[0])) {
2513
+ const gain = w - commDegree[c] * degree[i] / m2;
2514
+ if (gain > bestGain + 1e-12) {
2515
+ best = c;
2516
+ bestGain = gain;
2517
+ }
2518
+ }
2519
+ commDegree[best] += degree[i];
2520
+ if (best !== current) {
2521
+ comm[i] = best;
2522
+ moved = true;
2523
+ }
2524
+ }
2525
+ if (!moved) break;
2526
+ }
2527
+ return comm;
2528
+ }
2529
+ function clusterFiles(files, links) {
2530
+ const sorted = [...new Set(files)].sort();
2531
+ const index = new Map(sorted.map((f, i) => [f, i]));
2532
+ let level = { nodes: sorted.length, adj: sorted.map(() => /* @__PURE__ */ new Map()), selfLoops: new Array(sorted.length).fill(0) };
2533
+ let totalWeight = 0;
2534
+ for (const [a, b, w] of links) {
2535
+ const i = index.get(a);
2536
+ const j = index.get(b);
2537
+ if (i === void 0 || j === void 0 || w <= 0) continue;
2538
+ totalWeight += w;
2539
+ if (i === j) {
2540
+ level.selfLoops[i] += w;
2541
+ continue;
2542
+ }
2543
+ level.adj[i].set(j, (level.adj[i].get(j) ?? 0) + w);
2544
+ level.adj[j].set(i, (level.adj[j].get(i) ?? 0) + w);
2545
+ }
2546
+ let membership = sorted.map((_, i) => i);
2547
+ if (totalWeight === 0) return new Map(sorted.map((f, i) => [f, i]));
2548
+ for (let l = 0; l < MAX_LEVELS; l++) {
2549
+ const comm = localMoves(level, totalWeight);
2550
+ const ids = /* @__PURE__ */ new Map();
2551
+ for (const c of comm) if (!ids.has(c)) ids.set(c, ids.size);
2552
+ if (ids.size === level.nodes) break;
2553
+ membership = membership.map((c) => ids.get(comm[c]));
2554
+ const next = { nodes: ids.size, adj: Array.from({ length: ids.size }, () => /* @__PURE__ */ new Map()), selfLoops: new Array(ids.size).fill(0) };
2555
+ for (let i = 0; i < level.nodes; i++) {
2556
+ const ci = ids.get(comm[i]);
2557
+ next.selfLoops[ci] += level.selfLoops[i];
2558
+ for (const [j, w] of level.adj[i]) {
2559
+ const cj = ids.get(comm[j]);
2560
+ if (ci === cj) {
2561
+ if (i < j) next.selfLoops[ci] += w;
2562
+ } else next.adj[ci].set(cj, (next.adj[ci].get(cj) ?? 0) + w);
2563
+ }
2564
+ }
2565
+ level = next;
2566
+ }
2567
+ const members = /* @__PURE__ */ new Map();
2568
+ sorted.forEach((f, i) => {
2569
+ const c = membership[i];
2570
+ const list = members.get(c);
2571
+ if (list) list.push(f);
2572
+ else members.set(c, [f]);
2573
+ });
2574
+ const order = [...members.entries()].sort((a, b) => b[1].length - a[1].length || a[1][0].localeCompare(b[1][0]));
2575
+ const out = /* @__PURE__ */ new Map();
2576
+ order.forEach(([, list], rank) => {
2577
+ for (const f of list) out.set(f, rank);
2578
+ });
2579
+ return out;
2580
+ }
2581
+ var MAX_SWEEPS, MAX_LEVELS;
2582
+ var init_codegraph_cluster = __esm({
2583
+ "src/codegraph-cluster.ts"() {
2584
+ "use strict";
2585
+ MAX_SWEEPS = 20;
2586
+ MAX_LEVELS = 6;
2587
+ }
2588
+ });
2589
+
2590
+ // src/codegraph-report.ts
2591
+ var codegraph_report_exports = {};
2592
+ __export(codegraph_report_exports, {
2593
+ codeGraphReport: () => codeGraphReport,
2594
+ formatReport: () => formatReport
2595
+ });
2596
+ function areaLabel(paths) {
2597
+ const dirs = paths.map((p) => p.split("/").slice(0, -1));
2598
+ let label = [];
2599
+ for (let depth = 1; ; depth++) {
2600
+ const counts2 = /* @__PURE__ */ new Map();
2601
+ for (const d of dirs) if (d.length >= depth) {
2602
+ const k = d.slice(0, depth).join("/");
2603
+ counts2.set(k, (counts2.get(k) ?? 0) + 1);
2604
+ }
2605
+ const top2 = [...counts2.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
2606
+ if (!top2 || top2[1] / paths.length < LABEL_SHARE) break;
2607
+ label = top2[0].split("/");
2608
+ }
2609
+ if (label.length) return `${label.join("/")}/`;
2610
+ if (paths.length === 1) return paths[0];
2611
+ const first = /* @__PURE__ */ new Map();
2612
+ for (const d of dirs) {
2613
+ const k = d[0] ?? "(root)";
2614
+ first.set(k, (first.get(k) ?? 0) + 1);
2615
+ }
2616
+ const [top] = [...first.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
2617
+ return `${top}/ (mixed)`;
2618
+ }
2619
+ function codeGraphReport(opts = {}) {
2620
+ const graph = loadFreshGraph(opts.project, opts.refresh ?? true);
2621
+ if (!graph) return null;
2622
+ const nodes = graphNodes(graph);
2623
+ const byId = new Map(nodes.map((n) => [n.id, n]));
2624
+ const langByPath = new Map(graph.files.map((f) => [f.path, f.lang]));
2625
+ const own = graph.files.filter((f) => !isForeignPath(f.path));
2626
+ const codePaths = new Set(own.filter((f) => isCodeLang(f.lang)).map((f) => f.path));
2627
+ const docPaths = new Set(own.filter((f) => f.lang === "markdown").map((f) => f.path));
2628
+ const weights = /* @__PURE__ */ new Map();
2629
+ const directed = /* @__PURE__ */ new Map();
2630
+ const docRefs = /* @__PURE__ */ new Map();
2631
+ for (const e of graph.edges) {
2632
+ if (e[2] === "contains") continue;
2633
+ const a = byId.get(e[0])?.path;
2634
+ const b = byId.get(e[1])?.path;
2635
+ if (!a || !b || a === b) continue;
2636
+ if (docPaths.has(a) && codePaths.has(b)) {
2637
+ const refs = docRefs.get(a) ?? /* @__PURE__ */ new Map();
2638
+ refs.set(b, (refs.get(b) ?? 0) + 1);
2639
+ docRefs.set(a, refs);
2640
+ continue;
2641
+ }
2642
+ if (!codePaths.has(a) || !codePaths.has(b)) continue;
2643
+ const key = a < b ? `${a}
2644
+ ${b}` : `${b}
2645
+ ${a}`;
2646
+ weights.set(key, (weights.get(key) ?? 0) + 1);
2647
+ directed.set(`${a}
2648
+ ${b}`, (directed.get(`${a}
2649
+ ${b}`) ?? 0) + 1);
2650
+ }
2651
+ const links = [...weights.entries()].map(([k, w]) => {
2652
+ const [a, b] = k.split("\n");
2653
+ return [a, b, w];
2654
+ });
2655
+ const areaOf = clusterFiles([...codePaths], links);
2656
+ const members = /* @__PURE__ */ new Map();
2657
+ for (const [path, area] of areaOf) {
2658
+ const list = members.get(area);
2659
+ if (list) list.push(path);
2660
+ else members.set(area, [path]);
2661
+ }
2662
+ const deg = inDegree(graph.edges, true);
2663
+ const maxAreas = opts.maxAreas ?? DEFAULT_MAX_AREAS;
2664
+ const ranked = [...members.entries()].filter(([, paths]) => paths.length >= 2).sort((a, b) => b[1].length - a[1].length || a[0] - b[0]);
2665
+ const symbolsOf = /* @__PURE__ */ new Map();
2666
+ for (const n of nodes) {
2667
+ if (n.kind === "file" || isTestPath(n.path)) continue;
2668
+ const area = areaOf.get(n.path);
2669
+ if (area === void 0) continue;
2670
+ const list = symbolsOf.get(area);
2671
+ if (list) list.push(n);
2672
+ else symbolsOf.set(area, [n]);
2673
+ }
2674
+ const keys = /* @__PURE__ */ new Map();
2675
+ const keyOf = (area) => {
2676
+ let key = keys.get(area);
2677
+ if (key) return key;
2678
+ key = (symbolsOf.get(area) ?? []).map((n) => ({ name: n.name, kind: n.kind, path: n.path, line: n.line, inDegree: deg.get(n.id) ?? 0 })).filter((sym) => sym.inDegree > 0).sort((a, b) => b.inDegree - a.inDegree || a.name.localeCompare(b.name)).slice(0, KEY_SYMBOLS);
2679
+ keys.set(area, key);
2680
+ return key;
2681
+ };
2682
+ const base = new Map(ranked.map(([area, paths]) => [area, areaLabel(paths)]));
2683
+ const seenLabel = /* @__PURE__ */ new Map();
2684
+ for (const label of base.values()) seenLabel.set(label, (seenLabel.get(label) ?? 0) + 1);
2685
+ const labels = new Map([...base.entries()].map(([area, label]) => {
2686
+ if ((seenLabel.get(label) ?? 0) < 2) return [area, label];
2687
+ const around = keyOf(area)[0]?.name;
2688
+ return [area, around ? `${label} ~ ${around}` : label];
2689
+ }));
2690
+ const docsOf = /* @__PURE__ */ new Map();
2691
+ for (const [doc, refs] of docRefs) {
2692
+ const byArea = /* @__PURE__ */ new Map();
2693
+ for (const [code, w] of refs) {
2694
+ const area = areaOf.get(code);
2695
+ if (area !== void 0) byArea.set(area, (byArea.get(area) ?? 0) + w);
2696
+ }
2697
+ const best = [...byArea.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0];
2698
+ if (!best) continue;
2699
+ const list = docsOf.get(best[0]) ?? [];
2700
+ list.push(doc);
2701
+ docsOf.set(best[0], list);
2702
+ }
2703
+ const areas = ranked.slice(0, maxAreas).map(([area, paths]) => {
2704
+ const inArea = new Set(paths);
2705
+ const out = /* @__PURE__ */ new Map();
2706
+ for (const [key, w] of directed) {
2707
+ const [a, b] = key.split("\n");
2708
+ if (!inArea.has(a) || inArea.has(b)) continue;
2709
+ const target = areaOf.get(b);
2710
+ if (target !== void 0 && target !== area && labels.has(target)) out.set(target, (out.get(target) ?? 0) + w);
2711
+ }
2712
+ return {
2713
+ label: labels.get(area),
2714
+ files: paths.length,
2715
+ languages: [...new Set(paths.map((p) => langByPath.get(p)).filter((l) => !!l))].sort(),
2716
+ keySymbols: keyOf(area),
2717
+ docs: (docsOf.get(area) ?? []).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, AREA_DOCS),
2718
+ dependsOn: [...out.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, DEPENDS_ON).map(([t]) => labels.get(t))
2719
+ };
2720
+ });
2721
+ const titleOf = /* @__PURE__ */ new Map();
2722
+ for (const n of nodes) if (n.kind === "section" && !titleOf.has(n.path)) titleOf.set(n.path, n.name);
2723
+ const docFiles = own.filter((f) => f.lang === "markdown").sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
2724
+ const maxDocs = opts.maxDocs ?? DEFAULT_MAX_DOCS;
2725
+ return {
2726
+ name: graph.name,
2727
+ totals: {
2728
+ files: graph.files.length,
2729
+ symbols: nodes.length - graph.files.length,
2730
+ edges: graph.edges.length,
2731
+ inferred: graph.edges.filter((e) => e[3] === 1).length,
2732
+ docs: docFiles.length
2733
+ },
2734
+ areas,
2735
+ moreAreas: Math.max(0, ranked.length - areas.length),
2736
+ docs: docFiles.slice(0, maxDocs).map((f) => ({ path: f.path, title: titleOf.get(f.path) ?? "" })),
2737
+ moreDocs: Math.max(0, docFiles.length - maxDocs),
2738
+ foreign: graph.files.length - own.length,
2739
+ truncated: graph.truncated
2740
+ };
2741
+ }
2742
+ function formatReport(r) {
2743
+ const t = r.totals;
2744
+ const lines = [`repo report - ${r.name} - ${t.files} files (${t.docs} docs) - ${t.symbols} symbols - ${t.edges} edges (${t.inferred} inferred by name)`];
2745
+ if (r.foreign) lines.push(`${r.foreign} file(s) of vendored, reference or example material are left out of the areas below.`);
2746
+ lines.push("", "areas (files that depend on each other, largest first):");
2747
+ for (const a of r.areas) {
2748
+ lines.push(`- ${a.label} - ${a.files} files${a.languages.length ? ` - ${a.languages.join(", ")}` : ""}`);
2749
+ if (a.keySymbols.length) lines.push(` key: ${a.keySymbols.map((s) => `${s.name} (${s.path}:${s.line}, ${s.inDegree} in)`).join(", ")}`);
2750
+ if (a.dependsOn.length) lines.push(` uses: ${a.dependsOn.join(", ")}`);
2751
+ if (a.docs.length) lines.push(` docs: ${a.docs.join(", ")}`);
2752
+ }
2753
+ if (r.moreAreas) lines.push(` +${r.moreAreas} smaller area(s)`);
2754
+ if (r.docs.length) {
2755
+ lines.push("", "docs:");
2756
+ for (const d of r.docs) lines.push(`- ${d.path}${d.title ? ` - ${d.title}` : ""}`);
2757
+ if (r.moreDocs) lines.push(` +${r.moreDocs} more (enigma_codegraph_ask finds them by topic)`);
2758
+ }
2759
+ if (r.truncated) lines.push("", "The index hit its size cap; some files are not covered.");
2760
+ return `${lines.join("\n")}
2761
+ `;
2762
+ }
2763
+ var DEFAULT_MAX_AREAS, DEFAULT_MAX_DOCS, KEY_SYMBOLS, AREA_DOCS, DEPENDS_ON, LABEL_SHARE;
2764
+ var init_codegraph_report = __esm({
2765
+ "src/codegraph-report.ts"() {
2766
+ "use strict";
2767
+ init_codegraph();
2768
+ init_codegraph_rank();
2769
+ init_codegraph_extract();
2770
+ init_codegraph_cluster();
2771
+ DEFAULT_MAX_AREAS = 8;
2772
+ DEFAULT_MAX_DOCS = 12;
2773
+ KEY_SYMBOLS = 3;
2774
+ AREA_DOCS = 2;
2775
+ DEPENDS_ON = 2;
2776
+ LABEL_SHARE = 0.6;
2777
+ }
2778
+ });
2779
+
2255
2780
  // src/codegraph-hook.ts
2256
2781
  import { homedir as homedir3 } from "os";
2257
2782
 
@@ -2388,14 +2913,17 @@ async function sessionStart(dir) {
2388
2913
  " - enigma_codegraph_skeleton: a file's whole API surface, no bodies.",
2389
2914
  " - enigma_codegraph_grep: every occurrence, grouped by enclosing symbol - when you need them ALL, not the top matches."
2390
2915
  ].join("\n");
2391
- const map = q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
2916
+ const rep = await Promise.resolve().then(() => (init_codegraph_report(), codegraph_report_exports));
2917
+ const report = rep.codeGraphReport({ project, refresh: false });
2918
+ const map = report ? null : q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
2919
+ const body = report ? rep.formatReport(report) : map ? fmt.formatMap(map) : "";
2392
2920
  const fresh = q.codeGraphCheck(project);
2393
2921
  const stale = fresh && fresh.stale > 0 ? `
2394
2922
  ${fresh.stale} file(s) changed since the last index; the next query refreshes them.` : "";
2395
- writeStatuslineSnapshot(projectRoot(cg, project, dir), map?.totals.symbols ?? 0, fresh?.stale ?? 0);
2396
- emit("SessionStart", map ? `${directive}
2923
+ writeStatuslineSnapshot(projectRoot(cg, project, dir), report?.totals.symbols ?? map?.totals.symbols ?? 0, fresh?.stale ?? 0);
2924
+ emit("SessionStart", body ? `${directive}
2397
2925
 
2398
- ${fmt.formatMap(map).trimEnd()}${stale}` : directive);
2926
+ ${body.trimEnd()}${stale}` : directive);
2399
2927
  }
2400
2928
  function coveringProject(cg, dir) {
2401
2929
  const project = cg.findProjectForCwd(dir);