sigmap 8.19.0 → 8.20.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/CHANGELOG.md CHANGED
@@ -10,6 +10,19 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.20.0] — 2026-07-19
14
+
15
+ Minor release — **"Semantic Bridge I" (v8.20)**: the JS/TS extractors gain the same doc-comment hints Python has carried for releases, and the cross-session stores get a single inspect/prune surface.
16
+
17
+ ### Added
18
+ - **JS/TS doc-comment hints (#498, PR #499):** `buildDocHints` in the JavaScript and TypeScript extractors mines the first prose sentence of the JSDoc block immediately preceding each top-level function form (exported function, exported arrow const, top-level function) and appends it after the line anchor as ` # <hint>` — byte-format identical to the Python extractor's `extractDocHint`, restoring cross-language consistency. A tempered comment-body pattern prevents cross-block hint misattribution (caught in smoke testing before landing). The semantic value is proven directly by a new vocab-mismatch fixture: a query whose vocabulary is fully disjoint from every identifier retrieves the file **only** via its hint (BM25 score 0 without).
19
+ - **`sigmap memory` (#498, PR #499):** one view over the existing `.context/` cross-session stores — session, notes, weights, evidence, gain, usage — with per-store entry counts, size, and age; `--json`; and explicit `--clear <session|notes|weights|evidence|all>`. Tracking stores (gain/usage) are listed but protected (they keep their own reset flows). No new storage, zero dependencies.
20
+
21
+ ### Changed
22
+ - **Headline metric shift, documented honestly:** doc hints add English tokens that compete on the lexical-favoring corpus — one borderline task (`svelte-t002`) fell out of top-5 because competing public-API files' hints carry the query's words. **hit@5 86.4% → 85.5% task-level (−0.9pt), honest lift 2.02× → 2.00×**; grep baseline unchanged at 42.7%. Shipped default-on per the v8.18 anchors precedent and Python-parity; the v8.22 hard-split corpus (A3) will measure the semantic upside these hints exist for. 10 new integration tests (126 test files).
23
+
24
+ ---
25
+
13
26
  ## [8.19.0] — 2026-07-19
14
27
 
15
28
  Minor release — **"Honest Numbers" (v8.19, P0)**: the published retrieval lift now comes from a measured grep-agent comparison, not a random-file baseline, and every proxy metric says so on the label.
package/README.md CHANGED
@@ -59,10 +59,10 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
59
59
 
60
60
  **Proof it pays off** (full benchmark below):
61
61
  <!--SM:whyMetrics-->
62
- - **86.7% hit@5** — right file in top 5 results (vs 42.7% single-shot grep baseline — 2.02× lift)
63
- - **96.9% token reduction** — average across 21 real repos
64
- - **68.9% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
- - **1.44 prompts per task** — down from 2.84 (49.2% fewer retries, modeled)
62
+ - **85.6% hit@5** — right file in top 5 results (vs 42.7% single-shot grep baseline — 2.00× lift)
63
+ - **96.8% token reduction** — average across 21 real repos
64
+ - **67.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
+ - **1.47 prompts per task** — down from 2.84 (48.4% fewer retries, modeled)
66
66
  <!--/SM:whyMetrics-->
67
67
  - **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
68
68
  - **No vendor lock-in** — works with any AI assistant or local LLM
@@ -98,7 +98,7 @@ sigmap verify answer.md --report # standalone red/amber/green HTML report
98
98
  | Without SigMap | With SigMap |
99
99
  |---|---|
100
100
  | ❌ Non-reproducible agent guesses | ✅ Deterministic map — same input, same output, every time |
101
- | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->87%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
101
+ | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->86%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
102
102
  | ❌ Embeddings / vector DB required | ✅ Zero deps, no infra, fully offline |
103
103
 
104
104
  ---
@@ -122,13 +122,13 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.19-main (21 repositories, including R language)
125
+ Benchmark : sigmap-v8.20-main (21 repositories, including R language)
126
126
  Date : 2026-07-19
127
127
 
128
- Hit@5 : 86.7% (grep-agent baseline 42.7% — 2.02× lift)
129
- Token reduction: 96.9% (across 21 repos)
130
- Prompt reduction : 49.2% (2.84 → 1.44 prompts per task, modeled)
131
- Task success : 68.9% (proxy — modeled from retrieval tiers)
128
+ Hit@5 : 85.6% (grep-agent baseline 42.7% — 2.00× lift)
129
+ Token reduction: 96.8% (across 21 repos)
130
+ Prompt reduction : 48.4% (2.84 → 1.47 prompts per task, modeled)
131
+ Task success : 67.8% (proxy — modeled from retrieval tiers)
132
132
  Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
133
133
  ```
134
134
  <!--/SM:benchmarkBlock-->
package/gen-context.js CHANGED
@@ -6203,7 +6203,12 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6203
6203
  if (!src || typeof src !== 'string') return [];
6204
6204
  const sigs = [];
6205
6205
  const anchors = [];
6206
+ // docHintFor[i] is the doc-comment hint for sigs[i] (top-level functions
6207
+ // only), appended after the anchor as ` # <hint>` — same convention as the
6208
+ // Python extractor's extractDocHint.
6209
+ const docHintFor = [];
6206
6210
  const returnHints = buildReturnHints(src);
6211
+ const docHints = buildDocHints(src);
6207
6212
 
6208
6213
  // Block comments are blanked newline-by-newline (non-newline chars → spaces)
6209
6214
  // so character offsets AND line numbers stay exact for anchors.
@@ -6238,6 +6243,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6238
6243
  const retStr = formatReturnHint(returnHints.get(m[1]));
6239
6244
  const startLn = lineAt(stripped, m.index);
6240
6245
  sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6246
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6241
6247
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6242
6248
  }
6243
6249
 
@@ -6247,6 +6253,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6247
6253
  const retStr = formatReturnHint(returnHints.get(m[1]));
6248
6254
  const startLn = lineAt(stripped, m.index);
6249
6255
  sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
6256
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6250
6257
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6251
6258
  }
