sigmap 8.18.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 +26 -0
- package/README.md +11 -11
- package/gen-context.js +227 -4
- package/llms-full.txt +8 -6
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/javascript.js +42 -1
- package/src/extractors/typescript.js +41 -1
- package/src/mcp/server.js +1 -1
- package/src/session/memory-inspect.js +86 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,32 @@ 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
|
+
|
|
26
|
+
## [8.19.0] — 2026-07-19
|
|
27
|
+
|
|
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.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Honest grep-agent baseline benchmark (#495, PR #496):** new `scripts/run-honest-benchmark.mjs` (`npm run benchmark:honest`) scores the production ranker against an internal single-shot grep-agent baseline — a pure-Node, zero-dependency, no-child-process repo scan ranked by distinct-term coverage then occurrences, `.gitignore`-aware, deterministic ordering — on the same 110-task / 19-repo corpus and the same scorer. **Measured: SigMap 86.4% hit@5 / MRR .780 vs grep 42.7% / .228 → 2.02× lift (+43.6pt)** → `benchmarks/reports/honest-baseline.json`. `computeLatest` derives `grep_baseline_hit_at_5` + `grep_lift` from the report (optional-report pattern; hermetic fixture unaffected).
|
|
32
|
+
- **Claim-hygiene guard test:** `test/integration/honest-baseline.test.js` (7 checks) — report shape, derived-not-hand-typed metrics, version.json mirror, and a one-way door: the 6.4×-vs-random lift, the 13.6% random baseline, and the unsourced "10% without" claim can never reappear on README or llms surfaces; the benchmark script itself is guarded to stay child-process-free.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **Random-baseline lift retired from all human surfaces (#495, PR #496):** README and llms.txt/llms-full.txt now quote the measured grep-agent lift (2.02×) instead of 6.4×-vs-random; task success is labeled a **retrieval-tier proxy** ("modeled from retrieval tiers, not measured LLM sessions") and the "baseline 10%" / "up from 10%" claims are removed. The random-baseline fields (`baseline_hit_at_5`, `retrieval_lift`) remain in latest.json/version.json as data only. `readme-structure` guards advanced: README must show the grep baseline and must **not** show the random one.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
13
39
|
## [8.18.0] — 2026-07-12
|
|
14
40
|
|
|
15
41
|
Minor release — **the §7.4 Phase-2 closer**: with these three changes, every quality-ceiling row in the master plan's scorecard is done or measure-gated-closed.
|
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
|
-
- **
|
|
63
|
-
- **96.
|
|
64
|
-
- **
|
|
65
|
-
- **1.
|
|
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-->
|
|
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.
|
|
126
|
-
Date : 2026-07-
|
|
125
|
+
Benchmark : sigmap-v8.20-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-07-19
|
|
127
127
|
|
|
128
|
-
Hit@5 :
|
|
129
|
-
Token reduction: 96.
|
|
130
|
-
Prompt reduction :
|
|
131
|
-
Task success :
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 |
|
|
25
|
-
| Token reduction | — | 96.
|
|
26
|
-
| Task
|
|
27
|
-
| Prompts per task | 2.84 | 1.
|
|
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.
|
|
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.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.20-main, 2026-07-19)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval:
|
|
29
|
-
- Token reduction: 96.
|
|
30
|
-
- Task
|
|
31
|
-
- Prompts per task: 1.
|
|
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.
|
|
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": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"check:metrics": "node scripts/gen-benchmark-latest.mjs --check && node scripts/check-version-meta.mjs && node scripts/sync-metrics.mjs --check",
|
|
51
51
|
"prepublishOnly": "node scripts/check-bundle.mjs && node scripts/build-bundle.mjs --check && node scripts/gen-benchmark-latest.mjs --check && node scripts/check-version-meta.mjs && node scripts/sync-metrics.mjs --check && node scripts/generate-llms.mjs",
|
|
52
52
|
"benchmark:grounding": "node scripts/run-hallucination-benchmark.mjs",
|
|
53
|
+
"benchmark:honest": "node scripts/run-honest-benchmark.mjs --save",
|
|
53
54
|
"benchmark:llm-ablation": "node scripts/run-llm-ablation.mjs"
|
|
54
55
|
},
|
|
55
56
|
"files": [
|
|
@@ -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) =>
|
|
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) =>
|
|
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
|
@@ -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 };
|