sigmap 8.19.0 → 8.21.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.
package/gen-context.js CHANGED
@@ -1533,6 +1533,8 @@ __factories["./src/config/defaults"] = function(module, exports) {
1533
1533
  recencyBoost: 1.5,
1534
1534
  // Boost files call-graph-connected to query matches (opt-in, measure-gated)
1535
1535
  callGraphBoost: false,
1536
+ // Blend import-graph centrality into ranking as a small prior (opt-in, measure-gated)
1537
+ centralityBlend: false,
1536
1538
  // Append route pseudo-signatures to the rankable index (opt-in, measure-gated)
1537
1539
  surfaceEnrichment: false,
1538
1540
  },
@@ -5928,6 +5930,10 @@ __factories["./src/extractors/go"] = function(module, exports) {
5928
5930
  function extract(src) {
5929
5931
  if (!src || typeof src !== 'string') return [];
5930
5932
  const sigs = [];
5933
+ const docHints = buildDocHints(src);
5934
+ // Append the godoc hint after the anchor as ` # <hint>` — same convention
5935
+ // as the Python/JS extractors' doc hints.
5936
+ const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
5931
5937
 
5932
5938
  const stripped = src
5933
5939
  .replace(/\/\/.*$/gm, '')
@@ -5939,14 +5945,14 @@ __factories["./src/extractors/go"] = function(module, exports) {
5939
5945
  // Structs
5940
5946
  for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
5941
5947
  const end = blockEndIdx(m.index + m[0].length);
5942
- sigs.push(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)));
5948
+ sigs.push(hinted(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)), m[1]));
5943
5949
  }
5944
5950
 
5945
5951
  // Interfaces
5946
5952
  for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
5947
5953
  const bodyStart = m.index + m[0].length;
5948
5954
  const block = extractBlock(stripped, bodyStart);
5949
- sigs.push(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
5955
+ sigs.push(hinted(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[1]));
5950
5956
  for (const meth of extractInterfaceMethods(block)) {
5951
5957
  sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
5952
5958
  }
@@ -5958,7 +5964,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
5958
5964
  const retType = m[4] ? m[4].trim().replace(/\s+/g, ' ') : '';
5959
5965
  const retStr = retType ? ` → ${retType.slice(0, 30)}` : '';
5960
5966
  const end = blockEndIdx(m.index + m[0].length);
5961
- sigs.push(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)));
5967
+ sigs.push(hinted(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)), m[2]));
5962
5968
  }
5963
5969
 
5964
5970
  return sigs.slice(0, 25);
@@ -5994,6 +6000,30 @@ __factories["./src/extractors/go"] = function(module, exports) {
5994
6000
  return params.trim().replace(/\s+/g, ' ');
5995
6001
  }
5996
6002
 
6003
+ // Godoc: the `//` comment block directly above a top-level func/type/method
6004
+ // declaration → first prose sentence, 60-char cap. Runs on the ORIGINAL src
6005
+ // (extract strips comments before matching). Compiler directives (`//go:...`)
6006
+ // carry no prose and are skipped.
6007
+ function buildDocHints(src) {
6008
+ const hints = new Map();
6009
+ const re = /((?:^\/\/[^\n]*\n)+)(?:func\s+(?:\(\w+\s+[\w*]+\)\s+)?(\w+)\s*\(|type\s+(\w+)\s+(?:struct|interface)\b)/gm;
6010
+ for (const m of src.matchAll(re)) {
6011
+ const name = m[2] || m[3];
6012
+ const hint = firstDocSentence(m[1]);
6013
+ if (hint && !hints.has(name)) hints.set(name, hint);
6014
+ }
6015
+ return hints;
6016
+ }
6017
+
6018
+ // First non-directive prose line of a `//` block → first sentence, 60-char cap.
6019
+ function firstDocSentence(block) {
6020
+ const line = String(block).split('\n')
6021
+ .map((l) => l.replace(/^\/\/\s?/, '').trim())
6022
+ .find((l) => l && !l.startsWith('go:') && !l.startsWith('nolint'));
6023
+ if (!line) return '';
6024
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
6025
+ }
6026
+
5997
6027
  module.exports = { extract };
5998
6028
 
5999
6029
  };
