sigmap 8.0.0 → 8.1.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,14 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.1.0] — 2026-07-04
14
+
15
+ Minor release — **v9.0 G5/D5: the local-library signature index (the private-API grounding moat, v1).** SigMap's hallucination guard can now verify AI suggestions against the libraries **actually installed** in `node_modules`, not just declared dependency *names*. This is a capability no competitor offers — Context7 knows only *public* library docs; SigMap grounds against the real installed tree. Local, zero-dependency, deterministic (byte-stable given a fixed installed tree).
16
+
17
+ ### Added
18
+ - **Local-library signature index (#405, PR #406):** new `src/verify/lib-index.js` — `buildLibraryIndex(cwd)` resolves the **direct** dependencies declared in `package.json`, locates each under `node_modules/<dep>`, reads its version (**D8 version pinning**) and TypeScript declaration entry (`types`/`typings`, else `index.d.ts`), and deterministically extracts the exported symbol names. Bounded (per-file + dep caps), cached via `src/cache/sig-cache.js`, and graceful on missing/untyped/malformed packages.
19
+ - **Installed-library grounding in `verify-ai-output`:** `verify()` now unions installed-library symbols into its known-symbol universe, so genuine library calls (e.g. `Router()`, `debounce()`) stop being false-flagged as `fake-symbol`. The result summary gains `librariesIndexed` and `libraries` (`name@version`, D8). Auto-runs from the project's `node_modules`; opt-out via `libIndex:false`. Scope v1 is JS/TS `.d.ts`; Python site-packages and a standalone `verify_suggestion` MCP tool are deferred to a follow-up.
20
+
13
21
  ## [8.0.0] — 2026-07-04
14
22
 
15
23
  Major release — **v8.5 "Repo-Context Coverage & Test Discovery" (C1 + C2 + C3).** Marks the v8 milestone: the signature map now reaches beyond functions/classes/routes into the repo's operational surface, impl→test discovery is measured rather than best-effort, and every Evidence Pack file carries a risk label from a richer, precedence-ordered set. All zero-dependency, deterministic, and in-boundary with the North-Star constraints. **No breaking API changes** — the `8.0.0` bump aligns the published version with the roadmap's v8 framing; existing `riskLabel`/`relatedTests` consumers keep working.
package/README.md CHANGED
@@ -98,7 +98,7 @@ Ask → Rank → Context → Validate → Judge → Learn
98
98
 
99
99
  <!--SM:benchmarkBlock-->
100
100
  ```
101
- Benchmark : sigmap-v8.0-main (21 repositories, including R language)
101
+ Benchmark : sigmap-v8.1-main (21 repositories, including R language)
102
102
  Date : 2026-07-04
103
103
 
104
104
  Hit@5 : 86.7% (baseline 13.6% — 6.4× lift)
package/gen-context.js CHANGED
@@ -13063,7 +13063,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
13063
13063
 
13064
13064
  const SERVER_INFO = {
13065
13065
  name: 'sigmap',
13066
- version: '8.0.0',
13066
+ version: '8.1.0',
13067
13067
  description: 'SigMap MCP server — code signatures on demand',
13068
13068
  };
13069
13069
 
@@ -16403,6 +16403,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
16403
16403
  const path = require('path');
16404
16404
  const parsers = __require('./src/verify/parsers');
16405
16405
  const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
16406
+ const { buildLibraryIndex } = __require('./src/verify/lib-index');
16406
16407
 
16407
16408
  // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
16408
16409
  // a tests/__tests__ directory). Used to flag fake-test-file separately.
@@ -16571,6 +16572,28 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
16571
16572
  }
16572
16573
  if (!fileBasenames) fileBasenames = new Set();
16573
16574
 
16575
+ // Installed-library grounding (G5/D5, the moat): union the exported symbols of
16576
+ // the libraries actually installed in node_modules, so genuine library calls
16577
+ // stop false-flagging as fake-symbol and the summary can pin the versions the
16578
+ // answer was verified against. Auto-runs only when the caller did not override
16579
+ // the symbol set (keeps hermetic callers unchanged); disable with libIndex:false.
16580
+ let libraries = opts.libraries || [];
16581
+ {
16582
+ let libSyms = opts.libSymbols;
16583
+ if (!libSyms && opts.libIndex !== false && !opts.symbolSet) {
16584
+ try {
16585
+ const li = buildLibraryIndex(cwd, { version: opts.version });
16586
+ libSyms = li.symbols;
16587
+ if (!opts.libraries) libraries = li.libraries;
16588
+ } catch (_) { libSyms = null; }
16589
+ }
16590
+ if (libSyms && libSyms.size) {
16591
+ const merged = new Set(symbolSet);
16592
+ for (const s of libSyms) merged.add(s);
16593
+ symbolSet = merged;
16594
+ }
16595
+ }
16596
+
16574
16597
  let deps = opts.deps;
16575
16598
  let hasPkg = opts.hasPkg;
16576
16599
  if (!deps) {
@@ -16694,6 +16717,8 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
16694
16717
  clean: issues.length === 0,
16695
16718
  symbolsIndexed: symbolSet.size,
16696
16719
  withSuggestion: issues.filter((i) => i.suggestion).length,
16720
+ librariesIndexed: libraries.length,
16721
+ libraries: libraries.map((l) => ({ name: l.name, version: l.version, symbols: l.symbols, typed: l.typed })),
16697
16722
  };
16698
16723
 
16699
16724
  return { issues, summary };
@@ -16703,6 +16728,174 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
16703
16728
 
16704
16729
  };
16705
16730
 
16731
+ // ── ./src/verify/lib-index ──
16732
+ __factories["./src/verify/lib-index"] = function(module, exports) {
16733
+
16734
+ /**
16735
+ * Local-library signature index (v9.0 G5/D5 — the private-API grounding moat).
16736
+ *
16737
+ * Context7 knows only *public* library docs. SigMap can do something no
16738
+ * competitor can: index the signatures of the libraries **actually installed**
16739
+ * in `node_modules` and verify AI suggestions against repo + private +
16740
+ * installed-lib symbols. This module builds the installed-lib half.
16741
+ *
16742
+ * For each **direct** dependency declared in `package.json`, it locates the
16743
+ * package under `node_modules/<dep>`, reads its version (D8 version pinning),
16744
+ * and extracts the exported symbol names from its TypeScript declaration entry
16745
+ * (`types`/`typings`, else `index.d.ts`). Pure, zero-dependency, deterministic:
16746
+ * byte-stable given a fixed installed tree. Bounded (per-file read cap + dep
16747
+ * cap) and cached via `src/cache/sig-cache.js` so repeat builds are near-free.
16748
+ */
16749
+
16750
+ const fs = require('fs');
16751
+ const path = require('path');
16752
+ const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = __require('./src/cache/sig-cache');
16753
+
16754
+ const MAX_DTS_BYTES = 512 * 1024; // per-file read cap
16755
+ const MAX_DEPS = 1000; // dep count cap
16756
+ const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
16757
+
16758
+ /**
16759
+ * Extract exported symbol names from a `.d.ts` declaration file. Deterministic,
16760
+ * regex-based (declaration files are already normalized, so this is robust
16761
+ * without a full TS parser and stays zero-dependency).
16762
+ * @param {string} src
16763
+ * @returns {string[]} sorted unique exported names
16764
+ */
16765
+ function extractDtsExports(src) {
16766
+ const names = new Set();
16767
+ if (!src) return [];
16768
+
16769
+ // export [declare] [default] function|const|let|var|class|interface|type|enum|namespace Name
16770
+ const declRe = /\bexport\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|const|let|var|class|interface|type|enum|namespace|module)\s+([A-Za-z_$][\w$]*)/g;
16771
+ let m;
16772
+ while ((m = declRe.exec(src)) !== null) names.add(m[1]);
16773
+
16774
+ // export { a, b as c, default as d }
16775
+ const listRe = /\bexport\s*(?:type\s*)?\{([^}]*)\}/g;
16776
+ while ((m = listRe.exec(src)) !== null) {
16777
+ for (const part of m[1].split(',')) {
16778
+ const name = part.trim().split(/\s+as\s+/).pop().trim();
16779
+ if (/^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default') names.add(name);
16780
+ }
16781
+ }
16782
+
16783
+ // export as namespace Name / export = Name
16784
+ const nsRe = /\bexport\s+as\s+namespace\s+([A-Za-z_$][\w$]*)/g;
16785
+ while ((m = nsRe.exec(src)) !== null) names.add(m[1]);
16786
+ const assignRe = /\bexport\s*=\s*([A-Za-z_$][\w$]*)/g;
16787
+ while ((m = assignRe.exec(src)) !== null) names.add(m[1]);
16788
+
16789
+ return [...names].sort();
16790
+ }
16791
+
16792
+ /** Read direct dependency names declared in the project's package.json. */
16793
+ function directDeps(cwd) {
16794
+ const names = new Set();
16795
+ try {
16796
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
16797
+ for (const k of DEP_KEYS) {
16798
+ if (pkg[k] && typeof pkg[k] === 'object') {
16799
+ for (const n of Object.keys(pkg[k])) names.add(n);
16800
+ }
16801
+ }
16802
+ } catch (_) { /* no/invalid package.json → no deps */ }
16803
+ return [...names].sort();
16804
+ }
16805
+
16806
+ /**
16807
+ * Resolve an installed dependency's version + entry `.d.ts` path.
16808
+ * @returns {{ version: string|null, dtsPath: string|null }|null} null if not installed
16809
+ */
16810
+ function resolveEntry(cwd, dep) {
16811
+ const pkgDir = path.join(cwd, 'node_modules', dep);
16812
+ let pkg;
16813
+ try { pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')); } catch (_) { return null; }
16814
+ const version = typeof pkg.version === 'string' ? pkg.version : null;
16815
+
16816
+ const candidates = [];
16817
+ const typesField = pkg.types || pkg.typings;
16818
+ if (typeof typesField === 'string') {
16819
+ candidates.push(typesField);
16820
+ candidates.push(path.join(typesField, 'index.d.ts')); // typesField may be a dir
16821
+ }
16822
+ candidates.push('index.d.ts');
16823
+ if (typeof pkg.main === 'string') candidates.push(pkg.main.replace(/\.(js|cjs|mjs)$/, '.d.ts'));
16824
+
16825
+ for (const c of candidates) {
16826
+ const p = path.join(pkgDir, c);
16827
+ try { if (fs.statSync(p).isFile()) return { version, dtsPath: p }; } catch (_) { /* next */ }
16828
+ }
16829
+ return { version, dtsPath: null }; // installed but untyped
16830
+ }
16831
+
16832
+ /**
16833
+ * Build the installed-library signature index for `cwd`.
16834
+ *
16835
+ * @param {string} cwd
16836
+ * @param {object} [opts]
16837
+ * @param {string} [opts.version='0'] sigmap version, for cache busting
16838
+ * @param {boolean} [opts.cache=true] use the on-disk sig-cache
16839
+ * @returns {{ symbols: Set<string>, libraries: Array<{name,version,symbols,typed}>, count: number }}
16840
+ */
16841
+ function buildLibraryIndex(cwd, opts = {}) {
16842
+ const version = opts.version || '0';
16843
+ const useCache = opts.cache !== false;
16844
+ const deps = directDeps(cwd).slice(0, MAX_DEPS);
16845
+
16846
+ const entries = [];
16847
+ for (const dep of deps) {
16848
+ const r = resolveEntry(cwd, dep);
16849
+ if (r) entries.push({ dep, version: r.version, dtsPath: r.dtsPath });
16850
+ }
16851
+
16852
+ const cache = useCache ? loadCache(cwd, version) : new Map();
16853
+ const dtsFiles = entries.filter((e) => e.dtsPath).map((e) => e.dtsPath);
16854
+ const { unchanged } = getChangedFiles(dtsFiles, cache);
16855
+ const unchangedSet = new Set(unchanged);
16856
+
16857
+ const symbols = new Set();
16858
+ const libraries = [];
16859
+ const fresh = [];
16860
+
16861
+ for (const e of entries) {
16862
+ let names;
16863
+ if (!e.dtsPath) {
16864
+ names = [];
16865
+ } else if (unchangedSet.has(e.dtsPath) && cache.get(e.dtsPath)) {
16866
+ names = cache.get(e.dtsPath).sigs || [];
16867
+ } else {
16868
+ let src = '';
16869
+ try {
16870
+ if (fs.statSync(e.dtsPath).size <= MAX_DTS_BYTES) src = fs.readFileSync(e.dtsPath, 'utf8');
16871
+ } catch (_) { /* unreadable → empty */ }
16872
+ names = extractDtsExports(src);
16873
+ fresh.push({ file: e.dtsPath, sigs: names });
16874
+ }
16875
+ for (const n of names) symbols.add(n);
16876
+ libraries.push({ name: e.dep, version: e.version, symbols: names.length, typed: !!e.dtsPath });
16877
+ }
16878
+
16879
+ if (useCache && fresh.length) {
16880
+ updateCacheEntries(cache, fresh);
16881
+ saveCache(cwd, version, cache);
16882
+ }
16883
+
16884
+ libraries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
16885
+ return { symbols, libraries, count: symbols.size };
16886
+ }
16887
+
16888
+ /** D8: render `name@version` pins for the typed/installed libraries. */
16889
+ function formatVersionPins(libraries) {
16890
+ return (libraries || [])
16891
+ .filter((l) => l.version)
16892
+ .map((l) => `${l.name}@${l.version}`);
16893
+ }
16894
+
16895
+ module.exports = { buildLibraryIndex, extractDtsExports, directDeps, resolveEntry, formatVersionPins };
16896
+
16897
+ };
16898
+
16706
16899
  // ── ./src/verify/parsers ──
16707
16900
  __factories["./src/verify/parsers"] = function(module, exports) {
16708
16901
 
@@ -17032,7 +17225,7 @@ function __tryGit(args, opts = {}) {
17032
17225
  catch (_) { return ''; }
17033
17226
  }
17034
17227
 
17035
- const VERSION = '8.0.0';
17228
+ const VERSION = '8.1.0';
17036
17229
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
17037
17230
 
17038
17231
  function requireSourceOrBundled(key) {
package/llms-full.txt CHANGED
@@ -11,13 +11,13 @@ 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.0.0 | Benchmark: sigmap-v8.0-main (2026-07-04)
14
+ # Version: 8.1.0 | Benchmark: sigmap-v8.1-main (2026-07-04)
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.0-main, 2026-07-04)
20
+ ## Core metrics (benchmark: sigmap-v8.1-main, 2026-07-04)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
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.0.0 | Benchmark: sigmap-v8.0-main (2026-07-04)
14
+ # Version: 8.1.0 | Benchmark: sigmap-v8.1-main (2026-07-04)
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,7 +23,7 @@ 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.0-main, 2026-07-04)
26
+ ## Core metrics (benchmark: sigmap-v8.1-main, 2026-07-04)
27
27
 
28
28
  - hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
29
29
  - Token reduction: 97.0% average across benchmark repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.0.0",
3
+ "version": "8.1.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.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
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.0.0',
21
+ version: '8.1.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -21,6 +21,7 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
  const parsers = require('./parsers');
23
23
  const { closestMatch, buildSymbolCandidates, formatSuggestion } = require('./closest-match');
24
+ const { buildLibraryIndex } = require('./lib-index');
24
25
 
25
26
  // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
26
27
  // a tests/__tests__ directory). Used to flag fake-test-file separately.
@@ -189,6 +190,28 @@ function verify(answerText, cwd, opts = {}) {
189
190
  }
190
191
  if (!fileBasenames) fileBasenames = new Set();
191
192
 
193
+ // Installed-library grounding (G5/D5, the moat): union the exported symbols of
194
+ // the libraries actually installed in node_modules, so genuine library calls
195
+ // stop false-flagging as fake-symbol and the summary can pin the versions the
196
+ // answer was verified against. Auto-runs only when the caller did not override
197
+ // the symbol set (keeps hermetic callers unchanged); disable with libIndex:false.
198
+ let libraries = opts.libraries || [];
199
+ {
200
+ let libSyms = opts.libSymbols;
201
+ if (!libSyms && opts.libIndex !== false && !opts.symbolSet) {
202
+ try {
203
+ const li = buildLibraryIndex(cwd, { version: opts.version });
204
+ libSyms = li.symbols;
205
+ if (!opts.libraries) libraries = li.libraries;
206
+ } catch (_) { libSyms = null; }
207
+ }
208
+ if (libSyms && libSyms.size) {
209
+ const merged = new Set(symbolSet);
210
+ for (const s of libSyms) merged.add(s);
211
+ symbolSet = merged;
212
+ }
213
+ }
214
+
192
215
  let deps = opts.deps;
193
216
  let hasPkg = opts.hasPkg;
194
217
  if (!deps) {
@@ -312,6 +335,8 @@ function verify(answerText, cwd, opts = {}) {
312
335
  clean: issues.length === 0,
313
336
  symbolsIndexed: symbolSet.size,
314
337
  withSuggestion: issues.filter((i) => i.suggestion).length,
338
+ librariesIndexed: libraries.length,
339
+ libraries: libraries.map((l) => ({ name: l.name, version: l.version, symbols: l.symbols, typed: l.typed })),
315
340
  };
316
341
 
317
342
  return { issues, summary };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local-library signature index (v9.0 G5/D5 — the private-API grounding moat).
5
+ *
6
+ * Context7 knows only *public* library docs. SigMap can do something no
7
+ * competitor can: index the signatures of the libraries **actually installed**
8
+ * in `node_modules` and verify AI suggestions against repo + private +
9
+ * installed-lib symbols. This module builds the installed-lib half.
10
+ *
11
+ * For each **direct** dependency declared in `package.json`, it locates the
12
+ * package under `node_modules/<dep>`, reads its version (D8 version pinning),
13
+ * and extracts the exported symbol names from its TypeScript declaration entry
14
+ * (`types`/`typings`, else `index.d.ts`). Pure, zero-dependency, deterministic:
15
+ * byte-stable given a fixed installed tree. Bounded (per-file read cap + dep
16
+ * cap) and cached via `src/cache/sig-cache.js` so repeat builds are near-free.
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = require('../cache/sig-cache');
22
+
23
+ const MAX_DTS_BYTES = 512 * 1024; // per-file read cap
24
+ const MAX_DEPS = 1000; // dep count cap
25
+ const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
26
+
27
+ /**
28
+ * Extract exported symbol names from a `.d.ts` declaration file. Deterministic,
29
+ * regex-based (declaration files are already normalized, so this is robust
30
+ * without a full TS parser and stays zero-dependency).
31
+ * @param {string} src
32
+ * @returns {string[]} sorted unique exported names
33
+ */
34
+ function extractDtsExports(src) {
35
+ const names = new Set();
36
+ if (!src) return [];
37
+
38
+ // export [declare] [default] function|const|let|var|class|interface|type|enum|namespace Name
39
+ const declRe = /\bexport\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|const|let|var|class|interface|type|enum|namespace|module)\s+([A-Za-z_$][\w$]*)/g;
40
+ let m;
41
+ while ((m = declRe.exec(src)) !== null) names.add(m[1]);
42
+
43
+ // export { a, b as c, default as d }
44
+ const listRe = /\bexport\s*(?:type\s*)?\{([^}]*)\}/g;
45
+ while ((m = listRe.exec(src)) !== null) {
46
+ for (const part of m[1].split(',')) {
47
+ const name = part.trim().split(/\s+as\s+/).pop().trim();
48
+ if (/^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default') names.add(name);
49
+ }
50
+ }
51
+
52
+ // export as namespace Name / export = Name
53
+ const nsRe = /\bexport\s+as\s+namespace\s+([A-Za-z_$][\w$]*)/g;
54
+ while ((m = nsRe.exec(src)) !== null) names.add(m[1]);
55
+ const assignRe = /\bexport\s*=\s*([A-Za-z_$][\w$]*)/g;
56
+ while ((m = assignRe.exec(src)) !== null) names.add(m[1]);
57
+
58
+ return [...names].sort();
59
+ }
60
+
61
+ /** Read direct dependency names declared in the project's package.json. */
62
+ function directDeps(cwd) {
63
+ const names = new Set();
64
+ try {
65
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
66
+ for (const k of DEP_KEYS) {
67
+ if (pkg[k] && typeof pkg[k] === 'object') {
68
+ for (const n of Object.keys(pkg[k])) names.add(n);
69
+ }
70
+ }
71
+ } catch (_) { /* no/invalid package.json → no deps */ }
72
+ return [...names].sort();
73
+ }
74
+
75
+ /**
76
+ * Resolve an installed dependency's version + entry `.d.ts` path.
77
+ * @returns {{ version: string|null, dtsPath: string|null }|null} null if not installed
78
+ */
79
+ function resolveEntry(cwd, dep) {
80
+ const pkgDir = path.join(cwd, 'node_modules', dep);
81
+ let pkg;
82
+ try { pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')); } catch (_) { return null; }
83
+ const version = typeof pkg.version === 'string' ? pkg.version : null;
84
+
85
+ const candidates = [];
86
+ const typesField = pkg.types || pkg.typings;
87
+ if (typeof typesField === 'string') {
88
+ candidates.push(typesField);
89
+ candidates.push(path.join(typesField, 'index.d.ts')); // typesField may be a dir
90
+ }
91
+ candidates.push('index.d.ts');
92
+ if (typeof pkg.main === 'string') candidates.push(pkg.main.replace(/\.(js|cjs|mjs)$/, '.d.ts'));
93
+
94
+ for (const c of candidates) {
95
+ const p = path.join(pkgDir, c);
96
+ try { if (fs.statSync(p).isFile()) return { version, dtsPath: p }; } catch (_) { /* next */ }
97
+ }
98
+ return { version, dtsPath: null }; // installed but untyped
99
+ }
100
+
101
+ /**
102
+ * Build the installed-library signature index for `cwd`.
103
+ *
104
+ * @param {string} cwd
105
+ * @param {object} [opts]
106
+ * @param {string} [opts.version='0'] sigmap version, for cache busting
107
+ * @param {boolean} [opts.cache=true] use the on-disk sig-cache
108
+ * @returns {{ symbols: Set<string>, libraries: Array<{name,version,symbols,typed}>, count: number }}
109
+ */
110
+ function buildLibraryIndex(cwd, opts = {}) {
111
+ const version = opts.version || '0';
112
+ const useCache = opts.cache !== false;
113
+ const deps = directDeps(cwd).slice(0, MAX_DEPS);
114
+
115
+ const entries = [];
116
+ for (const dep of deps) {
117
+ const r = resolveEntry(cwd, dep);
118
+ if (r) entries.push({ dep, version: r.version, dtsPath: r.dtsPath });
119
+ }
120
+
121
+ const cache = useCache ? loadCache(cwd, version) : new Map();
122
+ const dtsFiles = entries.filter((e) => e.dtsPath).map((e) => e.dtsPath);
123
+ const { unchanged } = getChangedFiles(dtsFiles, cache);
124
+ const unchangedSet = new Set(unchanged);
125
+
126
+ const symbols = new Set();
127
+ const libraries = [];
128
+ const fresh = [];
129
+
130
+ for (const e of entries) {
131
+ let names;
132
+ if (!e.dtsPath) {
133
+ names = [];
134
+ } else if (unchangedSet.has(e.dtsPath) && cache.get(e.dtsPath)) {
135
+ names = cache.get(e.dtsPath).sigs || [];
136
+ } else {
137
+ let src = '';
138
+ try {
139
+ if (fs.statSync(e.dtsPath).size <= MAX_DTS_BYTES) src = fs.readFileSync(e.dtsPath, 'utf8');
140
+ } catch (_) { /* unreadable → empty */ }
141
+ names = extractDtsExports(src);
142
+ fresh.push({ file: e.dtsPath, sigs: names });
143
+ }
144
+ for (const n of names) symbols.add(n);
145
+ libraries.push({ name: e.dep, version: e.version, symbols: names.length, typed: !!e.dtsPath });
146
+ }
147
+
148
+ if (useCache && fresh.length) {
149
+ updateCacheEntries(cache, fresh);
150
+ saveCache(cwd, version, cache);
151
+ }
152
+
153
+ libraries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
154
+ return { symbols, libraries, count: symbols.size };
155
+ }
156
+
157
+ /** D8: render `name@version` pins for the typed/installed libraries. */
158
+ function formatVersionPins(libraries) {
159
+ return (libraries || [])
160
+ .filter((l) => l.version)
161
+ .map((l) => `${l.name}@${l.version}`);
162
+ }
163
+
164
+ module.exports = { buildLibraryIndex, extractDtsExports, directDeps, resolveEntry, formatVersionPins };