@zqagent/core 0.3.16 → 0.3.25

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.
package/dist/index.d.cts CHANGED
@@ -519,6 +519,13 @@ type SymbolKind = "class" | "interface" | "struct" | "enum" | "record" | "trait"
519
519
  interface SymbolNode {
520
520
  name: string;
521
521
  kind: SymbolKind;
522
+ /**
523
+ * 声明起始行(1 起始)。渲染成 `Name:12`,模型可直接 `read_file start_line=12`,
524
+ * 省掉先 grep 一行号的那一轮 —— 一轮工具调用要重发整个上下文,比这几个字符贵得多。
525
+ */
526
+ line?: number;
527
+ /** 声明结束行;与 {@link line} 相同时渲染层只写一个行号 */
528
+ endLine?: number;
522
529
  }
523
530
  /** 单文件的索引结果 */
524
531
  interface FileSymbols {
@@ -532,6 +539,14 @@ interface FileSymbols {
532
539
  * 比目录结构可靠,用于渲染分组;取不到则缺省。
533
540
  */
534
541
  pkg?: string;
542
+ /**
543
+ * 再导出语句数(`export * from "./x"` / `export { a } from "./x"`)。
544
+ *
545
+ * barrel 文件(`index.ts` 只有再导出)提取不到任何符号,但它往往是**全仓
546
+ * 被 import 最多的文件**,是「该从哪个包入口开始」的最佳答案。没有这个计数,
547
+ * 索引器会把它连同其它 0 符号文件一起丢掉,包入口在结构图里彻底不可见。
548
+ */
549
+ reexports?: number;
535
550
  /** 该文件由哪个提取器处理(诊断用) */
536
551
  via: "tree-sitter" | "regex" | "path-only";
537
552
  }
@@ -546,7 +561,7 @@ interface RepoMapCache {
546
561
  scan: {
547
562
  /** 枚举到的文件总数(可能远大于 indexed) */
548
563
  files: number;
549
- /** 实际提取了符号的文件数(受 MAX_FILES 封顶) */
564
+ /** 进地图的文件数(有声明 / 纯 barrel / 入口;受 MAX_FILES 封顶) */
550
565
  indexed: number;
551
566
  /** 是否因上限被截断 */
552
567
  truncated: boolean;
@@ -591,9 +606,14 @@ interface RepoMap {
591
606
  root: string;
592
607
  scale: Scale;
593
608
  scan: RepoMapCache["scan"];
594
- /** 已排序的文件(score 降序) */
609
+ /** 已排序的文件(score 降序),与 {@link files} 的键集完全一致 */
595
610
  ranked: RankedFile[];
596
- /** path → 索引结果 */
611
+ /**
612
+ * path → 索引结果,**只含进地图的文件**:有顶层声明、纯 barrel、或模块入口。
613
+ *
614
+ * 解析成功但三者都不满足的文件(例如只有顶层调用的脚本)不在这里 —— 它们在
615
+ * 结构图里只能占一行而不提供任何定位信息。索引磁盘缓存仍保留它们以支持增量。
616
+ */
597
617
  files: Record<string, FileSymbols>;
598
618
  shape?: ProjectShape;
599
619
  }
@@ -608,7 +628,8 @@ interface RepoMap {
608
628
  *
609
629
  * token 上的取舍(都按"能否帮模型少走一轮"来判断):
610
630
  * - 目录路径只写一次(分组标题),不在每行重复
611
- * - 符号只写名字,不写 kind、不写行号
631
+ * - 符号写「名字 + 行号」,不写 kind:行号能省掉先 grep 定位的那一轮,
632
+ * kind 省不了任何一次工具调用
612
633
  * - 不画树形符号(`├──` 每行要花 4-8 个 token)
613
634
  */
614
635
 
package/dist/index.d.ts CHANGED
@@ -519,6 +519,13 @@ type SymbolKind = "class" | "interface" | "struct" | "enum" | "record" | "trait"
519
519
  interface SymbolNode {
520
520
  name: string;
521
521
  kind: SymbolKind;
522
+ /**
523
+ * 声明起始行(1 起始)。渲染成 `Name:12`,模型可直接 `read_file start_line=12`,
524
+ * 省掉先 grep 一行号的那一轮 —— 一轮工具调用要重发整个上下文,比这几个字符贵得多。
525
+ */
526
+ line?: number;
527
+ /** 声明结束行;与 {@link line} 相同时渲染层只写一个行号 */
528
+ endLine?: number;
522
529
  }
523
530
  /** 单文件的索引结果 */
524
531
  interface FileSymbols {
@@ -532,6 +539,14 @@ interface FileSymbols {
532
539
  * 比目录结构可靠,用于渲染分组;取不到则缺省。
533
540
  */
534
541
  pkg?: string;
542
+ /**
543
+ * 再导出语句数(`export * from "./x"` / `export { a } from "./x"`)。
544
+ *
545
+ * barrel 文件(`index.ts` 只有再导出)提取不到任何符号,但它往往是**全仓
546
+ * 被 import 最多的文件**,是「该从哪个包入口开始」的最佳答案。没有这个计数,
547
+ * 索引器会把它连同其它 0 符号文件一起丢掉,包入口在结构图里彻底不可见。
548
+ */
549
+ reexports?: number;
535
550
  /** 该文件由哪个提取器处理(诊断用) */
536
551
  via: "tree-sitter" | "regex" | "path-only";
537
552
  }
@@ -546,7 +561,7 @@ interface RepoMapCache {
546
561
  scan: {
547
562
  /** 枚举到的文件总数(可能远大于 indexed) */
548
563
  files: number;
549
- /** 实际提取了符号的文件数(受 MAX_FILES 封顶) */
564
+ /** 进地图的文件数(有声明 / 纯 barrel / 入口;受 MAX_FILES 封顶) */
550
565
  indexed: number;
551
566
  /** 是否因上限被截断 */
552
567
  truncated: boolean;
@@ -591,9 +606,14 @@ interface RepoMap {
591
606
  root: string;
592
607
  scale: Scale;
593
608
  scan: RepoMapCache["scan"];
594
- /** 已排序的文件(score 降序) */
609
+ /** 已排序的文件(score 降序),与 {@link files} 的键集完全一致 */
595
610
  ranked: RankedFile[];
596
- /** path → 索引结果 */
611
+ /**
612
+ * path → 索引结果,**只含进地图的文件**:有顶层声明、纯 barrel、或模块入口。
613
+ *
614
+ * 解析成功但三者都不满足的文件(例如只有顶层调用的脚本)不在这里 —— 它们在
615
+ * 结构图里只能占一行而不提供任何定位信息。索引磁盘缓存仍保留它们以支持增量。
616
+ */
597
617
  files: Record<string, FileSymbols>;
598
618
  shape?: ProjectShape;
599
619
  }
@@ -608,7 +628,8 @@ interface RepoMap {
608
628
  *
609
629
  * token 上的取舍(都按"能否帮模型少走一轮"来判断):
610
630
  * - 目录路径只写一次(分组标题),不在每行重复
611
- * - 符号只写名字,不写 kind、不写行号
631
+ * - 符号写「名字 + 行号」,不写 kind:行号能省掉先 grep 定位的那一轮,
632
+ * kind 省不了任何一次工具调用
612
633
  * - 不画树形符号(`├──` 每行要花 4-8 个 token)
613
634
  */
614
635
 
package/dist/index.js CHANGED
@@ -8733,6 +8733,9 @@ function nameOf(node) {
8733
8733
  if (typeNode?.text) return typeNode.text.split("<")[0].trim();
8734
8734
  return null;
8735
8735
  }
8736
+ function spanOf(node) {
8737
+ return { line: node.startPosition.row + 1, endLine: node.endPosition.row + 1 };
8738
+ }
8736
8739
  var MAX_COLLECT = 200;
8737
8740
  var VARIABLE_DECLARATIONS = /* @__PURE__ */ new Set(["lexical_declaration", "variable_declaration"]);
8738
8741
  var MODULE_CONTAINERS = /* @__PURE__ */ new Set([
@@ -8770,7 +8773,12 @@ function collectVariables(node, out, exported) {
8770
8773
  if (!nameNode || nameNode.type !== "identifier") continue;
8771
8774
  const isFn = isFunctionValue(decl);
8772
8775
  if (!isFn && !exported) continue;
8773
- out.push({ name: nameNode.text, kind: isFn ? "function" : "const", exported });
8776
+ out.push({
8777
+ name: nameNode.text,
8778
+ kind: isFn ? "function" : "const",
8779
+ exported,
8780
+ ...spanOf(decl)
8781
+ });
8774
8782
  }
8775
8783
  }
8776
8784
  function collect(node, ruleTypes, out, max, depth, exported = false, lang = "", inModule = false) {
@@ -8786,7 +8794,12 @@ function collect(node, ruleTypes, out, max, depth, exported = false, lang = "",
8786
8794
  const name = nameOf(child);
8787
8795
  const usable = name && !name.includes("\n") && name.length <= 80;
8788
8796
  if (usable) {
8789
- out.push({ name, kind, exported: isExport || isPublicDeclaration(child, lang, name) });
8797
+ out.push({
8798
+ name,
8799
+ kind,
8800
+ exported: isExport || isPublicDeclaration(child, lang, name),
8801
+ ...spanOf(child)
8802
+ });
8790
8803
  if (RECORD_AND_DESCEND.has(child.type) && depth < MAX_DEPTH) {
8791
8804
  collect(child, ruleTypes, out, max, depth + 1, isExport, lang, MODULE_CONTAINERS.has(child.type));
8792
8805
  }
@@ -8801,7 +8814,8 @@ function collect(node, ruleTypes, out, max, depth, exported = false, lang = "",
8801
8814
  out.push({
8802
8815
  name: nameNode.text,
8803
8816
  kind: "type",
8804
- exported: isPublicDeclaration(spec, lang, nameNode.text)
8817
+ exported: isPublicDeclaration(spec, lang, nameNode.text),
8818
+ ...spanOf(spec)
8805
8819
  });
8806
8820
  }
8807
8821
  continue;
@@ -8830,10 +8844,13 @@ function prioritizeSymbols(symbols, maxSymbols) {
8830
8844
  seen.add(s.name);
8831
8845
  return true;
8832
8846
  });
8833
- if (unique.length <= maxSymbols) return unique.map(({ name, kind }) => ({ name, kind }));
8834
- const exported = unique.filter((s) => s.exported);
8835
- const inner = unique.filter((s) => !s.exported);
8836
- return [...exported, ...inner].slice(0, maxSymbols).map(({ name, kind }) => ({ name, kind }));
8847
+ const trimmed = unique.length <= maxSymbols ? unique : [...unique.filter((s) => s.exported), ...unique.filter((s) => !s.exported)].slice(
8848
+ 0,
8849
+ maxSymbols
8850
+ );
8851
+ return trimmed.map(
8852
+ (s) => s.line === void 0 ? { name: s.name, kind: s.kind } : { name: s.name, kind: s.kind, line: s.line, endLine: s.endLine }
8853
+ );
8837
8854
  }
8838
8855
  function packageOf(tree, packageNode) {
8839
8856
  if (!packageNode) return void 0;
@@ -8844,6 +8861,14 @@ function packageOf(tree, packageNode) {
8844
8861
  }
8845
8862
  return void 0;
8846
8863
  }
8864
+ function countReexports(tree) {
8865
+ let n = 0;
8866
+ for (const child of tree.rootNode.children) {
8867
+ if (child.type !== "export_statement") continue;
8868
+ if (child.childForFieldName("source")) n += 1;
8869
+ }
8870
+ return n;
8871
+ }
8847
8872
  async function extractWithTreeSitter(langId, bytes, source, maxSymbols) {
8848
8873
  const rule = TS_RULES[langId];
8849
8874
  if (!rule) return null;
@@ -8867,7 +8892,8 @@ async function extractWithTreeSitter(langId, bytes, source, maxSymbols) {
8867
8892
  }
8868
8893
  return {
8869
8894
  symbols: prioritizeSymbols(symbols, maxSymbols),
8870
- pkg: packageOf(tree, rule.packageNode)
8895
+ pkg: packageOf(tree, rule.packageNode),
8896
+ reexports: countReexports(tree)
8871
8897
  };
8872
8898
  } catch {
8873
8899
  return null;
@@ -8880,6 +8906,13 @@ async function extractWithTreeSitter(langId, bytes, source, maxSymbols) {
8880
8906
  // src/repomap/extract/regex.ts
8881
8907
  init_esm_shims();
8882
8908
  var MAX_LINE_CHARS = 500;
8909
+ var REEXPORT_RE = /^export\s+(?:\*\s*(?:as\s+[\w$]+\s*)?|type\s*(?:\*\s*|\{[^}]*\}\s*)|\{[^}]*\}\s*)from\s+['"]/gm;
8910
+ var BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
8911
+ function countReexports2(source, langId) {
8912
+ if (langId !== "typescript" && langId !== "tsx" && langId !== "javascript") return 0;
8913
+ const text = source.includes("/*") ? source.replace(BLOCK_COMMENT_RE, "") : source;
8914
+ return text.match(REEXPORT_RE)?.length ?? 0;
8915
+ }
8883
8916
  function stripBlockComments(line, state) {
8884
8917
  let out = "";
8885
8918
  let i = 0;
@@ -8913,7 +8946,8 @@ function extractWithRegex(langId, source, maxSymbols) {
8913
8946
  let pkg;
8914
8947
  const state = { inBlock: false };
8915
8948
  const lines = source.split("\n");
8916
- for (const raw of lines) {
8949
+ for (let i = 0; i < lines.length; i++) {
8950
+ const raw = lines[i];
8917
8951
  if (symbols.length >= maxSymbols) break;
8918
8952
  const stripped = stripBlockComments(raw, state);
8919
8953
  const line = /^\s/.test(raw) ? stripped : stripped.replace(/^\s+/, "");
@@ -8926,9 +8960,9 @@ function extractWithRegex(langId, source, maxSymbols) {
8926
8960
  if (!name) continue;
8927
8961
  if (seen.has(name)) continue;
8928
8962
  seen.add(name);
8929
- symbols.push({ name, kind: rule.kind });
8963
+ symbols.push({ name, kind: rule.kind, line: i + 1 });
8930
8964
  }
8931
- return { symbols, pkg };
8965
+ return { symbols, pkg, reexports: countReexports2(source, langId) };
8932
8966
  }
8933
8967
 
8934
8968
  // src/repomap/grammar.ts
@@ -8986,6 +9020,7 @@ function markLoaded(langId) {
8986
9020
  failedAt.delete(langId);
8987
9021
  }
8988
9022
  var inflight = /* @__PURE__ */ new Map();
9023
+ var readyCache = /* @__PURE__ */ new Map();
8989
9024
  function grammarDir() {
8990
9025
  const explicit = process.env.ZQAGENT_REPOMAP_GRAMMAR_DIR?.trim();
8991
9026
  if (explicit) return explicit;
@@ -9009,10 +9044,7 @@ function verify(spec, buf) {
9009
9044
  return sha256Hex(buf) === spec.sha256;
9010
9045
  }
9011
9046
  function readLocal(spec) {
9012
- const candidates = [cachePath(spec)];
9013
- const explicit = process.env.ZQAGENT_REPOMAP_GRAMMAR_DIR?.trim();
9014
- if (explicit) candidates.push(path14.join(explicit, `${spec.id}.wasm`));
9015
- for (const file of candidates) {
9047
+ for (const file of localCandidates(spec)) {
9016
9048
  try {
9017
9049
  const buf = fs11.readFileSync(file);
9018
9050
  if (verify(spec, buf)) return new Uint8Array(buf);
@@ -9021,6 +9053,24 @@ function readLocal(spec) {
9021
9053
  }
9022
9054
  return null;
9023
9055
  }
9056
+ function localCandidates(spec) {
9057
+ const candidates = [cachePath(spec)];
9058
+ const explicit = process.env.ZQAGENT_REPOMAP_GRAMMAR_DIR?.trim();
9059
+ if (explicit) candidates.push(path14.join(explicit, `${spec.id}.wasm`));
9060
+ return candidates;
9061
+ }
9062
+ function localSignature(spec) {
9063
+ const parts = [];
9064
+ for (const file of localCandidates(spec)) {
9065
+ try {
9066
+ const st = fs11.statSync(file);
9067
+ parts.push(`${st.mtimeMs}:${st.size}`);
9068
+ } catch {
9069
+ parts.push("-");
9070
+ }
9071
+ }
9072
+ return parts.join("|");
9073
+ }
9024
9074
  async function download(spec) {
9025
9075
  if (offline()) return null;
9026
9076
  for (const build of CDN_TEMPLATES) {
@@ -9079,7 +9129,13 @@ async function loadGrammar(langId) {
9079
9129
  function grammarReady(langId) {
9080
9130
  if (resolved.get(langId)) return true;
9081
9131
  const spec = GRAMMARS[langId];
9082
- return spec ? readLocal(spec) !== null : false;
9132
+ if (!spec) return false;
9133
+ const sig = localSignature(spec);
9134
+ const hit = readyCache.get(langId);
9135
+ if (hit && hit.sig === sig) return hit.ready;
9136
+ const ready = readLocal(spec) !== null;
9137
+ readyCache.set(langId, { sig, ready });
9138
+ return ready;
9083
9139
  }
9084
9140
 
9085
9141
  // src/repomap/extract/index.ts
@@ -9098,10 +9154,10 @@ async function extractFile(relPath, source) {
9098
9154
  const bytes = await loadGrammar(lang);
9099
9155
  if (bytes) {
9100
9156
  const ts = await extractWithTreeSitter(lang, bytes, source, MAX_SYMBOLS_PER_FILE);
9101
- if (ts) return { symbols: ts.symbols, pkg: ts.pkg, via: "tree-sitter" };
9157
+ if (ts) return { symbols: ts.symbols, pkg: ts.pkg, reexports: ts.reexports, via: "tree-sitter" };
9102
9158
  }
9103
9159
  const re = extractWithRegex(lang, source, MAX_SYMBOLS_PER_FILE);
9104
- return { symbols: re.symbols, pkg: re.pkg, via: "regex" };
9160
+ return { symbols: re.symbols, pkg: re.pkg, reexports: re.reexports, via: "regex" };
9105
9161
  }
9106
9162
  function wantsContent(relPath) {
9107
9163
  if (!langOf(relPath)) return false;
@@ -9288,6 +9344,10 @@ function hasLayerWord(name) {
9288
9344
  }
9289
9345
  var ENTRY_RE = /(^|\/)(main|index|program|startup|app|application|bootstrap|mod|__init__)(\.|\/)/i;
9290
9346
  var ENTRY_SUFFIX_RE = /(application|program|startup)\.(java|kt|cs|scala)$/i;
9347
+ function isEntryFile(relPath, sourceRoots = []) {
9348
+ if (ENTRY_SUFFIX_RE.test(relPath)) return true;
9349
+ return ENTRY_RE.test(relPath) && depthFromSourceRoot(relPath, sourceRoots) <= 1;
9350
+ }
9291
9351
  var TEST_RE = /(^|\/)(test|tests|__tests__|__mocks__|spec|specs|e2e)(\/|$)|\.(test|spec)\.[a-z]+$|test\.(java|kt|cs)$|_test\.(go|py|rb)$/i;
9292
9352
  var DOC_RE = /(^|\/)(docs?|examples?|samples?|demo|benchmarks?|bench|fixtures?)(\/|$)|\.(md|mdx|rst|txt|adoc)$/i;
9293
9353
  var GENERATED_RE = /\.(min|generated|designer)\.|_pb2?\.|\.g\.(cs|java)$|(^|\/)gen\//i;
@@ -9323,7 +9383,7 @@ function scorePath(relPath, sourceRoots) {
9323
9383
  score += W.sourceRoot;
9324
9384
  }
9325
9385
  const depth = depthFromSourceRoot(relPath, sourceRoots);
9326
- if (ENTRY_SUFFIX_RE.test(relPath) || ENTRY_RE.test(relPath) && depth <= 1) {
9386
+ if (isEntryFile(relPath, sourceRoots)) {
9327
9387
  score += W.entry;
9328
9388
  }
9329
9389
  const base = relPath.slice(relPath.lastIndexOf("/") + 1).replace(/\.[^.]+$/, "");
@@ -9351,7 +9411,7 @@ function rankFiles(paths, sourceRoots, hasSymbols) {
9351
9411
 
9352
9412
  // src/repomap/types.ts
9353
9413
  init_esm_shims();
9354
- var REPOMAP_CACHE_VERSION = 3;
9414
+ var REPOMAP_CACHE_VERSION = 5;
9355
9415
 
9356
9416
  // src/repomap/indexer.ts
9357
9417
  import { createHash as createHash2 } from "crypto";
@@ -9559,6 +9619,7 @@ async function buildRepoMap(root, opts = {}) {
9559
9619
  size: stat.size,
9560
9620
  symbols: ex.symbols,
9561
9621
  pkg: ex.pkg,
9622
+ reexports: ex.reexports,
9562
9623
  via: ex.via
9563
9624
  };
9564
9625
  } catch {
@@ -9566,15 +9627,18 @@ async function buildRepoMap(root, opts = {}) {
9566
9627
  }
9567
9628
  await yieldToLoop();
9568
9629
  }
9569
- const withSymbols = new Set(
9570
- Object.entries(result).filter(([, v]) => v.symbols.length > 0).map(([k]) => k)
9571
- );
9572
- const ranked = rankFiles([...withSymbols], shape.sourceRoots);
9630
+ const retained = {};
9631
+ for (const [rel, v] of Object.entries(result)) {
9632
+ if (v.symbols.length > 0 || (v.reexports ?? 0) > 0 || isEntryFile(rel, shape.sourceRoots)) {
9633
+ retained[rel] = v;
9634
+ }
9635
+ }
9636
+ const ranked = rankFiles(Object.keys(retained), shape.sourceRoots);
9573
9637
  const scan = {
9574
9638
  files: files.length,
9575
9639
  // 报「地图里有多少文件」而不是「本次新提取了多少」:命中缓存时
9576
- // 后者是 0,模型会以为结构图是空的(实测曾把 1324 个文件报成 0)
9577
- indexed: Object.keys(result).length,
9640
+ // 后者为 0,模型会误判结构图为空(实测曾把 1324 个文件报成 0)
9641
+ indexed: Object.keys(retained).length,
9578
9642
  truncated: enumTruncated || limit < rankedAll.length,
9579
9643
  elapsedMs: Date.now() - started
9580
9644
  };
@@ -9591,7 +9655,7 @@ async function buildRepoMap(root, opts = {}) {
9591
9655
  shape
9592
9656
  });
9593
9657
  }
9594
- return { root: absRoot, scale, scan, ranked, files: result, shape };
9658
+ return { root: absRoot, scale, scan, ranked, files: retained, shape };
9595
9659
  } catch {
9596
9660
  return empty;
9597
9661
  }
@@ -9632,6 +9696,11 @@ var GENERIC_SYMBOLS = /* @__PURE__ */ new Set([
9632
9696
  function meaningful(symbols) {
9633
9697
  return symbols.filter((s) => s.name.length > 3 && !GENERIC_SYMBOLS.has(s.name));
9634
9698
  }
9699
+ function symbolRef(s, withRange = false) {
9700
+ if (s.line === void 0 || s.name.includes(":")) return s.name;
9701
+ const end = withRange && s.endLine !== void 0 && s.endLine > s.line ? `-${s.endLine}` : "";
9702
+ return `${s.name}:${s.line}${end}`;
9703
+ }
9635
9704
  var GROUP_FILE_LIMIT = { small: 5, medium: 4, large: 1 };
9636
9705
  var BREADTH_SHARE = 0.5;
9637
9706
  var TAIL_RESERVE = 64;
@@ -9655,7 +9724,7 @@ function baseName(relPath) {
9655
9724
  function symbolsInline(symbols) {
9656
9725
  const kept = meaningful(symbols);
9657
9726
  if (kept.length === 0) return "";
9658
- const names = kept.slice(0, MAX_SYMBOLS_PER_LINE).map((s) => s.name);
9727
+ const names = kept.slice(0, MAX_SYMBOLS_PER_LINE).map((s) => symbolRef(s));
9659
9728
  const more = kept.length > MAX_SYMBOLS_PER_LINE ? ", \u2026" : "";
9660
9729
  return names.join(", ") + more;
9661
9730
  }
@@ -9663,14 +9732,14 @@ function buildGroups(map) {
9663
9732
  const groups = /* @__PURE__ */ new Map();
9664
9733
  for (const { path: rel } of map.ranked) {
9665
9734
  const entry = map.files[rel];
9666
- if (!entry || entry.symbols.length === 0) continue;
9735
+ if (!entry) continue;
9667
9736
  const label = groupLabelOf(rel, entry.pkg);
9668
9737
  let g = groups.get(label);
9669
9738
  if (!g) {
9670
9739
  g = { label, files: [] };
9671
9740
  groups.set(label, g);
9672
9741
  }
9673
- g.files.push({ path: rel, symbols: entry.symbols });
9742
+ g.files.push({ path: rel, symbols: entry.symbols, reexports: entry.reexports });
9674
9743
  }
9675
9744
  return [...groups.values()];
9676
9745
  }
@@ -9681,12 +9750,20 @@ function moduleLine(mod, info) {
9681
9750
  return ` ${label} (${info.files} \u6587\u4EF6)${layers ? ` ${layers}` : ""}${entry}`;
9682
9751
  }
9683
9752
  function renderLarge(map, budget) {
9753
+ const bestOfModule = /* @__PURE__ */ new Map();
9754
+ for (const r of map.ranked) {
9755
+ const slash = r.path.indexOf("/");
9756
+ const mod = slash < 0 ? "." : r.path.slice(0, slash);
9757
+ const prev = bestOfModule.get(mod);
9758
+ if (prev === void 0 || r.score > prev) bestOfModule.set(mod, r.score);
9759
+ }
9684
9760
  const perModule = /* @__PURE__ */ new Map();
9685
9761
  for (const rel of Object.keys(map.files)) {
9686
9762
  const slash = rel.indexOf("/");
9687
9763
  const first = slash < 0 ? "." : rel.slice(0, slash);
9688
9764
  const bucket = perModule.get(first) ?? {
9689
9765
  files: 0,
9766
+ best: bestOfModule.get(first) ?? Number.NEGATIVE_INFINITY,
9690
9767
  layers: /* @__PURE__ */ new Set(),
9691
9768
  entryDepth: Number.POSITIVE_INFINITY
9692
9769
  };
@@ -9706,7 +9783,9 @@ function renderLarge(map, budget) {
9706
9783
  }
9707
9784
  perModule.set(first, bucket);
9708
9785
  }
9709
- const sorted = [...perModule.entries()].sort((a, b) => b[1].files - a[1].files);
9786
+ const sorted = [...perModule.entries()].sort(
9787
+ (a, b) => b[1].best - a[1].best || b[1].files - a[1].files || a[0].localeCompare(b[0])
9788
+ );
9710
9789
  const lines = [];
9711
9790
  const lineBudget = Math.max(200, budget - TAIL_RESERVE);
9712
9791
  let used = 0;
@@ -9728,8 +9807,13 @@ function renderLarge(map, budget) {
9728
9807
  return lines.join("\n");
9729
9808
  }
9730
9809
  function fileLine(f) {
9731
- const sym = symbolsInline(f.symbols);
9732
- return ` ${baseName(f.path)}${sym ? `: ${sym}` : ""}`;
9810
+ const name = baseName(f.path);
9811
+ if (f.symbols.length > 0) {
9812
+ const sym = symbolsInline(f.symbols);
9813
+ return ` ${name}${sym ? `: ${sym}` : ""}`;
9814
+ }
9815
+ if (f.reexports) return ` ${name}: (\u518D\u5BFC\u51FA ${f.reexports})`;
9816
+ return ` ${name}: (\u5165\u53E3)`;
9733
9817
  }
9734
9818
  function renderDetailed(map, budget) {
9735
9819
  const limit = GROUP_FILE_LIMIT[map.scale === "medium" ? "medium" : "small"];
@@ -9764,23 +9848,36 @@ function renderDetailed(map, budget) {
9764
9848
  const out = [];
9765
9849
  for (const b of blocks) out.push(b.header, ...b.lines);
9766
9850
  const omitted = map.ranked.length - shownFiles;
9767
- if (omitted > 0) out.push(`\u2026 \u53E6\u6709 ${omitted} \u4E2A\u6587\u4EF6\u672A\u5217\u51FA\uFF0C\u7528 repo_map \u5DE5\u5177\u67E5\u770B`);
9851
+ if (omitted > 0) out.push(`\u2026 \u53E6\u6709 ${omitted} \u4E2A\u6587\u4EF6\u672A\u5217\u51FA\uFF0C\u7528 repo_map \u5DE5\u5177\u6309\u76EE\u5F55\u67E5\u770B`);
9768
9852
  return out.join("\n");
9769
9853
  }
9854
+ function shapeLine(shape) {
9855
+ if (!shape) return "";
9856
+ const parts = [];
9857
+ const kinds = shape.kinds.filter((k) => k !== "npm" || !shape.kinds.includes("npm-workspace"));
9858
+ if (kinds.length) parts.push(kinds.slice(0, 3).join(" \xB7 "));
9859
+ if (shape.modules.length > 1) parts.push(`${shape.modules.length} \u6A21\u5757`);
9860
+ const roots = [...new Set(shape.sourceRoots)].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)).slice(0, 3);
9861
+ if (roots.length) parts.push(`\u6E90\u7801\u6839 ${roots.join(", ")}`);
9862
+ return parts.length ? `\u9879\u76EE\u5F62\u6001: ${parts.join(" \xB7 ")}` : "";
9863
+ }
9770
9864
  function renderRepoMap(map) {
9771
9865
  if (map.ranked.length === 0 && !map.shape) return "";
9772
9866
  const budget = renderBudget(map.scale);
9773
- const head = map.scale === "large" ? "\u4EE3\u7801\u7ED3\u6784\uFF08\u81EA\u52A8\u6458\u8981 \xB7 \u5927\u578B\u9879\u76EE\uFF1B\u6309\u6A21\u5757\u4E0B\u94BB\u7528 repo_map \u5DE5\u5177\uFF09" : "\u4EE3\u7801\u7ED3\u6784\uFF08\u81EA\u52A8\u751F\u6210\uFF1B\u66F4\u7EC6\u7684\u89C6\u56FE\u7528 repo_map \u5DE5\u5177\uFF09";
9867
+ const title = map.scale === "large" ? "\u4EE3\u7801\u7ED3\u6784\uFF08\u81EA\u52A8\u6458\u8981 \xB7 \u5927\u578B\u9879\u76EE\uFF1B\u6309\u6A21\u5757\u4E0B\u94BB\u7528 repo_map \u5DE5\u5177\uFF09" : "\u4EE3\u7801\u7ED3\u6784\uFF08\u81EA\u52A8\u751F\u6210\uFF1B\u7B26\u53F7\u540E\u7684\u6570\u5B57\u662F\u884C\u53F7\uFF1B\u66F4\u7EC6\u7684\u89C6\u56FE\u7528 repo_map \u5DE5\u5177\uFF09";
9868
+ const shape = shapeLine(map.shape);
9869
+ const head = shape ? `${title}
9870
+ ${shape}` : title;
9774
9871
  const bodyBudget = Math.max(200, budget - head.length - 1);
9775
9872
  const body = map.scale === "large" ? renderLarge(map, bodyBudget) : renderDetailed(map, bodyBudget);
9776
- if (!body.trim()) return "";
9873
+ if (!body.trim()) return shape;
9777
9874
  const text = `${head}
9778
9875
  ${body}`;
9779
9876
  if (text.length <= budget) return text;
9780
9877
  return cutAtLine(text, budget);
9781
9878
  }
9782
9879
  function cutAtLine(text, budget) {
9783
- const marker = "\n\u2026\uFF08\u7ED3\u6784\u56FE\u5DF2\u622A\u65AD\uFF0C\u7528 repo_map \u5DE5\u5177\u67E5\u770B\uFF09";
9880
+ const marker = "\n\u2026\uFF08\u7ED3\u6784\u56FE\u5DF2\u622A\u65AD\uFF0C\u7528 repo_map \u5DE5\u5177\u6309\u76EE\u5F55\u67E5\u770B\uFF09";
9784
9881
  const room = budget - marker.length;
9785
9882
  if (room <= 0) return marker.slice(1);
9786
9883
  const cut = text.slice(0, room);
@@ -9890,10 +9987,12 @@ function attachRepoMap(engine, opts) {
9890
9987
  const original = spec.func;
9891
9988
  engine.registry.register(
9892
9989
  async (args, ctx) => {
9893
- const out = await original(args, ctx);
9894
- invalidateRepoMap(workspace);
9895
- schedule();
9896
- return out;
9990
+ try {
9991
+ return await original(args, ctx);
9992
+ } finally {
9993
+ invalidateRepoMap(workspace);
9994
+ schedule();
9995
+ }
9897
9996
  },
9898
9997
  spec.metadata,
9899
9998
  spec.schema,
@@ -13088,17 +13187,26 @@ function registerSearchTool(registry, workspace) {
13088
13187
  // src/tools/repoMap.ts
13089
13188
  init_esm_shims();
13090
13189
  var MAX_CHARS = 16e3;
13190
+ function filesUnder(map, prefix) {
13191
+ const keys = Object.keys(map.files);
13192
+ if (!prefix) return keys;
13193
+ if (prefix === ".") return keys.filter((rel) => !rel.includes("/"));
13194
+ return keys.filter((rel) => rel === prefix || rel.startsWith(`${prefix}/`));
13195
+ }
13091
13196
  function renderSubset(map, prefix, maxFiles) {
13092
13197
  const order = new Map(map.ranked.map((r, i) => [r.path, i]));
13093
- const under = Object.keys(map.files).filter((rel) => !prefix || rel === prefix || rel.startsWith(`${prefix}/`)).sort((a, b) => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity));
13198
+ const under = filesUnder(map, prefix).sort(
13199
+ (a, b) => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity)
13200
+ );
13094
13201
  const matched = under.slice(0, maxFiles);
13095
13202
  if (matched.length === 0) return null;
13096
13203
  const entries = [];
13097
13204
  let used = 0;
13098
13205
  for (const rel of matched) {
13099
13206
  const f = map.files[rel];
13100
- const sym = f.symbols.slice(0, 8).map((s) => s.name).join(", ");
13101
- const line = `${rel}${sym ? `: ${sym}` : ""}`;
13207
+ const sym = f.symbols.slice(0, 8).map((s) => symbolRef(s, true)).join(", ");
13208
+ const body = f.symbols.length > 0 ? sym : f.reexports ? `(\u518D\u5BFC\u51FA ${f.reexports})` : "(\u5165\u53E3)";
13209
+ const line = `${rel}: ${body}`;
13102
13210
  if (used + line.length + 1 > MAX_CHARS) break;
13103
13211
  entries.push(line);
13104
13212
  used += line.length + 1;
@@ -13130,18 +13238,20 @@ function registerRepoMapTool(registry, workspace) {
13130
13238
  if (sub) return sub;
13131
13239
  return { error: `\u6CA1\u6709\u5339\u914D\u7684\u76EE\u5F55\uFF1A${path31}` };
13132
13240
  }
13133
- const groups = buildGroups(map).slice(0, 40).map((g) => ({
13134
- label: g.label,
13135
- files: g.files.length,
13136
- path: g.files[0]?.path.split("/").slice(0, -1).join("/") || "."
13137
- }));
13241
+ const groups = buildGroups(map).slice(0, 40).map((g) => {
13242
+ const dir = g.files[0]?.path.split("/").slice(0, -1).join("/") || ".";
13243
+ return {
13244
+ label: g.label,
13245
+ files: filesUnder(map, dir).length,
13246
+ ...dir === g.label ? {} : { path: dir }
13247
+ };
13248
+ });
13138
13249
  return {
13139
13250
  scale: map.scale,
13140
13251
  files: map.scan.files,
13141
13252
  indexed: map.scan.indexed,
13142
13253
  groups,
13143
- map: renderRepoMap(map),
13144
- hint: '\u7528 path \u53C2\u6570\u53EF\u53EA\u770B\u67D0\u4E2A\u6A21\u5757/\u76EE\u5F55\uFF08\u53D6 groups[].path\uFF09\uFF0C\u4F8B\u5982 path="packages/core/src"'
13254
+ hint: '\u7ED3\u6784\u56FE\u5DF2\u5728\u7CFB\u7EDF\u63D0\u793A\u8BCD\u91CC\uFF0C\u8FD9\u91CC\u53EA\u7ED9\u5168\u5C40\u5206\u7EC4\uFF08files \u542B\u5B50\u76EE\u5F55\uFF09\u3002\u7528 path \u53C2\u6570\u6309\u76EE\u5F55\u4E0B\u94BB\uFF1A\u5206\u7EC4\u6807\u9898\u662F\u5305\u540D\u65F6\u7528 groups[].path\uFF0C\u5426\u5219\u76F4\u63A5\u7528 label\uFF0C\u4F8B\u5982 path="packages/core/src"'
13145
13255
  };
13146
13256
  },
13147
13257
  { category: "search", riskLevel: "low", capabilities: ["repo_map"], requiresApproval: false },