6252
6259
 
@@ -6267,10 +6274,14 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6267
6274
  const retStr = formatReturnHint(returnHints.get(m[1]));
6268
6275
  const startLn = lineAt(stripped, m.index);
6269
6276
  sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6277
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
6270
6278
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6271
6279
  }
6272
6280
 
6273
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
6281
+ const withAnchors = sigs.map((s, i) => {
6282
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
6283
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
6284
+ });
6274
6285
  return capWithNotice(withAnchors, 25, 'signatures');
6275
6286
  }
6276
6287
 
@@ -6319,6 +6330,36 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6319
6330
  return hints;
6320
6331
  }
6321
6332
 
6333
+ // First prose sentence of the JSDoc block immediately preceding a top-level
6334
+ // function (same three shapes as buildReturnHints). Mirrors the Python
6335
+ // extractor's extractDocHint: first sentence only, 60-char cap.
6336
+ function buildDocHints(src) {
6337
+ const hints = new Map();
6338
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
6339
+ // the match expand across a whole function to the next comment block and
6340
+ // misattribute the hint.
6341
+ const patterns = [
6342
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g,
6343
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g,
6344
+ ];
6345
+ for (const re of patterns) {
6346
+ for (const m of src.matchAll(re)) {
6347
+ const hint = firstDocSentence(m[1]);
6348
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
6349
+ }
6350
+ }
6351
+ return hints;
6352
+ }
6353
+
6354
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
6355
+ function firstDocSentence(body) {
6356
+ const line = String(body).split('\n')
6357
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
6358
+ .find((l) => l && !l.startsWith('@'));
6359
+ if (!line) return '';
6360
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
6361
+ }
6362
+
6322
6363
  function normalizeType(type) {
6323
6364
  if (!type) return '';
6324
6365
  return type.trim().replace(/\s+/g, ' ').slice(0, 25);
@@ -8307,6 +8348,11 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8307
8348
  function extract(src) {
8308
8349
  if (!src || typeof src !== 'string') return [];
8309
8350
  const sigs = [];
8351
+ // docHintFor[i] is the doc-comment hint for sigs[i] (exported top-level
8352
+ // functions only), appended after the anchor as ` # <hint>` — same
8353
+ // convention as the Python extractor's extractDocHint.
8354
+ const docHintFor = [];
8355
+ const docHints = buildDocHints(src);
8310
8356
  // anchors[i] is [start, end] for a top-level sig, or null for an indented member.
8311
8357
  // Kept parallel to `sigs` so existing push/mutation logic stays untouched;
8312
8358
  // anchors are applied once at return.
@@ -8374,6 +8420,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8374
8420
  const retStr = retType ? ` → ${retType}` : '';
8375
8421
  const bodyStart = m.index + m[0].length;
8376
8422
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
8423
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
8377
8424
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
8378
8425
 
8379
8426
  // Hooks: capture compact return object shape for use* functions.
@@ -8398,6 +8445,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8398
8445
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
8399
8446
  const params = normalizeParams(m[2]);
8400
8447
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
8448
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
8401
8449
  const bodyStart = stripped.indexOf('{', m.index + m[0].length);
8402
8450
  const endLn = bodyStart !== -1
8403
8451
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
@@ -8447,7 +8495,10 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8447
8495
  }
8448
8496
  }