@@ -6126,6 +6156,10 @@ __factories["./src/extractors/java"] = function(module, exports) {
6126
6156
  function extract(src) {
6127
6157
  if (!src || typeof src !== 'string') return [];
6128
6158
  const sigs = [];
6159
+ const docHints = buildDocHints(src);
6160
+ // Append the Javadoc hint after the anchor as ` # <hint>` — same convention
6161
+ // as the Python/JS extractors' doc hints.
6162
+ const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
6129
6163
 
6130
6164
  const stripped = src
6131
6165
  .replace(/\/\/.*$/gm, '')
@@ -6136,9 +6170,9 @@ __factories["./src/extractors/java"] = function(module, exports) {
6136
6170
  for (const m of stripped.matchAll(typeRegex)) {
6137
6171
  const bodyStart = m.index + m[0].length;
6138
6172
  const block = extractBlock(stripped, bodyStart);
6139
- sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
6173
+ sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
6140
6174
  for (const meth of extractMembers(block)) {
6141
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
6175
+ sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
6142
6176
  }
6143
6177
  }
6144
6178
 
@@ -6165,6 +6199,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
6165
6199
  const retStr = ret ? ` → ${ret}` : '';
6166
6200
  members.push({
6167
6201
  text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
6202
+ name: m[2],
6168
6203
  declIdx: m.index + (m[0].length - m[0].trimStart().length),
6169
6204
  endIdx: m.index + m[0].length,
6170
6205
  });
@@ -6182,6 +6217,36 @@ __factories["./src/extractors/java"] = function(module, exports) {
6182
6217
  return type.trim().replace(/\s+/g, ' ').slice(0, 30);
6183
6218
  }
6184
6219
 
6220
+ // Javadoc: the `/** ... */` block directly above a type or public/protected
6221
+ // member declaration → first prose sentence, 60-char cap. Runs on the
6222
+ // ORIGINAL src (extract strips comments before matching). Annotation lines
6223
+ // (`@Override` etc.) between the doc block and the declaration are tolerated.
6224
+ // Body may not contain `*/` so a failed adjacency check can't expand across
6225
+ // code to the next comment block and misattribute the hint.
6226
+ function buildDocHints(src) {
6227
+ const hints = new Map();
6228
+ const patterns = [
6229
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum)\s+(\w+)/g,
6230
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?[\w<>\[\], ?.]+\s+(\w+)\s*\(/g,
6231
+ ];
6232
+ for (const re of patterns) {
6233
+ for (const m of src.matchAll(re)) {
6234
+ const hint = firstDocSentence(m[1]);
6235
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
6236
+ }
6237
+ }
6238
+ return hints;
6239
+ }
6240
+
6241
+ // First non-tag prose line of a Javadoc body → first sentence, 60-char cap.
6242
+ function firstDocSentence(body) {
6243
+ const line = String(body).split('\n')
6244
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
6245
+ .find((l) => l && !l.startsWith('@'));
6246
+ if (!line) return '';
6247
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
6248
+ }
6249
+
6185
6250
  module.exports = { extract };
6186
6251
 
6187
6252
  };
@@ -6203,7 +6268,12 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6203
6268
  if (!src || typeof src !== 'string') return [];
6204
6269
  const sigs = [];
6205
6270
  const anchors = [];
6271
+ // docHintFor[i] is the doc-comment hint for sigs[i] (top-level functions
6272
+ // only), appended after the anchor as ` # <hint>` — same convention as the
6273
+ // Python extractor's extractDocHint.
6274
+ const docHintFor = [];
6206
6275
  const returnHints = buildReturnHints(src);
6276
+ const docHints = buildDocHints(src);
6207
6277
 
6208
6278
  // Block comments are blanked newline-by-newline (non-newline chars → spaces)
6209
6279
  // so character offsets AND line numbers stay exact for anchors.
@@ -6238,6 +6308,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6238
6308
  const retStr = formatReturnHint(returnHints.get(m[1]));
6239
6309
  const startLn = lineAt(stripped, m.index);
6240
6310
  sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6311
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6241
6312
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6242
6313
  }
6243
6314
 
@@ -6247,6 +6318,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6247
6318
  const retStr = formatReturnHint(returnHints.get(m[1]));
6248
6319
  const startLn = lineAt(stripped, m.index);
6249
6320
  sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
6321
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6250
6322
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6251
6323
  }
6252
6324
 
@@ -6267,10 +6339,14 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6267
6339
  const retStr = formatReturnHint(returnHints.get(m[1]));
6268
6340
  const startLn = lineAt(stripped, m.index);
6269
6341
  sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6342
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6270
6343
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6271
6344
  }
6272
6345
 
6273
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
6346
+ const withAnchors = sigs.map((s, i) => {
6347
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
6348
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
6349
+ });
6274
6350
  return capWithNotice(withAnchors, 25, 'signatures');
6275
6351
  }
