sigmap 8.1.0 → 8.3.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 +14 -0
- package/README.md +2 -2
- package/gen-context.js +231 -25
- package/llms-full.txt +12 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/mcp/handlers.js +46 -1
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +20 -0
- package/src/verify/lib-index.js +160 -20
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.3.0] — 2026-07-05
|
|
14
|
+
|
|
15
|
+
Minor release — **Python site-packages grounding: the moat now spans both major ecosystems.** v8.1/v8.2 built local-library grounding for JS/TS (`node_modules` `.d.ts`); this extends it to **Python**, so `verify-ai-output` and the `verify_suggestion` MCP tool ground AI-suggested Python code against the libraries actually installed in the project's venv — with pinned versions (D8). Zero-dependency, no Python runtime, deterministic.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Python site-packages grounding (#413, PR #414):** `buildLibraryIndex` (`src/verify/lib-index.js`) gains a Python pass alongside the JS/TS one. It reads direct deps from `requirements.txt` / `pyproject.toml` (PEP 621 `[project].dependencies` + Poetry), discovers the venv `site-packages` (`.venv|venv|env` → `lib/python*/site-packages`, or `Lib/site-packages` on Windows) **without spawning Python**, resolves each dep's installed module + version (`*.dist-info`, D8) with PEP 503 import-name normalization, and extracts exported names from the package's `__init__.py`/`.pyi` (`__all__`, top-level `def`/`class`, public assignments, and `from … import` re-exports). Both ecosystems merge into one symbol index — genuine installed-Python-library calls stop being false-flagged as `fake-symbol`. Byte-stable given a fixed installed tree; cached via `src/cache/sig-cache.js`; graceful on missing venv / unresolved deps.
|
|
19
|
+
|
|
20
|
+
## [8.2.0] — 2026-07-04
|
|
21
|
+
|
|
22
|
+
Minor release — **`verify_suggestion` MCP tool: the grounding moat, made consumable by agents.** v8.1.0 built local-library grounding inside the `verify-ai-output` CLI; this exposes it as the **18th MCP tool**, so a coding agent can verify its own generated code against the repo **and the libraries actually installed** in `node_modules` — *before it writes* — and get back the flagged issues plus the pinned versions it verified against (D8).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **`verify_suggestion` MCP tool (#409, PR #410):** ground an AI code suggestion before writing it. `verify_suggestion({ code })` runs the Hallucination Guard against the repo signature index **and** the installed-library symbol index (the G5/D5 moat), returning a clean/✗ verdict, one line per issue (fake file / import / symbol / npm-script, with closest-match suggestions), and a **D8** line listing the installed libraries it verified against with pinned versions (`name@version`). Deterministic, offline, zero-dependency. Reuses the shipped `verify()` core; graceful on missing/empty `code`. MCP surface **17 → 18 tools**.
|
|
26
|
+
|
|
13
27
|
## [8.1.0] — 2026-07-04
|
|
14
28
|
|
|
15
29
|
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).
|
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.3-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)
|
|
@@ -198,7 +198,7 @@ Use SigMap with open-source tools and fully self-hosted setups:
|
|
|
198
198
|
| **JetBrains** | [Marketplace](https://plugins.jetbrains.com/plugin/31109-sigmap--ai-context-engine/) | [github.com/manojmallick/sigmap-jetbrains](https://github.com/manojmallick/sigmap-jetbrains) | IntelliJ IDEA, WebStorm, PyCharm, GoLand — tool window + actions |
|
|
199
199
|
| **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
|
|
200
200
|
|
|
201
|
-
**MCP server** —
|
|
201
|
+
**MCP server** — 18 on-demand tools for Claude Code and Cursor:
|
|
202
202
|
|
|
203
203
|
```bash
|
|
204
204
|
sigmap --mcp
|
package/gen-context.js
CHANGED
|
@@ -12892,7 +12892,52 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
12892
12892
|
}
|
|
12893
12893
|
}
|
|
12894
12894
|
|
|
12895
|
-
|
|
12895
|
+
/**
|
|
12896
|
+
* verify_suggestion({ code }) → string
|
|
12897
|
+
*
|
|
12898
|
+
* Ground an AI code suggestion before it is written: run the Hallucination
|
|
12899
|
+
* Guard against the repo AND the installed-library symbol index (the moat), and
|
|
12900
|
+
* render a verdict + issues + the installed libraries verified against (pinned
|
|
12901
|
+
* versions, D8). Deterministic, offline.
|
|
12902
|
+
*/
|
|
12903
|
+
function verifySuggestion(args, cwd) {
|
|
12904
|
+
const code = args && typeof args.code === 'string' ? args.code : '';
|
|
12905
|
+
if (!code.trim()) {
|
|
12906
|
+
return 'Usage: verify_suggestion({ code: "<AI-suggested code or answer>" }) — provide the snippet to verify against the repo + installed libraries.';
|
|
12907
|
+
}
|
|
12908
|
+
|
|
12909
|
+
let result;
|
|
12910
|
+
try {
|
|
12911
|
+
const { verify } = __require('./src/verify/hallucination-guard');
|
|
12912
|
+
result = verify(code, cwd);
|
|
12913
|
+
} catch (err) {
|
|
12914
|
+
return `_verify_suggestion failed: ${err.message}_`;
|
|
12915
|
+
}
|
|
12916
|
+
|
|
12917
|
+
const { issues, summary } = result;
|
|
12918
|
+
const out = [];
|
|
12919
|
+
if (summary.clean) {
|
|
12920
|
+
out.push('✓ Grounded — no fake files, imports, symbols, or scripts detected.');
|
|
12921
|
+
} else {
|
|
12922
|
+
out.push(`✗ ${summary.total} issue(s) found:`);
|
|
12923
|
+
for (const i of issues) {
|
|
12924
|
+
out.push(` L${i.line} [${i.type}] ${i.message}`);
|
|
12925
|
+
if (i.suggestion) out.push(` ↳ ${i.suggestion}`);
|
|
12926
|
+
}
|
|
12927
|
+
}
|
|
12928
|
+
|
|
12929
|
+
// D8: report the installed libraries the suggestion was verified against.
|
|
12930
|
+
const pins = (summary.libraries || []).filter((l) => l.version).map((l) => `${l.name}@${l.version}`);
|
|
12931
|
+
const n = summary.librariesIndexed || 0;
|
|
12932
|
+
out.push('');
|
|
12933
|
+
out.push(
|
|
12934
|
+
`Grounded against ${summary.symbolsIndexed} repo + library symbol(s)` +
|
|
12935
|
+
(n ? ` · ${n} installed librar${n === 1 ? 'y' : 'ies'}${pins.length ? ': ' + pins.join(', ') : ''}` : '')
|
|
12936
|
+
);
|
|
12937
|
+
return out.join('\n');
|
|
12938
|
+
}
|
|
12939
|
+
|
|
12940
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion };
|
|
12896
12941
|
|
|
12897
12942
|
};
|
|
12898
12943
|
|
|
@@ -13053,17 +13098,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
13053
13098
|
*
|
|
13054
13099
|
* Supported methods:
|
|
13055
13100
|
* initialize → serverInfo + capabilities
|
|
13056
|
-
* tools/list →
|
|
13101
|
+
* tools/list → 18 tool definitions
|
|
13057
13102
|
* tools/call → dispatch to handler, return result
|
|
13058
13103
|
*/
|
|
13059
13104
|
|
|
13060
13105
|
const readline = require('readline');
|
|
13061
13106
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
13062
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview } = __require('./src/mcp/handlers');
|
|
13107
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion } = __require('./src/mcp/handlers');
|
|
13063
13108
|
|
|
13064
13109
|
const SERVER_INFO = {
|
|
13065
13110
|
name: 'sigmap',
|
|
13066
|
-
version: '8.
|
|
13111
|
+
version: '8.3.0',
|
|
13067
13112
|
description: 'SigMap MCP server — code signatures on demand',
|
|
13068
13113
|
};
|
|
13069
13114
|
|
|
@@ -13128,6 +13173,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
13128
13173
|
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
13129
13174
|
else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
|
|
13130
13175
|
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
13176
|
+
else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
|
|
13131
13177
|
else {
|
|
13132
13178
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
13133
13179
|
return;
|
|
@@ -13503,6 +13549,26 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
13503
13549
|
required: [],
|
|
13504
13550
|
},
|
|
13505
13551
|
},
|
|
13552
|
+
{
|
|
13553
|
+
name: 'verify_suggestion',
|
|
13554
|
+
description:
|
|
13555
|
+
'Ground an AI code suggestion before writing it: verify a snippet or answer against the ' +
|
|
13556
|
+
'repository AND the libraries actually installed in node_modules (the grounding moat). ' +
|
|
13557
|
+
'Flags fake file paths, unresolvable imports, symbols absent from both the repo index and ' +
|
|
13558
|
+
'the installed libraries, and non-existent npm scripts — deterministic, offline, no LLM. ' +
|
|
13559
|
+
'Reports the installed libraries it verified against with pinned versions.',
|
|
13560
|
+
inputSchema: {
|
|
13561
|
+
type: 'object',
|
|
13562
|
+
properties: {
|
|
13563
|
+
code: {
|
|
13564
|
+
type: 'string',
|
|
13565
|
+
description:
|
|
13566
|
+
'The AI-suggested code snippet or answer text to verify against the repo + installed libraries.',
|
|
13567
|
+
},
|
|
13568
|
+
},
|
|
13569
|
+
required: ['code'],
|
|
13570
|
+
},
|
|
13571
|
+
},
|
|
13506
13572
|
];
|
|
13507
13573
|
|
|
13508
13574
|
module.exports = { TOOLS };
|
|
@@ -16739,12 +16805,17 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16739
16805
|
* in `node_modules` and verify AI suggestions against repo + private +
|
|
16740
16806
|
* installed-lib symbols. This module builds the installed-lib half.
|
|
16741
16807
|
*
|
|
16742
|
-
*
|
|
16743
|
-
*
|
|
16744
|
-
*
|
|
16745
|
-
*
|
|
16746
|
-
*
|
|
16747
|
-
*
|
|
16808
|
+
* Two ecosystems, one index:
|
|
16809
|
+
* - **JS/TS** — each **direct** dependency in `package.json` resolved under
|
|
16810
|
+
* `node_modules/<dep>`; exports read from its TypeScript declaration entry
|
|
16811
|
+
* (`types`/`typings`, else `index.d.ts`).
|
|
16812
|
+
* - **Python** — each direct dependency in `requirements.txt`/`pyproject.toml`
|
|
16813
|
+
* resolved in the project's venv `site-packages`; exports read from the
|
|
16814
|
+
* package's `__init__.py`/`.pyi`. No Python runtime is spawned (North-Star #1).
|
|
16815
|
+
*
|
|
16816
|
+
* Pure, zero-dependency, deterministic: byte-stable given a fixed installed
|
|
16817
|
+
* tree. Bounded (per-file read cap + dep cap) and cached via
|
|
16818
|
+
* `src/cache/sig-cache.js` so repeat builds are near-free.
|
|
16748
16819
|
*/
|
|
16749
16820
|
|
|
16750
16821
|
const fs = require('fs');
|
|
@@ -16754,6 +16825,7 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16754
16825
|
const MAX_DTS_BYTES = 512 * 1024; // per-file read cap
|
|
16755
16826
|
const MAX_DEPS = 1000; // dep count cap
|
|
16756
16827
|
const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
16828
|
+
const VENV_DIRS = ['.venv', 'venv', 'env', '.env'];
|
|
16757
16829
|
|
|
16758
16830
|
/**
|
|
16759
16831
|
* Extract exported symbol names from a `.d.ts` declaration file. Deterministic,
|
|
@@ -16829,6 +16901,130 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16829
16901
|
return { version, dtsPath: null }; // installed but untyped
|
|
16830
16902
|
}
|
|
16831
16903
|
|
|
16904
|
+
// ── Python ──────────────────────────────────────────────────────────────────
|
|
16905
|
+
|
|
16906
|
+
/**
|
|
16907
|
+
* Extract exported symbol names from a Python module's `__init__.py`/`.pyi`.
|
|
16908
|
+
* Deterministic, regex-based, top-level only: `__all__`, `def`/`class`, public
|
|
16909
|
+
* module-level assignments, and `from … import …` re-exports (a package's
|
|
16910
|
+
* public API is largely re-exports). Private names (leading `_`) are skipped
|
|
16911
|
+
* unless listed in `__all__`.
|
|
16912
|
+
* @param {string} src
|
|
16913
|
+
* @returns {string[]} sorted unique exported names
|
|
16914
|
+
*/
|
|
16915
|
+
function extractPyExports(src) {
|
|
16916
|
+
const names = new Set();
|
|
16917
|
+
if (!src) return [];
|
|
16918
|
+
|
|
16919
|
+
// __all__ = [ 'a', 'b', ... ] (authoritative when present; keeps privates)
|
|
16920
|
+
const allMatch = src.match(/^__all__\s*[:+]?=\s*[\[(]([\s\S]*?)[\])]/m);
|
|
16921
|
+
if (allMatch) {
|
|
16922
|
+
for (const m of allMatch[1].matchAll(/['"]([A-Za-z_]\w*)['"]/g)) names.add(m[1]);
|
|
16923
|
+
}
|
|
16924
|
+
|
|
16925
|
+
// top-level def / class (column 0)
|
|
16926
|
+
for (const m of src.matchAll(/^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);
|
|
16927
|
+
for (const m of src.matchAll(/^class\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);
|
|
16928
|
+
|
|
16929
|
+
// top-level public assignments: NAME = … / NAME: type = … (not ==, +=, etc.)
|
|
16930
|
+
for (const m of src.matchAll(/^([A-Za-z_]\w*)\s*(?::[^=\n]+)?=(?!=)/gm)) {
|
|
16931
|
+
if (!m[1].startsWith('_')) names.add(m[1]);
|
|
16932
|
+
}
|
|
16933
|
+
|
|
16934
|
+
// re-exports: from .mod import Name, Other as Alias
|
|
16935
|
+
for (const m of src.matchAll(/^from\s+[^\n]+?\s+import\s+([^\n#]+)/gm)) {
|
|
16936
|
+
for (const part of m[1].split(',')) {
|
|
16937
|
+
const name = part.trim().replace(/[()]/g, '').split(/\s+as\s+/).pop().trim();
|
|
16938
|
+
if (/^[A-Za-z_]\w*$/.test(name) && !name.startsWith('_')) names.add(name);
|
|
16939
|
+
}
|
|
16940
|
+
}
|
|
16941
|
+
|
|
16942
|
+
return [...names].sort();
|
|
16943
|
+
}
|
|
16944
|
+
|
|
16945
|
+
/** Read direct Python dependency names from requirements.txt + pyproject.toml. */
|
|
16946
|
+
function pythonDirectDeps(cwd) {
|
|
16947
|
+
const names = new Set();
|
|
16948
|
+
try {
|
|
16949
|
+
const req = fs.readFileSync(path.join(cwd, 'requirements.txt'), 'utf8');
|
|
16950
|
+
for (const line of req.split('\n')) {
|
|
16951
|
+
const t = line.trim();
|
|
16952
|
+
if (!t || t.startsWith('#') || t.startsWith('-')) continue;
|
|
16953
|
+
const m = t.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
|
|
16954
|
+
if (m) names.add(m[1]);
|
|
16955
|
+
}
|
|
16956
|
+
} catch (_) { /* none */ }
|
|
16957
|
+
try {
|
|
16958
|
+
const py = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf8');
|
|
16959
|
+
// PEP 621: [project] dependencies = ["foo>=1", "bar"]
|
|
16960
|
+
const projDeps = py.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
16961
|
+
if (projDeps) for (const m of projDeps[1].matchAll(/['"]([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
|
|
16962
|
+
// Poetry: [tool.poetry.dependencies]\n foo = "^1"
|
|
16963
|
+
const poetry = py.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/);
|
|
16964
|
+
if (poetry) for (const m of poetry[1].matchAll(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/gm)) {
|
|
16965
|
+
if (m[1] !== 'python') names.add(m[1]);
|
|
16966
|
+
}
|
|
16967
|
+
} catch (_) { /* none */ }
|
|
16968
|
+
return [...names].sort();
|
|
16969
|
+
}
|
|
16970
|
+
|
|
16971
|
+
/** Locate the project's venv `site-packages` directories (no Python runtime). */
|
|
16972
|
+
function findSitePackages(cwd) {
|
|
16973
|
+
const out = [];
|
|
16974
|
+
for (const v of VENV_DIRS) {
|
|
16975
|
+
const base = path.join(cwd, v);
|
|
16976
|
+
const libDir = path.join(base, 'lib'); // POSIX: <venv>/lib/pythonX.Y/site-packages
|
|
16977
|
+
let pyDirs = [];
|
|
16978
|
+
try { pyDirs = fs.readdirSync(libDir).filter((d) => /^python\d/.test(d)).sort(); } catch (_) { /* none */ }
|
|
16979
|
+
for (const py of pyDirs) {
|
|
16980
|
+
const sp = path.join(libDir, py, 'site-packages');
|
|
16981
|
+
try { if (fs.statSync(sp).isDirectory()) out.push(sp); } catch (_) { /* next */ }
|
|
16982
|
+
}
|
|
16983
|
+
const winSp = path.join(base, 'Lib', 'site-packages'); // Windows
|
|
16984
|
+
try { if (fs.statSync(winSp).isDirectory()) out.push(winSp); } catch (_) { /* next */ }
|
|
16985
|
+
}
|
|
16986
|
+
return out;
|
|
16987
|
+
}
|
|
16988
|
+
|
|
16989
|
+
/** PEP 503 name normalization (case-insensitive, `-`/`_`/`.` collapsed). */
|
|
16990
|
+
function normalizePy(name) {
|
|
16991
|
+
return String(name).toLowerCase().replace(/[-_.]+/g, '-');
|
|
16992
|
+
}
|
|
16993
|
+
|
|
16994
|
+
/** Find an installed distribution's version from its `*.dist-info`/`*.egg-info`. */
|
|
16995
|
+
function findPyVersion(sitePkgsDir, dep) {
|
|
16996
|
+
const norm = normalizePy(dep);
|
|
16997
|
+
let entries;
|
|
16998
|
+
try { entries = fs.readdirSync(sitePkgsDir); } catch (_) { return null; }
|
|
16999
|
+
for (const e of entries.sort()) {
|
|
17000
|
+
const m = e.match(/^(.+?)-(\d[^-]*)\.(?:dist-info|egg-info)$/);
|
|
17001
|
+
if (m && normalizePy(m[1]) === norm) return m[2];
|
|
17002
|
+
}
|
|
17003
|
+
return null;
|
|
17004
|
+
}
|
|
17005
|
+
|
|
17006
|
+
/**
|
|
17007
|
+
* Resolve a Python dependency to its installed module entry file + version.
|
|
17008
|
+
* @returns {{ version: string|null, sourcePath: string|null }|null} null if not installed
|
|
17009
|
+
*/
|
|
17010
|
+
function resolvePyEntry(sitePkgsDirs, dep) {
|
|
17011
|
+
const candidates = [...new Set([dep, dep.replace(/-/g, '_'), dep.toLowerCase(), dep.toLowerCase().replace(/-/g, '_')])];
|
|
17012
|
+
for (const sp of sitePkgsDirs) {
|
|
17013
|
+
const version = findPyVersion(sp, dep);
|
|
17014
|
+
for (const cand of candidates) {
|
|
17015
|
+
for (const entry of ['__init__.pyi', '__init__.py']) { // package
|
|
17016
|
+
const p = path.join(sp, cand, entry);
|
|
17017
|
+
try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
|
|
17018
|
+
}
|
|
17019
|
+
for (const ext of ['.pyi', '.py']) { // single-module
|
|
17020
|
+
const p = path.join(sp, cand + ext);
|
|
17021
|
+
try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
|
|
17022
|
+
}
|
|
17023
|
+
}
|
|
17024
|
+
}
|
|
17025
|
+
return null;
|
|
17026
|
+
}
|
|
17027
|
+
|
|
16832
17028
|
/**
|
|
16833
17029
|
* Build the installed-library signature index for `cwd`.
|
|
16834
17030
|
*
|
|
@@ -16841,17 +17037,24 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16841
17037
|
function buildLibraryIndex(cwd, opts = {}) {
|
|
16842
17038
|
const version = opts.version || '0';
|
|
16843
17039
|
const useCache = opts.cache !== false;
|
|
16844
|
-
const deps = directDeps(cwd).slice(0, MAX_DEPS);
|
|
16845
17040
|
|
|
16846
|
-
|
|
16847
|
-
|
|
17041
|
+
// Collect entries from both ecosystems; each carries its extractor kind.
|
|
17042
|
+
const entries = []; // { name, version, sourcePath, kind: 'dts'|'py' }
|
|
17043
|
+
for (const dep of directDeps(cwd).slice(0, MAX_DEPS)) {
|
|
16848
17044
|
const r = resolveEntry(cwd, dep);
|
|
16849
|
-
if (r) entries.push({ dep, version: r.version,
|
|
17045
|
+
if (r) entries.push({ name: dep, version: r.version, sourcePath: r.dtsPath, kind: 'dts' });
|
|
17046
|
+
}
|
|
17047
|
+
const sitePkgs = findSitePackages(cwd);
|
|
17048
|
+
if (sitePkgs.length) {
|
|
17049
|
+
for (const dep of pythonDirectDeps(cwd).slice(0, MAX_DEPS)) {
|
|
17050
|
+
const r = resolvePyEntry(sitePkgs, dep);
|
|
17051
|
+
if (r) entries.push({ name: dep, version: r.version, sourcePath: r.sourcePath, kind: 'py' });
|
|
17052
|
+
}
|
|
16850
17053
|
}
|
|
16851
17054
|
|
|
16852
17055
|
const cache = useCache ? loadCache(cwd, version) : new Map();
|
|
16853
|
-
const
|
|
16854
|
-
const { unchanged } = getChangedFiles(
|
|
17056
|
+
const files = entries.filter((e) => e.sourcePath).map((e) => e.sourcePath);
|
|
17057
|
+
const { unchanged } = getChangedFiles(files, cache);
|
|
16855
17058
|
const unchangedSet = new Set(unchanged);
|
|
16856
17059
|
|
|
16857
17060
|
const symbols = new Set();
|
|
@@ -16860,20 +17063,20 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16860
17063
|
|
|
16861
17064
|
for (const e of entries) {
|
|
16862
17065
|
let names;
|
|
16863
|
-
if (!e.
|
|
17066
|
+
if (!e.sourcePath) {
|
|
16864
17067
|
names = [];
|
|
16865
|
-
} else if (unchangedSet.has(e.
|
|
16866
|
-
names = cache.get(e.
|
|
17068
|
+
} else if (unchangedSet.has(e.sourcePath) && cache.get(e.sourcePath)) {
|
|
17069
|
+
names = cache.get(e.sourcePath).sigs || [];
|
|
16867
17070
|
} else {
|
|
16868
17071
|
let src = '';
|
|
16869
17072
|
try {
|
|
16870
|
-
if (fs.statSync(e.
|
|
17073
|
+
if (fs.statSync(e.sourcePath).size <= MAX_DTS_BYTES) src = fs.readFileSync(e.sourcePath, 'utf8');
|
|
16871
17074
|
} catch (_) { /* unreadable → empty */ }
|
|
16872
|
-
names = extractDtsExports(src);
|
|
16873
|
-
fresh.push({ file: e.
|
|
17075
|
+
names = e.kind === 'py' ? extractPyExports(src) : extractDtsExports(src);
|
|
17076
|
+
fresh.push({ file: e.sourcePath, sigs: names });
|
|
16874
17077
|
}
|
|
16875
17078
|
for (const n of names) symbols.add(n);
|
|
16876
|
-
libraries.push({ name: e.
|
|
17079
|
+
libraries.push({ name: e.name, version: e.version, symbols: names.length, typed: !!e.sourcePath });
|
|
16877
17080
|
}
|
|
16878
17081
|
|
|
16879
17082
|
if (useCache && fresh.length) {
|
|
@@ -16892,7 +17095,10 @@ __factories["./src/verify/lib-index"] = function(module, exports) {
|
|
|
16892
17095
|
.map((l) => `${l.name}@${l.version}`);
|
|
16893
17096
|
}
|
|
16894
17097
|
|
|
16895
|
-
module.exports = {
|
|
17098
|
+
module.exports = {
|
|
17099
|
+
buildLibraryIndex, extractDtsExports, directDeps, resolveEntry, formatVersionPins,
|
|
17100
|
+
extractPyExports, pythonDirectDeps, findSitePackages, resolvePyEntry,
|
|
17101
|
+
};
|
|
16896
17102
|
|
|
16897
17103
|
};
|
|
16898
17104
|
|
|
@@ -17225,7 +17431,7 @@ function __tryGit(args, opts = {}) {
|
|
|
17225
17431
|
catch (_) { return ''; }
|
|
17226
17432
|
}
|
|
17227
17433
|
|
|
17228
|
-
const VERSION = '8.
|
|
17434
|
+
const VERSION = '8.3.0';
|
|
17229
17435
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
17230
17436
|
|
|
17231
17437
|
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.3.0 | Benchmark: sigmap-v8.3-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.3-main, 2026-07-04)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -26,7 +26,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
26
26
|
| Task success proxy | 10% | 67.8% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.46 (48.8% fewer) |
|
|
28
28
|
| Supported languages | — | 33 |
|
|
29
|
-
| MCP tools | — |
|
|
29
|
+
| MCP tools | — | 18 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -127,7 +127,7 @@ sigmap --version Show version
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
## MCP server —
|
|
130
|
+
## MCP server — 18 tools
|
|
131
131
|
|
|
132
132
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
133
133
|
|
|
@@ -271,6 +271,14 @@ A high-level map of the codebase in one call: module breakdown (files/tokens), t
|
|
|
271
271
|
Input: { } (no arguments)
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
+
### verify_suggestion
|
|
275
|
+
|
|
276
|
+
Ground an AI code suggestion before writing it: verify a snippet or answer against the repository AND the libraries actually installed in node_modules (the grounding moat). Flags fake file paths, unresolvable imports, symbols absent from both the repo index and the installed libraries, and non-existent npm scripts — deterministic, offline, no LLM. Reports the installed libraries it verified against with pinned versions.
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
Input: { code: string }
|
|
280
|
+
```
|
|
281
|
+
|
|
274
282
|
---
|
|
275
283
|
|
|
276
284
|
## Configuration (gen-context.config.json)
|
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.3.0 | Benchmark: sigmap-v8.3-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,13 +23,13 @@ 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.3-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
|
|
30
30
|
- Task success: 67.8% vs 10% without SigMap
|
|
31
31
|
- Prompts per task: 1.46 vs 2.84 baseline (48.8% fewer)
|
|
32
|
-
- Languages: 33 supported · MCP tools:
|
|
32
|
+
- Languages: 33 supported · MCP tools: 18
|
|
33
33
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
34
34
|
|
|
35
35
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.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/handlers.js
CHANGED
|
@@ -853,4 +853,49 @@ function getArchitectureOverview(args, cwd) {
|
|
|
853
853
|
}
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
-
|
|
856
|
+
/**
|
|
857
|
+
* verify_suggestion({ code }) → string
|
|
858
|
+
*
|
|
859
|
+
* Ground an AI code suggestion before it is written: run the Hallucination
|
|
860
|
+
* Guard against the repo AND the installed-library symbol index (the moat), and
|
|
861
|
+
* render a verdict + issues + the installed libraries verified against (pinned
|
|
862
|
+
* versions, D8). Deterministic, offline.
|
|
863
|
+
*/
|
|
864
|
+
function verifySuggestion(args, cwd) {
|
|
865
|
+
const code = args && typeof args.code === 'string' ? args.code : '';
|
|
866
|
+
if (!code.trim()) {
|
|
867
|
+
return 'Usage: verify_suggestion({ code: "<AI-suggested code or answer>" }) — provide the snippet to verify against the repo + installed libraries.';
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
let result;
|
|
871
|
+
try {
|
|
872
|
+
const { verify } = require('../verify/hallucination-guard');
|
|
873
|
+
result = verify(code, cwd);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
return `_verify_suggestion failed: ${err.message}_`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const { issues, summary } = result;
|
|
879
|
+
const out = [];
|
|
880
|
+
if (summary.clean) {
|
|
881
|
+
out.push('✓ Grounded — no fake files, imports, symbols, or scripts detected.');
|
|
882
|
+
} else {
|
|
883
|
+
out.push(`✗ ${summary.total} issue(s) found:`);
|
|
884
|
+
for (const i of issues) {
|
|
885
|
+
out.push(` L${i.line} [${i.type}] ${i.message}`);
|
|
886
|
+
if (i.suggestion) out.push(` ↳ ${i.suggestion}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// D8: report the installed libraries the suggestion was verified against.
|
|
891
|
+
const pins = (summary.libraries || []).filter((l) => l.version).map((l) => `${l.name}@${l.version}`);
|
|
892
|
+
const n = summary.librariesIndexed || 0;
|
|
893
|
+
out.push('');
|
|
894
|
+
out.push(
|
|
895
|
+
`Grounded against ${summary.symbolsIndexed} repo + library symbol(s)` +
|
|
896
|
+
(n ? ` · ${n} installed librar${n === 1 ? 'y' : 'ies'}${pins.length ? ': ' + pins.join(', ') : ''}` : '')
|
|
897
|
+
);
|
|
898
|
+
return out.join('\n');
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion };
|
package/src/mcp/server.js
CHANGED
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Supported methods:
|
|
10
10
|
* initialize → serverInfo + capabilities
|
|
11
|
-
* tools/list →
|
|
11
|
+
* tools/list → 18 tool definitions
|
|
12
12
|
* tools/call → dispatch to handler, return result
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { TOOLS } = require('./tools');
|
|
17
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '8.
|
|
21
|
+
version: '8.3.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -83,6 +83,7 @@ function dispatch(msg, cwd) {
|
|
|
83
83
|
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
84
84
|
else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
|
|
85
85
|
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
86
|
+
else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
|
|
86
87
|
else {
|
|
87
88
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
88
89
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -316,6 +316,26 @@ const TOOLS = [
|
|
|
316
316
|
required: [],
|
|
317
317
|
},
|
|
318
318
|
},
|
|
319
|
+
{
|
|
320
|
+
name: 'verify_suggestion',
|
|
321
|
+
description:
|
|
322
|
+
'Ground an AI code suggestion before writing it: verify a snippet or answer against the ' +
|
|
323
|
+
'repository AND the libraries actually installed in node_modules (the grounding moat). ' +
|
|
324
|
+
'Flags fake file paths, unresolvable imports, symbols absent from both the repo index and ' +
|
|
325
|
+
'the installed libraries, and non-existent npm scripts — deterministic, offline, no LLM. ' +
|
|
326
|
+
'Reports the installed libraries it verified against with pinned versions.',
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: {
|
|
330
|
+
code: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description:
|
|
333
|
+
'The AI-suggested code snippet or answer text to verify against the repo + installed libraries.',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
required: ['code'],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
319
339
|
];
|
|
320
340
|
|
|
321
341
|
module.exports = { TOOLS };
|
package/src/verify/lib-index.js
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* in `node_modules` and verify AI suggestions against repo + private +
|
|
9
9
|
* installed-lib symbols. This module builds the installed-lib half.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Two ecosystems, one index:
|
|
12
|
+
* - **JS/TS** — each **direct** dependency in `package.json` resolved under
|
|
13
|
+
* `node_modules/<dep>`; exports read from its TypeScript declaration entry
|
|
14
|
+
* (`types`/`typings`, else `index.d.ts`).
|
|
15
|
+
* - **Python** — each direct dependency in `requirements.txt`/`pyproject.toml`
|
|
16
|
+
* resolved in the project's venv `site-packages`; exports read from the
|
|
17
|
+
* package's `__init__.py`/`.pyi`. No Python runtime is spawned (North-Star #1).
|
|
18
|
+
*
|
|
19
|
+
* Pure, zero-dependency, deterministic: byte-stable given a fixed installed
|
|
20
|
+
* tree. Bounded (per-file read cap + dep cap) and cached via
|
|
21
|
+
* `src/cache/sig-cache.js` so repeat builds are near-free.
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
24
|
const fs = require('fs');
|
|
@@ -23,6 +28,7 @@ const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = require('.
|
|
|
23
28
|
const MAX_DTS_BYTES = 512 * 1024; // per-file read cap
|
|
24
29
|
const MAX_DEPS = 1000; // dep count cap
|
|
25
30
|
const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
31
|
+
const VENV_DIRS = ['.venv', 'venv', 'env', '.env'];
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Extract exported symbol names from a `.d.ts` declaration file. Deterministic,
|
|
@@ -98,6 +104,130 @@ function resolveEntry(cwd, dep) {
|
|
|
98
104
|
return { version, dtsPath: null }; // installed but untyped
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
// ── Python ──────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract exported symbol names from a Python module's `__init__.py`/`.pyi`.
|
|
111
|
+
* Deterministic, regex-based, top-level only: `__all__`, `def`/`class`, public
|
|
112
|
+
* module-level assignments, and `from … import …` re-exports (a package's
|
|
113
|
+
* public API is largely re-exports). Private names (leading `_`) are skipped
|
|
114
|
+
* unless listed in `__all__`.
|
|
115
|
+
* @param {string} src
|
|
116
|
+
* @returns {string[]} sorted unique exported names
|
|
117
|
+
*/
|
|
118
|
+
function extractPyExports(src) {
|
|
119
|
+
const names = new Set();
|
|
120
|
+
if (!src) return [];
|
|
121
|
+
|
|
122
|
+
// __all__ = [ 'a', 'b', ... ] (authoritative when present; keeps privates)
|
|
123
|
+
const allMatch = src.match(/^__all__\s*[:+]?=\s*[\[(]([\s\S]*?)[\])]/m);
|
|
124
|
+
if (allMatch) {
|
|
125
|
+
for (const m of allMatch[1].matchAll(/['"]([A-Za-z_]\w*)['"]/g)) names.add(m[1]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// top-level def / class (column 0)
|
|
129
|
+
for (const m of src.matchAll(/^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);
|
|
130
|
+
for (const m of src.matchAll(/^class\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);
|
|
131
|
+
|
|
132
|
+
// top-level public assignments: NAME = … / NAME: type = … (not ==, +=, etc.)
|
|
133
|
+
for (const m of src.matchAll(/^([A-Za-z_]\w*)\s*(?::[^=\n]+)?=(?!=)/gm)) {
|
|
134
|
+
if (!m[1].startsWith('_')) names.add(m[1]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// re-exports: from .mod import Name, Other as Alias
|
|
138
|
+
for (const m of src.matchAll(/^from\s+[^\n]+?\s+import\s+([^\n#]+)/gm)) {
|
|
139
|
+
for (const part of m[1].split(',')) {
|
|
140
|
+
const name = part.trim().replace(/[()]/g, '').split(/\s+as\s+/).pop().trim();
|
|
141
|
+
if (/^[A-Za-z_]\w*$/.test(name) && !name.startsWith('_')) names.add(name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [...names].sort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read direct Python dependency names from requirements.txt + pyproject.toml. */
|
|
149
|
+
function pythonDirectDeps(cwd) {
|
|
150
|
+
const names = new Set();
|
|
151
|
+
try {
|
|
152
|
+
const req = fs.readFileSync(path.join(cwd, 'requirements.txt'), 'utf8');
|
|
153
|
+
for (const line of req.split('\n')) {
|
|
154
|
+
const t = line.trim();
|
|
155
|
+
if (!t || t.startsWith('#') || t.startsWith('-')) continue;
|
|
156
|
+
const m = t.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
|
|
157
|
+
if (m) names.add(m[1]);
|
|
158
|
+
}
|
|
159
|
+
} catch (_) { /* none */ }
|
|
160
|
+
try {
|
|
161
|
+
const py = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf8');
|
|
162
|
+
// PEP 621: [project] dependencies = ["foo>=1", "bar"]
|
|
163
|
+
const projDeps = py.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
164
|
+
if (projDeps) for (const m of projDeps[1].matchAll(/['"]([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
|
|
165
|
+
// Poetry: [tool.poetry.dependencies]\n foo = "^1"
|
|
166
|
+
const poetry = py.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/);
|
|
167
|
+
if (poetry) for (const m of poetry[1].matchAll(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/gm)) {
|
|
168
|
+
if (m[1] !== 'python') names.add(m[1]);
|
|
169
|
+
}
|
|
170
|
+
} catch (_) { /* none */ }
|
|
171
|
+
return [...names].sort();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Locate the project's venv `site-packages` directories (no Python runtime). */
|
|
175
|
+
function findSitePackages(cwd) {
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const v of VENV_DIRS) {
|
|
178
|
+
const base = path.join(cwd, v);
|
|
179
|
+
const libDir = path.join(base, 'lib'); // POSIX: <venv>/lib/pythonX.Y/site-packages
|
|
180
|
+
let pyDirs = [];
|
|
181
|
+
try { pyDirs = fs.readdirSync(libDir).filter((d) => /^python\d/.test(d)).sort(); } catch (_) { /* none */ }
|
|
182
|
+
for (const py of pyDirs) {
|
|
183
|
+
const sp = path.join(libDir, py, 'site-packages');
|
|
184
|
+
try { if (fs.statSync(sp).isDirectory()) out.push(sp); } catch (_) { /* next */ }
|
|
185
|
+
}
|
|
186
|
+
const winSp = path.join(base, 'Lib', 'site-packages'); // Windows
|
|
187
|
+
try { if (fs.statSync(winSp).isDirectory()) out.push(winSp); } catch (_) { /* next */ }
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** PEP 503 name normalization (case-insensitive, `-`/`_`/`.` collapsed). */
|
|
193
|
+
function normalizePy(name) {
|
|
194
|
+
return String(name).toLowerCase().replace(/[-_.]+/g, '-');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Find an installed distribution's version from its `*.dist-info`/`*.egg-info`. */
|
|
198
|
+
function findPyVersion(sitePkgsDir, dep) {
|
|
199
|
+
const norm = normalizePy(dep);
|
|
200
|
+
let entries;
|
|
201
|
+
try { entries = fs.readdirSync(sitePkgsDir); } catch (_) { return null; }
|
|
202
|
+
for (const e of entries.sort()) {
|
|
203
|
+
const m = e.match(/^(.+?)-(\d[^-]*)\.(?:dist-info|egg-info)$/);
|
|
204
|
+
if (m && normalizePy(m[1]) === norm) return m[2];
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a Python dependency to its installed module entry file + version.
|
|
211
|
+
* @returns {{ version: string|null, sourcePath: string|null }|null} null if not installed
|
|
212
|
+
*/
|
|
213
|
+
function resolvePyEntry(sitePkgsDirs, dep) {
|
|
214
|
+
const candidates = [...new Set([dep, dep.replace(/-/g, '_'), dep.toLowerCase(), dep.toLowerCase().replace(/-/g, '_')])];
|
|
215
|
+
for (const sp of sitePkgsDirs) {
|
|
216
|
+
const version = findPyVersion(sp, dep);
|
|
217
|
+
for (const cand of candidates) {
|
|
218
|
+
for (const entry of ['__init__.pyi', '__init__.py']) { // package
|
|
219
|
+
const p = path.join(sp, cand, entry);
|
|
220
|
+
try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
|
|
221
|
+
}
|
|
222
|
+
for (const ext of ['.pyi', '.py']) { // single-module
|
|
223
|
+
const p = path.join(sp, cand + ext);
|
|
224
|
+
try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
101
231
|
/**
|
|
102
232
|
* Build the installed-library signature index for `cwd`.
|
|
103
233
|
*
|
|
@@ -110,17 +240,24 @@ function resolveEntry(cwd, dep) {
|
|
|
110
240
|
function buildLibraryIndex(cwd, opts = {}) {
|
|
111
241
|
const version = opts.version || '0';
|
|
112
242
|
const useCache = opts.cache !== false;
|
|
113
|
-
const deps = directDeps(cwd).slice(0, MAX_DEPS);
|
|
114
243
|
|
|
115
|
-
|
|
116
|
-
|
|
244
|
+
// Collect entries from both ecosystems; each carries its extractor kind.
|
|
245
|
+
const entries = []; // { name, version, sourcePath, kind: 'dts'|'py' }
|
|
246
|
+
for (const dep of directDeps(cwd).slice(0, MAX_DEPS)) {
|
|
117
247
|
const r = resolveEntry(cwd, dep);
|
|
118
|
-
if (r) entries.push({ dep, version: r.version,
|
|
248
|
+
if (r) entries.push({ name: dep, version: r.version, sourcePath: r.dtsPath, kind: 'dts' });
|
|
249
|
+
}
|
|
250
|
+
const sitePkgs = findSitePackages(cwd);
|
|
251
|
+
if (sitePkgs.length) {
|
|
252
|
+
for (const dep of pythonDirectDeps(cwd).slice(0, MAX_DEPS)) {
|
|
253
|
+
const r = resolvePyEntry(sitePkgs, dep);
|
|
254
|
+
if (r) entries.push({ name: dep, version: r.version, sourcePath: r.sourcePath, kind: 'py' });
|
|
255
|
+
}
|
|
119
256
|
}
|
|
120
257
|
|
|
121
258
|
const cache = useCache ? loadCache(cwd, version) : new Map();
|
|
122
|
-
const
|
|
123
|
-
const { unchanged } = getChangedFiles(
|
|
259
|
+
const files = entries.filter((e) => e.sourcePath).map((e) => e.sourcePath);
|
|
260
|
+
const { unchanged } = getChangedFiles(files, cache);
|
|
124
261
|
const unchangedSet = new Set(unchanged);
|
|
125
262
|
|
|
126
263
|
const symbols = new Set();
|
|
@@ -129,20 +266,20 @@ function buildLibraryIndex(cwd, opts = {}) {
|
|
|
129
266
|
|
|
130
267
|
for (const e of entries) {
|
|
131
268
|
let names;
|
|
132
|
-
if (!e.
|
|
269
|
+
if (!e.sourcePath) {
|
|
133
270
|
names = [];
|
|
134
|
-
} else if (unchangedSet.has(e.
|
|
135
|
-
names = cache.get(e.
|
|
271
|
+
} else if (unchangedSet.has(e.sourcePath) && cache.get(e.sourcePath)) {
|
|
272
|
+
names = cache.get(e.sourcePath).sigs || [];
|
|
136
273
|
} else {
|
|
137
274
|
let src = '';
|
|
138
275
|
try {
|
|
139
|
-
if (fs.statSync(e.
|
|
276
|
+
if (fs.statSync(e.sourcePath).size <= MAX_DTS_BYTES) src = fs.readFileSync(e.sourcePath, 'utf8');
|
|
140
277
|
} catch (_) { /* unreadable → empty */ }
|
|
141
|
-
names = extractDtsExports(src);
|
|
142
|
-
fresh.push({ file: e.
|
|
278
|
+
names = e.kind === 'py' ? extractPyExports(src) : extractDtsExports(src);
|
|
279
|
+
fresh.push({ file: e.sourcePath, sigs: names });
|
|
143
280
|
}
|
|
144
281
|
for (const n of names) symbols.add(n);
|
|
145
|
-
libraries.push({ name: e.
|
|
282
|
+
libraries.push({ name: e.name, version: e.version, symbols: names.length, typed: !!e.sourcePath });
|
|
146
283
|
}
|
|
147
284
|
|
|
148
285
|
if (useCache && fresh.length) {
|
|
@@ -161,4 +298,7 @@ function formatVersionPins(libraries) {
|
|
|
161
298
|
.map((l) => `${l.name}@${l.version}`);
|
|
162
299
|
}
|
|
163
300
|
|
|
164
|
-
module.exports = {
|
|
301
|
+
module.exports = {
|
|
302
|
+
buildLibraryIndex, extractDtsExports, directDeps, resolveEntry, formatVersionPins,
|
|
303
|
+
extractPyExports, pythonDirectDeps, findSitePackages, resolvePyEntry,
|
|
304
|
+
};
|