enigma-cli 1.59.1 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/commands/handoff.md +48 -0
  2. package/assets/mods/enigma-relay/.claude-plugin/plugin.json +1 -0
  3. package/assets/mods/enigma-relay/hooks/hooks.json +1 -0
  4. package/assets/mods/enigma-relay/hooks/register.ts +86 -0
  5. package/assets/mods/enigma-relay/tests/relay.test.ts +65 -0
  6. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  7. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  8. package/assets/skills/backend-policy/skill.json +1 -1
  9. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  10. package/assets/skills/code-review-policy/skill.json +1 -1
  11. package/assets/skills/core-engineering-policy/skill.json +1 -1
  12. package/assets/skills/database-expert/skill.json +1 -1
  13. package/assets/skills/debugging-policy/skill.json +1 -1
  14. package/assets/skills/dependency-policy/skill.json +1 -1
  15. package/assets/skills/email-policy/skill.json +1 -1
  16. package/assets/skills/frontend-design/skill.json +1 -1
  17. package/assets/skills/frontend-policy/skill.json +1 -1
  18. package/assets/skills/git-policy/skill.json +1 -1
  19. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  20. package/assets/skills/security-policy/skill.json +1 -1
  21. package/assets/skills/skill-creator/skill.json +1 -1
  22. package/assets/skills/task-completion-policy/skill.json +1 -1
  23. package/assets/skills/technical-writing-policy/skill.json +1 -1
  24. package/assets/skills/testing-policy/skill.json +1 -1
  25. package/assets/skills/validation-policy/skill.json +1 -1
  26. package/bin/checksums.json +4 -4
  27. package/dist/ci-watch.js +2 -0
  28. package/dist/codegraph-hook.js +576 -48
  29. package/dist/post-edit.js +576 -48
  30. package/package.json +1 -1