6276
6352
 
@@ -6319,6 +6395,36 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6319
6395
  return hints;
6320
6396
  }
6321
6397
 
6398
+ // First prose sentence of the JSDoc block immediately preceding a top-level
6399
+ // function (same three shapes as buildReturnHints). Mirrors the Python
6400
+ // extractor's extractDocHint: first sentence only, 60-char cap.
6401
+ function buildDocHints(src) {
6402
+ const hints = new Map();
6403
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
6404
+ // the match expand across a whole function to the next comment block and
6405
+ // misattribute the hint.
6406
+ const patterns = [
6407
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g,
6408
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g,
6409
+ ];
6410
+ for (const re of patterns) {
6411
+ for (const m of src.matchAll(re)) {
6412
+ const hint = firstDocSentence(m[1]);
6413
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
6414
+ }
6415
+ }
6416
+ return hints;
6417
+ }
6418
+
6419
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
6420
+ function firstDocSentence(body) {
6421
+ const line = String(body).split('\n')
6422
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
6423
+ .find((l) => l && !l.startsWith('@'));
6424
+ if (!line) return '';
6425
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
6426
+ }
6427
+
6322
6428
  function normalizeType(type) {
6323
6429
  if (!type) return '';
6324
6430
  return type.trim().replace(/\s+/g, ' ').slice(0, 25);
@@ -7639,6 +7745,10 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7639
7745
  function extract(src) {
7640
7746
  if (!src || typeof src !== 'string') return [];
7641
7747
  const sigs = [];
7748
+ const docHints = buildDocHints(src);
7749
+ // Append the doc-comment hint after the anchor as ` # <hint>` — same
7750
+ // convention as the Python/JS extractors' doc hints.
7751
+ const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
7642
7752
 
7643
7753
  const stripped = src
7644
7754
  .replace(/\/\/.*$/gm, '')
@@ -7660,19 +7770,19 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7660
7770
  // Structs
7661
7771
  for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
7662
7772
  const [s, e] = rangeFor(m.index, m.index + m[0].length);
7663
- sigs.push(withAnchor(`pub struct ${m[1]}`, s, e));
7773
+ sigs.push(hinted(withAnchor(`pub struct ${m[1]}`, s, e), m[1]));
7664
7774
  }
7665
7775
 
7666
7776
  // Enums
7667
7777
  for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
7668
7778
  const [s, e] = rangeFor(m.index, m.index + m[0].length);
7669
- sigs.push(withAnchor(`pub enum ${m[1]}`, s, e));
7779
+ sigs.push(hinted(withAnchor(`pub enum ${m[1]}`, s, e), m[1]));
7670
7780
  }
7671
7781
 
7672
7782
  // Traits
7673
7783
  for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
7674
7784
  const [s, e] = rangeFor(m.index, m.index + m[0].length);
7675
- sigs.push(withAnchor(`pub trait ${m[1]}`, s, e));
7785
+ sigs.push(hinted(withAnchor(`pub trait ${m[1]}`, s, e), m[1]));
7676
7786
  }
7677
7787
 
7678
7788
  // impl blocks
@@ -7681,7 +7791,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7681
7791
  const block = extractBlock(stripped, bodyStart);
7682
7792
  sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
7683
7793
  for (const fn of extractMethods(block)) {
7684
- sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
7794
+ sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)), fn.name));
7685
7795
  }
7686
7796
  }
7687
7797
 
@@ -7690,7 +7800,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7690
7800
  const asyncKw = m[0].includes('async') ? 'async ' : '';
7691
7801
  const retStr = extractReturnType(m[3]);
7692
7802
  const [s, e] = rangeFor(m.index, m.index + m[0].length);
7693
- sigs.push(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
7803
+ sigs.push(hinted(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e), m[1]));
7694
7804
  }
7695
7805
 
7696
7806
  return sigs.slice(0, 25);
@@ -7714,6 +7824,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7714
7824
  const retStr = extractReturnType(m[3]);
7715
7825
  methods.push({
7716
7826
  text: `pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`,
7827
+ name: m[1],
7717
7828
  declIdx: m.index + (m[0].length - m[0].trimStart().length),
7718
7829
  endIdx: m.index + m[0].length,
7719
7830
  });