8449
8497
 
8450
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
8498
+ const withAnchors = sigs.map((s, i) => {
8499
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
8500
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
8501
+ });
8451
8502
  return capWithNotice(withAnchors, 35, 'signatures');
8452
8503
  }
8453
8504
 
@@ -8512,6 +8563,36 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8512
8563
  return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
8513
8564
  }
8514
8565
 
8566
+ // First prose sentence of the JSDoc block immediately preceding an exported
8567
+ // top-level function (function or arrow-const form). Mirrors the Python
8568
+ // extractor's extractDocHint: first sentence only, 60-char cap.
8569
+ function buildDocHints(src) {
8570
+ const hints = new Map();
8571
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
8572
+ // the match expand across a whole function to the next comment block and
8573
+ // misattribute the hint.
8574
+ const patterns = [
8575
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/g,
8576
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*[:=]/g,
8577
+ ];
8578
+ for (const re of patterns) {
8579
+ for (const m of src.matchAll(re)) {
8580
+ const hint = firstDocSentence(m[1]);
8581
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
8582
+ }
8583
+ }
8584
+ return hints;
8585
+ }
8586
+
8587
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
8588
+ function firstDocSentence(body) {
8589
+ const line = String(body).split('\n')
8590
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
8591
+ .find((l) => l && !l.startsWith('@'));
8592
+ if (!line) return '';
8593
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
8594
+ }
8595
+
8515
8596
  module.exports = { extract };
8516
8597
 
8517
8598
  };
@@ -14607,7 +14688,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
14607
14688
 
14608
14689
  const SERVER_INFO = {
14609
14690
  name: 'sigmap',
14610
- version: '8.19.0',
14691
+ version: '8.20.0',
14611
14692
  description: 'SigMap MCP server — code signatures on demand',
14612
14693
  };
14613
14694
 
@@ -17202,6 +17283,96 @@ __factories["./src/session/memory"] = function(module, exports) {
17202
17283
 
17203
17284
  };
17204
17285
 
