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 +8 -0
- package/README.md +1 -1
- package/gen-context.js +195 -2
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/mcp/server.js +1 -1
- package/src/verify/hallucination-guard.js +25 -0
- package/src/verify/lib-index.js +164 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": {
|
package/src/mcp/server.js
CHANGED
|
@@ -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 };
|