@@ -7734,6 +7845,29 @@ __factories["./src/extractors/rust"] = function(module, exports) {
7734
7845
  return ` → ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
7735
7846
  }
7736
7847
 
7848
+ // Rustdoc: the `///` block directly above a declaration → first prose
7849
+ // sentence, 60-char cap. Runs on the ORIGINAL src (extract strips comments
7850
+ // before matching). Attribute lines (`#[...]`) between the doc block and the
7851
+ // declaration are tolerated.
7852
+ function buildDocHints(src) {
7853
+ const hints = new Map();
7854
+ const re = /((?:^[ \t]*\/\/\/[^\n]*\n)+)(?:[ \t]*#\[[^\n]*\n)*[ \t]*pub(?:\s+async)?\s+(?:fn|struct|enum|trait)\s+(\w+)/gm;
7855
+ for (const m of src.matchAll(re)) {
7856
+ const hint = firstDocSentence(m[1]);
7857
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
7858
+ }
7859
+ return hints;
7860
+ }
7861
+
7862
+ // First prose line of a `///` block → first sentence, 60-char cap.
7863
+ function firstDocSentence(block) {
7864
+ const line = String(block).split('\n')
7865
+ .map((l) => l.replace(/^[ \t]*\/\/\/\s?/, '').trim())
7866
+ .find((l) => l);
7867
+ if (!line) return '';
7868
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
7869
+ }
7870
+
7737
7871
  module.exports = { extract };
7738
7872
 
7739
7873
  };
@@ -8307,6 +8441,11 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8307
8441
  function extract(src) {
8308
8442
  if (!src || typeof src !== 'string') return [];
8309
8443
  const sigs = [];
8444
+ // docHintFor[i] is the doc-comment hint for sigs[i] (exported top-level
8445
+ // functions only), appended after the anchor as ` # <hint>` — same
8446
+ // convention as the Python extractor's extractDocHint.
8447
+ const docHintFor = [];
8448
+ const docHints = buildDocHints(src);
8310
8449
  // anchors[i] is [start, end] for a top-level sig, or null for an indented member.
8311
8450
  // Kept parallel to `sigs` so existing push/mutation logic stays untouched;
8312
8451
  // anchors are applied once at return.
@@ -8374,6 +8513,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8374
8513
  const retStr = retType ? ` → ${retType}` : '';
8375
8514
  const bodyStart = m.index + m[0].length;
8376
8515
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
8516
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
8377
8517
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
8378
8518
 
8379
8519
  // Hooks: capture compact return object shape for use* functions.
@@ -8398,6 +8538,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8398
8538
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
8399
8539
  const params = normalizeParams(m[2]);
8400
8540
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
8541
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
8401
8542
  const bodyStart = stripped.indexOf('{', m.index + m[0].length);
8402
8543
  const endLn = bodyStart !== -1
8403
8544
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
@@ -8447,7 +8588,10 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8447
8588
  }
8448
8589
  }
8449
8590
 
8450
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
8591
+ const withAnchors = sigs.map((s, i) => {
8592
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
8593
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
8594
+ });
8451
8595
  return capWithNotice(withAnchors, 35, 'signatures');
8452
8596
  }
8453
8597
 
@@ -8512,6 +8656,36 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8512
8656
  return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
8513
8657
  }
8514
8658
 
8659
+ // First prose sentence of the JSDoc block immediately preceding an exported
8660
+ // top-level function (function or arrow-const form). Mirrors the Python
8661
+ // extractor's extractDocHint: first sentence only, 60-char cap.
8662
+ function buildDocHints(src) {
8663
+ const hints = new Map();
8664
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
8665
+ // the match expand across a whole function to the next comment block and
8666
+ // misattribute the hint.
8667
+ const patterns = [
8668
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/g,
8669
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*[:=]/g,
8670
+ ];
8671
+ for (const re of patterns) {
8672
+ for (const m of src.matchAll(re)) {
8673
+ const hint = firstDocSentence(m[1]);
8674
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
8675
+ }
8676
+ }
8677
+ return hints;
8678
+ }
8679
+
8680
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
8681
+ function firstDocSentence(body) {
8682
+ const line = String(body).split('\n')
8683
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
8684
+ .find((l) => l && !l.startsWith('@'));
8685
+ if (!line) return '';
8686
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
8687
+ }
8688
+
8515
8689
  module.exports = { extract };
8516
8690
 
8517
8691
  };
@@ -11725,6 +11899,71 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11725
11899
 
11726
11900
  };
11727
11901
 