package/dist/post-edit.js CHANGED
@@ -304,6 +304,8 @@ var init_config = __esm({
304
304
  gateTrivialLines: 20,
305
305
  subagentLimit: 4,
306
306
  compactWindow: 4e5,
307
+ relay: true,
308
+ relayAt: 3e5,
307
309
  planSessionLimit: 0,
308
310
  planWeeklyLimit: 0,
309
311
  planWeeklySonnetLimit: 0,
@@ -5686,10 +5688,222 @@ var init_codegraph_rank = __esm({
5686
5688
  }
5687
5689
  });
5688
5690
 
5691
+ // src/codegraph-extract-docs.ts
5692
+ function cap(text) {
5693
+ const t = text.trim();
5694
+ return t.length > MAX_SIGNATURE ? `${t.slice(0, MAX_SIGNATURE)}...` : t;
5695
+ }
5696
+ function indentOf(line) {
5697
+ let n = 0;
5698
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
5699
+ return n;
5700
+ }
5701
+ function fencedLines(lines) {
5702
+ const inside = new Uint8Array(lines.length);
5703
+ let fence = null;
5704
+ for (let i = 0; i < lines.length; i++) {
5705
+ const m = /^\s*(`{3,}|~{3,})/.exec(lines[i]);
5706
+ if (fence) {
5707
+ inside[i] = 1;
5708
+ if (m && m[1][0] === fence[0] && m[1].length >= fence.length) fence = null;
5709
+ continue;
5710
+ }
5711
+ if (m) {
5712
+ fence = m[1];
5713
+ inside[i] = 1;
5714
+ }
5715
+ }
5716
+ return inside;
5717
+ }
5718
+ function markdownSymbols(lines) {
5719
+ const fenced = fencedLines(lines);
5720
+ const heads = [];
5721
+ for (let i = 0; i < lines.length; i++) {
5722
+ if (fenced[i]) continue;
5723
+ const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i]);
5724
+ if (m && m[1].length <= MAX_HEADING_LEVEL) heads.push({ level: m[1].length, name: m[2].replace(/[`*_]/g, "").trim(), line: i + 1 });
5725
+ }
5726
+ return heads.filter((h) => h.name).map((h, idx) => {
5727
+ const next = heads.slice(idx + 1).find((o) => o.level <= h.level);
5728
+ return { name: h.name, kind: "section", line: h.line, endLine: next ? next.line - 1 : lines.length, signature: cap(lines[h.line - 1]) };
5729
+ });
5730
+ }
5731
+ function sqlEnd(lines, start) {
5732
+ let dollar = false;
5733
+ for (let i = start; i < lines.length; i++) {
5734
+ const parts = lines[i].replace(/--.*$/, "").split("$$");
5735
+ for (let p = 0; p < parts.length; p++) {
5736
+ if (p > 0) dollar = !dollar;
5737
+ if (!dollar && parts[p].includes(";")) return i + 1;
5738
+ }
5739
+ }
5740
+ return lines.length;
5741
+ }
5742
+ function sqlSymbols(lines) {
5743
+ const rules = [
5744
+ ["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")],
5745
+ ["function", new RegExp(String.raw`^\s*create\s+(?:or\s+replace\s+)?(?:function|procedure)\s+${SQL_NAME}`, "i")],
5746
+ ["type", new RegExp(String.raw`^\s*create\s+type\s+${SQL_NAME}`, "i")]
5747
+ ];
5748
+ const out = [];
5749
+ for (let i = 0; i < lines.length; i++) {
5750
+ for (const [kind, re] of rules) {
5751
+ const m = re.exec(lines[i]);
5752
+ if (!m) continue;
5753
+ out.push({ name: m[1], kind, line: i + 1, endLine: sqlEnd(lines, i), signature: cap(lines[i].replace(/\(.*$/, "")) });
5754
+ break;
5755
+ }
5756
+ }
5757
+ return out;
5758
+ }
5759
+ function prismaSymbols(lines) {
5760
+ const out = [];
5761
+ for (let i = 0; i < lines.length; i++) {
5762
+ const m = /^\s*(model|view|enum|type)\s+([A-Za-z_]\w*)\s*\{/.exec(lines[i]);
5763
+ if (!m) continue;
5764
+ let end = i;
5765
+ while (end < lines.length - 1 && !/^\s*\}/.test(lines[end])) end++;
5766
+ const kind = m[1] === "enum" ? "enum" : m[1] === "type" ? "type" : "table";
5767
+ out.push({ name: m[2], kind, line: i + 1, endLine: end + 1, signature: cap(`${m[1]} ${m[2]}`) });
5768
+ }
5769
+ return out;
5770
+ }
5771
+ function yamlEnd(lines, start) {
5772
+ const base = indentOf(lines[start]);
5773
+ let end = start + 1;
5774
+ for (let i = start + 1; i < lines.length; i++) {
5775
+ const line = lines[i];
5776
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
5777
+ if (indentOf(line) <= base && !line.trimStart().startsWith("- ")) break;
5778
+ end = i + 1;
5779
+ }
5780
+ return end;
5781
+ }
5782
+ function yamlSymbols(lines) {
5783
+ const out = [];
5784
+ const KEY = /^(\s*)([A-Za-z0-9_][\w.-]*)\s*:(?:\s|$)/;
5785
+ for (let i = 0; i < lines.length; i++) {
5786
+ const m = KEY.exec(lines[i]);
5787
+ if (!m || m[1].length) continue;
5788
+ const end = yamlEnd(lines, i);
5789
+ out.push({ name: m[2], kind: "key", line: i + 1, endLine: end, signature: cap(lines[i]) });
5790
+ if (!YAML_CONTAINERS.has(m[2])) continue;
5791
+ let childIndent = -1;
5792
+ for (let j = i + 1; j < end; j++) {
5793
+ const c = KEY.exec(lines[j]);
5794
+ if (!c || !c[1].length) continue;
5795
+ if (childIndent === -1) childIndent = c[1].length;
5796
+ if (c[1].length !== childIndent) continue;
5797
+ out.push({ name: c[2], kind: "key", line: j + 1, endLine: yamlEnd(lines, j), signature: cap(`${m[2]}.${c[2]}`) });
5798
+ }
5799
+ }
5800
+ return out;
5801
+ }
5802
+ function tomlSymbols(lines) {
5803
+ const heads = [];
5804
+ for (let i = 0; i < lines.length; i++) {
5805
+ const m = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/.exec(lines[i]);
5806
+ if (m) heads.push({ name: m[1], line: i + 1 });
5807
+ }
5808
+ return heads.map((h, idx) => ({
5809
+ name: h.name,
5810
+ kind: "key",
5811
+ line: h.line,
5812
+ endLine: idx + 1 < heads.length ? heads[idx + 1].line - 1 : lines.length,
5813
+ signature: cap(lines[h.line - 1])
5814
+ }));
5815
+ }
5816
+ function markdownLinks(lines) {
5817
+ const fenced = fencedLines(lines);
5818
+ const out = /* @__PURE__ */ new Set();
5819
+ for (let i = 0; i < lines.length; i++) {
5820
+ if (fenced[i]) continue;
5821
+ const re = /\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
5822
+ let m;
5823
+ while (m = re.exec(lines[i])) {
5824
+ const target = m[1].replace(/[#?].*$/, "");
5825
+ if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue;
5826
+ try {
5827
+ out.add(decodeURI(target));
5828
+ } catch {
5829
+ out.add(target);
5830
+ }
5831
+ }
5832
+ }
5833
+ return [...out];
5834
+ }
5835
+ function markdownCodeMentions(content) {
5836
+ const lines = content.split("\n");
5837
+ const fenced = fencedLines(lines);
5838
+ const out = [];
5839
+ for (let i = 0; i < lines.length; i++) {
5840
+ if (fenced[i]) continue;
5841
+ const re = /`([A-Za-z_$][\w$]*)(?:\(\))?`/g;
5842
+ let m;
5843
+ while (m = re.exec(lines[i])) out.push({ name: m[1], line: i + 1 });
5844
+ }
5845
+ return out;
5846
+ }
5847
+ function extractDocFile(lang, content) {
5848
+ const lines = content.split("\n");
5849
+ const out = (() => {
5850
+ switch (lang) {
5851
+ case "markdown":
5852
+ return { symbols: markdownSymbols(lines), imports: markdownLinks(lines) };
5853
+ case "sql":
5854
+ return { symbols: sqlSymbols(lines), imports: [] };
5855
+ case "prisma":
5856
+ return { symbols: prismaSymbols(lines), imports: [] };
5857
+ case "yaml":
5858
+ return { symbols: yamlSymbols(lines), imports: [] };
5859
+ case "toml":
5860
+ return { symbols: tomlSymbols(lines), imports: [] };
5861
+ default:
5862
+ return { symbols: [], imports: [] };
5863
+ }
5864
+ })();
5865
+ for (const s of out.symbols) while (s.endLine > s.line && !(lines[s.endLine - 1] ?? "").trim()) s.endLine--;
5866
+ return out;
5867
+ }
5868
+ var DOC_LANG_BY_EXT, DOC_LANGS, DOC_REFERENCE_LANGS, YAML_CONTAINERS, MAX_SIGNATURE, MAX_HEADING_LEVEL, SQL_NAME;
5869
+ var init_codegraph_extract_docs = __esm({
5870
+ "src/codegraph-extract-docs.ts"() {
5871
+ "use strict";
5872
+ DOC_LANG_BY_EXT = {
5873
+ ".md": "markdown",
5874
+ ".mdx": "markdown",
5875
+ ".sql": "sql",
5876
+ ".prisma": "prisma",
5877
+ ".yaml": "yaml",
5878
+ ".yml": "yaml",
5879
+ ".toml": "toml"
5880
+ };
5881
+ DOC_LANGS = new Set(Object.values(DOC_LANG_BY_EXT));
5882
+ DOC_REFERENCE_LANGS = /* @__PURE__ */ new Set(["sql", "prisma"]);
5883
+ YAML_CONTAINERS = /* @__PURE__ */ new Set(["services", "jobs", "workflows", "volumes", "networks"]);
5884
+ MAX_SIGNATURE = 200;
5885
+ MAX_HEADING_LEVEL = 3;
5886
+ SQL_NAME = String.raw`(?:[\w"\x60\[\]]+\.)?["\x60\[]?([A-Za-z_][\w$]*)["\x60\]]?`;
5887
+ }
5888
+ });
5889
+
5689
5890
  // src/codegraph-extract.ts
5690
5891
  import { execFileSync as execFileSync4 } from "child_process";
5691
5892
  import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
5692
5893
  import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
5894
+ function isCodeLang(lang) {
5895
+ return !DOC_LANGS.has(lang);
5896
+ }
5897
+ function isDocPath(path) {
5898
+ return !!DOC_LANG_BY_EXT[extname2(path).toLowerCase()];
5899
+ }
5900
+ function isForeignPath(rel) {
5901
+ return FOREIGN_DOC_DIRS.test(rel);
5902
+ }
5903
+ function pickDocs(root, docs, room) {
5904
+ const depth = (p) => relPath(root, p).split("/").length;
5905
+ 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)));
5906
+ }
5693
5907
  function symRules(lang) {
5694
5908
  switch (lang) {
5695
5909
  case "ts":
@@ -5854,7 +6068,7 @@ function lineResolver(content) {
5854
6068
  return lo + 1;
5855
6069
  };
5856
6070
  }
5857
- function indentOf(line) {
6071
+ function indentOf2(line) {
5858
6072
  let n = 0;
5859
6073
  while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
5860
6074
  return n;
@@ -5920,18 +6134,18 @@ function braceEnd(lines, startIdx, lang) {
5920
6134
  return lines.length;
5921
6135
  }
5922
6136
  function indentEnd(lines, startIdx) {
5923
- const base = indentOf(lines[startIdx]);
6137
+ const base = indentOf2(lines[startIdx]);
5924
6138
  let end = startIdx + 1;
5925
6139
  for (let i = startIdx + 1; i < lines.length; i++) {
5926
6140
  const line = lines[i];
5927
6141
  if (!line.trim()) continue;
5928
- if (indentOf(line) <= base) break;
6142
+ if (indentOf2(line) <= base) break;
5929
6143
  end = i + 1;
5930
6144
  }
5931
6145
  return end;
5932
6146
  }
5933
6147
  function clampToSiblings(symbols, lines) {
5934
- const indentAt = (line) => indentOf(lines[line - 1] ?? "");
6148
+ const indentAt = (line) => indentOf2(lines[line - 1] ?? "");
5935
6149
  for (let i = 0; i < symbols.length; i++) {
5936
6150
  const s = symbols[i];
5937
6151
  for (let j = i + 1; j < symbols.length; j++) {
@@ -5944,14 +6158,15 @@ function clampToSiblings(symbols, lines) {
5944
6158
  }
5945
6159
  }
5946
6160
  function signatureOf(line) {
5947
- const MAX_SIGNATURE = 200;
6161
+ const MAX_SIGNATURE2 = 200;
5948
6162
  let sig = line.trim();
5949
6163
  const brace = sig.indexOf("{");
5950
6164
  if (brace > 0) sig = sig.slice(0, brace).trim();
5951
6165
  if (sig.endsWith(":")) sig = sig.slice(0, -1).trim();
5952
- return sig.length > MAX_SIGNATURE ? `${sig.slice(0, MAX_SIGNATURE)}...` : sig;
6166
+ return sig.length > MAX_SIGNATURE2 ? `${sig.slice(0, MAX_SIGNATURE2)}...` : sig;
5953
6167
  }
5954
6168
  function extractFile(lang, content) {
6169
+ if (DOC_LANGS.has(lang)) return extractDocFile(lang, content);
5955
6170
  const lineAt = lineResolver(content);
5956
6171
  const lines = content.split("\n");
5957
6172
  const braced = BRACE_LANGS.has(lang);
@@ -5993,25 +6208,27 @@ function gitSourceFiles(root) {
5993
6208
  const out = gitList(root, ["--cached", "--others", "--exclude-standard"]);
5994
6209
  if (out === null) return null;
5995
6210
  const nested = existsSync5(join6(root, ".gitmodules")) ? gitList(root, ["--recurse-submodules"]) : null;
5996
- const files = [];
6211
+ const code = [];
6212
+ const docs = [];
5997
6213
  const seen = /* @__PURE__ */ new Set();
5998
6214
  for (const rel of [...out.split("\0"), ...(nested ?? "").split("\0")]) {
5999
- if (files.length >= MAX_FILES) break;
6215
+ if (code.length >= MAX_FILES) break;
6000
6216
  if (!rel || seen.has(rel)) continue;
6001
6217
  seen.add(rel);
6002
6218
  if (!LANG_BY_EXT[extname2(rel).toLowerCase()]) continue;
6003
6219
  if (rel.split("/").some((seg) => seg.startsWith(".") || IGNORE_DIRS.has(seg))) continue;
6004
- files.push(join6(root, rel));
6220
+ (isDocPath(rel) ? docs : code).push(join6(root, rel));
6005
6221
  }
6006
- return files;
6222
+ return [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
6007
6223
  }
6008
6224
  function walk(root) {
6009
6225
  const tracked = gitSourceFiles(root);
6010
6226
  if (tracked && tracked.length) return { files: tracked, truncated: tracked.length >= MAX_FILES };
6011
- const files = [];
6227
+ const code = [];
6228
+ const docs = [];
6012
6229
  const queue = [root];
6013
6230
  let visited = 0;
6014
- for (let head = 0; head < queue.length && files.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
6231
+ for (let head = 0; head < queue.length && code.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
6015
6232
  let entries;
6016
6233
  try {
6017
6234
  entries = readdirSync3(queue[head], { withFileTypes: true });
@@ -6019,16 +6236,17 @@ function walk(root) {
6019
6236
  continue;
6020
6237
  }
6021
6238
  for (const e of entries) {
6022
- if (files.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
6239
+ if (code.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
6023
6240
  if (e.isDirectory()) {
6024
6241
  if (!e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) queue.push(join6(queue[head], e.name));
6025
6242
  continue;
6026
6243
  }
6027
- if (!e.isFile()) continue;
6028
- if (LANG_BY_EXT[extname2(e.name).toLowerCase()]) files.push(join6(queue[head], e.name));
6244
+ if (!e.isFile() || !LANG_BY_EXT[extname2(e.name).toLowerCase()]) continue;
6245
+ (isDocPath(e.name) ? docs : code).push(join6(queue[head], e.name));
6029
6246
  }
6030
6247
  }
6031
- return { files, truncated: files.length >= MAX_FILES || visited >= MAX_ENTRIES };
6248
+ const ordered = [...code, ...pickDocs(root, docs, MAX_FILES - code.length)];
6249
+ return { files: ordered, truncated: code.length >= MAX_FILES || visited >= MAX_ENTRIES };
6032
6250
  }
6033
6251
  function relPath(root, abs) {
6034
6252
  return relative2(root, abs).split("\\").join("/");
@@ -6086,10 +6304,11 @@ function statSourceFiles(root) {
6086
6304
  }
6087
6305
  return out;
6088
6306
  }
6089
- var IGNORE_DIRS, LANG_BY_EXT, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
6307
+ 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
6308
  var init_codegraph_extract = __esm({
6091
6309
  "src/codegraph-extract.ts"() {
6092
6310
  "use strict";
6311
+ init_codegraph_extract_docs();
6093
6312
  IGNORE_DIRS = /* @__PURE__ */ new Set([
6094
6313
  "node_modules",
6095
6314
  ".git",
@@ -6114,7 +6333,7 @@ var init_codegraph_extract = __esm({
6114
6333
  ".gradle",
6115
6334
  ".dart_tool"
6116
6335
  ]);
6117
- LANG_BY_EXT = {
6336
+ CODE_LANG_BY_EXT = {
6118
6337
  ".ts": "ts",
6119
6338
  ".tsx": "ts",
6120
6339
  ".mts": "ts",
@@ -6140,6 +6359,9 @@ var init_codegraph_extract = __esm({
6140
6359
  ".hpp": "cpp",
6141
6360
  ".hh": "cpp"
6142
6361
  };
6362
+ LANG_BY_EXT = { ...CODE_LANG_BY_EXT, ...DOC_LANG_BY_EXT };
6363
+ FOREIGN_DOC_DIRS = /(?:^|\/)(?:references?|examples?|samples?|fixtures?|third[_-]party|vendor(?:ed)?)\//i;
6364
+ MAX_DOC_FILES = 1500;
6143
6365
  MAX_FILES = 8e3;
6144
6366
  MAX_FILE_BYTES = 512 * 1024;
6145
6367
  BRACE_LANGS = /* @__PURE__ */ new Set(["ts", "js", "go", "rust", "java", "csharp", "kotlin", "php", "c", "cpp"]);
@@ -6355,12 +6577,12 @@ function buildEdges(root, files, importEdges, resolveImport) {
6355
6577
  }
6356
6578
  const edges = [];
6357
6579
  const seen = /* @__PURE__ */ new Set();
6358
- const push = (from, to, rel) => {
6580
+ const push = (from, to, rel, inferred = false) => {
6359
6581
  if (edges.length >= MAX_EDGES || from === to) return;
6360
6582
  const key = `${from}|${to}|${rel}`;
6361
6583
  if (seen.has(key)) return;
6362
6584
  seen.add(key);
6363
- edges.push([from, to, rel]);
6585
+ edges.push(inferred ? [from, to, rel, 1] : [from, to, rel]);
6364
6586
  };
6365
6587
  for (const f of files) {
6366
6588
  for (const span of spansByFile.get(f.path) ?? []) push(f.path, span.id, "contains");
@@ -6389,12 +6611,14 @@ function buildEdges(root, files, importEdges, resolveImport) {
6389
6611
  const candidates = defsByName.get(name)?.filter((c) => imported?.has(c.path) && reachable(c.path));
6390
6612
  return candidates?.length === 1 ? candidates[0].id : null;
6391
6613
  };
6614
+ const extracted = (id) => id ? { id, inferred: false } : null;
6615
+ const inferred = (id) => id ? { id, inferred: true } : null;
6392
6616
  if (!f.bindings) {
6393
6617
  return (name) => {
6394
- if (local.has(name)) return local.get(name) ?? null;
6395
- if (imported?.size) return unique(name);
6618
+ if (local.has(name)) return extracted(local.get(name));
6619
+ if (imported?.size) return inferred(unique(name));
6396
6620
  const all = defsByName.get(name)?.filter((c) => reachable(c.path));
6397
- return all?.length === 1 ? all[0].id : null;
6621
+ return all?.length === 1 ? inferred(all[0].id) : null;
6398
6622
  };
6399
6623
  }
6400
6624
  const bound = new Set(f.bindings.names);
@@ -6406,10 +6630,10 @@ function buildEdges(root, files, importEdges, resolveImport) {
6406
6630
  return (name, memberOf) => {
6407
6631
  if (memberOf !== void 0) {
6408
6632
  const path = namespaces.get(memberOf);
6409
- return path ? idByPathName.get(`${path}#${name}`) ?? null : null;
6633
+ return path ? extracted(idByPathName.get(`${path}#${name}`)) : null;
6410
6634
  }
6411
- if (local.has(name)) return local.get(name) ?? null;
6412
- return bound.has(name) ? unique(name) : null;
6635
+ if (local.has(name)) return extracted(local.get(name));
6636
+ return bound.has(name) ? extracted(unique(name)) : null;
6413
6637
  };
6414
6638
  }
6415
6639
  const bodies = [];
@@ -6423,7 +6647,7 @@ function buildEdges(root, files, importEdges, resolveImport) {
6423
6647
  const spans = spansByFile.get(f.path) ?? [];
6424
6648
  const declLines = new Set(f.symbols.map((s) => s.line));
6425
6649
  const lines = content.split("\n");
6426
- indexBodies(bodies, f.path, spans, lines);
6650
+ indexBodies(bodies, f.path, spans, lines, DOC_LANGS.has(f.lang) ? MAX_DOC_BODY_TOKENS : MAX_BODY_TOKENS);
6427
6651
  const bind = binderFor(f);
6428
6652
  const strict = !!f.bindings;
6429
6653
  for (const s of f.symbols) {
@@ -6432,9 +6656,17 @@ function buildEdges(root, files, importEdges, resolveImport) {
6432
6656
  if (!self) continue;
6433
6657
  for (const [rel, name] of inheritanceTargets(s.signature)) {
6434
6658
  const target = bind(name);
6435
- if (target) push(self.id, target, rel);
6659
+ if (target) push(self.id, target.id, rel, target.inferred);
6660
+ }
6661
+ }
6662
+ if (f.lang === "markdown") {
6663
+ for (const { name, line } of markdownCodeMentions(content)) {
6664
+ if (name.length < MIN_DOC_MENTION) continue;
6665
+ const code = defsByName.get(name)?.filter((c) => !DOC_LANGS.has(langByPath.get(c.path) ?? ""));
6666
+ if (code?.length === 1) push(enclosingSymbol(spans, line) ?? f.path, code[0].id, "references", true);
6436
6667
  }
6437
6668
  }
6669
+ if (DOC_LANGS.has(f.lang) && !DOC_REFERENCE_LANGS.has(f.lang)) continue;
6438
6670
  for (let i = 0; i < lines.length; i++) {
6439
6671
  const lineNo = i + 1;
6440
6672
  if (declLines.has(lineNo)) continue;
@@ -6447,31 +6679,32 @@ function buildEdges(root, files, importEdges, resolveImport) {
6447
6679
  if (name.length < MIN_REF_NAME || !defsByName.has(name)) continue;
6448
6680
  const memberOf = strict ? /([A-Za-z_$][\w$]*)\s*\.\s*$/.exec(line.slice(0, m.index))?.[1] : void 0;
6449
6681
  const target = bind(name, memberOf);
6450
- if (!target || target === from) continue;
6682
+ if (!target || target.id === from) continue;
6451
6683
  const after = line.slice(m.index + name.length);
6452
- push(from, target, /^\s*\(/.test(after) ? "calls" : "references");
6684
+ const calls = !DOC_LANGS.has(f.lang) && /^\s*\(/.test(after);
6685
+ push(from, target.id, calls ? "calls" : "references", target.inferred);
6453
6686
  }
6454
6687
  }
6455
6688
  }
6456
6689
  return { edges, bodies };
6457
6690
  }
6458
- function indexBodies(out, path, spans, lines) {
6691
+ function indexBodies(out, path, spans, lines, limit) {
6459
6692
  const covered = new Uint8Array(lines.length + 1);
6460
6693
  for (const s of spans) {
6461
6694
  const text = lines.slice(s.line - 1, s.endLine).join("\n");
6462
6695
  for (let l = s.line; l <= Math.min(s.endLine, lines.length); l++) covered[l] = 1;
6463
- const bag = topTokens(text);
6696
+ const bag = topTokens(text, limit);
6464
6697
  if (bag.length) out.push([s.id, bag]);
6465
6698
  }
6466
6699
  const residual = [];
6467
6700
  for (let l = 1; l <= lines.length; l++) if (!covered[l]) residual.push(lines[l - 1]);
6468
- const fileBag = topTokens(residual.join("\n"));
6701
+ const fileBag = topTokens(residual.join("\n"), limit);
6469
6702
  if (fileBag.length) out.push([path, fileBag]);
6470
6703
  }
6471
- function topTokens(text) {
6704
+ function topTokens(text, limit) {
6472
6705
  const m = /* @__PURE__ */ new Map();
6473
6706
  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);
6707
+ return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit);
6475
6708
  }
6476
6709
  function isManagedPath(dir) {
6477
6710
  const managed = resolve4(join7(enigmaHome(), ".enigma"));
@@ -6689,17 +6922,18 @@ function codeGraphAvailable() {
6689
6922
  function codeGraphStatus() {
6690
6923
  return { enabled: readConfig().config.codeGraph, available: true, projects: listProjects().length };
6691
6924
  }
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;
6925
+ 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
6926
  var init_codegraph = __esm({
6694
6927
  "src/codegraph.ts"() {
6695
6928
  "use strict";
6696
6929
  init_util();
6697
6930
  init_config();
6698
6931
  init_codegraph_rank();
6932
+ init_codegraph_extract_docs();
6699
6933
  init_codegraph_extract();
6700
6934
  init_codegraph_extract();
6701
6935
  init_codegraph_types();
6702
- GRAPH_VERSION = 4;
6936
+ GRAPH_VERSION = 5;
6703
6937
  RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".kt"];
6704
6938
  LANG_EXTS = {
6705
6939
  ts: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
@@ -6715,6 +6949,7 @@ var init_codegraph = __esm({
6715
6949
  };
6716
6950
  LANG_INDEX = { python: ["__init__"], go: ["mod"], rust: ["mod"] };
6717
6951
  MIN_REF_NAME = 3;
6952
+ MIN_DOC_MENTION = 4;
6718
6953
  MAX_EDGES = 4e5;
6719
6954
  LANG_FAMILY = {
6720
6955
  ts: "js",
@@ -6731,6 +6966,7 @@ var init_codegraph = __esm({
6731
6966
  EXT_ORDER = orders(RESOLVE_EXTS, LANG_EXTS);
6732
6967
  INDEX_ORDER = orders(INDEX_NAMES, LANG_INDEX);
6733
6968
  MAX_BODY_TOKENS = 80;
6969
+ MAX_DOC_BODY_TOKENS = 24;
6734
6970
  ROOT_MARKERS = [".git", "package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml", "composer.json"];
6735
6971
  MAX_ROOT_WALK = 6;
6736
6972
  }
@@ -6963,11 +7199,11 @@ ${result.note}` : structural.fallthrough;
6963
7199
  }
6964
7200
  function adjacency(edges, direction) {
6965
7201
  const adj = /* @__PURE__ */ new Map();
6966
- for (const [source, target, rel] of edges) {
7202
+ for (const [source, target, rel, guess] of edges) {
6967
7203
  if (!WALK_RELATIONS.has(rel)) continue;
6968
7204
  const key = direction === "in" ? target : source;
6969
7205
  const other = direction === "in" ? source : target;
6970
- const entry = { other, relation: rel };
7206
+ const entry = { other, relation: rel, inferred: guess === 1 };
6971
7207
  const arr = adj.get(key);
6972
7208
  if (arr) arr.push(entry);
6973
7209
  else adj.set(key, [entry]);
@@ -6996,13 +7232,13 @@ function codeGraphTrace(symbol, opts = {}) {
6996
7232
  for (let d = 1; d <= depth && frontier.length; d++) {
6997
7233
  const next = [];
6998
7234
  for (const current of frontier) {
6999
- for (const { other, relation } of adj.get(current) ?? []) {
7235
+ for (const { other, relation, inferred } of adj.get(current) ?? []) {
7000
7236
  if (visited.has(other)) continue;
7001
7237
  visited.add(other);
7002
7238
  const node = loaded.byId.get(other);
7003
7239
  if (!node) continue;
7004
7240
  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 });
7241
+ hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d, ...inferred ? { inferred } : {} });
7006
7242
  next.push(other);
7007
7243
  }
7008
7244
  }
@@ -7041,8 +7277,8 @@ function dirKey(path, depth) {
7041
7277
  const segments = path.split("/");
7042
7278
  return segments.slice(0, Math.min(depth, segments.length)).join("/");
7043
7279
  }
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);
7280
+ function topHubs(nodes, deg, cap2) {
7281
+ 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
7282
  }
7047
7283
  function codeGraphMap(opts = {}) {
7048
7284
  const loaded = load(opts);
@@ -7270,7 +7506,7 @@ ${r.note ?? empty}
7270
7506
  const lines = [head, ""];
7271
7507
  lines.push(`defined at: ${r.matched.map((m) => `${m.path}:${m.line}`).join(", ")}`, "");
7272
7508
  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}` : ""}`);
7509
+ lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${h.inferred ? " (inferred)" : ""}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
7274
7510
  }
7275
7511
  lines.push("", `${r.hits.length} node${r.hits.length === 1 ? "" : "s"}.`);
7276
7512
  return `${lines.join("\n")}
@@ -7393,6 +7629,295 @@ var init_codegraph_format = __esm({
7393
7629
  }
7394
7630
  });
7395
7631
 
7632
+ // src/codegraph-cluster.ts
7633
+ function localMoves(level, totalWeight) {
7634
+ const { nodes, adj, selfLoops } = level;
7635
+ const degree = new Array(nodes).fill(0);
7636
+ for (let i = 0; i < nodes; i++) {
7637
+ let d = selfLoops[i] * 2;
7638
+ for (const w of adj[i].values()) d += w;
7639
+ degree[i] = d;
7640
+ }
7641
+ const comm = Array.from({ length: nodes }, (_, i) => i);
7642
+ const commDegree = degree.slice();
7643
+ const m2 = totalWeight * 2;
7644
+ for (let sweep = 0; sweep < MAX_SWEEPS; sweep++) {
7645
+ let moved = false;
7646
+ for (let i = 0; i < nodes; i++) {
7647
+ const current = comm[i];
7648
+ const toComm = /* @__PURE__ */ new Map();
7649
+ for (const [j, w] of adj[i]) toComm.set(comm[j], (toComm.get(comm[j]) ?? 0) + w);
7650
+ commDegree[current] -= degree[i];
7651
+ let best = current;
7652
+ let bestGain = (toComm.get(current) ?? 0) - commDegree[current] * degree[i] / m2;
7653
+ for (const [c, w] of [...toComm.entries()].sort((a, b) => a[0] - b[0])) {
7654
+ const gain = w - commDegree[c] * degree[i] / m2;
7655
+ if (gain > bestGain + 1e-12) {
7656
+ best = c;
7657
+ bestGain = gain;
7658
+ }
7659
+ }
7660
+ commDegree[best] += degree[i];
7661
+ if (best !== current) {
7662
+ comm[i] = best;
7663
+ moved = true;
7664
+ }
7665
+ }
7666
+ if (!moved) break;
7667
+ }
7668
+ return comm;
7669
+ }
7670
+ function clusterFiles(files, links) {
7671
+ const sorted = [...new Set(files)].sort();
7672
+ const index = new Map(sorted.map((f, i) => [f, i]));
7673
+ let level = { nodes: sorted.length, adj: sorted.map(() => /* @__PURE__ */ new Map()), selfLoops: new Array(sorted.length).fill(0) };
7674
+ let totalWeight = 0;
7675
+ for (const [a, b, w] of links) {
7676
+ const i = index.get(a);
7677
+ const j = index.get(b);
7678
+ if (i === void 0 || j === void 0 || w <= 0) continue;
7679
+ totalWeight += w;
7680
+ if (i === j) {
7681
+ level.selfLoops[i] += w;
7682
+ continue;
7683
+ }
7684
+ level.adj[i].set(j, (level.adj[i].get(j) ?? 0) + w);
7685
+ level.adj[j].set(i, (level.adj[j].get(i) ?? 0) + w);
7686
+ }
7687
+ let membership = sorted.map((_, i) => i);
7688
+ if (totalWeight === 0) return new Map(sorted.map((f, i) => [f, i]));
7689
+ for (let l = 0; l < MAX_LEVELS; l++) {
7690
+ const comm = localMoves(level, totalWeight);
7691
+ const ids = /* @__PURE__ */ new Map();
7692
+ for (const c of comm) if (!ids.has(c)) ids.set(c, ids.size);
7693
+ if (ids.size === level.nodes) break;
7694
+ membership = membership.map((c) => ids.get(comm[c]));
7695
+ const next = { nodes: ids.size, adj: Array.from({ length: ids.size }, () => /* @__PURE__ */ new Map()), selfLoops: new Array(ids.size).fill(0) };
7696
+ for (let i = 0; i < level.nodes; i++) {
7697
+ const ci = ids.get(comm[i]);
7698
+ next.selfLoops[ci] += level.selfLoops[i];
7699
+ for (const [j, w] of level.adj[i]) {
7700
+ const cj = ids.get(comm[j]);
7701
+ if (ci === cj) {
7702
+ if (i < j) next.selfLoops[ci] += w;
7703
+ } else next.adj[ci].set(cj, (next.adj[ci].get(cj) ?? 0) + w);
7704
+ }
7705
+ }
7706
+ level = next;
7707
+ }
7708
+ const members = /* @__PURE__ */ new Map();
7709
+ sorted.forEach((f, i) => {
7710
+ const c = membership[i];
7711
+ const list = members.get(c);
7712
+ if (list) list.push(f);
7713
+ else members.set(c, [f]);
7714
+ });
7715
+ const order = [...members.entries()].sort((a, b) => b[1].length - a[1].length || a[1][0].localeCompare(b[1][0]));
7716
+ const out = /* @__PURE__ */ new Map();
7717
+ order.forEach(([, list], rank) => {
7718
+ for (const f of list) out.set(f, rank);
7719
+ });
7720
+ return out;
7721
+ }
7722
+ var MAX_SWEEPS, MAX_LEVELS;
7723
+ var init_codegraph_cluster = __esm({
7724
+ "src/codegraph-cluster.ts"() {
7725
+ "use strict";
7726
+ MAX_SWEEPS = 20;
7727
+ MAX_LEVELS = 6;
7728
+ }
7729
+ });
7730
+
7731
+ // src/codegraph-report.ts
7732
+ var codegraph_report_exports = {};
7733
+ __export(codegraph_report_exports, {
7734
+ codeGraphReport: () => codeGraphReport,
7735
+ formatReport: () => formatReport
7736
+ });
7737
+ function areaLabel(paths) {
7738
+ const dirs = paths.map((p) => p.split("/").slice(0, -1));
7739
+ let label = [];
7740
+ for (let depth = 1; ; depth++) {
7741
+ const counts2 = /* @__PURE__ */ new Map();
7742
+ for (const d of dirs) if (d.length >= depth) {
7743
+ const k = d.slice(0, depth).join("/");
7744
+ counts2.set(k, (counts2.get(k) ?? 0) + 1);
7745
+ }
7746
+ const top2 = [...counts2.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
7747
+ if (!top2 || top2[1] / paths.length < LABEL_SHARE) break;
7748
+ label = top2[0].split("/");
7749
+ }
7750
+ if (label.length) return `${label.join("/")}/`;
7751
+ if (paths.length === 1) return paths[0];
7752
+ const first = /* @__PURE__ */ new Map();
7753
+ for (const d of dirs) {
7754
+ const k = d[0] ?? "(root)";
7755
+ first.set(k, (first.get(k) ?? 0) + 1);
7756
+ }
7757
+ const [top] = [...first.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
7758
+ return `${top}/ (mixed)`;
7759
+ }
7760
+ function codeGraphReport(opts = {}) {
7761
+ const graph = loadFreshGraph(opts.project, opts.refresh ?? true);
7762
+ if (!graph) return null;
7763
+ const nodes = graphNodes(graph);
7764
+ const byId = new Map(nodes.map((n) => [n.id, n]));
7765
+ const langByPath = new Map(graph.files.map((f) => [f.path, f.lang]));
7766
+ const own = graph.files.filter((f) => !isForeignPath(f.path));
7767
+ const codePaths = new Set(own.filter((f) => isCodeLang(f.lang)).map((f) => f.path));
7768
+ const docPaths = new Set(own.filter((f) => f.lang === "markdown").map((f) => f.path));
7769
+ const weights = /* @__PURE__ */ new Map();
7770
+ const directed = /* @__PURE__ */ new Map();
7771
+ const docRefs = /* @__PURE__ */ new Map();
7772
+ for (const e of graph.edges) {
7773
+ if (e[2] === "contains") continue;
7774
+ const a = byId.get(e[0])?.path;
7775
+ const b = byId.get(e[1])?.path;
7776
+ if (!a || !b || a === b) continue;
7777
+ if (docPaths.has(a) && codePaths.has(b)) {
7778
+ const refs = docRefs.get(a) ?? /* @__PURE__ */ new Map();
7779
+ refs.set(b, (refs.get(b) ?? 0) + 1);
7780
+ docRefs.set(a, refs);
7781
+ continue;
7782
+ }
7783
+ if (!codePaths.has(a) || !codePaths.has(b)) continue;
7784
+ const key = a < b ? `${a}
7785
+ ${b}` : `${b}
7786
+ ${a}`;
7787
+ weights.set(key, (weights.get(key) ?? 0) + 1);
7788
+ directed.set(`${a}
7789
+ ${b}`, (directed.get(`${a}
7790
+ ${b}`) ?? 0) + 1);
7791
+ }
7792
+ const links = [...weights.entries()].map(([k, w]) => {
7793
+ const [a, b] = k.split("\n");
7794
+ return [a, b, w];
7795
+ });
7796
+ const areaOf = clusterFiles([...codePaths], links);
7797
+ const members = /* @__PURE__ */ new Map();
7798
+ for (const [path, area] of areaOf) {
7799
+ const list = members.get(area);
7800
+ if (list) list.push(path);
7801
+ else members.set(area, [path]);
7802
+ }
7803
+ const deg = inDegree(graph.edges, true);
7804
+ const maxAreas = opts.maxAreas ?? DEFAULT_MAX_AREAS;
7805
+ const ranked = [...members.entries()].filter(([, paths]) => paths.length >= 2).sort((a, b) => b[1].length - a[1].length || a[0] - b[0]);
7806
+ const symbolsOf = /* @__PURE__ */ new Map();
7807
+ for (const n of nodes) {
7808
+ if (n.kind === "file" || isTestPath(n.path)) continue;
7809
+ const area = areaOf.get(n.path);
7810
+ if (area === void 0) continue;
7811
+ const list = symbolsOf.get(area);
7812
+ if (list) list.push(n);
7813
+ else symbolsOf.set(area, [n]);
7814
+ }
7815
+ const keys = /* @__PURE__ */ new Map();
7816
+ const keyOf = (area) => {
7817
+ let key = keys.get(area);
7818
+ if (key) return key;
7819
+ 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);
7820
+ keys.set(area, key);
7821
+ return key;
7822
+ };
7823
+ const base = new Map(ranked.map(([area, paths]) => [area, areaLabel(paths)]));
7824
+ const seenLabel = /* @__PURE__ */ new Map();
7825
+ for (const label of base.values()) seenLabel.set(label, (seenLabel.get(label) ?? 0) + 1);
7826
+ const labels = new Map([...base.entries()].map(([area, label]) => {
7827
+ if ((seenLabel.get(label) ?? 0) < 2) return [area, label];
7828
+ const around = keyOf(area)[0]?.name;
7829
+ return [area, around ? `${label} ~ ${around}` : label];
7830
+ }));
7831
+ const docsOf = /* @__PURE__ */ new Map();
7832
+ for (const [doc, refs] of docRefs) {
7833
+ const byArea = /* @__PURE__ */ new Map();
7834
+ for (const [code, w] of refs) {
7835
+ const area = areaOf.get(code);
7836
+ if (area !== void 0) byArea.set(area, (byArea.get(area) ?? 0) + w);
7837
+ }
7838
+ const best = [...byArea.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0];
7839
+ if (!best) continue;
7840
+ const list = docsOf.get(best[0]) ?? [];
7841
+ list.push(doc);
7842
+ docsOf.set(best[0], list);
7843
+ }
7844
+ const areas = ranked.slice(0, maxAreas).map(([area, paths]) => {
7845
+ const inArea = new Set(paths);
7846
+ const out = /* @__PURE__ */ new Map();
7847
+ for (const [key, w] of directed) {
7848
+ const [a, b] = key.split("\n");
7849
+ if (!inArea.has(a) || inArea.has(b)) continue;
7850
+ const target = areaOf.get(b);
7851
+ if (target !== void 0 && target !== area && labels.has(target)) out.set(target, (out.get(target) ?? 0) + w);
7852
+ }
7853
+ return {
7854
+ label: labels.get(area),
7855
+ files: paths.length,
7856
+ languages: [...new Set(paths.map((p) => langByPath.get(p)).filter((l) => !!l))].sort(),
7857
+ keySymbols: keyOf(area),
7858
+ docs: (docsOf.get(area) ?? []).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, AREA_DOCS),
7859
+ dependsOn: [...out.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, DEPENDS_ON).map(([t]) => labels.get(t))
7860
+ };
7861
+ });
7862
+ const titleOf = /* @__PURE__ */ new Map();
7863
+ for (const n of nodes) if (n.kind === "section" && !titleOf.has(n.path)) titleOf.set(n.path, n.name);
7864
+ const docFiles = own.filter((f) => f.lang === "markdown").sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
7865
+ const maxDocs = opts.maxDocs ?? DEFAULT_MAX_DOCS;
7866
+ return {
7867
+ name: graph.name,
7868
+ totals: {
7869
+ files: graph.files.length,
7870
+ symbols: nodes.length - graph.files.length,
7871
+ edges: graph.edges.length,
7872
+ inferred: graph.edges.filter((e) => e[3] === 1).length,
7873
+ docs: docFiles.length
7874
+ },
7875
+ areas,
7876
+ moreAreas: Math.max(0, ranked.length - areas.length),
7877
+ docs: docFiles.slice(0, maxDocs).map((f) => ({ path: f.path, title: titleOf.get(f.path) ?? "" })),
7878
+ moreDocs: Math.max(0, docFiles.length - maxDocs),
7879
+ foreign: graph.files.length - own.length,
7880
+ truncated: graph.truncated
7881
+ };
7882
+ }
7883
+ function formatReport(r) {
7884
+ const t = r.totals;
7885
+ const lines = [`repo report - ${r.name} - ${t.files} files (${t.docs} docs) - ${t.symbols} symbols - ${t.edges} edges (${t.inferred} inferred by name)`];
7886
+ if (r.foreign) lines.push(`${r.foreign} file(s) of vendored, reference or example material are left out of the areas below.`);
7887
+ lines.push("", "areas (files that depend on each other, largest first):");
7888
+ for (const a of r.areas) {
7889
+ lines.push(`- ${a.label} - ${a.files} files${a.languages.length ? ` - ${a.languages.join(", ")}` : ""}`);
7890
+ if (a.keySymbols.length) lines.push(` key: ${a.keySymbols.map((s) => `${s.name} (${s.path}:${s.line}, ${s.inDegree} in)`).join(", ")}`);
7891
+ if (a.dependsOn.length) lines.push(` uses: ${a.dependsOn.join(", ")}`);
7892
+ if (a.docs.length) lines.push(` docs: ${a.docs.join(", ")}`);
7893
+ }
7894
+ if (r.moreAreas) lines.push(` +${r.moreAreas} smaller area(s)`);
7895
+ if (r.docs.length) {
7896
+ lines.push("", "docs:");
7897
+ for (const d of r.docs) lines.push(`- ${d.path}${d.title ? ` - ${d.title}` : ""}`);
7898
+ if (r.moreDocs) lines.push(` +${r.moreDocs} more (enigma_codegraph_ask finds them by topic)`);
7899
+ }
7900
+ if (r.truncated) lines.push("", "The index hit its size cap; some files are not covered.");
7901
+ return `${lines.join("\n")}
7902
+ `;
7903
+ }
7904
+ var DEFAULT_MAX_AREAS, DEFAULT_MAX_DOCS, KEY_SYMBOLS, AREA_DOCS, DEPENDS_ON, LABEL_SHARE;
7905
+ var init_codegraph_report = __esm({
7906
+ "src/codegraph-report.ts"() {
7907
+ "use strict";
7908
+ init_codegraph();
7909
+ init_codegraph_rank();
7910
+ init_codegraph_extract();
7911
+ init_codegraph_cluster();
7912
+ DEFAULT_MAX_AREAS = 8;
7913
+ DEFAULT_MAX_DOCS = 12;
7914
+ KEY_SYMBOLS = 3;
7915
+ AREA_DOCS = 2;
7916
+ DEPENDS_ON = 2;
7917
+ LABEL_SHARE = 0.6;
7918
+ }
7919
+ });
7920
+
7396
7921
  // src/codegraph-hook.ts
7397
7922
  var codegraph_hook_exports = {};
7398
7923
  __export(codegraph_hook_exports, {
@@ -7471,14 +7996,17 @@ async function sessionStart(dir) {
7471
7996
  " - enigma_codegraph_skeleton: a file's whole API surface, no bodies.",
7472
7997
  " - enigma_codegraph_grep: every occurrence, grouped by enclosing symbol - when you need them ALL, not the top matches."
7473
7998
  ].join("\n");
7474
- const map = q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
7999
+ const rep = await Promise.resolve().then(() => (init_codegraph_report(), codegraph_report_exports));
8000
+ const report = rep.codeGraphReport({ project, refresh: false });
8001
+ const map = report ? null : q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
8002
+ const body = report ? rep.formatReport(report) : map ? fmt.formatMap(map) : "";
7475
8003
  const fresh = q.codeGraphCheck(project);
7476
8004
  const stale = fresh && fresh.stale > 0 ? `
7477
8005
  ${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}
8006
+ writeStatuslineSnapshot(projectRoot(cg, project, dir), report?.totals.symbols ?? map?.totals.symbols ?? 0, fresh?.stale ?? 0);
8007
+ emit("SessionStart", body ? `${directive}
7480
8008
 
7481
- ${fmt.formatMap(map).trimEnd()}${stale}` : directive);
8009
+ ${body.trimEnd()}${stale}` : directive);
7482
8010
  }
7483
8011
  function coveringProject(cg, dir) {
7484
8012
  const project = cg.findProjectForCwd(dir);