enigma-cli 1.59.1 → 1.61.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 +5 -0
  28. package/dist/codegraph-hook.js +588 -50
  29. package/dist/post-edit.js +588 -50
  30. package/package.json +1 -1
@@ -94,6 +94,7 @@ var init_secret_box = __esm({
94
94
  // src/config.ts
95
95
  var config_exports = {};
96
96
  __export(config_exports, {
97
+ API_ROTATIONS: () => API_ROTATIONS,
97
98
  CONFIG_CHOICES: () => CONFIG_CHOICES,
98
99
  CONFIG_DEFAULTS: () => CONFIG_DEFAULTS,
99
100
  CONFIG_FILE: () => CONFIG_FILE,
@@ -106,6 +107,7 @@ __export(config_exports, {
106
107
  PROMPT_SECRET_MODES: () => PROMPT_SECRET_MODES,
107
108
  RECALL_PROVIDERS: () => RECALL_PROVIDERS,
108
109
  SKILL_UPDATE_POLICIES: () => SKILL_UPDATE_POLICIES,
110
+ apiRotationOf: () => apiRotationOf,
109
111
  getRecallApiKey: () => getRecallApiKey,
110
112
  readConfig: () => readConfig,
111
113
  readConfigAt: () => readConfigAt,
@@ -126,6 +128,9 @@ __export(config_exports, {
126
128
  });
127
129
  import { join as join3 } from "path";
128
130
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
131
+ function apiRotationOf(value) {
132
+ return API_ROTATIONS.includes(value) ? value : "off";
133
+ }
129
134
  function configPath(scope) {
130
135
  if (scope === "local") return join3(process.cwd(), CONFIG_FILE);
131
136
  return join3(enigmaHome(), CONFIG_FILE);
@@ -240,7 +245,7 @@ function setSkillAgentOff(skill, agent, off) {
240
245
  if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
241
246
  writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
242
247
  }
243
- var CONFIG_FILE, OUTPUT_STYLES, MINIMAL_CODE_LEVELS, DASHBOARD_MODES, DASHBOARD_BINDS, PROMPT_SECRET_MODES, SKILL_UPDATE_POLICIES, LOGO_COLOR_POLICIES, RECALL_PROVIDERS, GATE_SEVERITIES, CONFIG_DEFAULTS, CONFIG_CHOICES;
248
+ var CONFIG_FILE, OUTPUT_STYLES, MINIMAL_CODE_LEVELS, API_ROTATIONS, DASHBOARD_MODES, DASHBOARD_BINDS, PROMPT_SECRET_MODES, SKILL_UPDATE_POLICIES, LOGO_COLOR_POLICIES, RECALL_PROVIDERS, GATE_SEVERITIES, CONFIG_DEFAULTS, CONFIG_CHOICES;
244
249
  var init_config = __esm({
245
250
  "src/config.ts"() {
246
251
  "use strict";
@@ -249,6 +254,7 @@ var init_config = __esm({
249
254
  CONFIG_FILE = ".enigma.json";
250
255
  OUTPUT_STYLES = ["off", "lite", "full", "ultra"];
251
256
  MINIMAL_CODE_LEVELS = ["off", "lite", "full", "ultra"];
257
+ API_ROTATIONS = ["off", "round-robin", "least-used", "fill-first", "random"];
252
258
  DASHBOARD_MODES = ["off", "on-demand", "always"];
253
259
  DASHBOARD_BINDS = ["loopback", "lan", "custom"];
254
260
  PROMPT_SECRET_MODES = ["redact", "reject"];
@@ -304,6 +310,8 @@ var init_config = __esm({
304
310
  gateTrivialLines: 20,
305
311
  subagentLimit: 4,
306
312
  compactWindow: 4e5,
313
+ relay: true,
314
+ relayAt: 3e5,
307
315
  planSessionLimit: 0,
308
316
  planWeeklyLimit: 0,
309
317
  planWeeklySonnetLimit: 0,
@@ -317,6 +325,9 @@ var init_config = __esm({
317
325
  apiAccount: "",
318
326
  apiProfile: "",
319
327
  apiPack: "",
328
+ apiRotation: "off",
329
+ apiAccountPool: [],
330
+ apiClientContext: true,
320
331
  toolPaths: {},
321
332
  bypassDisabled: [],
322
333
  discardedSkills: [],
@@ -337,7 +348,8 @@ var init_config = __esm({
337
348
  skillUpdatePolicy: SKILL_UPDATE_POLICIES,
338
349
  logoColorPolicy: LOGO_COLOR_POLICIES,
339
350
  recallProvider: RECALL_PROVIDERS,
340
- gateSeverity: GATE_SEVERITIES
351
+ gateSeverity: GATE_SEVERITIES,
352
+ apiRotation: API_ROTATIONS
341
353
  };
342
354
  }
343
355
  });
@@ -545,10 +557,222 @@ var init_codegraph_rank = __esm({
545
557
  }
546
558
  });
547
559
 
560
+ // src/codegraph-extract-docs.ts
561
+ function cap(text) {
562
+ const t = text.trim();
563
+ return t.length > MAX_SIGNATURE ? `${t.slice(0, MAX_SIGNATURE)}...` : t;
564
+ }
565
+ function indentOf(line) {
566
+ let n = 0;
567
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
568
+ return n;
569
+ }
570
+ function fencedLines(lines) {
571
+ const inside = new Uint8Array(lines.length);
572
+ let fence = null;
573
+ for (let i = 0; i < lines.length; i++) {
574
+ const m = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
575
+ if (fence) {
576
+ inside[i] = 1;
577
+ if (m && m[1][0] === fence[0] && m[1].length >= fence.length) fence = null;
578
+ continue;
579
+ }
580
+ if (m) {
581
+ fence = m[1];
582
+ inside[i] = 1;
583
+ }
584
+ }
585
+ return inside;
586
+ }
587
+ function markdownSymbols(lines) {
588
+ const fenced = fencedLines(lines);
589
+ const heads = [];
590
+ for (let i = 0; i < lines.length; i++) {
591
+ if (fenced[i]) continue;
592
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i]);
593
+ if (m && m[1].length <= MAX_HEADING_LEVEL) heads.push({ level: m[1].length, name: m[2].replace(/[`*_]/g, "").trim(), line: i + 1 });
594
+ }
595
+ return heads.filter((h) => h.name).map((h, idx) => {
596
+ const next = heads.slice(idx + 1).find((o) => o.level <= h.level);
597
+ return { name: h.name, kind: "section", line: h.line, endLine: next ? next.line - 1 : lines.length, signature: cap(lines[h.line - 1]) };
598
+ });
599
+ }
600
+ function sqlEnd(lines, start) {
601
+ let dollar = false;
602
+ for (let i = start; i < lines.length; i++) {
603
+ const parts = lines[i].replace(/--.*$/, "").split("$$");
604
+ for (let p = 0; p < parts.length; p++) {
605
+ if (p > 0) dollar = !dollar;
606
+ if (!dollar && parts[p].includes(";")) return i + 1;
607
+ }
608
+ }
609
+ return lines.length;
610
+ }
611
+ function sqlSymbols(lines) {
612
+ const rules = [
613
+ ["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")],
614
+ ["function", new RegExp(String.raw`^\s*create\s+(?:or\s+replace\s+)?(?:function|procedure)\s+${SQL_NAME}`, "i")],
615
+ ["type", new RegExp(String.raw`^\s*create\s+type\s+${SQL_NAME}`, "i")]
616
+ ];
617
+ const out = [];
618
+ for (let i = 0; i < lines.length; i++) {
619
+ for (const [kind, re] of rules) {
620
+ const m = re.exec(lines[i]);
621
+ if (!m) continue;
622
+ out.push({ name: m[1], kind, line: i + 1, endLine: sqlEnd(lines, i), signature: cap(lines[i].replace(/\(.*$/, "")) });
623
+ break;
624
+ }
625
+ }
626
+ return out;
627
+ }
628
+ function prismaSymbols(lines) {
629
+ const out = [];
630
+ for (let i = 0; i < lines.length; i++) {
631
+ const m = /^\s*(model|view|enum|type)\s+([A-Za-z_]\w*)\s*\{/.exec(lines[i]);
632
+ if (!m) continue;
633
+ let end = i;
634
+ while (end < lines.length - 1 && !/^\s*\}/.test(lines[end])) end++;
635
+ const kind = m[1] === "enum" ? "enum" : m[1] === "type" ? "type" : "table";
636
+ out.push({ name: m[2], kind, line: i + 1, endLine: end + 1, signature: cap(`${m[1]} ${m[2]}`) });
637
+ }
638
+ return out;
639
+ }
640
+ function yamlEnd(lines, start) {
641
+ const base = indentOf(lines[start]);
642
+ let end = start + 1;
643
+ for (let i = start + 1; i < lines.length; i++) {
644
+ const line = lines[i];
645
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
646
+ if (indentOf(line) <= base && !line.trimStart().startsWith("- ")) break;
647
+ end = i + 1;
648
+ }
649
+ return end;
650
+ }
651
+ function yamlSymbols(lines) {
652
+ const out = [];
653
+ const KEY = /^(\s*)([A-Za-z0-9_][\w.-]*)\s*:(?:\s|$)/;
654
+ for (let i = 0; i < lines.length; i++) {
655
+ const m = KEY.exec(lines[i]);
656
+ if (!m || m[1].length) continue;
657
+ const end = yamlEnd(lines, i);
658
+ out.push({ name: m[2], kind: "key", line: i + 1, endLine: end, signature: cap(lines[i]) });
659
+ if (!YAML_CONTAINERS.has(m[2])) continue;
660
+ let childIndent = -1;
661
+ for (let j = i + 1; j < end; j++) {
662
+ const c = KEY.exec(lines[j]);
663
+ if (!c || !c[1].length) continue;
664
+ if (childIndent === -1) childIndent = c[1].length;
665
+ if (c[1].length !== childIndent) continue;
666
+ out.push({ name: c[2], kind: "key", line: j + 1, endLine: yamlEnd(lines, j), signature: cap(`${m[2]}.${c[2]}`) });
667
+ }
668
+ }
669
+ return out;
670
+ }
671
+ function tomlSymbols(lines) {
672
+ const heads = [];
673
+ for (let i = 0; i < lines.length; i++) {
674
+ const m = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/.exec(lines[i]);
675
+ if (m) heads.push({ name: m[1], line: i + 1 });
676
+ }
677
+ return heads.map((h, idx) => ({
678
+ name: h.name,
679
+ kind: "key",
680
+ line: h.line,
681
+ endLine: idx + 1 < heads.length ? heads[idx + 1].line - 1 : lines.length,
682
+ signature: cap(lines[h.line - 1])
683
+ }));
684
+ }
685
+ function markdownLinks(lines) {
686
+ const fenced = fencedLines(lines);
687
+ const out = /* @__PURE__ */ new Set();
688
+ for (let i = 0; i < lines.length; i++) {
689
+ if (fenced[i]) continue;
690
+ const re = /\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
691
+ let m;
692
+ while (m = re.exec(lines[i])) {
693
+ const target = m[1].replace(/[#?].*$/, "");
694
+ if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue;
695
+ try {
696
+ out.add(decodeURI(target));
697
+ } catch {
698
+ out.add(target);
699
+ }
700
+ }
701
+ }
702
+ return [...out];
703
+ }
704
+ function markdownCodeMentions(content) {
705
+ const lines = content.split("\n");
706
+ const fenced = fencedLines(lines);
707
+ const out = [];
708
+ for (let i = 0; i < lines.length; i++) {
709
+ if (fenced[i]) continue;
710
+ const re = /`([A-Za-z_$][\w$]*)(?:\(\))?`/g;
711
+ let m;
712
+ while (m = re.exec(lines[i])) out.push({ name: m[1], line: i + 1 });
713
+ }
714
+ return out;
715
+ }
716
+ function extractDocFile(lang, content) {
717
+ const lines = content.split("\n");
718
+ const out = (() => {
719
+ switch (lang) {
720
+ case "markdown":
721
+ return { symbols: markdownSymbols(lines), imports: markdownLinks(lines) };
722
+ case "sql":
723
+ return { symbols: sqlSymbols(lines), imports: [] };
724
+ case "prisma":
725
+ return { symbols: prismaSymbols(lines), imports: [] };
726
+ case "yaml":
727
+ return { symbols: yamlSymbols(lines), imports: [] };
728
+ case "toml":
729
+ return { symbols: tomlSymbols(lines), imports: [] };
730
+ default:
731
+ return { symbols: [], imports: [] };
732
+ }
733
+ })();
734
+ for (const s of out.symbols) while (s.endLine > s.line && !(lines[s.endLine - 1] ?? "").trim()) s.endLine--;
735
+ return out;
736
+ }
737
+ var DOC_LANG_BY_EXT, DOC_LANGS, DOC_REFERENCE_LANGS, YAML_CONTAINERS, MAX_SIGNATURE, MAX_HEADING_LEVEL, SQL_NAME;
738
+ var init_codegraph_extract_docs = __esm({
739
+ "src/codegraph-extract-docs.ts"() {
740
+ "use strict";
741
+ DOC_LANG_BY_EXT = {
742
+ ".md": "markdown",
743
+ ".mdx": "markdown",
744
+ ".sql": "sql",
745
+ ".prisma": "prisma",
746
+ ".yaml": "yaml",
747
+ ".yml": "yaml",
748
+ ".toml": "toml"
749
+ };
750
+ DOC_LANGS = new Set(Object.values(DOC_LANG_BY_EXT));
751
+ DOC_REFERENCE_LANGS = /* @__PURE__ */ new Set(["sql", "prisma"]);
752
+ YAML_CONTAINERS = /* @__PURE__ */ new Set(["services", "jobs", "workflows", "volumes", "networks"]);
753
+ MAX_SIGNATURE = 200;
754
+ MAX_HEADING_LEVEL = 3;
755
+ SQL_NAME = String.raw`(?:[\w"\x60\[\]]+\.)?["\x60\[]?([A-Za-z_][\w$]*)["\x60\]]?`;
756
+ }
757
+ });
758
+
548
759
  // src/codegraph-extract.ts
549
760
  import { execFileSync as execFileSync2 } from "child_process";
550
761
  import { extname, join as join4, relative as relative2, resolve as resolve2 } from "path";
551
762
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
763
+ function isCodeLang(lang) {
764
+ return !DOC_LANGS.has(lang);
765
+ }
766
+ function isDocPath(path) {
767
+ return !!DOC_LANG_BY_EXT[extname(path).toLowerCase()];
768
+ }
769
+ function isForeignPath(rel) {
770
+ return FOREIGN_DOC_DIRS.test(rel);
771
+ }
772
+ function pickDocs(root, docs, room) {
773
+ const depth = (p) => relPath(root, p).split("/").length;
774
+ 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)));
775
+ }
552
776
  function symRules(lang) {
553
777
  switch (lang) {
554
778
  case "ts":
@@ -713,7 +937,7 @@ function lineResolver(content) {
713
937
  return lo + 1;
714
938
  };
715
939
  }
716
- function indentOf(line) {
940
+ function indentOf2(line) {
717
941
  let n = 0;
718
942
  while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
719
943
  return n;
@@ -779,18 +1003,18 @@ function braceEnd(lines, startIdx, lang) {
779
1003
  return lines.length;
780
1004
  }
781
1005
  function indentEnd(lines, startIdx) {
782
- const base = indentOf(lines[startIdx]);
1006
+ const base = indentOf2(lines[startIdx]);
783
1007
  let end = startIdx + 1;
784
1008
  for (let i = startIdx + 1; i < lines.length; i++) {
785
1009
  const line = lines[i];
786
1010
  if (!line.trim()) continue;
787
- if (indentOf(line) <= base) break;
1011
+ if (indentOf2(line) <= base) break;
788
1012
  end = i + 1;
789
1013
  }
790
1014
  return end;
791
1015
  }
792
1016
  function clampToSiblings(symbols, lines) {
793
- const indentAt = (line) => indentOf(lines[line - 1] ?? "");
1017
+ const indentAt = (line) => indentOf2(lines[line - 1] ?? "");
794
1018
  for (let i = 0; i < symbols.length; i++) {
795
1019
  const s = symbols[i];
796
1020
  for (let j = i + 1; j < symbols.length; j++) {
@@ -803,14 +1027,15 @@ function clampToSiblings(symbols, lines) {
803
1027
  }
804
1028
  }
805
1029
  function signatureOf(line) {
806
- const MAX_SIGNATURE = 200;
1030
+ const MAX_SIGNATURE2 = 200;
807
1031
  let sig = line.trim();
808
1032
  const brace = sig.indexOf("{");
809
1033
  if (brace > 0) sig = sig.slice(0, brace).trim();
810
1034
  if (sig.endsWith(":")) sig = sig.slice(0, -1).trim();
811
- return sig.length > MAX_SIGNATURE ? `${sig.slice(0, MAX_SIGNATURE)}...` : sig;
1035
+ return sig.length > MAX_SIGNATURE2 ? `${sig.slice(0, MAX_SIGNATURE2)}...` : sig;
812
1036
  }
813
1037
  function extractFile(lang, content) {
1038
+ if (DOC_LANGS.has(lang)) return extractDocFile(lang, content);
814
1039
  const lineAt = lineResolver(content);
815
1040
  const lines = content.split("\n");
816
1041
  const braced = BRACE_LANGS.has(lang);
@@ -852,25 +1077,27 @@ function gitSourceFiles(root) {
852
1077
  const out = gitList(root, ["--cached", "--others", "--exclude-standard"]);
853
1078
  if (out === null) return null;
854
1079
  const nested = existsSync4(join4(root, ".gitmodules")) ? gitList(root, ["--recurse-submodules"]) : null;
855
- const files = [];
1080
+ const code = [];
1081
+ const docs = [];
856
1082
  const seen = /* @__PURE__ */ new Set();
857
1083
  for (const rel of [...out.split("\0"), ...(nested ?? "").split("\0")]) {
858
- if (files.length >= MAX_FILES) break;
1084
+ if (code.length >= MAX_FILES) break;
859
1085
  if (!rel || seen.has(rel)) continue;
860
1086
  seen.add(rel);
861
1087
  if (!LANG_BY_EXT[extname(rel).toLowerCase()]) continue;
862
1088
  if (rel.split("/").some((seg) => seg.startsWith(".") || IGNORE_DIRS.has(seg))) continue;
863
- files.push(join4(root, rel));
1089
+ (isDocPath(rel) ? docs : code).push(join4(root, rel));
864
1090
  }
865
- return files;
1091
+ return [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
866
1092
  }
867
1093
  function walk(root) {
868
1094
  const tracked = gitSourceFiles(root);
869
1095
  if (tracked && tracked.length) return { files: tracked, truncated: tracked.length >= MAX_FILES };
870
- const files = [];
1096
+ const code = [];
1097
+ const docs = [];
871
1098
  const queue = [root];
872
1099
  let visited = 0;
873
- for (let head = 0; head < queue.length && files.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
1100
+ for (let head = 0; head < queue.length && code.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
874
1101
  let entries;
875
1102
  try {
876
1103
  entries = readdirSync2(queue[head], { withFileTypes: true });
@@ -878,16 +1105,17 @@ function walk(root) {
878
1105
  continue;
879
1106
  }
880
1107
  for (const e of entries) {
881
- if (files.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
1108
+ if (code.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
882
1109
  if (e.isDirectory()) {
883
1110
  if (!e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) queue.push(join4(queue[head], e.name));
884
1111
  continue;
885
1112
  }
886
- if (!e.isFile()) continue;
887
- if (LANG_BY_EXT[extname(e.name).toLowerCase()]) files.push(join4(queue[head], e.name));
1113
+ if (!e.isFile() || !LANG_BY_EXT[extname(e.name).toLowerCase()]) continue;
1114
+ (isDocPath(e.name) ? docs : code).push(join4(queue[head], e.name));
888
1115
  }
889
1116
  }
890
- return { files, truncated: files.length >= MAX_FILES || visited >= MAX_ENTRIES };
1117
+ const ordered = [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
1118
+ return { files: ordered, truncated: code.length >= MAX_FILES || visited >= MAX_ENTRIES };
891
1119
  }
892
1120
  function relPath(root, abs) {
893
1121
  return relative2(root, abs).split("\\").join("/");
@@ -945,10 +1173,11 @@ function statSourceFiles(root) {
945
1173
  }
946
1174
  return out;
947
1175
  }
948
- var IGNORE_DIRS, LANG_BY_EXT, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
1176
+ 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
1177
  var init_codegraph_extract = __esm({
950
1178
  "src/codegraph-extract.ts"() {
951
1179
  "use strict";
1180
+ init_codegraph_extract_docs();
952
1181
  IGNORE_DIRS = /* @__PURE__ */ new Set([
953
1182
  "node_modules",
954
1183
  ".git",
@@ -973,7 +1202,7 @@ var init_codegraph_extract = __esm({
973
1202
  ".gradle",
974
1203
  ".dart_tool"
975
1204
  ]);
976
- LANG_BY_EXT = {
1205
+ CODE_LANG_BY_EXT = {
977
1206
  ".ts": "ts",
978
1207
  ".tsx": "ts",
979
1208
  ".mts": "ts",
@@ -999,6 +1228,9 @@ var init_codegraph_extract = __esm({
999
1228
  ".hpp": "cpp",
1000
1229
  ".hh": "cpp"
1001
1230
  };
1231
+ LANG_BY_EXT = { ...CODE_LANG_BY_EXT, ...DOC_LANG_BY_EXT };
1232
+ FOREIGN_DOC_DIRS = /(?:^|\/)(?:references?|examples?|samples?|fixtures?|third[_-]party|vendor(?:ed)?)\//i;
1233
+ MAX_DOC_FILES = 1500;
1002
1234
  MAX_FILES = 8e3;
1003
1235
  MAX_FILE_BYTES = 512 * 1024;
1004
1236
  BRACE_LANGS = /* @__PURE__ */ new Set(["ts", "js", "go", "rust", "java", "csharp", "kotlin", "php", "c", "cpp"]);
@@ -1214,12 +1446,12 @@ function buildEdges(root, files, importEdges, resolveImport) {
1214
1446
  }
1215
1447
  const edges = [];
1216
1448
  const seen = /* @__PURE__ */ new Set();
1217
- const push = (from, to, rel) => {
1449
+ const push = (from, to, rel, inferred = false) => {
1218
1450
  if (edges.length >= MAX_EDGES || from === to) return;
1219
1451
  const key = `${from}|${to}|${rel}`;
1220
1452
  if (seen.has(key)) return;
1221
1453
  seen.add(key);
1222
- edges.push([from, to, rel]);
1454
+ edges.push(inferred ? [from, to, rel, 1] : [from, to, rel]);
1223
1455
  };
1224
1456
  for (const f of files) {
1225
1457
  for (const span of spansByFile.get(f.path) ?? []) push(f.path, span.id, "contains");
@@ -1248,12 +1480,14 @@ function buildEdges(root, files, importEdges, resolveImport) {
1248
1480
  const candidates = defsByName.get(name)?.filter((c) => imported?.has(c.path) && reachable(c.path));
1249
1481
  return candidates?.length === 1 ? candidates[0].id : null;
1250
1482
  };
1483
+ const extracted = (id) => id ? { id, inferred: false } : null;
1484
+ const inferred = (id) => id ? { id, inferred: true } : null;
1251
1485
  if (!f.bindings) {
1252
1486
  return (name) => {
1253
- if (local.has(name)) return local.get(name) ?? null;
1254
- if (imported?.size) return unique(name);
1487
+ if (local.has(name)) return extracted(local.get(name));
1488
+ if (imported?.size) return inferred(unique(name));
1255
1489
  const all = defsByName.get(name)?.filter((c) => reachable(c.path));
1256
- return all?.length === 1 ? all[0].id : null;
1490
+ return all?.length === 1 ? inferred(all[0].id) : null;
1257
1491
  };
1258
1492
  }
1259
1493
  const bound = new Set(f.bindings.names);
@@ -1265,10 +1499,10 @@ function buildEdges(root, files, importEdges, resolveImport) {
1265
1499
  return (name, memberOf) => {
1266
1500
  if (memberOf !== void 0) {
1267
1501
  const path = namespaces.get(memberOf);
1268
- return path ? idByPathName.get(`${path}#${name}`) ?? null : null;
1502
+ return path ? extracted(idByPathName.get(`${path}#${name}`)) : null;
1269
1503
  }
1270
- if (local.has(name)) return local.get(name) ?? null;
1271
- return bound.has(name) ? unique(name) : null;
1504
+ if (local.has(name)) return extracted(local.get(name));
1505
+ return bound.has(name) ? extracted(unique(name)) : null;
1272
1506
  };
1273
1507
  }
1274
1508
  const bodies = [];
@@ -1282,7 +1516,7 @@ function buildEdges(root, files, importEdges, resolveImport) {
1282
1516
  const spans = spansByFile.get(f.path) ?? [];
1283
1517
  const declLines = new Set(f.symbols.map((s) => s.line));
1284
1518
  const lines = content.split("\n");
1285
- indexBodies(bodies, f.path, spans, lines);
1519
+ indexBodies(bodies, f.path, spans, lines, DOC_LANGS.has(f.lang) ? MAX_DOC_BODY_TOKENS : MAX_BODY_TOKENS);
1286
1520
  const bind = binderFor(f);
1287
1521
  const strict = !!f.bindings;
1288
1522
  for (const s of f.symbols) {
@@ -1291,9 +1525,17 @@ function buildEdges(root, files, importEdges, resolveImport) {
1291
1525
  if (!self) continue;
1292
1526
  for (const [rel, name] of inheritanceTargets(s.signature)) {
1293
1527
  const target = bind(name);
1294
- if (target) push(self.id, target, rel);
1528
+ if (target) push(self.id, target.id, rel, target.inferred);
1295
1529
  }
1296
1530
  }
1531
+ if (f.lang === "markdown") {
1532
+ for (const { name, line } of markdownCodeMentions(content)) {
1533
+ if (name.length < MIN_DOC_MENTION) continue;
1534
+ const code = defsByName.get(name)?.filter((c) => !DOC_LANGS.has(langByPath.get(c.path) ?? ""));
1535
+ if (code?.length === 1) push(enclosingSymbol(spans, line) ?? f.path, code[0].id, "references", true);
1536
+ }
1537
+ }
1538
+ if (DOC_LANGS.has(f.lang) && !DOC_REFERENCE_LANGS.has(f.lang)) continue;
1297
1539
  for (let i = 0; i < lines.length; i++) {
1298
1540
  const lineNo = i + 1;
1299
1541
  if (declLines.has(lineNo)) continue;
@@ -1306,31 +1548,32 @@ function buildEdges(root, files, importEdges, resolveImport) {
1306
1548
  if (name.length < MIN_REF_NAME || !defsByName.has(name)) continue;
1307
1549
  const memberOf = strict ? /([A-Za-z_$][\w$]*)\s*\.\s*$/.exec(line.slice(0, m.index))?.[1] : void 0;
1308
1550
  const target = bind(name, memberOf);
1309
- if (!target || target === from) continue;
1551
+ if (!target || target.id === from) continue;
1310
1552
  const after = line.slice(m.index + name.length);
1311
- push(from, target, /^\s*\(/.test(after) ? "calls" : "references");
1553
+ const calls = !DOC_LANGS.has(f.lang) && /^\s*\(/.test(after);
1554
+ push(from, target.id, calls ? "calls" : "references", target.inferred);
1312
1555
  }
1313
1556
  }
1314
1557
  }
1315
1558
  return { edges, bodies };
1316
1559
  }
1317
- function indexBodies(out, path, spans, lines) {
1560
+ function indexBodies(out, path, spans, lines, limit) {
1318
1561
  const covered = new Uint8Array(lines.length + 1);
1319
1562
  for (const s of spans) {
1320
1563
  const text = lines.slice(s.line - 1, s.endLine).join("\n");
1321
1564
  for (let l = s.line; l <= Math.min(s.endLine, lines.length); l++) covered[l] = 1;
1322
- const bag = topTokens(text);
1565
+ const bag = topTokens(text, limit);
1323
1566
  if (bag.length) out.push([s.id, bag]);
1324
1567
  }
1325
1568
  const residual = [];
1326
1569
  for (let l = 1; l <= lines.length; l++) if (!covered[l]) residual.push(lines[l - 1]);
1327
- const fileBag = topTokens(residual.join("\n"));
1570
+ const fileBag = topTokens(residual.join("\n"), limit);
1328
1571
  if (fileBag.length) out.push([path, fileBag]);
1329
1572
  }
1330
- function topTokens(text) {
1573
+ function topTokens(text, limit) {
1331
1574
  const m = /* @__PURE__ */ new Map();
1332
1575
  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);
1576
+ return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit);
1334
1577
  }
1335
1578
  function isManagedPath(dir) {
1336
1579
  const managed = resolve3(join5(enigmaHome(), ".enigma"));
@@ -1548,17 +1791,18 @@ function codeGraphAvailable() {
1548
1791
  function codeGraphStatus() {
1549
1792
  return { enabled: readConfig().config.codeGraph, available: true, projects: listProjects().length };
1550
1793
  }
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;
1794
+ 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
1795
  var init_codegraph = __esm({
1553
1796
  "src/codegraph.ts"() {
1554
1797
  "use strict";
1555
1798
  init_util();
1556
1799
  init_config();
1557
1800
  init_codegraph_rank();
1801
+ init_codegraph_extract_docs();
1558
1802
  init_codegraph_extract();
1559
1803
  init_codegraph_extract();
1560
1804
  init_codegraph_types();
1561
- GRAPH_VERSION = 4;
1805
+ GRAPH_VERSION = 5;
1562
1806
  RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".kt"];
1563
1807
  LANG_EXTS = {
1564
1808
  ts: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
@@ -1574,6 +1818,7 @@ var init_codegraph = __esm({
1574
1818
  };
1575
1819
  LANG_INDEX = { python: ["__init__"], go: ["mod"], rust: ["mod"] };
1576
1820
  MIN_REF_NAME = 3;
1821
+ MIN_DOC_MENTION = 4;
1577
1822
  MAX_EDGES = 4e5;
1578
1823
  LANG_FAMILY = {
1579
1824
  ts: "js",
@@ -1590,6 +1835,7 @@ var init_codegraph = __esm({
1590
1835
  EXT_ORDER = orders(RESOLVE_EXTS, LANG_EXTS);
1591
1836
  INDEX_ORDER = orders(INDEX_NAMES, LANG_INDEX);
1592
1837
  MAX_BODY_TOKENS = 80;
1838
+ MAX_DOC_BODY_TOKENS = 24;
1593
1839
  ROOT_MARKERS = [".git", "package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml", "composer.json"];
1594
1840
  MAX_ROOT_WALK = 6;
1595
1841
  }
@@ -1822,11 +2068,11 @@ ${result.note}` : structural.fallthrough;
1822
2068
  }
1823
2069
  function adjacency(edges, direction) {
1824
2070
  const adj = /* @__PURE__ */ new Map();
1825
- for (const [source, target, rel] of edges) {
2071
+ for (const [source, target, rel, guess] of edges) {
1826
2072
  if (!WALK_RELATIONS.has(rel)) continue;
1827
2073
  const key = direction === "in" ? target : source;
1828
2074
  const other = direction === "in" ? source : target;
1829
- const entry = { other, relation: rel };
2075
+ const entry = { other, relation: rel, inferred: guess === 1 };
1830
2076
  const arr = adj.get(key);
1831
2077
  if (arr) arr.push(entry);
1832
2078
  else adj.set(key, [entry]);
@@ -1855,13 +2101,13 @@ function codeGraphTrace(symbol, opts = {}) {
1855
2101
  for (let d = 1; d <= depth && frontier.length; d++) {
1856
2102
  const next = [];
1857
2103
  for (const current of frontier) {
1858
- for (const { other, relation } of adj.get(current) ?? []) {
2104
+ for (const { other, relation, inferred } of adj.get(current) ?? []) {
1859
2105
  if (visited.has(other)) continue;
1860
2106
  visited.add(other);
1861
2107
  const node = loaded.byId.get(other);
1862
2108
  if (!node) continue;
1863
2109
  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 });
2110
+ hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d, ...inferred ? { inferred } : {} });
1865
2111
  next.push(other);
1866
2112
  }
1867
2113
  }
@@ -1900,8 +2146,8 @@ function dirKey(path, depth) {
1900
2146
  const segments = path.split("/");
1901
2147
  return segments.slice(0, Math.min(depth, segments.length)).join("/");
1902
2148
  }
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);
2149
+ function topHubs(nodes, deg, cap2) {
2150
+ 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
2151
  }
1906
2152
  function codeGraphMap(opts = {}) {
1907
2153
  const loaded = load(opts);
@@ -2129,7 +2375,7 @@ ${r.note ?? empty}
2129
2375
  const lines = [head, ""];
2130
2376
  lines.push(`defined at: ${r.matched.map((m) => `${m.path}:${m.line}`).join(", ")}`, "");
2131
2377
  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}` : ""}`);
2378
+ lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${h.inferred ? " (inferred)" : ""}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
2133
2379
  }
2134
2380
  lines.push("", `${r.hits.length} node${r.hits.length === 1 ? "" : "s"}.`);
2135
2381
  return `${lines.join("\n")}
@@ -2252,6 +2498,295 @@ var init_codegraph_format = __esm({
2252
2498
  }
2253
2499
  });
2254
2500
 
2501
+ // src/codegraph-cluster.ts
2502
+ function localMoves(level, totalWeight) {
2503
+ const { nodes, adj, selfLoops } = level;
2504
+ const degree = new Array(nodes).fill(0);
2505
+ for (let i = 0; i < nodes; i++) {
2506
+ let d = selfLoops[i] * 2;
2507
+ for (const w of adj[i].values()) d += w;
2508
+ degree[i] = d;
2509
+ }
2510
+ const comm = Array.from({ length: nodes }, (_, i) => i);
2511
+ const commDegree = degree.slice();
2512
+ const m2 = totalWeight * 2;
2513
+ for (let sweep = 0; sweep < MAX_SWEEPS; sweep++) {
2514
+ let moved = false;
2515
+ for (let i = 0; i < nodes; i++) {
2516
+ const current = comm[i];
2517
+ const toComm = /* @__PURE__ */ new Map();
2518
+ for (const [j, w] of adj[i]) toComm.set(comm[j], (toComm.get(comm[j]) ?? 0) + w);
2519
+ commDegree[current] -= degree[i];
2520
+ let best = current;
2521
+ let bestGain = (toComm.get(current) ?? 0) - commDegree[current] * degree[i] / m2;
2522
+ for (const [c, w] of [...toComm.entries()].sort((a, b) => a[0] - b[0])) {
2523
+ const gain = w - commDegree[c] * degree[i] / m2;
2524
+ if (gain > bestGain + 1e-12) {
2525
+ best = c;
2526
+ bestGain = gain;
2527
+ }
2528
+ }
2529
+ commDegree[best] += degree[i];
2530
+ if (best !== current) {
2531
+ comm[i] = best;
2532
+ moved = true;
2533
+ }
2534
+ }
2535
+ if (!moved) break;
2536
+ }
2537
+ return comm;
2538
+ }
2539
+ function clusterFiles(files, links) {
2540
+ const sorted = [...new Set(files)].sort();
2541
+ const index = new Map(sorted.map((f, i) => [f, i]));
2542
+ let level = { nodes: sorted.length, adj: sorted.map(() => /* @__PURE__ */ new Map()), selfLoops: new Array(sorted.length).fill(0) };
2543
+ let totalWeight = 0;
2544
+ for (const [a, b, w] of links) {
2545
+ const i = index.get(a);
2546
+ const j = index.get(b);
2547
+ if (i === void 0 || j === void 0 || w <= 0) continue;
2548
+ totalWeight += w;
2549
+ if (i === j) {
2550
+ level.selfLoops[i] += w;
2551
+ continue;
2552
+ }
2553
+ level.adj[i].set(j, (level.adj[i].get(j) ?? 0) + w);
2554
+ level.adj[j].set(i, (level.adj[j].get(i) ?? 0) + w);
2555
+ }
2556
+ let membership = sorted.map((_, i) => i);
2557
+ if (totalWeight === 0) return new Map(sorted.map((f, i) => [f, i]));
2558
+ for (let l = 0; l < MAX_LEVELS; l++) {
2559
+ const comm = localMoves(level, totalWeight);
2560
+ const ids = /* @__PURE__ */ new Map();
2561
+ for (const c of comm) if (!ids.has(c)) ids.set(c, ids.size);
2562
+ if (ids.size === level.nodes) break;
2563
+ membership = membership.map((c) => ids.get(comm[c]));
2564
+ const next = { nodes: ids.size, adj: Array.from({ length: ids.size }, () => /* @__PURE__ */ new Map()), selfLoops: new Array(ids.size).fill(0) };
2565
+ for (let i = 0; i < level.nodes; i++) {
2566
+ const ci = ids.get(comm[i]);
2567
+ next.selfLoops[ci] += level.selfLoops[i];
2568
+ for (const [j, w] of level.adj[i]) {
2569
+ const cj = ids.get(comm[j]);
2570
+ if (ci === cj) {
2571
+ if (i < j) next.selfLoops[ci] += w;
2572
+ } else next.adj[ci].set(cj, (next.adj[ci].get(cj) ?? 0) + w);
2573
+ }
2574
+ }
2575
+ level = next;
2576
+ }
2577
+ const members = /* @__PURE__ */ new Map();
2578
+ sorted.forEach((f, i) => {
2579
+ const c = membership[i];
2580
+ const list = members.get(c);
2581
+ if (list) list.push(f);
2582
+ else members.set(c, [f]);
2583
+ });
2584
+ const order = [...members.entries()].sort((a, b) => b[1].length - a[1].length || a[1][0].localeCompare(b[1][0]));
2585
+ const out = /* @__PURE__ */ new Map();
2586
+ order.forEach(([, list], rank) => {
2587
+ for (const f of list) out.set(f, rank);
2588
+ });
2589
+ return out;
2590
+ }
2591
+ var MAX_SWEEPS, MAX_LEVELS;
2592
+ var init_codegraph_cluster = __esm({
2593
+ "src/codegraph-cluster.ts"() {
2594
+ "use strict";
2595
+ MAX_SWEEPS = 20;
2596
+ MAX_LEVELS = 6;
2597
+ }
2598
+ });
2599
+
2600
+ // src/codegraph-report.ts
2601
+ var codegraph_report_exports = {};
2602
+ __export(codegraph_report_exports, {
2603
+ codeGraphReport: () => codeGraphReport,
2604
+ formatReport: () => formatReport
2605
+ });
2606
+ function areaLabel(paths) {
2607
+ const dirs = paths.map((p) => p.split("/").slice(0, -1));
2608
+ let label = [];
2609
+ for (let depth = 1; ; depth++) {
2610
+ const counts2 = /* @__PURE__ */ new Map();
2611
+ for (const d of dirs) if (d.length >= depth) {
2612
+ const k = d.slice(0, depth).join("/");
2613
+ counts2.set(k, (counts2.get(k) ?? 0) + 1);
2614
+ }
2615
+ const top2 = [...counts2.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
2616
+ if (!top2 || top2[1] / paths.length < LABEL_SHARE) break;
2617
+ label = top2[0].split("/");
2618
+ }
2619
+ if (label.length) return `${label.join("/")}/`;
2620
+ if (paths.length === 1) return paths[0];
2621
+ const first = /* @__PURE__ */ new Map();
2622
+ for (const d of dirs) {
2623
+ const k = d[0] ?? "(root)";
2624
+ first.set(k, (first.get(k) ?? 0) + 1);
2625
+ }
2626
+ const [top] = [...first.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
2627
+ return `${top}/ (mixed)`;
2628
+ }
2629
+ function codeGraphReport(opts = {}) {
2630
+ const graph = loadFreshGraph(opts.project, opts.refresh ?? true);
2631
+ if (!graph) return null;
2632
+ const nodes = graphNodes(graph);
2633
+ const byId = new Map(nodes.map((n) => [n.id, n]));
2634
+ const langByPath = new Map(graph.files.map((f) => [f.path, f.lang]));
2635
+ const own = graph.files.filter((f) => !isForeignPath(f.path));
2636
+ const codePaths = new Set(own.filter((f) => isCodeLang(f.lang)).map((f) => f.path));
2637
+ const docPaths = new Set(own.filter((f) => f.lang === "markdown").map((f) => f.path));
2638
+ const weights = /* @__PURE__ */ new Map();
2639
+ const directed = /* @__PURE__ */ new Map();
2640
+ const docRefs = /* @__PURE__ */ new Map();
2641
+ for (const e of graph.edges) {
2642
+ if (e[2] === "contains") continue;
2643
+ const a = byId.get(e[0])?.path;
2644
+ const b = byId.get(e[1])?.path;
2645
+ if (!a || !b || a === b) continue;
2646
+ if (docPaths.has(a) && codePaths.has(b)) {
2647
+ const refs = docRefs.get(a) ?? /* @__PURE__ */ new Map();
2648
+ refs.set(b, (refs.get(b) ?? 0) + 1);
2649
+ docRefs.set(a, refs);
2650
+ continue;
2651
+ }
2652
+ if (!codePaths.has(a) || !codePaths.has(b)) continue;
2653
+ const key = a < b ? `${a}
2654
+ ${b}` : `${b}
2655
+ ${a}`;
2656
+ weights.set(key, (weights.get(key) ?? 0) + 1);
2657
+ directed.set(`${a}
2658
+ ${b}`, (directed.get(`${a}
2659
+ ${b}`) ?? 0) + 1);
2660
+ }
2661
+ const links = [...weights.entries()].map(([k, w]) => {
2662
+ const [a, b] = k.split("\n");
2663
+ return [a, b, w];
2664
+ });
2665
+ const areaOf = clusterFiles([...codePaths], links);
2666
+ const members = /* @__PURE__ */ new Map();
2667
+ for (const [path, area] of areaOf) {
2668
+ const list = members.get(area);
2669
+ if (list) list.push(path);
2670
+ else members.set(area, [path]);
2671
+ }
2672
+ const deg = inDegree(graph.edges, true);
2673
+ const maxAreas = opts.maxAreas ?? DEFAULT_MAX_AREAS;
2674
+ const ranked = [...members.entries()].filter(([, paths]) => paths.length >= 2).sort((a, b) => b[1].length - a[1].length || a[0] - b[0]);
2675
+ const symbolsOf = /* @__PURE__ */ new Map();
2676
+ for (const n of nodes) {
2677
+ if (n.kind === "file" || isTestPath(n.path)) continue;
2678
+ const area = areaOf.get(n.path);
2679
+ if (area === void 0) continue;
2680
+ const list = symbolsOf.get(area);
2681
+ if (list) list.push(n);
2682
+ else symbolsOf.set(area, [n]);
2683
+ }
2684
+ const keys = /* @__PURE__ */ new Map();
2685
+ const keyOf = (area) => {
2686
+ let key = keys.get(area);
2687
+ if (key) return key;
2688
+ 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);
2689
+ keys.set(area, key);
2690
+ return key;
2691
+ };
2692
+ const base = new Map(ranked.map(([area, paths]) => [area, areaLabel(paths)]));
2693
+ const seenLabel = /* @__PURE__ */ new Map();
2694
+ for (const label of base.values()) seenLabel.set(label, (seenLabel.get(label) ?? 0) + 1);
2695
+ const labels = new Map([...base.entries()].map(([area, label]) => {
2696
+ if ((seenLabel.get(label) ?? 0) < 2) return [area, label];
2697
+ const around = keyOf(area)[0]?.name;
2698
+ return [area, around ? `${label} ~ ${around}` : label];
2699
+ }));
2700
+ const docsOf = /* @__PURE__ */ new Map();
2701
+ for (const [doc, refs] of docRefs) {
2702
+ const byArea = /* @__PURE__ */ new Map();
2703
+ for (const [code, w] of refs) {
2704
+ const area = areaOf.get(code);
2705
+ if (area !== void 0) byArea.set(area, (byArea.get(area) ?? 0) + w);
2706
+ }
2707
+ const best = [...byArea.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0];
2708
+ if (!best) continue;
2709
+ const list = docsOf.get(best[0]) ?? [];
2710
+ list.push(doc);
2711
+ docsOf.set(best[0], list);
2712
+ }
2713
+ const areas = ranked.slice(0, maxAreas).map(([area, paths]) => {
2714
+ const inArea = new Set(paths);
2715
+ const out = /* @__PURE__ */ new Map();
2716
+ for (const [key, w] of directed) {
2717
+ const [a, b] = key.split("\n");
2718
+ if (!inArea.has(a) || inArea.has(b)) continue;
2719
+ const target = areaOf.get(b);
2720
+ if (target !== void 0 && target !== area && labels.has(target)) out.set(target, (out.get(target) ?? 0) + w);
2721
+ }
2722
+ return {
2723
+ label: labels.get(area),
2724
+ files: paths.length,
2725
+ languages: [...new Set(paths.map((p) => langByPath.get(p)).filter((l) => !!l))].sort(),
2726
+ keySymbols: keyOf(area),
2727
+ docs: (docsOf.get(area) ?? []).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, AREA_DOCS),
2728
+ dependsOn: [...out.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, DEPENDS_ON).map(([t]) => labels.get(t))
2729
+ };
2730
+ });
2731
+ const titleOf = /* @__PURE__ */ new Map();
2732
+ for (const n of nodes) if (n.kind === "section" && !titleOf.has(n.path)) titleOf.set(n.path, n.name);
2733
+ const docFiles = own.filter((f) => f.lang === "markdown").sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
2734
+ const maxDocs = opts.maxDocs ?? DEFAULT_MAX_DOCS;
2735
+ return {
2736
+ name: graph.name,
2737
+ totals: {
2738
+ files: graph.files.length,
2739
+ symbols: nodes.length - graph.files.length,
2740
+ edges: graph.edges.length,
2741
+ inferred: graph.edges.filter((e) => e[3] === 1).length,
2742
+ docs: docFiles.length
2743
+ },
2744
+ areas,
2745
+ moreAreas: Math.max(0, ranked.length - areas.length),
2746
+ docs: docFiles.slice(0, maxDocs).map((f) => ({ path: f.path, title: titleOf.get(f.path) ?? "" })),
2747
+ moreDocs: Math.max(0, docFiles.length - maxDocs),
2748
+ foreign: graph.files.length - own.length,
2749
+ truncated: graph.truncated
2750
+ };
2751
+ }
2752
+ function formatReport(r) {
2753
+ const t = r.totals;
2754
+ const lines = [`repo report - ${r.name} - ${t.files} files (${t.docs} docs) - ${t.symbols} symbols - ${t.edges} edges (${t.inferred} inferred by name)`];
2755
+ if (r.foreign) lines.push(`${r.foreign} file(s) of vendored, reference or example material are left out of the areas below.`);
2756
+ lines.push("", "areas (files that depend on each other, largest first):");
2757
+ for (const a of r.areas) {
2758
+ lines.push(`- ${a.label} - ${a.files} files${a.languages.length ? ` - ${a.languages.join(", ")}` : ""}`);
2759
+ if (a.keySymbols.length) lines.push(` key: ${a.keySymbols.map((s) => `${s.name} (${s.path}:${s.line}, ${s.inDegree} in)`).join(", ")}`);
2760
+ if (a.dependsOn.length) lines.push(` uses: ${a.dependsOn.join(", ")}`);
2761
+ if (a.docs.length) lines.push(` docs: ${a.docs.join(", ")}`);
2762
+ }
2763
+ if (r.moreAreas) lines.push(` +${r.moreAreas} smaller area(s)`);
2764
+ if (r.docs.length) {
2765
+ lines.push("", "docs:");
2766
+ for (const d of r.docs) lines.push(`- ${d.path}${d.title ? ` - ${d.title}` : ""}`);
2767
+ if (r.moreDocs) lines.push(` +${r.moreDocs} more (enigma_codegraph_ask finds them by topic)`);
2768
+ }
2769
+ if (r.truncated) lines.push("", "The index hit its size cap; some files are not covered.");
2770
+ return `${lines.join("\n")}
2771
+ `;
2772
+ }
2773
+ var DEFAULT_MAX_AREAS, DEFAULT_MAX_DOCS, KEY_SYMBOLS, AREA_DOCS, DEPENDS_ON, LABEL_SHARE;
2774
+ var init_codegraph_report = __esm({
2775
+ "src/codegraph-report.ts"() {
2776
+ "use strict";
2777
+ init_codegraph();
2778
+ init_codegraph_rank();
2779
+ init_codegraph_extract();
2780
+ init_codegraph_cluster();
2781
+ DEFAULT_MAX_AREAS = 8;
2782
+ DEFAULT_MAX_DOCS = 12;
2783
+ KEY_SYMBOLS = 3;
2784
+ AREA_DOCS = 2;
2785
+ DEPENDS_ON = 2;
2786
+ LABEL_SHARE = 0.6;
2787
+ }
2788
+ });
2789
+
2255
2790
  // src/codegraph-hook.ts
2256
2791
  import { homedir as homedir3 } from "os";
2257
2792
 
@@ -2388,14 +2923,17 @@ async function sessionStart(dir) {
2388
2923
  " - enigma_codegraph_skeleton: a file's whole API surface, no bodies.",
2389
2924
  " - enigma_codegraph_grep: every occurrence, grouped by enclosing symbol - when you need them ALL, not the top matches."
2390
2925
  ].join("\n");
2391
- const map = q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
2926
+ const rep = await Promise.resolve().then(() => (init_codegraph_report(), codegraph_report_exports));
2927
+ const report = rep.codeGraphReport({ project, refresh: false });
2928
+ const map = report ? null : q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
2929
+ const body = report ? rep.formatReport(report) : map ? fmt.formatMap(map) : "";
2392
2930
  const fresh = q.codeGraphCheck(project);
2393
2931
  const stale = fresh && fresh.stale > 0 ? `
2394
2932
  ${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}
2933
+ writeStatuslineSnapshot(projectRoot(cg, project, dir), report?.totals.symbols ?? map?.totals.symbols ?? 0, fresh?.stale ?? 0);
2934
+ emit("SessionStart", body ? `${directive}
2397
2935
 
2398
- ${fmt.formatMap(map).trimEnd()}${stale}` : directive);
2936
+ ${body.trimEnd()}${stale}` : directive);
2399
2937
  }
2400
2938
  function coveringProject(cg, dir) {
2401
2939
  const project = cg.findProjectForCwd(dir);