11902
+ // ── ./src/graph/centrality ──
11903
+ __factories["./src/graph/centrality"] = function(module, exports) {
11904
+
11905
+ /**
11906
+ * Zero-dependency import-graph centrality (Semantic Bridge II, B3).
11907
+ *
11908
+ * Power iteration over the forward dependency graph: rank flows from each
11909
+ * importer to the files it imports, so heavily-referenced files accumulate
11910
+ * centrality and one-off helpers do not. Deterministic — fixed damping,
11911
+ * fixed iteration count, nodes processed in sorted order.
11912
+ *
11913
+ * The result feeds the opt-in `retrieval.centralityBlend` ranking prior
11914
+ * (see src/retrieval/ranker.js) — a principled deepening of the existing
11915
+ * graph-boost idea, not a replacement for query relevance.
11916
+ */
11917
+
11918
+ const DAMPING = 0.85;
11919
+ const ITERATIONS = 20;
11920
+
11921
+ /**
11922
+ * Compute a normalized centrality score for every file in a dependency graph.
11923
+ *
11924
+ * @param {{ forward: Map<string, string[]> }} graph - forward dependency graph
11925
+ * (file → files it imports), as built by src/graph/builder.js
11926
+ * @returns {Map<string, number>} file → centrality in (0, 1], max-normalized;
11927
+ * empty Map when the graph is missing or empty
11928
+ */
11929
+ function computeCentrality(graph) {
11930
+ if (!graph || !(graph.forward instanceof Map) || graph.forward.size === 0) return new Map();
11931
+
11932
+ const nodes = new Set(graph.forward.keys());
11933
+ for (const deps of graph.forward.values()) {
11934
+ for (const dep of deps || []) nodes.add(dep);
11935
+ }
11936
+ const nodeList = [...nodes].sort();
11937
+ const n = nodeList.length;
11938
+ const indexOf = new Map(nodeList.map((file, i) => [file, i]));
11939
+ const outLinks = nodeList.map((file) =>
11940
+ (graph.forward.get(file) || []).map((dep) => indexOf.get(dep)).filter((i) => i !== undefined));
11941
+
11942
+ let ranks = new Array(n).fill(1 / n);
11943
+ for (let iter = 0; iter < ITERATIONS; iter++) {
11944
+ const next = new Array(n).fill((1 - DAMPING) / n);
11945
+ let dangling = 0;
11946
+ for (let i = 0; i < n; i++) {
11947
+ if (outLinks[i].length === 0) { dangling += ranks[i]; continue; }
11948
+ const share = (DAMPING * ranks[i]) / outLinks[i].length;
11949
+ for (const j of outLinks[i]) next[j] += share;
11950
+ }
11951
+ // Dangling mass (files that import nothing) is redistributed uniformly.
11952
+ const danglingShare = (DAMPING * dangling) / n;
11953
+ for (let i = 0; i < n; i++) next[i] += danglingShare;
11954
+ ranks = next;
11955
+ }
11956
+
11957
+ const max = Math.max(...ranks) || 1;
11958
+ const result = new Map();
11959
+ for (let i = 0; i < n; i++) result.set(nodeList[i], ranks[i] / max);
11960
+ return result;
11961
+ }
11962
+
11963
+ module.exports = { computeCentrality, DAMPING, ITERATIONS };
11964
+
11965
+ };
11966
+
11728
11967
  // ── ./src/graph/impact ──
11729
11968
  __factories["./src/graph/impact"] = function(module, exports) {
11730
11969
 
@@ -13882,8 +14121,9 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
13882
14121
  // Build dependency graph for neighbor boost — non-fatal if it fails
13883
14122
  let graph = null;
13884
14123
  try { graph = buildFromCwd(cwd); } catch (_) {}
13885
- // Opt-in call-graph neighbor boost + surface enrichment — non-fatal
14124
+ // Opt-in call-graph neighbor boost + surface enrichment + centrality blend — non-fatal
13886
14125
  let callGraph = null;
14126
+ let centrality = null;
13887
14127
  try {
13888
14128
  const { loadConfig } = __require('./src/config/loader');
13889
14129
  const retrieval = loadConfig(cwd).retrieval;
@@ -13893,8 +14133,11 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
13893
14133
  if (retrieval && retrieval.surfaceEnrichment) {
13894
14134
  __require('./src/retrieval/enrich-from-maps').enrichWithSurfaces(index, cwd);
13895
14135
  }
14136
+ if (retrieval && retrieval.centralityBlend && graph) {
14137
+ centrality = __require('./src/graph/centrality').computeCentrality(graph);
14138
+ }
13896
14139
  } catch (_) {}
13897
- const results = rank(args.query, index, { topK, cwd, graph, callGraph });
14140
+ const results = rank(args.query, index, { topK, cwd, graph, callGraph, centrality });
13898
14141
  return formatRankTable(results, args.query);
13899
14142
  } catch (err) {
13900
14143
  return `_query_context failed: ${err.message}_`;
@@ -14607,7 +14850,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
14607
14850
 
14608
14851
  const SERVER_INFO = {
14609
14852
  name: 'sigmap',
14610
- version: '8.19.0',
14853
+ version: '8.21.0',
14611
14854
  description: 'SigMap MCP server — code signatures on demand',
14612
14855
  };
14613
14856
 
@@ -15737,6 +15980,9 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15737
15980
  callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
15738
15981
  };