17286
+ // ── ./src/session/memory-inspect ──
17287
+ __factories["./src/session/memory-inspect"] = function(module, exports) {
17288
+
17289
+ /**
17290
+ * memory-inspect.js — one view over SigMap's existing cross-session stores.
17291
+ * No new storage: reads the JSON/NDJSON files the session, notes, weights,
17292
+ * evidence, and tracking modules already own under `.context/`.
17293
+ */
17294
+
17295
+ const fs = require('fs');
17296
+ const path = require('path');
17297
+
17298
+ /** store name → { file, kind } (kind drives the entry count). */
17299
+ const STORES = {
17300
+ session: { file: 'session.json', kind: 'json' },
17301
+ notes: { file: 'notes.ndjson', kind: 'ndjson' },
17302
+ weights: { file: 'weights.json', kind: 'weights' },
17303
+ evidence: { file: 'evidence-pack.json', kind: 'json' },
17304
+ gain: { file: 'gain.ndjson', kind: 'ndjson' },
17305
+ usage: { file: 'usage.ndjson', kind: 'ndjson' },
17306
+ };
17307
+
17308
+ /** Stores `clearMemory` may delete ('gain'/'usage' have their own reset flows). */
17309
+ const CLEARABLE = ['session', 'notes', 'weights', 'evidence'];
17310
+
17311
+ function storePath(cwd, name) {
17312
+ return path.join(cwd, '.context', STORES[name].file);
17313
+ }
17314
+
17315
+ function countEntries(kind, filePath) {
17316
+ try {
17317
+ if (kind === 'ndjson') {
17318
+ return fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean).length;
17319
+ }
17320
+ if (kind === 'weights') {
17321
+ const w = JSON.parse(fs.readFileSync(filePath, 'utf8'));
17322
+ return Object.keys((w && w.files) || w || {}).length;
17323
+ }
17324
+ return 1; // json: a single snapshot object
17325
+ } catch (_) {
17326
+ return 0;
17327
+ }
17328
+ }
17329
+
17330
+ /**
17331
+ * Describe every cross-session store.
17332
+ * @param {string} cwd
17333
+ * @returns {Array<{store:string, path:string, exists:boolean, entries:number, bytes:number, modified:string|null, clearable:boolean}>}
17334
+ */
17335
+ function inspectMemory(cwd) {
17336
+ return Object.entries(STORES).map(([store, { file, kind }]) => {
17337
+ const p = storePath(cwd, store);
17338
+ let stat = null;
17339
+ try { stat = fs.statSync(p); } catch (_) {}
17340
+ return {
17341
+ store,
17342
+ path: path.join('.context', file),
17343
+ exists: !!stat,
17344
+ entries: stat ? countEntries(kind, p) : 0,
17345
+ bytes: stat ? stat.size : 0,
17346
+ modified: stat ? new Date(stat.mtimeMs).toISOString() : null,
17347
+ clearable: CLEARABLE.includes(store),
17348
+ };
17349
+ });
17350
+ }
17351
+
17352
+ /**
17353
+ * Delete one clearable store (or 'all' clearable stores).
17354
+ * @param {string} cwd
17355
+ * @param {string} store - session|notes|weights|evidence|all
17356
+ * @returns {string[]} names of stores actually removed
17357
+ */
17358
+ function clearMemory(cwd, store) {
17359
+ const targets = store === 'all' ? CLEARABLE : [store];
17360
+ for (const t of targets) {
17361
+ if (!CLEARABLE.includes(t)) {
17362
+ throw new Error(`unknown or protected store "${t}" — clearable: ${CLEARABLE.join(', ')}, all`);
17363
+ }
17364
+ }
17365
+ const removed = [];
17366
+ for (const t of targets) {
17367
+ try { fs.unlinkSync(storePath(cwd, t)); removed.push(t); } catch (_) {}
17368
+ }
17369
+ return removed;
17370
+ }
17371
+
17372
+ module.exports = { inspectMemory, clearMemory, STORES, CLEARABLE };
17373
+
17374
+ };
17375
+
17205
17376
  // ── ./src/session/notes ──
