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
package/dist/post-edit.js CHANGED
@@ -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, writeFileSync as writeFileSync2 } 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)) mkdirSync(dir, { recursive: true });
241
246
  writeFileSync2(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
  });
@@ -5686,10 +5698,222 @@ var init_codegraph_rank = __esm({
5686
5698
  }
5687
5699
  });
5688
5700
 
5701
+ // src/codegraph-extract-docs.ts
5702
+ function cap(text) {
5703
+ const t = text.trim();
5704
+ return t.length > MAX_SIGNATURE ? `${t.slice(0, MAX_SIGNATURE)}...` : t;
5705
+ }
5706
+ function indentOf(line) {
5707
+ let n = 0;
5708
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
5709
+ return n;
5710
+ }
5711
+ function fencedLines(lines) {
5712
+ const inside = new Uint8Array(lines.length);
5713
+ let fence = null;
5714
+ for (let i = 0; i < lines.length; i++) {
5715
+ const m = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
5716
+ if (fence) {
5717
+ inside[i] = 1;
5718
+ if (m && m[1][0] === fence[0] && m[1].length >= fence.length) fence = null;
5719
+ continue;
5720
+ }
5721
+ if (m) {
5722
+ fence = m[1];
5723
+ inside[i] = 1;
5724
+ }
5725
+ }
5726
+ return inside;
5727
+ }
5728
+ function markdownSymbols(lines) {
5729
+ const fenced = fencedLines(lines);
5730
+ const heads = [];
5731
+ for (let i = 0; i < lines.length; i++) {
5732
+ if (fenced[i]) continue;
5733
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i]);
5734
+ if (m && m[1].length <= MAX_HEADING_LEVEL) heads.push({ level: m[1].length, name: m[2].replace(/[`*_]/g, "").trim(), line: i + 1 });
5735
+ }
5736
+ return heads.filter((h) => h.name).map((h, idx) => {
5737
+ const next = heads.slice(idx + 1).find((o) => o.level <= h.level);
5738
+ return { name: h.name, kind: "section", line: h.line, endLine: next ? next.line - 1 : lines.length, signature: cap(lines[h.line - 1]) };
5739
+ });
5740
+ }
5741
+ function sqlEnd(lines, start) {
5742
+ let dollar = false;
5743
+ for (let i = start; i < lines.length; i++) {
5744
+ const parts = lines[i].replace(/--.*$/, "").split("$$");
5745
+ for (let p = 0; p < parts.length; p++) {
5746
+ if (p > 0) dollar = !dollar;
5747
+ if (!dollar && parts[p].includes(";")) return i + 1;
5748
+ }
5749
+ }
5750
+ return lines.length;
5751
+ }
5752
+ function sqlSymbols(lines) {
5753
+ const rules = [
5754
+ ["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")],
5755
+ ["function", new RegExp(String.raw`^\s*create\s+(?:or\s+replace\s+)?(?:function|procedure)\s+${SQL_NAME}`, "i")],
5756
+ ["type", new RegExp(String.raw`^\s*create\s+type\s+${SQL_NAME}`, "i")]
5757
+ ];
5758
+ const out = [];
5759
+ for (let i = 0; i < lines.length; i++) {
5760
+ for (const [kind, re] of rules) {
5761
+ const m = re.exec(lines[i]);
5762
+ if (!m) continue;
5763
+ out.push({ name: m[1], kind, line: i + 1, endLine: sqlEnd(lines, i), signature: cap(lines[i].replace(/\(.*$/, "")) });
5764
+ break;
5765
+ }
5766
+ }
5767
+ return out;
5768
+ }
5769
+ function prismaSymbols(lines) {
5770
+ const out = [];
5771
+ for (let i = 0; i < lines.length; i++) {
5772
+ const m = /^\s*(model|view|enum|type)\s+([A-Za-z_]\w*)\s*\{/.exec(lines[i]);
5773
+ if (!m) continue;
5774
+ let end = i;
5775
+ while (end < lines.length - 1 && !/^\s*\}/.test(lines[end])) end++;
5776
+ const kind = m[1] === "enum" ? "enum" : m[1] === "type" ? "type" : "table";
5777
+ out.push({ name: m[2], kind, line: i + 1, endLine: end + 1, signature: cap(`${m[1]} ${m[2]}`) });
5778
+ }
5779
+ return out;
5780
+ }
5781
+ function yamlEnd(lines, start) {
5782
+ const base = indentOf(lines[start]);
5783
+ let end = start + 1;
5784
+ for (let i = start + 1; i < lines.length; i++) {
5785
+ const line = lines[i];
5786
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
5787
+ if (indentOf(line) <= base && !line.trimStart().startsWith("- ")) break;
5788
+ end = i + 1;
5789
+ }
5790
+ return end;
5791
+ }
5792
+ function yamlSymbols(lines) {
5793
+ const out = [];
5794
+ const KEY = /^(\s*)([A-Za-z0-9_][\w.-]*)\s*:(?:\s|$)/;
5795
+ for (let i = 0; i < lines.length; i++) {
5796
+ const m = KEY.exec(lines[i]);
5797
+ if (!m || m[1].length) continue;
5798
+ const end = yamlEnd(lines, i);
5799
+ out.push({ name: m[2], kind: "key", line: i + 1, endLine: end, signature: cap(lines[i]) });
5800
+ if (!YAML_CONTAINERS.has(m[2])) continue;
5801
+ let childIndent = -1;
5802
+ for (let j = i + 1; j < end; j++) {
5803
+ const c = KEY.exec(lines[j]);
5804
+ if (!c || !c[1].length) continue;
5805
+ if (childIndent === -1) childIndent = c[1].length;
5806
+ if (c[1].length !== childIndent) continue;
5807
+ out.push({ name: c[2], kind: "key", line: j + 1, endLine: yamlEnd(lines, j), signature: cap(`${m[2]}.${c[2]}`) });
5808
+ }
5809
+ }
5810
+ return out;
5811
+ }
5812
+ function tomlSymbols(lines) {
5813
+ const heads = [];
5814
+ for (let i = 0; i < lines.length; i++) {
5815
+ const m = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/.exec(lines[i]);
5816
+ if (m) heads.push({ name: m[1], line: i + 1 });
5817
+ }
5818
+ return heads.map((h, idx) => ({
5819
+ name: h.name,
5820
+ kind: "key",
5821
+ line: h.line,
5822
+ endLine: idx + 1 < heads.length ? heads[idx + 1].line - 1 : lines.length,
5823
+ signature: cap(lines[h.line - 1])
5824
+ }));
5825
+ }
5826
+ function markdownLinks(lines) {
5827
+ const fenced = fencedLines(lines);
5828
+ const out = /* @__PURE__ */ new Set();
5829
+ for (let i = 0; i < lines.length; i++) {
5830
+ if (fenced[i]) continue;
5831
+ const re = /\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
5832
+ let m;
5833
+ while (m = re.exec(lines[i])) {
5834
+ const target = m[1].replace(/[#?].*$/, "");
5835
+ if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue;
5836
+ try {
5837
+ out.add(decodeURI(target));
5838
+ } catch {
5839
+ out.add(target);
5840
+ }
5841
+ }
5842
+ }
5843
+ return [...out];
5844
+ }
5845
+ function markdownCodeMentions(content) {
5846
+ const lines = content.split("\n");
5847
+ const fenced = fencedLines(lines);
5848
+ const out = [];
5849
+ for (let i = 0; i < lines.length; i++) {
5850
+ if (fenced[i]) continue;
5851
+ const re = /`([A-Za-z_$][\w$]*)(?:\(\))?`/g;
5852
+ let m;
5853
+ while (m = re.exec(lines[i])) out.push({ name: m[1], line: i + 1 });
5854
+ }
5855
+ return out;
5856
+ }
5857
+ function extractDocFile(lang, content) {
5858
+ const lines = content.split("\n");
5859
+ const out = (() => {
5860
+ switch (lang) {
5861
+ case "markdown":
5862
+ return { symbols: markdownSymbols(lines), imports: markdownLinks(lines) };
5863
+ case "sql":
5864
+ return { symbols: sqlSymbols(lines), imports: [] };
5865
+ case "prisma":
5866
+ return { symbols: prismaSymbols(lines), imports: [] };
5867
+ case "yaml":
5868
+ return { symbols: yamlSymbols(lines), imports: [] };
5869
+ case "toml":
5870
+ return { symbols: tomlSymbols(lines), imports: [] };
5871
+ default:
5872
+ return { symbols: [], imports: [] };
5873
+ }
5874
+ })();
5875
+ for (const s of out.symbols) while (s.endLine > s.line && !(lines[s.endLine - 1] ?? "").trim()) s.endLine--;
5876
+ return out;
5877
+ }
5878
+ var DOC_LANG_BY_EXT, DOC_LANGS, DOC_REFERENCE_LANGS, YAML_CONTAINERS, MAX_SIGNATURE, MAX_HEADING_LEVEL, SQL_NAME;
5879
+ var init_codegraph_extract_docs = __esm({
5880
+ "src/codegraph-extract-docs.ts"() {
5881
+ "use strict";
5882
+ DOC_LANG_BY_EXT = {
5883
+ ".md": "markdown",
5884
+ ".mdx": "markdown",
5885
+ ".sql": "sql",
5886
+ ".prisma": "prisma",
5887
+ ".yaml": "yaml",
5888
+ ".yml": "yaml",
5889
+ ".toml": "toml"
5890
+ };
5891
+ DOC_LANGS = new Set(Object.values(DOC_LANG_BY_EXT));
5892
+ DOC_REFERENCE_LANGS = /* @__PURE__ */ new Set(["sql", "prisma"]);
5893
+ YAML_CONTAINERS = /* @__PURE__ */ new Set(["services", "jobs", "workflows", "volumes", "networks"]);
5894
+ MAX_SIGNATURE = 200;
5895
+ MAX_HEADING_LEVEL = 3;
5896
+ SQL_NAME = String.raw`(?:[\w"\x60\[\]]+\.)?["\x60\[]?([A-Za-z_][\w$]*)["\x60\]]?`;
5897
+ }
5898
+ });
5899
+
5689
5900
  // src/codegraph-extract.ts
5690
5901
  import { execFileSync as execFileSync4 } from "child_process";
5691
5902
  import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
5692
5903
  import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
5904
+ function isCodeLang(lang) {
5905
+ return !DOC_LANGS.has(lang);
5906
+ }
5907
+ function isDocPath(path) {
5908
+ return !!DOC_LANG_BY_EXT[extname2(path).toLowerCase()];
5909
+ }
5910
+ function isForeignPath(rel) {
5911
+ return FOREIGN_DOC_DIRS.test(rel);
5912
+ }
5913
+ function pickDocs(root, docs, room) {
5914
+ const depth = (p) => relPath(root, p).split("/").length;
5915
+ 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)));
5916
+ }
5693
5917
  function symRules(lang) {
5694
5918
  switch (lang) {
5695
5919
  case "ts":
@@ -5854,7 +6078,7 @@ function lineResolver(content) {
5854
6078
  return lo + 1;
5855
6079
  };
5856
6080
  }
5857
- function indentOf(line) {
6081
+ function indentOf2(line) {
5858
6082
  let n = 0;
5859
6083
  while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
5860
6084
  return n;
@@ -5920,18 +6144,18 @@ function braceEnd(lines, startIdx, lang) {
5920
6144
  return lines.length;
5921
6145
  }
5922
6146
  function indentEnd(lines, startIdx) {
5923
- const base = indentOf(lines[startIdx]);
6147
+ const base = indentOf2(lines[startIdx]);
5924
6148
  let end = startIdx + 1;
5925
6149
  for (let i = startIdx + 1; i < lines.length; i++) {
5926
6150
  const line = lines[i];
5927
6151
  if (!line.trim()) continue;
5928
- if (indentOf(line) <= base) break;
6152
+ if (indentOf2(line) <= base) break;
5929
6153
  end = i + 1;
5930
6154
  }
5931
6155
  return end;
5932
6156
  }
5933
6157
  function clampToSiblings(symbols, lines) {
5934
- const indentAt = (line) => indentOf(lines[line - 1] ?? "");
6158
+ const indentAt = (line) => indentOf2(lines[line - 1] ?? "");
5935
6159
  for (let i = 0; i < symbols.length; i++) {
5936
6160
  const s = symbols[i];
5937
6161
  for (let j = i + 1; j < symbols.length; j++) {
@@ -5944,14 +6168,15 @@ function clampToSiblings(symbols, lines) {
5944
6168
  }
5945
6169
  }
5946
6170
  function signatureOf(line) {
5947
- const MAX_SIGNATURE = 200;
6171
+ const MAX_SIGNATURE2 = 200;
5948
6172
  let sig = line.trim();
5949
6173
  const brace = sig.indexOf("{");
5950
6174
  if (brace > 0) sig = sig.slice(0, brace).trim();
5951
6175
  if (sig.endsWith(":")) sig = sig.slice(0, -1).trim();
5952
- return sig.length > MAX_SIGNATURE ? `${sig.slice(0, MAX_SIGNATURE)}...` : sig;
6176
+ return sig.length > MAX_SIGNATURE2 ? `${sig.slice(0, MAX_SIGNATURE2)}...` : sig;
5953
6177
  }
5954
6178
  function extractFile(lang, content) {
6179
+ if (DOC_LANGS.has(lang)) return extractDocFile(lang, content);
5955
6180
  const lineAt = lineResolver(content);
5956
6181
  const lines = content.split("\n");
5957
6182
  const braced = BRACE_LANGS.has(lang);
@@ -5993,25 +6218,27 @@ function gitSourceFiles(root) {
5993
6218
  const out = gitList(root, ["--cached", "--others", "--exclude-standard"]);
5994
6219
  if (out === null) return null;
5995
6220
  const nested = existsSync5(join6(root, ".gitmodules")) ? gitList(root, ["--recurse-submodules"]) : null;
5996
- const files = [];
6221
+ const code = [];
6222
+ const docs = [];
5997
6223
  const seen = /* @__PURE__ */ new Set();
5998
6224
  for (const rel of [...out.split("\0"), ...(nested ?? "").split("\0")]) {
5999
- if (files.length >= MAX_FILES) break;
6225
+ if (code.length >= MAX_FILES) break;
6000
6226
  if (!rel || seen.has(rel)) continue;
6001
6227
  seen.add(rel);
6002
6228
  if (!LANG_BY_EXT[extname2(rel).toLowerCase()]) continue;
6003
6229
  if (rel.split("/").some((seg) => seg.startsWith(".") || IGNORE_DIRS.has(seg))) continue;
6004
- files.push(join6(root, rel));
6230
+ (isDocPath(rel) ? docs : code).push(join6(root, rel));
6005
6231
  }
6006
- return files;
6232
+ return [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
6007
6233
  }
6008
6234
  function walk(root) {
6009
6235
  const tracked = gitSourceFiles(root);
6010
6236
  if (tracked && tracked.length) return { files: tracked, truncated: tracked.length >= MAX_FILES };
6011
- const files = [];
6237
+ const code = [];
6238
+ const docs = [];
6012
6239
  const queue = [root];
6013
6240
  let visited = 0;
6014
- for (let head = 0; head < queue.length && files.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
6241
+ for (let head = 0; head < queue.length && code.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
6015
6242
  let entries;
6016
6243
  try {
6017
6244
  entries = readdirSync3(queue[head], { withFileTypes: true });
@@ -6019,16 +6246,17 @@ function walk(root) {
6019
6246
  continue;
6020
6247
  }
6021
6248
  for (const e of entries) {
6022
- if (files.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
6249
+ if (code.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
6023
6250
  if (e.isDirectory()) {
6024
6251
  if (!e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) queue.push(join6(queue[head], e.name));
6025
6252
  continue;
6026
6253
  }
6027
- if (!e.isFile()) continue;
6028
- if (LANG_BY_EXT[extname2(e.name).toLowerCase()]) files.push(join6(queue[head], e.name));
6254
+ if (!e.isFile() || !LANG_BY_EXT[extname2(e.name).toLowerCase()]) continue;
6255
+ (isDocPath(e.name) ? docs : code).push(join6(queue[head], e.name));
6029
6256
  }
6030
6257
  }
6031
- return { files, truncated: files.length >= MAX_FILES || visited >= MAX_ENTRIES };
6258
+ const ordered = [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
6259
+ return { files: ordered, truncated: code.length >= MAX_FILES || visited >= MAX_ENTRIES };
6032
6260
  }
6033
6261
  function relPath(root, abs) {
6034
6262
  return relative2(root, abs).split("\\").join("/");
@@ -6086,10 +6314,11 @@ function statSourceFiles(root) {
6086
6314
  }
6087
6315
  return out;
6088
6316
  }
6089
- var IGNORE_DIRS, LANG_BY_EXT, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
6317
+ 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;
6090
6318
  var init_codegraph_extract = __esm({
6091
6319
  "src/codegraph-extract.ts"() {
6092
6320
  "use strict";
6321
+ init_codegraph_extract_docs();
6093
6322
  IGNORE_DIRS = /* @__PURE__ */ new Set([
6094
6323
  "node_modules",
6095
6324
  ".git",
@@ -6114,7 +6343,7 @@ var init_codegraph_extract = __esm({
6114
6343
  ".gradle",
6115
6344
  ".dart_tool"
6116
6345
  ]);
6117
- LANG_BY_EXT = {
6346
+ CODE_LANG_BY_EXT = {
6118
6347
  ".ts": "ts",
6119
6348
  ".tsx": "ts",
6120
6349
  ".mts": "ts",
@@ -6140,6 +6369,9 @@ var init_codegraph_extract = __esm({
6140
6369
  ".hpp": "cpp",
6141
6370
  ".hh": "cpp"
6142
6371
  };
6372
+ LANG_BY_EXT = { ...CODE_LANG_BY_EXT, ...DOC_LANG_BY_EXT };
6373
+ FOREIGN_DOC_DIRS = /(?:^|\/)(?:references?|examples?|samples?|fixtures?|third[_-]party|vendor(?:ed)?)\//i;
6374
+ MAX_DOC_FILES = 1500;
6143
6375
  MAX_FILES = 8e3;
6144
6376
  MAX_FILE_BYTES = 512 * 1024;
6145
6377
  BRACE_LANGS = /* @__PURE__ */ new Set(["ts", "js", "go", "rust", "java", "csharp", "kotlin", "php", "c", "cpp"]);
@@ -6355,12 +6587,12 @@ function buildEdges(root, files, importEdges, resolveImport) {
6355
6587
  }
6356
6588
  const edges = [];
6357
6589
  const seen = /* @__PURE__ */ new Set();
6358
- const push = (from, to, rel) => {
6590
+ const push = (from, to, rel, inferred = false) => {
6359
6591
  if (edges.length >= MAX_EDGES || from === to) return;
6360
6592
  const key = `${from}|${to}|${rel}`;
6361
6593
  if (seen.has(key)) return;
6362
6594
  seen.add(key);
6363
- edges.push([from, to, rel]);
6595
+ edges.push(inferred ? [from, to, rel, 1] : [from, to, rel]);
6364
6596
  };
6365
6597
  for (const f of files) {
6366
6598
  for (const span of spansByFile.get(f.path) ?? []) push(f.path, span.id, "contains");
@@ -6389,12 +6621,14 @@ function buildEdges(root, files, importEdges, resolveImport) {
6389
6621
  const candidates = defsByName.get(name)?.filter((c) => imported?.has(c.path) && reachable(c.path));
6390
6622
  return candidates?.length === 1 ? candidates[0].id : null;
6391
6623
  };
6624
+ const extracted = (id) => id ? { id, inferred: false } : null;
6625
+ const inferred = (id) => id ? { id, inferred: true } : null;
6392
6626
  if (!f.bindings) {
6393
6627
  return (name) => {
6394
- if (local.has(name)) return local.get(name) ?? null;
6395
- if (imported?.size) return unique(name);
6628
+ if (local.has(name)) return extracted(local.get(name));
6629
+ if (imported?.size) return inferred(unique(name));
6396
6630
  const all = defsByName.get(name)?.filter((c) => reachable(c.path));
6397
- return all?.length === 1 ? all[0].id : null;
6631
+ return all?.length === 1 ? inferred(all[0].id) : null;
6398
6632
  };
6399
6633
  }
6400
6634
  const bound = new Set(f.bindings.names);
@@ -6406,10 +6640,10 @@ function buildEdges(root, files, importEdges, resolveImport) {
6406
6640
  return (name, memberOf) => {
6407
6641
  if (memberOf !== void 0) {
6408
6642
  const path = namespaces.get(memberOf);
6409
- return path ? idByPathName.get(`${path}#${name}`) ?? null : null;
6643
+ return path ? extracted(idByPathName.get(`${path}#${name}`)) : null;
6410
6644
  }
6411
- if (local.has(name)) return local.get(name) ?? null;
6412
- return bound.has(name) ? unique(name) : null;
6645
+ if (local.has(name)) return extracted(local.get(name));
6646
+ return bound.has(name) ? extracted(unique(name)) : null;
6413
6647
  };
6414
6648
  }
6415
6649
  const bodies = [];
@@ -6423,7 +6657,7 @@ function buildEdges(root, files, importEdges, resolveImport) {
6423
6657
  const spans = spansByFile.get(f.path) ?? [];
6424
6658
  const declLines = new Set(f.symbols.map((s) => s.line));
6425
6659
  const lines = content.split("\n");
6426
- indexBodies(bodies, f.path, spans, lines);
6660
+ indexBodies(bodies, f.path, spans, lines, DOC_LANGS.has(f.lang) ? MAX_DOC_BODY_TOKENS : MAX_BODY_TOKENS);
6427
6661
  const bind = binderFor(f);
6428
6662
  const strict = !!f.bindings;
6429
6663
  for (const s of f.symbols) {
@@ -6432,9 +6666,17 @@ function buildEdges(root, files, importEdges, resolveImport) {
6432
6666
  if (!self) continue;
6433
6667
  for (const [rel, name] of inheritanceTargets(s.signature)) {
6434
6668
  const target = bind(name);
6435
- if (target) push(self.id, target, rel);
6669
+ if (target) push(self.id, target.id, rel, target.inferred);
6670
+ }
6671
+ }
6672
+ if (f.lang === "markdown") {
6673
+ for (const { name, line } of markdownCodeMentions(content)) {
6674
+ if (name.length < MIN_DOC_MENTION) continue;
6675
+ const code = defsByName.get(name)?.filter((c) => !DOC_LANGS.has(langByPath.get(c.path) ?? ""));
6676
+ if (code?.length === 1) push(enclosingSymbol(spans, line) ?? f.path, code[0].id, "references", true);
6436
6677
  }
6437
6678
  }
6679
+ if (DOC_LANGS.has(f.lang) && !DOC_REFERENCE_LANGS.has(f.lang)) continue;
6438
6680
  for (let i = 0; i < lines.length; i++) {
6439
6681
  const lineNo = i + 1;
6440
6682
  if (declLines.has(lineNo)) continue;
@@ -6447,31 +6689,32 @@ function buildEdges(root, files, importEdges, resolveImport) {
6447
6689
  if (name.length < MIN_REF_NAME || !defsByName.has(name)) continue;
6448
6690
  const memberOf = strict ? /([A-Za-z_$][\w$]*)\s*\.\s*$/.exec(line.slice(0, m.index))?.[1] : void 0;
6449
6691
  const target = bind(name, memberOf);
6450
- if (!target || target === from) continue;
6692
+ if (!target || target.id === from) continue;
6451
6693
  const after = line.slice(m.index + name.length);
6452
- push(from, target, /^\s*\(/.test(after) ? "calls" : "references");
6694
+ const calls = !DOC_LANGS.has(f.lang) && /^\s*\(/.test(after);
6695
+ push(from, target.id, calls ? "calls" : "references", target.inferred);
6453
6696
  }
6454
6697
  }
6455
6698
  }
6456
6699
  return { edges, bodies };
6457
6700
  }
6458
- function indexBodies(out, path, spans, lines) {
6701
+ function indexBodies(out, path, spans, lines, limit) {
6459
6702
  const covered = new Uint8Array(lines.length + 1);
6460
6703
  for (const s of spans) {
6461
6704
  const text = lines.slice(s.line - 1, s.endLine).join("\n");
6462
6705
  for (let l = s.line; l <= Math.min(s.endLine, lines.length); l++) covered[l] = 1;
6463
- const bag = topTokens(text);
6706
+ const bag = topTokens(text, limit);
6464
6707
  if (bag.length) out.push([s.id, bag]);
6465
6708
  }
6466
6709
  const residual = [];
6467
6710
  for (let l = 1; l <= lines.length; l++) if (!covered[l]) residual.push(lines[l - 1]);
6468
- const fileBag = topTokens(residual.join("\n"));
6711
+ const fileBag = topTokens(residual.join("\n"), limit);
6469
6712
  if (fileBag.length) out.push([path, fileBag]);
6470
6713
  }
6471
- function topTokens(text) {
6714
+ function topTokens(text, limit) {
6472
6715
  const m = /* @__PURE__ */ new Map();
6473
6716
  for (const t of tokenize(text)) m.set(t, (m.get(t) ?? 0) + 1);
6474
- return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_BODY_TOKENS);
6717
+ return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit);
6475
6718
  }
6476
6719
  function isManagedPath(dir) {
6477
6720
  const managed = resolve4(join7(enigmaHome(), ".enigma"));
@@ -6689,17 +6932,18 @@ function codeGraphAvailable() {
6689
6932
  function codeGraphStatus() {
6690
6933
  return { enabled: readConfig().config.codeGraph, available: true, projects: listProjects().length };
6691
6934
  }
6692
- 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;
6935
+ 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;
6693
6936
  var init_codegraph = __esm({
6694
6937
  "src/codegraph.ts"() {
6695
6938
  "use strict";
6696
6939
  init_util();
6697
6940
  init_config();
6698
6941
  init_codegraph_rank();
6942
+ init_codegraph_extract_docs();
6699
6943
  init_codegraph_extract();
6700
6944
  init_codegraph_extract();
6701
6945
  init_codegraph_types();
6702
- GRAPH_VERSION = 4;
6946
+ GRAPH_VERSION = 5;
6703
6947
  RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".kt"];
6704
6948
  LANG_EXTS = {
6705
6949
  ts: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
@@ -6715,6 +6959,7 @@ var init_codegraph = __esm({
6715
6959
  };
6716
6960
  LANG_INDEX = { python: ["__init__"], go: ["mod"], rust: ["mod"] };
6717
6961
  MIN_REF_NAME = 3;
6962
+ MIN_DOC_MENTION = 4;
6718
6963
  MAX_EDGES = 4e5;
6719
6964
  LANG_FAMILY = {
6720
6965
  ts: "js",
@@ -6731,6 +6976,7 @@ var init_codegraph = __esm({
6731
6976
  EXT_ORDER = orders(RESOLVE_EXTS, LANG_EXTS);
6732
6977
  INDEX_ORDER = orders(INDEX_NAMES, LANG_INDEX);
6733
6978
  MAX_BODY_TOKENS = 80;
6979
+ MAX_DOC_BODY_TOKENS = 24;
6734
6980
  ROOT_MARKERS = [".git", "package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml", "composer.json"];
6735
6981
  MAX_ROOT_WALK = 6;
6736
6982
  }
@@ -6963,11 +7209,11 @@ ${result.note}` : structural.fallthrough;
6963
7209
  }
6964
7210
  function adjacency(edges, direction) {
6965
7211
  const adj = /* @__PURE__ */ new Map();
6966
- for (const [source, target, rel] of edges) {
7212
+ for (const [source, target, rel, guess] of edges) {
6967
7213
  if (!WALK_RELATIONS.has(rel)) continue;
6968
7214
  const key = direction === "in" ? target : source;
6969
7215
  const other = direction === "in" ? source : target;
6970
- const entry = { other, relation: rel };
7216
+ const entry = { other, relation: rel, inferred: guess === 1 };
6971
7217
  const arr = adj.get(key);
6972
7218
  if (arr) arr.push(entry);
6973
7219
  else adj.set(key, [entry]);
@@ -6996,13 +7242,13 @@ function codeGraphTrace(symbol, opts = {}) {
6996
7242
  for (let d = 1; d <= depth && frontier.length; d++) {
6997
7243
  const next = [];
6998
7244
  for (const current of frontier) {
6999
- for (const { other, relation } of adj.get(current) ?? []) {
7245
+ for (const { other, relation, inferred } of adj.get(current) ?? []) {
7000
7246
  if (visited.has(other)) continue;
7001
7247
  visited.add(other);
7002
7248
  const node = loaded.byId.get(other);
7003
7249
  if (!node) continue;
7004
7250
  if (inPrefix !== void 0 && !underPrefix(node.path, inPrefix)) continue;
7005
- hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d });
7251
+ hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d, ...inferred ? { inferred } : {} });
7006
7252
  next.push(other);
7007
7253
  }
7008
7254
  }
@@ -7041,8 +7287,8 @@ function dirKey(path, depth) {
7041
7287
  const segments = path.split("/");
7042
7288
  return segments.slice(0, Math.min(depth, segments.length)).join("/");
7043
7289
  }
7044
- function topHubs(nodes, deg, cap) {
7045
- 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);
7290
+ function topHubs(nodes, deg, cap2) {
7291
+ 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);
7046
7292
  }
7047
7293
  function codeGraphMap(opts = {}) {
7048
7294
  const loaded = load(opts);
@@ -7270,7 +7516,7 @@ ${r.note ?? empty}
7270
7516
  const lines = [head, ""];
7271
7517
  lines.push(`defined at: ${r.matched.map((m) => `${m.path}:${m.line}`).join(", ")}`, "");
7272
7518
  for (const h of r.hits) {
7273
- lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
7519
+ lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${h.inferred ? " (inferred)" : ""}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
7274
7520
  }
7275
7521
  lines.push("", `${r.hits.length} node${r.hits.length === 1 ? "" : "s"}.`);
7276
7522
  return `${lines.join("\n")}
@@ -7393,6 +7639,295 @@ var init_codegraph_format = __esm({
7393
7639
  }
7394
7640
  });
7395
7641
 
7642
+ // src/codegraph-cluster.ts
7643
+ function localMoves(level, totalWeight) {
7644
+ const { nodes, adj, selfLoops } = level;
7645
+ const degree = new Array(nodes).fill(0);
7646
+ for (let i = 0; i < nodes; i++) {
7647
+ let d = selfLoops[i] * 2;
7648
+ for (const w of adj[i].values()) d += w;
7649
+ degree[i] = d;
7650
+ }
7651
+ const comm = Array.from({ length: nodes }, (_, i) => i);
7652
+ const commDegree = degree.slice();
7653
+ const m2 = totalWeight * 2;
7654
+ for (let sweep = 0; sweep < MAX_SWEEPS; sweep++) {
7655
+ let moved = false;
7656
+ for (let i = 0; i < nodes; i++) {
7657
+ const current = comm[i];
7658
+ const toComm = /* @__PURE__ */ new Map();
7659
+ for (const [j, w] of adj[i]) toComm.set(comm[j], (toComm.get(comm[j]) ?? 0) + w);
7660
+ commDegree[current] -= degree[i];
7661
+ let best = current;
7662
+ let bestGain = (toComm.get(current) ?? 0) - commDegree[current] * degree[i] / m2;
7663
+ for (const [c, w] of [...toComm.entries()].sort((a, b) => a[0] - b[0])) {
7664
+ const gain = w - commDegree[c] * degree[i] / m2;
7665
+ if (gain > bestGain + 1e-12) {
7666
+ best = c;
7667
+ bestGain = gain;
7668
+ }
7669
+ }
7670
+ commDegree[best] += degree[i];
7671
+ if (best !== current) {
7672
+ comm[i] = best;
7673
+ moved = true;
7674
+ }
7675
+ }
7676
+ if (!moved) break;
7677
+ }
7678
+ return comm;
7679
+ }
7680
+ function clusterFiles(files, links) {
7681
+ const sorted = [...new Set(files)].sort();
7682
+ const index = new Map(sorted.map((f, i) => [f, i]));
7683
+ let level = { nodes: sorted.length, adj: sorted.map(() => /* @__PURE__ */ new Map()), selfLoops: new Array(sorted.length).fill(0) };
7684
+ let totalWeight = 0;
7685
+ for (const [a, b, w] of links) {
7686
+ const i = index.get(a);
7687
+ const j = index.get(b);
7688
+ if (i === void 0 || j === void 0 || w <= 0) continue;
7689
+ totalWeight += w;
7690
+ if (i === j) {
7691
+ level.selfLoops[i] += w;
7692
+ continue;
7693
+ }
7694
+ level.adj[i].set(j, (level.adj[i].get(j) ?? 0) + w);
7695
+ level.adj[j].set(i, (level.adj[j].get(i) ?? 0) + w);
7696
+ }
7697
+ let membership = sorted.map((_, i) => i);
7698
+ if (totalWeight === 0) return new Map(sorted.map((f, i) => [f, i]));
7699
+ for (let l = 0; l < MAX_LEVELS; l++) {
7700
+ const comm = localMoves(level, totalWeight);
7701
+ const ids = /* @__PURE__ */ new Map();
7702
+ for (const c of comm) if (!ids.has(c)) ids.set(c, ids.size);
7703
+ if (ids.size === level.nodes) break;
7704
+ membership = membership.map((c) => ids.get(comm[c]));
7705
+ const next = { nodes: ids.size, adj: Array.from({ length: ids.size }, () => /* @__PURE__ */ new Map()), selfLoops: new Array(ids.size).fill(0) };
7706
+ for (let i = 0; i < level.nodes; i++) {
7707
+ const ci = ids.get(comm[i]);
7708
+ next.selfLoops[ci] += level.selfLoops[i];
7709
+ for (const [j, w] of level.adj[i]) {
7710
+ const cj = ids.get(comm[j]);
7711
+ if (ci === cj) {
7712
+ if (i < j) next.selfLoops[ci] += w;
7713
+ } else next.adj[ci].set(cj, (next.adj[ci].get(cj) ?? 0) + w);
7714
+ }
7715
+ }
7716
+ level = next;
7717
+ }
7718
+ const members = /* @__PURE__ */ new Map();
7719
+ sorted.forEach((f, i) => {
7720
+ const c = membership[i];
7721
+ const list = members.get(c);
7722
+ if (list) list.push(f);
7723
+ else members.set(c, [f]);
7724
+ });
7725
+ const order = [...members.entries()].sort((a, b) => b[1].length - a[1].length || a[1][0].localeCompare(b[1][0]));
7726
+ const out = /* @__PURE__ */ new Map();
7727
+ order.forEach(([, list], rank) => {
7728
+ for (const f of list) out.set(f, rank);
7729
+ });
7730
+ return out;
7731
+ }
7732
+ var MAX_SWEEPS, MAX_LEVELS;
7733
+ var init_codegraph_cluster = __esm({
7734
+ "src/codegraph-cluster.ts"() {
7735
+ "use strict";
7736
+ MAX_SWEEPS = 20;
7737
+ MAX_LEVELS = 6;
7738
+ }
7739
+ });
7740
+
7741
+ // src/codegraph-report.ts
7742
+ var codegraph_report_exports = {};
7743
+ __export(codegraph_report_exports, {
7744
+ codeGraphReport: () => codeGraphReport,
7745
+ formatReport: () => formatReport
7746
+ });
7747
+ function areaLabel(paths) {
7748
+ const dirs = paths.map((p) => p.split("/").slice(0, -1));
7749
+ let label = [];
7750
+ for (let depth = 1; ; depth++) {
7751
+ const counts2 = /* @__PURE__ */ new Map();
7752
+ for (const d of dirs) if (d.length >= depth) {
7753
+ const k = d.slice(0, depth).join("/");
7754
+ counts2.set(k, (counts2.get(k) ?? 0) + 1);
7755
+ }
7756
+ const top2 = [...counts2.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
7757
+ if (!top2 || top2[1] / paths.length < LABEL_SHARE) break;
7758
+ label = top2[0].split("/");
7759
+ }
7760
+ if (label.length) return `${label.join("/")}/`;
7761
+ if (paths.length === 1) return paths[0];
7762
+ const first = /* @__PURE__ */ new Map();
7763
+ for (const d of dirs) {
7764
+ const k = d[0] ?? "(root)";
7765
+ first.set(k, (first.get(k) ?? 0) + 1);
7766
+ }
7767
+ const [top] = [...first.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
7768
+ return `${top}/ (mixed)`;
7769
+ }
7770
+ function codeGraphReport(opts = {}) {
7771
+ const graph = loadFreshGraph(opts.project, opts.refresh ?? true);
7772
+ if (!graph) return null;
7773
+ const nodes = graphNodes(graph);
7774
+ const byId = new Map(nodes.map((n) => [n.id, n]));
7775
+ const langByPath = new Map(graph.files.map((f) => [f.path, f.lang]));
7776
+ const own = graph.files.filter((f) => !isForeignPath(f.path));
7777
+ const codePaths = new Set(own.filter((f) => isCodeLang(f.lang)).map((f) => f.path));
7778
+ const docPaths = new Set(own.filter((f) => f.lang === "markdown").map((f) => f.path));
7779
+ const weights = /* @__PURE__ */ new Map();
7780
+ const directed = /* @__PURE__ */ new Map();
7781
+ const docRefs = /* @__PURE__ */ new Map();
7782
+ for (const e of graph.edges) {
7783
+ if (e[2] === "contains") continue;
7784
+ const a = byId.get(e[0])?.path;
7785
+ const b = byId.get(e[1])?.path;
7786
+ if (!a || !b || a === b) continue;
7787
+ if (docPaths.has(a) && codePaths.has(b)) {
7788
+ const refs = docRefs.get(a) ?? /* @__PURE__ */ new Map();
7789
+ refs.set(b, (refs.get(b) ?? 0) + 1);
7790
+ docRefs.set(a, refs);
7791
+ continue;
7792
+ }
7793
+ if (!codePaths.has(a) || !codePaths.has(b)) continue;
7794
+ const key = a < b ? `${a}
7795
+ ${b}` : `${b}
7796
+ ${a}`;
7797
+ weights.set(key, (weights.get(key) ?? 0) + 1);
7798
+ directed.set(`${a}
7799
+ ${b}`, (directed.get(`${a}
7800
+ ${b}`) ?? 0) + 1);
7801
+ }
7802
+ const links = [...weights.entries()].map(([k, w]) => {
7803
+ const [a, b] = k.split("\n");
7804
+ return [a, b, w];
7805
+ });
7806
+ const areaOf = clusterFiles([...codePaths], links);
7807
+ const members = /* @__PURE__ */ new Map();
7808
+ for (const [path, area] of areaOf) {
7809
+ const list = members.get(area);
7810
+ if (list) list.push(path);
7811
+ else members.set(area, [path]);
7812
+ }
7813
+ const deg = inDegree(graph.edges, true);
7814
+ const maxAreas = opts.maxAreas ?? DEFAULT_MAX_AREAS;
7815
+ const ranked = [...members.entries()].filter(([, paths]) => paths.length >= 2).sort((a, b) => b[1].length - a[1].length || a[0] - b[0]);
7816
+ const symbolsOf = /* @__PURE__ */ new Map();
7817
+ for (const n of nodes) {
7818
+ if (n.kind === "file" || isTestPath(n.path)) continue;
7819
+ const area = areaOf.get(n.path);
7820
+ if (area === void 0) continue;
7821
+ const list = symbolsOf.get(area);
7822
+ if (list) list.push(n);
7823
+ else symbolsOf.set(area, [n]);
7824
+ }
7825
+ const keys = /* @__PURE__ */ new Map();
7826
+ const keyOf = (area) => {
7827
+ let key = keys.get(area);
7828
+ if (key) return key;
7829
+ 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);
7830
+ keys.set(area, key);
7831
+ return key;
7832
+ };
7833
+ const base = new Map(ranked.map(([area, paths]) => [area, areaLabel(paths)]));
7834
+ const seenLabel = /* @__PURE__ */ new Map();
7835
+ for (const label of base.values()) seenLabel.set(label, (seenLabel.get(label) ?? 0) + 1);
7836
+ const labels = new Map([...base.entries()].map(([area, label]) => {
7837
+ if ((seenLabel.get(label) ?? 0) < 2) return [area, label];
7838
+ const around = keyOf(area)[0]?.name;
7839
+ return [area, around ? `${label} ~ ${around}` : label];
7840
+ }));
7841
+ const docsOf = /* @__PURE__ */ new Map();
7842
+ for (const [doc, refs] of docRefs) {
7843
+ const byArea = /* @__PURE__ */ new Map();
7844
+ for (const [code, w] of refs) {
7845
+ const area = areaOf.get(code);
7846
+ if (area !== void 0) byArea.set(area, (byArea.get(area) ?? 0) + w);
7847
+ }
7848
+ const best = [...byArea.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0];
7849
+ if (!best) continue;
7850
+ const list = docsOf.get(best[0]) ?? [];
7851
+ list.push(doc);
7852
+ docsOf.set(best[0], list);
7853
+ }
7854
+ const areas = ranked.slice(0, maxAreas).map(([area, paths]) => {
7855
+ const inArea = new Set(paths);
7856
+ const out = /* @__PURE__ */ new Map();
7857
+ for (const [key, w] of directed) {
7858
+ const [a, b] = key.split("\n");
7859
+ if (!inArea.has(a) || inArea.has(b)) continue;
7860
+ const target = areaOf.get(b);
7861
+ if (target !== void 0 && target !== area && labels.has(target)) out.set(target, (out.get(target) ?? 0) + w);
7862
+ }
7863
+ return {
7864
+ label: labels.get(area),
7865
+ files: paths.length,
7866
+ languages: [...new Set(paths.map((p) => langByPath.get(p)).filter((l) => !!l))].sort(),
7867
+ keySymbols: keyOf(area),
7868
+ docs: (docsOf.get(area) ?? []).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, AREA_DOCS),
7869
+ dependsOn: [...out.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, DEPENDS_ON).map(([t]) => labels.get(t))
7870
+ };
7871
+ });
7872
+ const titleOf = /* @__PURE__ */ new Map();
7873
+ for (const n of nodes) if (n.kind === "section" && !titleOf.has(n.path)) titleOf.set(n.path, n.name);
7874
+ const docFiles = own.filter((f) => f.lang === "markdown").sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
7875
+ const maxDocs = opts.maxDocs ?? DEFAULT_MAX_DOCS;
7876
+ return {
7877
+ name: graph.name,
7878
+ totals: {
7879
+ files: graph.files.length,
7880
+ symbols: nodes.length - graph.files.length,
7881
+ edges: graph.edges.length,
7882
+ inferred: graph.edges.filter((e) => e[3] === 1).length,
7883
+ docs: docFiles.length
7884
+ },
7885
+ areas,
7886
+ moreAreas: Math.max(0, ranked.length - areas.length),
7887
+ docs: docFiles.slice(0, maxDocs).map((f) => ({ path: f.path, title: titleOf.get(f.path) ?? "" })),
7888
+ moreDocs: Math.max(0, docFiles.length - maxDocs),
7889
+ foreign: graph.files.length - own.length,
7890
+ truncated: graph.truncated
7891
+ };
7892
+ }
7893
+ function formatReport(r) {
7894
+ const t = r.totals;
7895
+ const lines = [`repo report - ${r.name} - ${t.files} files (${t.docs} docs) - ${t.symbols} symbols - ${t.edges} edges (${t.inferred} inferred by name)`];
7896
+ if (r.foreign) lines.push(`${r.foreign} file(s) of vendored, reference or example material are left out of the areas below.`);
7897
+ lines.push("", "areas (files that depend on each other, largest first):");
7898
+ for (const a of r.areas) {
7899
+ lines.push(`- ${a.label} - ${a.files} files${a.languages.length ? ` - ${a.languages.join(", ")}` : ""}`);
7900
+ if (a.keySymbols.length) lines.push(` key: ${a.keySymbols.map((s) => `${s.name} (${s.path}:${s.line}, ${s.inDegree} in)`).join(", ")}`);
7901
+ if (a.dependsOn.length) lines.push(` uses: ${a.dependsOn.join(", ")}`);
7902
+ if (a.docs.length) lines.push(` docs: ${a.docs.join(", ")}`);
7903
+ }
7904
+ if (r.moreAreas) lines.push(` +${r.moreAreas} smaller area(s)`);
7905
+ if (r.docs.length) {
7906
+ lines.push("", "docs:");
7907
+ for (const d of r.docs) lines.push(`- ${d.path}${d.title ? ` - ${d.title}` : ""}`);
7908
+ if (r.moreDocs) lines.push(` +${r.moreDocs} more (enigma_codegraph_ask finds them by topic)`);
7909
+ }
7910
+ if (r.truncated) lines.push("", "The index hit its size cap; some files are not covered.");
7911
+ return `${lines.join("\n")}
7912
+ `;
7913
+ }
7914
+ var DEFAULT_MAX_AREAS, DEFAULT_MAX_DOCS, KEY_SYMBOLS, AREA_DOCS, DEPENDS_ON, LABEL_SHARE;
7915
+ var init_codegraph_report = __esm({
7916
+ "src/codegraph-report.ts"() {
7917
+ "use strict";
7918
+ init_codegraph();
7919
+ init_codegraph_rank();
7920
+ init_codegraph_extract();
7921
+ init_codegraph_cluster();
7922
+ DEFAULT_MAX_AREAS = 8;
7923
+ DEFAULT_MAX_DOCS = 12;
7924
+ KEY_SYMBOLS = 3;
7925
+ AREA_DOCS = 2;
7926
+ DEPENDS_ON = 2;
7927
+ LABEL_SHARE = 0.6;
7928
+ }
7929
+ });
7930
+
7396
7931
  // src/codegraph-hook.ts
7397
7932
  var codegraph_hook_exports = {};
7398
7933
  __export(codegraph_hook_exports, {
@@ -7471,14 +8006,17 @@ async function sessionStart(dir) {
7471
8006
  " - enigma_codegraph_skeleton: a file's whole API surface, no bodies.",
7472
8007
  " - enigma_codegraph_grep: every occurrence, grouped by enclosing symbol - when you need them ALL, not the top matches."
7473
8008
  ].join("\n");
7474
- const map = q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
8009
+ const rep = await Promise.resolve().then(() => (init_codegraph_report(), codegraph_report_exports));
8010
+ const report = rep.codeGraphReport({ project, refresh: false });
8011
+ const map = report ? null : q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
8012
+ const body = report ? rep.formatReport(report) : map ? fmt.formatMap(map) : "";
7475
8013
  const fresh = q.codeGraphCheck(project);
7476
8014
  const stale = fresh && fresh.stale > 0 ? `
7477
8015
  ${fresh.stale} file(s) changed since the last index; the next query refreshes them.` : "";
7478
- writeStatuslineSnapshot(projectRoot(cg, project, dir), map?.totals.symbols ?? 0, fresh?.stale ?? 0);
7479
- emit("SessionStart", map ? `${directive}
8016
+ writeStatuslineSnapshot(projectRoot(cg, project, dir), report?.totals.symbols ?? map?.totals.symbols ?? 0, fresh?.stale ?? 0);
8017
+ emit("SessionStart", body ? `${directive}
7480
8018
 
7481
- ${fmt.formatMap(map).trimEnd()}${stale}` : directive);
8019
+ ${body.trimEnd()}${stale}` : directive);
7482
8020
  }
7483
8021
  function coveringProject(cg, dir) {
7484
8022
  const project = cg.findProjectForCwd(dir);