15739
15982
 
15983
+ // Max additive prior for import-graph centrality (opt-in retrieval.centralityBlend)
15984
+ const CENTRALITY_BLEND_WEIGHT = 0.3;
15985
+
15740
15986
  // Intent-specific weight adjustments
15741
15987
  const INTENT_WEIGHTS = {
15742
15988
  search: DEFAULT_WEIGHTS,
@@ -15869,6 +16115,8 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15869
16115
  * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
15870
16116
  * @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
15871
16117
  * (from buildCallFileGraph) for the opt-in call-neighbor boost
16118
+ * @param {Map<string,number>} [opts.centrality] - absolute file → normalized
16119
+ * centrality (from computeCentrality) for the opt-in centrality blend
15872
16120
  * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
15873
16121
  */
15874
16122
  function rank(query, sigIndex, opts) {
@@ -16017,6 +16265,26 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16017
16265
  }
16018
16266
  }
16019
16267
 
16268
+ // Centrality blend (opt-in via retrieval.centralityBlend): a small additive
16269
+ // prior from import-graph centrality so heavily-referenced files rank above
16270
+ // one-off helpers on ambiguous queries. Applied only to positively-scored
16271
+ // files — a tie-breaker among matches, never a way to surface non-matches.
16272
+ const centrality = (opts && opts.centrality instanceof Map && opts.centrality.size > 0) ? opts.centrality : null;
16273
+ if (centrality && cwd) {
16274
+ const path = require('path');
16275
+ for (const entry of scored) {
16276
+ if (entry.score <= 0) continue;
16277
+ const abs = path.resolve(cwd, entry.file);
16278
+ // The graph builder lowercases paths (normalizePath) — probe both forms.
16279
+ const c = centrality.get(abs) || centrality.get(abs.toLowerCase());
16280
+ if (c) {
16281
+ const bonus = CENTRALITY_BLEND_WEIGHT * c;
16282
+ entry.score += bonus;
16283
+ entry.signals.centrality = bonus;
16284
+ }
16285
+ }
16286
+ }
16287
+
16020
16288
  // Compute confidence levels based on score distribution
16021
16289
  if (scored.length > 0) {
16022
16290
  const scores = scored.map(s => s.score);
@@ -16292,7 +16560,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16292
16560
  return 'search';
16293
16561
  }
16294
16562
 
16295
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
16563
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
16296
16564
 
16297
16565
  };
16298
16566
 
@@ -17202,6 +17470,96 @@ __factories["./src/session/memory"] = function(module, exports) {
17202
17470
 
17203
17471
  };
17204
17472
 