17206
17377
  __factories["./src/session/notes"] = function(module, exports) {
17207
17378
 
@@ -19710,7 +19881,7 @@ function __tryGit(args, opts = {}) {
19710
19881
  catch (_) { return ''; }
19711
19882
  }
19712
19883
 
19713
- const VERSION = '8.19.0';
19884
+ const VERSION = '8.20.0';
19714
19885
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
19715
19886
 
19716
19887
  function requireSourceOrBundled(key) {
@@ -21593,6 +21764,8 @@ Usage:
21593
21764
  ${cmd} evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
21594
21765
  ${cmd} evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
21595
21766
  ${cmd} evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
21767
+ ${cmd} memory List cross-session stores (.context/) — entries, size, age
21768
+ ${cmd} memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
21596
21769
  ${cmd} note "<text>" Append a note to the cross-session decision log
21597
21770
  ${cmd} note List recent notes (also: note --list <N>)
21598
21771
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -22986,6 +23159,56 @@ function main() {
22986
23159
 
22987
23160
  // `sigmap note "<text>"` — append to the cross-session decision log.
22988
23161
  // With no text, lists recent notes (also `note --list [N]`).
23162
+ // `sigmap memory` — one view over the cross-session stores in .context/.
23163
+ if (args[0] === 'memory') {
23164
+ const jsonOut = args.includes('--json');
23165
+ const { inspectMemory, clearMemory } = requireSourceOrBundled('./src/session/memory-inspect');
23166
+ const clearIdx = args.indexOf('--clear');
23167
+
23168
+ if (clearIdx !== -1) {
23169
+ const store = args[clearIdx + 1];
23170
+ if (!store || store.startsWith('--')) {
23171
+ console.error('[sigmap] usage: sigmap memory --clear <session|notes|weights|evidence|all>');
23172
+ process.exit(1);
23173
+ }
23174
+ let removed;
23175
+ try {
23176
+ removed = clearMemory(cwd, store);
23177
+ } catch (err) {
23178
+ console.error(`[sigmap] ${err.message}`);
23179
+ process.exit(1);
23180
+ }
23181
+ if (jsonOut) {
23182
+ process.stdout.write(JSON.stringify({ cleared: removed }) + '\n');
23183
+ } else {
23184
+ console.log(removed.length
23185
+ ? `[sigmap] cleared: ${removed.join(', ')}`
23186
+ : `[sigmap] nothing to clear for "${store}"`);
23187
+ }
23188
+ process.exit(0);
23189
+ }
23190
+
23191
+ const stores = inspectMemory(cwd);
23192
+ if (jsonOut) {
23193
+ process.stdout.write(JSON.stringify({ stores }) + '\n');
23194
+ process.exit(0);
23195
+ }
23196
+ const fmtAge = (iso) => {
23197
+ if (!iso) return '—';
23198
+ const m = Math.floor((Date.now() - Date.parse(iso)) / 60000);
23199
+ const h = Math.floor(m / 60), d = Math.floor(h / 24);
23200
+ return d > 0 ? `${d}d ago` : h > 0 ? `${h}h ago` : `${m}m ago`;
23201
+ };
23202
+ const fmtKB = (b) => (b >= 1024 ? `${(b / 1024).toFixed(1)}KB` : `${b}B`);
23203
+ console.log('[sigmap] cross-session memory (.context/)');
23204
+ for (const s of stores) {
23205
+ const state = s.exists ? `${String(s.entries).padStart(5)} entries ${fmtKB(s.bytes).padStart(8)} ${fmtAge(s.modified)}` : ' — empty';
23206
+ console.log(` ${s.store.padEnd(9)} ${state}${s.clearable ? '' : ' (reset via its own command)'}`);
23207
+ }
23208
+ console.log(' clear: sigmap memory --clear <session|notes|weights|evidence|all>');
23209
+ process.exit(0);
23210
+ }
23211
+
22989
23212
  if (args[0] === 'note') {
22990
23213
  const jsonOut = args.includes('--json');
22991
23214
  const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
package/llms-full.txt CHANGED
@@ -11,20 +11,20 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.19.0 | Benchmark: sigmap-v8.19-main (2026-07-19)
14
+ # Version: 8.20.0 | Benchmark: sigmap-v8.20-main (2026-07-19)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v8.19-main, 2026-07-19)
20
+ ## Core metrics (benchmark: sigmap-v8.20-main, 2026-07-19)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 42.7% (single-shot grep) | 86.7% (2.02× lift) |
25
- | Token reduction | — | 96.9% average |
26
- | Task-success proxy (modeled) | — | 68.9% |
27
- | Prompts per task | 2.84 | 1.44 (49.2% fewer) |
24
+ | Retrieval hit@5 | 42.7% (single-shot grep) | 85.6% (2.00× lift) |
25
+ | Token reduction | — | 96.8% average |
26
+ | Task-success proxy (modeled) | — | 67.8% |
27
+ | Prompts per task | 2.84 | 1.47 (48.4% fewer) |
28
28
  | Supported languages | — | 33 |
29
29
  | MCP tools | — | 20 |
30
30
  | npm runtime dependencies | — | 0 |
@@ -123,6 +123,8 @@ sigmap ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default
123
123
  sigmap evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
124
124
  sigmap evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
125
125
  sigmap evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
126
+ sigmap memory List cross-session stores (.context/) — entries, size, age
127
+ sigmap memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
126
128
  sigmap note "<text>" Append a note to the cross-session decision log
127
129
  sigmap note List recent notes (also: note --list <N>)
128
130
  sigmap status Show repo state — branch, dirty files, index freshness, notes
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.19.0 | Benchmark: sigmap-v8.19-main (2026-07-19)
14
+ # Version: 8.20.0 | Benchmark: sigmap-v8.20-main (2026-07-19)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,12 +23,12 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v8.19-main, 2026-07-19)
26
+ ## Core metrics (benchmark: sigmap-v8.20-main, 2026-07-19)
27
27
 
28
- - hit@5 retrieval: 86.7% vs 42.7% single-shot grep baseline (2.02× lift)
29
- - Token reduction: 96.9% average across benchmark repos
30
- - Task-success proxy: 68.9% (modeled from retrieval tiers, not measured LLM sessions)
31
- - Prompts per task: 1.44 vs 2.84 baseline (49.2% fewer, modeled)
28
+ - hit@5 retrieval: 85.6% vs 42.7% single-shot grep baseline (2.00× lift)
29
+ - Token reduction: 96.8% average across benchmark repos
30
+ - Task-success proxy: 67.8% (modeled from retrieval tiers, not measured LLM sessions)
31
+ - Prompts per task: 1.47 vs 2.84 baseline (48.4% fewer, modeled)
32
32
  - Languages: 33 supported · MCP tools: 20
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.19.0",
3
+ "version": "8.20.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.19.0",
3
+ "version": "8.20.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.19.0",
3
+ "version": "8.20.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -14,7 +14,12 @@ function extract(src) {
14
14
  if (!src || typeof src !== 'string') return [];
15
15
  const sigs = [];
16
16
  const anchors = [];
17
+ // docHintFor[i] is the doc-comment hint for sigs[i] (top-level functions
18
+ // only), appended after the anchor as ` # <hint>` — same convention as the
19
+ // Python extractor's extractDocHint.
20
+ const docHintFor = [];
17
21
  const returnHints = buildReturnHints(src);
22
+ const docHints = buildDocHints(src);
18
23
 
19
24
  // Block comments are blanked newline-by-newline (non-newline chars → spaces)
20
25
  // so character offsets AND line numbers stay exact for anchors.
@@ -49,6 +54,7 @@ function extract(src) {
49
54
  const retStr = formatReturnHint(returnHints.get(m[1]));
50
55
  const startLn = lineAt(stripped, m.index);
51
56
  sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
57
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
52
58
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
53
59
  }
54
60
 
@@ -58,6 +64,7 @@ function extract(src) {
58
64
  const retStr = formatReturnHint(returnHints.get(m[1]));
59
65
  const startLn = lineAt(stripped, m.index);
60
66
  sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
67
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
61
68
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
62
69
  }
63
70
 
@@ -78,10 +85,14 @@ function extract(src) {
78
85
  const retStr = formatReturnHint(returnHints.get(m[1]));
79
86
  const startLn = lineAt(stripped, m.index);
80
87
  sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
88
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
81
89
  anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
82
90
  }
83
91
 
84
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
92
+ const withAnchors = sigs.map((s, i) => {
93
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
94
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
95
+ });
85
96
  return capWithNotice(withAnchors, 25, 'signatures');
86
97
  }
87
98
 
@@ -130,6 +141,36 @@ function buildReturnHints(src) {
130
141
  return hints;
131
142
  }
132
143
 
144
+ // First prose sentence of the JSDoc block immediately preceding a top-level
145
+ // function (same three shapes as buildReturnHints). Mirrors the Python
146
+ // extractor's extractDocHint: first sentence only, 60-char cap.
147
+ function buildDocHints(src) {
148
+ const hints = new Map();
149
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
150
+ // the match expand across a whole function to the next comment block and
151
+ // misattribute the hint.
152
+ const patterns = [
153
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g,
154
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g,
155
+ ];
156
+ for (const re of patterns) {
157
+ for (const m of src.matchAll(re)) {
158
+ const hint = firstDocSentence(m[1]);
159
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
160
+ }
161
+ }
162
+ return hints;
163
+ }
164
+
165
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
166
+ function firstDocSentence(body) {
167
+ const line = String(body).split('\n')
168
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
169
+ .find((l) => l && !l.startsWith('@'));
170
+ if (!line) return '';
171
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
172
+ }
173
+
133
174
  function normalizeType(type) {
134
175
  if (!type) return '';
135
176
  return type.trim().replace(/\s+/g, ' ').slice(0, 25);
@@ -13,6 +13,11 @@ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
13
13
  function extract(src) {
14
14
  if (!src || typeof src !== 'string') return [];
15
15
  const sigs = [];
16
+ // docHintFor[i] is the doc-comment hint for sigs[i] (exported top-level
17
+ // functions only), appended after the anchor as ` # <hint>` — same
18
+ // convention as the Python extractor's extractDocHint.
19
+ const docHintFor = [];
20
+ const docHints = buildDocHints(src);
16
21
  // anchors[i] is [start, end] for a top-level sig, or null for an indented member.
17
22
  // Kept parallel to `sigs` so existing push/mutation logic stays untouched;
18
23
  // anchors are applied once at return.
@@ -80,6 +85,7 @@ function extract(src) {
80
85
  const retStr = retType ? ` → ${retType}` : '';
81
86
  const bodyStart = m.index + m[0].length;
82
87
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
88
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
83
89
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
84
90
 
85
91
  // Hooks: capture compact return object shape for use* functions.
@@ -104,6 +110,7 @@ function extract(src) {
104
110
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
105
111
  const params = normalizeParams(m[2]);
106
112
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
113
+ docHintFor[sigs.length - 1] = docHints.get(m[1]);
107
114
  const bodyStart = stripped.indexOf('{', m.index + m[0].length);
108
115
  const endLn = bodyStart !== -1
109
116
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
@@ -153,7 +160,10 @@ function extract(src) {
153
160
  }
154
161
  }
155
162
 
156
- const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
163
+ const withAnchors = sigs.map((s, i) => {
164
+ const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
165
+ return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
166
+ });
157
167
  return capWithNotice(withAnchors, 35, 'signatures');
158
168
  }
159
169
 
@@ -218,4 +228,34 @@ function normalizeParams(params) {
218
228
  return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
219
229
  }
220
230
 
231
+ // First prose sentence of the JSDoc block immediately preceding an exported
232
+ // top-level function (function or arrow-const form). Mirrors the Python
233
+ // extractor's extractDocHint: first sentence only, 60-char cap.
234
+ function buildDocHints(src) {
235
+ const hints = new Map();
236
+ // Body may not contain `*/` — otherwise a failed adjacency check would let
237
+ // the match expand across a whole function to the next comment block and
238
+ // misattribute the hint.
239
+ const patterns = [
240
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/g,
241
+ /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*[:=]/g,
242
+ ];
243
+ for (const re of patterns) {
244
+ for (const m of src.matchAll(re)) {
245
+ const hint = firstDocSentence(m[1]);
246
+ if (hint && !hints.has(m[2])) hints.set(m[2], hint);
247
+ }
248
+ }
249
+ return hints;
250
+ }
251
+
252
+ // First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
253
+ function firstDocSentence(body) {
254
+ const line = String(body).split('\n')
255
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
256
+ .find((l) => l && !l.startsWith('@'));
257
+ if (!line) return '';
258
+ return line.split(/[.!?]/)[0].trim().slice(0, 60);
259
+ }
260
+
221
261
  module.exports = { extract };
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.19.0',
21
+ version: '8.20.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * memory-inspect.js — one view over SigMap's existing cross-session stores.
5
+ * No new storage: reads the JSON/NDJSON files the session, notes, weights,
6
+ * evidence, and tracking modules already own under `.context/`.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /** store name → { file, kind } (kind drives the entry count). */
13
+ const STORES = {
14
+ session: { file: 'session.json', kind: 'json' },
15
+ notes: { file: 'notes.ndjson', kind: 'ndjson' },
16
+ weights: { file: 'weights.json', kind: 'weights' },
17
+ evidence: { file: 'evidence-pack.json', kind: 'json' },
18
+ gain: { file: 'gain.ndjson', kind: 'ndjson' },
19
+ usage: { file: 'usage.ndjson', kind: 'ndjson' },
20
+ };
21
+
22
+ /** Stores `clearMemory` may delete ('gain'/'usage' have their own reset flows). */
23
+ const CLEARABLE = ['session', 'notes', 'weights', 'evidence'];
24
+
25
+ function storePath(cwd, name) {
26
+ return path.join(cwd, '.context', STORES[name].file);
27
+ }
28
+
29
+ function countEntries(kind, filePath) {
30
+ try {
31
+ if (kind === 'ndjson') {
32
+ return fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean).length;
33
+ }
34
+ if (kind === 'weights') {
35
+ const w = JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ return Object.keys((w && w.files) || w || {}).length;
37
+ }
38
+ return 1; // json: a single snapshot object
39
+ } catch (_) {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Describe every cross-session store.
46
+ * @param {string} cwd
47
+ * @returns {Array<{store:string, path:string, exists:boolean, entries:number, bytes:number, modified:string|null, clearable:boolean}>}
48
+ */
49
+ function inspectMemory(cwd) {
50
+ return Object.entries(STORES).map(([store, { file, kind }]) => {
51
+ const p = storePath(cwd, store);
52
+ let stat = null;
53
+ try { stat = fs.statSync(p); } catch (_) {}
54
+ return {
55
+ store,
56
+ path: path.join('.context', file),
57
+ exists: !!stat,
58
+ entries: stat ? countEntries(kind, p) : 0,
59
+ bytes: stat ? stat.size : 0,
60
+ modified: stat ? new Date(stat.mtimeMs).toISOString() : null,
61
+ clearable: CLEARABLE.includes(store),
62
+ };
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Delete one clearable store (or 'all' clearable stores).
68
+ * @param {string} cwd
69
+ * @param {string} store - session|notes|weights|evidence|all
70
+ * @returns {string[]} names of stores actually removed
71
+ */
72
+ function clearMemory(cwd, store) {
73
+ const targets = store === 'all' ? CLEARABLE : [store];
74
+ for (const t of targets) {
75
+ if (!CLEARABLE.includes(t)) {
76
+ throw new Error(`unknown or protected store "${t}" — clearable: ${CLEARABLE.join(', ')}, all`);
77
+ }
78
+ }
79
+ const removed = [];
80
+ for (const t of targets) {
81
+ try { fs.unlinkSync(storePath(cwd, t)); removed.push(t); } catch (_) {}
82
+ }
83
+ return removed;
84
+ }
85
+
86
+ module.exports = { inspectMemory, clearMemory, STORES, CLEARABLE };