17473
+ // ── ./src/session/memory-inspect ──
17474
+ __factories["./src/session/memory-inspect"] = function(module, exports) {
17475
+
17476
+ /**
17477
+ * memory-inspect.js — one view over SigMap's existing cross-session stores.
17478
+ * No new storage: reads the JSON/NDJSON files the session, notes, weights,
17479
+ * evidence, and tracking modules already own under `.context/`.
17480
+ */
17481
+
17482
+ const fs = require('fs');
17483
+ const path = require('path');
17484
+
17485
+ /** store name → { file, kind } (kind drives the entry count). */
17486
+ const STORES = {
17487
+ session: { file: 'session.json', kind: 'json' },
17488
+ notes: { file: 'notes.ndjson', kind: 'ndjson' },
17489
+ weights: { file: 'weights.json', kind: 'weights' },
17490
+ evidence: { file: 'evidence-pack.json', kind: 'json' },
17491
+ gain: { file: 'gain.ndjson', kind: 'ndjson' },
17492
+ usage: { file: 'usage.ndjson', kind: 'ndjson' },
17493
+ };
17494
+
17495
+ /** Stores `clearMemory` may delete ('gain'/'usage' have their own reset flows). */
17496
+ const CLEARABLE = ['session', 'notes', 'weights', 'evidence'];
17497
+
17498
+ function storePath(cwd, name) {
17499
+ return path.join(cwd, '.context', STORES[name].file);
17500
+ }
17501
+
17502
+ function countEntries(kind, filePath) {
17503
+ try {
17504
+ if (kind === 'ndjson') {
17505
+ return fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean).length;
17506
+ }
17507
+ if (kind === 'weights') {
17508
+ const w = JSON.parse(fs.readFileSync(filePath, 'utf8'));
17509
+ return Object.keys((w && w.files) || w || {}).length;
17510
+ }
17511
+ return 1; // json: a single snapshot object
17512
+ } catch (_) {
17513
+ return 0;
17514
+ }
17515
+ }
17516
+
17517
+ /**
17518
+ * Describe every cross-session store.
17519
+ * @param {string} cwd
17520
+ * @returns {Array<{store:string, path:string, exists:boolean, entries:number, bytes:number, modified:string|null, clearable:boolean}>}
17521
+ */
17522
+ function inspectMemory(cwd) {
17523
+ return Object.entries(STORES).map(([store, { file, kind }]) => {
17524
+ const p = storePath(cwd, store);
17525
+ let stat = null;
17526
+ try { stat = fs.statSync(p); } catch (_) {}
17527
+ return {
17528
+ store,
17529
+ path: path.join('.context', file),
17530
+ exists: !!stat,
17531
+ entries: stat ? countEntries(kind, p) : 0,
17532
+ bytes: stat ? stat.size : 0,
17533
+ modified: stat ? new Date(stat.mtimeMs).toISOString() : null,
17534
+ clearable: CLEARABLE.includes(store),
17535
+ };
17536
+ });
17537
+ }
17538
+
17539
+ /**
17540
+ * Delete one clearable store (or 'all' clearable stores).
17541
+ * @param {string} cwd
17542
+ * @param {string} store - session|notes|weights|evidence|all
17543
+ * @returns {string[]} names of stores actually removed
17544
+ */
17545
+ function clearMemory(cwd, store) {
17546
+ const targets = store === 'all' ? CLEARABLE : [store];
17547
+ for (const t of targets) {
17548
+ if (!CLEARABLE.includes(t)) {
17549
+ throw new Error(`unknown or protected store "${t}" — clearable: ${CLEARABLE.join(', ')}, all`);
17550
+ }
17551
+ }
17552
+ const removed = [];
17553
+ for (const t of targets) {
17554
+ try { fs.unlinkSync(storePath(cwd, t)); removed.push(t); } catch (_) {}
17555
+ }
17556
+ return removed;
17557
+ }
17558
+
17559
+ module.exports = { inspectMemory, clearMemory, STORES, CLEARABLE };
17560
+
17561
+ };
17562
+
17205
17563
  // ── ./src/session/notes ──
17206
17564
  __factories["./src/session/notes"] = function(module, exports) {
17207
17565
 
@@ -19710,7 +20068,7 @@ function __tryGit(args, opts = {}) {
19710
20068
  catch (_) { return ''; }
19711
20069
  }
19712
20070
 
19713
- const VERSION = '8.19.0';
20071
+ const VERSION = '8.21.0';
19714
20072
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
19715
20073
 
19716
20074
  function requireSourceOrBundled(key) {
@@ -21593,6 +21951,8 @@ Usage:
21593
21951
  ${cmd} evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
21594
21952
  ${cmd} evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
21595
21953
  ${cmd} evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
21954
+ ${cmd} memory List cross-session stores (.context/) — entries, size, age
21955
+ ${cmd} memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
21596
21956
  ${cmd} note "<text>" Append a note to the cross-session decision log
21597
21957
  ${cmd} note List recent notes (also: note --list <N>)
21598
21958
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -22042,8 +22402,16 @@ function main() {
22042
22402
  if (config && config.retrieval && config.retrieval.surfaceEnrichment) {
22043
22403
  try { requireSourceOrBundled('./src/retrieval/enrich-from-maps').enrichWithSurfaces(sigIndex, cwd); } catch (_) {}
22044
22404
  }
22405
+ // Opt-in import-graph centrality blend (retrieval.centralityBlend) — non-fatal
22406
+ let askCentrality = null;
22407
+ if (config && config.retrieval && config.retrieval.centralityBlend) {
22408
+ try {
22409
+ const askCentralityGraph = requireSourceOrBundled('./src/graph/builder').buildFromCwd(cwd);
22410
+ askCentrality = requireSourceOrBundled('./src/graph/centrality').computeCentrality(askCentralityGraph);
22411
+ } catch (_) {}
22412
+ }
22045
22413
 
22046
- let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph });
22414
+ let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph, centrality: askCentrality });
22047
22415
 
22048
22416
  // v6.10: Workspace scoping — infer package from query and apply boost
22049
22417
  const workspaces = detectWorkspaces(cwd);
@@ -22986,6 +23354,56 @@ function main() {
22986
23354
 
22987
23355
  // `sigmap note "<text>"` — append to the cross-session decision log.
22988
23356
  // With no text, lists recent notes (also `note --list [N]`).
23357
+ // `sigmap memory` — one view over the cross-session stores in .context/.
23358
+ if (args[0] === 'memory') {
23359
+ const jsonOut = args.includes('--json');
23360
+ const { inspectMemory, clearMemory } = requireSourceOrBundled('./src/session/memory-inspect');
23361
+ const clearIdx = args.indexOf('--clear');
23362
+
23363
+ if (clearIdx !== -1) {
23364
+ const store = args[clearIdx + 1];
23365
+ if (!store || store.startsWith('--')) {
23366
+ console.error('[sigmap] usage: sigmap memory --clear <session|notes|weights|evidence|all>');
23367
+ process.exit(1);
23368
+ }
23369
+ let removed;
23370
+ try {
23371
+ removed = clearMemory(cwd, store);
23372
+ } catch (err) {
23373
+ console.error(`[sigmap] ${err.message}`);
23374
+ process.exit(1);
23375
+ }
23376
+ if (jsonOut) {
23377
+ process.stdout.write(JSON.stringify({ cleared: removed }) + '\n');
23378
+ } else {
23379
+ console.log(removed.length
23380
+ ? `[sigmap] cleared: ${removed.join(', ')}`
23381
+ : `[sigmap] nothing to clear for "${store}"`);
23382
+ }
23383
+ process.exit(0);
23384
+ }
23385
+
23386
+ const stores = inspectMemory(cwd);
23387
+ if (jsonOut) {
23388
+ process.stdout.write(JSON.stringify({ stores }) + '\n');
23389
+ process.exit(0);
23390
+ }
23391
+ const fmtAge = (iso) => {
23392
+ if (!iso) return '—';
23393
+ const m = Math.floor((Date.now() - Date.parse(iso)) / 60000);
23394
+ const h = Math.floor(m / 60), d = Math.floor(h / 24);
23395
+ return d > 0 ? `${d}d ago` : h > 0 ? `${h}h ago` : `${m}m ago`;
23396
+ };
23397
+ const fmtKB = (b) => (b >= 1024 ? `${(b / 1024).toFixed(1)}KB` : `${b}B`);
23398
+ console.log('[sigmap] cross-session memory (.context/)');
23399
+ for (const s of stores) {
23400
+ const state = s.exists ? `${String(s.entries).padStart(5)} entries ${fmtKB(s.bytes).padStart(8)} ${fmtAge(s.modified)}` : ' — empty';
23401
+ console.log(` ${s.store.padEnd(9)} ${state}${s.clearable ? '' : ' (reset via its own command)'}`);
23402
+ }
23403
+ console.log(' clear: sigmap memory --clear <session|notes|weights|evidence|all>');
23404
+ process.exit(0);
23405
+ }
23406
+
22989
23407
  if (args[0] === 'note') {
22990
23408
  const jsonOut = args.includes('--json');
22991
23409
  const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
@@ -24224,7 +24642,15 @@ function main() {
24224
24642
  if (config && config.retrieval && config.retrieval.surfaceEnrichment) {
24225
24643
  try { requireSourceOrBundled('./src/retrieval/enrich-from-maps').enrichWithSurfaces(index, cwd); } catch (_) {}
24226
24644
  }
24227
- const results = rank(query, index, { topK, recencyBoost, cwd, callGraph: queryCallGraph });
24645
+ // Opt-in import-graph centrality blend (retrieval.centralityBlend) non-fatal
24646
+ let queryCentrality = null;
24647
+ if (config && config.retrieval && config.retrieval.centralityBlend) {
24648
+ try {
24649
+ const centralityGraph = requireSourceOrBundled('./src/graph/builder').buildFromCwd(cwd);
24650
+ queryCentrality = requireSourceOrBundled('./src/graph/centrality').computeCentrality(centralityGraph);
24651
+ } catch (_) {}
24652
+ }
24653
+ const results = rank(query, index, { topK, recencyBoost, cwd, callGraph: queryCallGraph, centrality: queryCentrality });
24228
24654
  if (args.includes('--context')) {
24229
24655
  const miniCtx = buildMiniContext(results, cwd);
24230
24656
  const ctxOut = path.join(cwd, '.context', 'query-context.md');