sigmap 8.9.1 → 8.11.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/AGENTS.md +589 -182
- package/CHANGELOG.md +38 -0
- package/README.md +7 -3
- package/gen-context.js +759 -212
- package/llms-full.txt +4 -2
- package/llms.txt +2 -2
- package/package.json +10 -3
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +3 -0
- package/src/conventions/extract.js +13 -3
- package/src/conventions/fix.js +4 -1
- package/src/extractors/javascript.js +4 -4
- package/src/extractors/typescript.js +5 -5
- package/src/format/terse.js +86 -0
- package/src/graph/builder.js +124 -16
- package/src/graph/impact.js +2 -2
- package/src/health/scorer.js +172 -97
- package/src/judge/judge-engine.js +62 -2
- package/src/mcp/server.js +1 -1
- package/src/plan/planner.js +44 -15
- package/src/review/pr-evidence.js +2 -1
- package/src/review/review-pr.js +24 -2
- package/src/util/truncate.js +42 -0
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.11.0 | Benchmark: sigmap-v8.11-main (2026-07-11)
|
|
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.11-main, 2026-07-11)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -55,6 +55,7 @@ sigmap Generate context once and exit
|
|
|
55
55
|
sigmap --monorepo Generate per-package context (monorepo)
|
|
56
56
|
sigmap --each Run for every repo in the current directory
|
|
57
57
|
sigmap --routing Include model routing hints in output
|
|
58
|
+
sigmap --terse Compact signature encoding (deterministic; line anchors preserved)
|
|
58
59
|
sigmap --format cache Also write Anthropic prompt-cache JSON
|
|
59
60
|
sigmap --track Append run metrics to .context/usage.ndjson
|
|
60
61
|
sigmap --watch Generate + watch for file changes
|
|
@@ -325,6 +326,7 @@ tracking = false
|
|
|
325
326
|
mcp = {"autoRegister":true}
|
|
326
327
|
depMap = true
|
|
327
328
|
versionPins = true
|
|
329
|
+
terse = false
|
|
328
330
|
todos = true
|
|
329
331
|
changes = true
|
|
330
332
|
changesCommits = 10
|
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.11.0 | Benchmark: sigmap-v8.11-main (2026-07-11)
|
|
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.11-main, 2026-07-11)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "8.11.0",
|
|
4
|
+
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./packages/core/index.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"benchmark:verify": "node scripts/run-verify-benchmark.mjs",
|
|
30
30
|
"benchmark:squeeze": "node scripts/run-squeeze-benchmark.mjs --save",
|
|
31
31
|
"benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
|
|
32
|
+
"benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
|
|
32
33
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
|
33
34
|
"health": "node gen-context.js --health",
|
|
34
35
|
"map": "node gen-project-map.js",
|
|
@@ -80,7 +81,13 @@
|
|
|
80
81
|
"mcp",
|
|
81
82
|
"function-signatures",
|
|
82
83
|
"code-intelligence",
|
|
83
|
-
"
|
|
84
|
+
"ai-grounding",
|
|
85
|
+
"hallucination-detection",
|
|
86
|
+
"deterministic",
|
|
87
|
+
"verifiable-ai",
|
|
88
|
+
"code-signatures",
|
|
89
|
+
"ai-code-review",
|
|
90
|
+
"evidence-pack",
|
|
84
91
|
"local-llm",
|
|
85
92
|
"ollama",
|
|
86
93
|
"ai-coding"
|
package/src/config/defaults.js
CHANGED
|
@@ -117,6 +117,9 @@ const DEFAULTS = {
|
|
|
117
117
|
// Include a compact `name@version` list of installed direct deps (D8)
|
|
118
118
|
versionPins: true,
|
|
119
119
|
|
|
120
|
+
// Terse signature encoding — deterministic compaction of sig lines (D7, opt-in)
|
|
121
|
+
terse: false,
|
|
122
|
+
|
|
120
123
|
// Include TODO/FIXME/HACK/XXX comments as compact section
|
|
121
124
|
todos: true,
|
|
122
125
|
|
|
@@ -26,8 +26,16 @@ const TIER_MOSTLY = 0.7;
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Classify a file's base name (without extension) into a naming style.
|
|
29
|
+
*
|
|
30
|
+
* A single lowercase word (`user`, `index`, `loader`) is classified
|
|
31
|
+
* `single-word`, NOT `camelCase`: it has no case boundary or separator, so it is
|
|
32
|
+
* compatible with camelCase, kebab-case AND snake_case at once and expresses no
|
|
33
|
+
* distinguishable convention. `scoreConvention` treats it as style-neutral
|
|
34
|
+
* (excluded), so a repo of single-word files reports "unknown" rather than a
|
|
35
|
+
* spurious "100% camelCase".
|
|
36
|
+
*
|
|
29
37
|
* @param {string} basename a file basename, e.g. "user-service.ts"
|
|
30
|
-
* @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
|
|
38
|
+
* @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'single-word'|'other'}
|
|
31
39
|
*/
|
|
32
40
|
function classifyNaming(basename) {
|
|
33
41
|
let stem = String(basename || '');
|
|
@@ -38,7 +46,7 @@ function classifyNaming(basename) {
|
|
|
38
46
|
if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
|
|
39
47
|
if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
|
|
40
48
|
if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
|
|
41
|
-
if (/^[a-z][a-z0-9]*$/.test(stem)) return '
|
|
49
|
+
if (/^[a-z][a-z0-9]*$/.test(stem)) return 'single-word'; // style-neutral (no case boundary)
|
|
42
50
|
return 'other';
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -61,7 +69,9 @@ function scoreConvention(labels, refs) {
|
|
|
61
69
|
let total = 0;
|
|
62
70
|
for (let i = 0; i < all.length; i++) {
|
|
63
71
|
const l = all[i];
|
|
64
|
-
|
|
72
|
+
// 'other' = unclassifiable; 'single-word' = style-neutral. Both express no
|
|
73
|
+
// distinguishable convention and are excluded from the score.
|
|
74
|
+
if (l == null || l === 'other' || l === 'single-word') continue;
|
|
65
75
|
total++;
|
|
66
76
|
counts.set(l, (counts.get(l) || 0) + 1);
|
|
67
77
|
if (refs && refs[i] != null) {
|
package/src/conventions/fix.js
CHANGED
|
@@ -47,7 +47,10 @@ function buildFixList(cwd, files, conventions) {
|
|
|
47
47
|
if (TEST_RE.test(f)) continue;
|
|
48
48
|
const base = path.basename(f);
|
|
49
49
|
const style = classifyNaming(base);
|
|
50
|
-
|
|
50
|
+
// Skip unclassifiable ('other') and style-neutral single-word names — a
|
|
51
|
+
// single lowercase word already satisfies any convention, so renaming it
|
|
52
|
+
// (e.g. user.js → user.js) is a no-op at best and noise at worst.
|
|
53
|
+
if (style === 'other' || style === 'single-word' || style === dominant) continue;
|
|
51
54
|
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
52
55
|
renames.push({ from: rel, to: _renamePath(rel, dominant), fromStyle: style });
|
|
53
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { lineAt, withAnchor } = require('./line-anchor');
|
|
4
|
+
const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Extract signatures from JavaScript source code.
|
|
@@ -80,9 +81,8 @@ function extract(src) {
|
|
|
80
81
|
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.slice(0, 25);
|
|
84
|
+
const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
|
|
85
|
+
return capWithNotice(withAnchors, 25, 'signatures');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function extractBlock(src, startIndex) {
|
|
@@ -113,7 +113,7 @@ function extractClassMembers(block, returnHints) {
|
|
|
113
113
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
114
114
|
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
115
115
|
}
|
|
116
|
-
return members
|
|
116
|
+
return capMembersWithNotice(members, 8, 'methods');
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
function buildReturnHints(src) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { lineAt, withAnchor } = require('./line-anchor');
|
|
4
|
+
const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Extract signatures from TypeScript source code.
|
|
@@ -152,9 +153,8 @@ function extract(src) {
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.slice(0, 35);
|
|
156
|
+
const withAnchors = sigs.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s));
|
|
157
|
+
return capWithNotice(withAnchors, 35, 'signatures');
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function extractBlock(src, startIndex) {
|
|
@@ -184,7 +184,7 @@ function extractInterfaceMembers(block) {
|
|
|
184
184
|
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
185
185
|
members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
|
|
186
186
|
}
|
|
187
|
-
return members
|
|
187
|
+
return capMembersWithNotice(members, 8, 'members');
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
|
|
@@ -210,7 +210,7 @@ function extractClassMembers(block) {
|
|
|
210
210
|
const retStr = retType ? ` → ${retType}` : '';
|
|
211
211
|
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
|
|
212
212
|
}
|
|
213
|
-
return members
|
|
213
|
+
return capMembersWithNotice(members, 8, 'methods');
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function normalizeParams(params) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terse signature encoder (D7).
|
|
5
|
+
*
|
|
6
|
+
* Deterministic compaction of signature lines for the generated context —
|
|
7
|
+
* opt-in via `--terse` / `terse: true`. Every transform is a fixed string
|
|
8
|
+
* rewrite (no heuristics, no LLM), so terse output stays byte-stable.
|
|
9
|
+
*
|
|
10
|
+
* The line anchor (` :start-end`) and everything after it (Python/R doc
|
|
11
|
+
* hints) are preserved byte-exactly: `parseAnchor`, `get_lines`, and the
|
|
12
|
+
* evidence pack keep working on terse output. Symbol extraction is safe too —
|
|
13
|
+
* `extractName` in src/extractors/prdiff.js already recognizes `fn <name>`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** First ` :start[-end]` anchor token (two-space prefix, as emitted by line-anchor.js). */
|
|
17
|
+
const ANCHOR_RE = /\s{2}:\d+(?:-\d+)?(?=\s|$)/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Split a signature into the compactable text and the byte-preserved suffix
|
|
21
|
+
* (anchor + any trailing doc hint).
|
|
22
|
+
* @param {string} sig
|
|
23
|
+
* @returns {{ text: string, suffix: string }}
|
|
24
|
+
*/
|
|
25
|
+
function splitAnchor(sig) {
|
|
26
|
+
const s = String(sig);
|
|
27
|
+
const m = ANCHOR_RE.exec(s);
|
|
28
|
+
if (!m) return { text: s, suffix: '' };
|
|
29
|
+
return { text: s.slice(0, m.index), suffix: s.slice(m.index) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compact one signature line. Leading whitespace (member indentation) is kept.
|
|
34
|
+
* @param {string} sig
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function encodeTerseSig(sig) {
|
|
38
|
+
const { text, suffix } = splitAnchor(sig);
|
|
39
|
+
let t = text
|
|
40
|
+
.replace(/\basync function\b/g, 'async fn')
|
|
41
|
+
.replace(/\bfunction\b/g, 'fn')
|
|
42
|
+
.replace(/\s+→\s+/g, '→')
|
|
43
|
+
.replace(/,\s+/g, ',')
|
|
44
|
+
.replace(/\s+=\s+/g, '=')
|
|
45
|
+
.replace(/\{\s+/g, '{')
|
|
46
|
+
.replace(/\s+\}/g, '}')
|
|
47
|
+
.replace(/(\S)\s{2,}(?=\S)/g, '$1 ')
|
|
48
|
+
.replace(/^module\.exports=/, 'exports=');
|
|
49
|
+
return t + suffix;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compact an array of signature lines.
|
|
54
|
+
* @param {string[]} sigs
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function encodeTerseSigs(sigs) {
|
|
58
|
+
return (sigs || []).map(encodeTerseSig);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Estimated tokens of joined signature lines (same chars/4 rule as elsewhere). */
|
|
62
|
+
function _tokens(sigs) {
|
|
63
|
+
return Math.ceil(sigs.join('\n').length / 4);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Measure the real reduction terse encoding buys over a set of signature
|
|
68
|
+
* lists — the D7 "measure first" gate. Never quote a number this didn't produce.
|
|
69
|
+
* @param {string[][]} sigsList one string[] per file
|
|
70
|
+
* @returns {{ beforeTokens: number, afterTokens: number, reductionPct: number }}
|
|
71
|
+
*/
|
|
72
|
+
function measureTerse(sigsList) {
|
|
73
|
+
let beforeTokens = 0;
|
|
74
|
+
let afterTokens = 0;
|
|
75
|
+
for (const sigs of sigsList || []) {
|
|
76
|
+
if (!sigs || !sigs.length) continue;
|
|
77
|
+
beforeTokens += _tokens(sigs);
|
|
78
|
+
afterTokens += _tokens(encodeTerseSigs(sigs));
|
|
79
|
+
}
|
|
80
|
+
const reductionPct = beforeTokens > 0
|
|
81
|
+
? Math.round(((beforeTokens - afterTokens) / beforeTokens) * 1000) / 10
|
|
82
|
+
: 0;
|
|
83
|
+
return { beforeTokens, afterTokens, reductionPct };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor };
|
package/src/graph/builder.js
CHANGED
|
@@ -31,20 +31,19 @@ const RB_EXTS = new Set(['.rb', '.rake']);
|
|
|
31
31
|
const R_EXTS = new Set(['.r', '.R']);
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @param {string}
|
|
34
|
+
* Probe an absolute base path for a JS/TS module file in fileSet, trying the
|
|
35
|
+
* usual extension and index-file candidates.
|
|
36
|
+
* @param {string} base - absolute path (no extension) to probe
|
|
37
37
|
* @param {Set<string>} fileSet
|
|
38
38
|
* @returns {string|null}
|
|
39
39
|
*/
|
|
40
|
-
function
|
|
41
|
-
const base = path.resolve(dir, importStr);
|
|
40
|
+
function probeJs(base, fileSet) {
|
|
42
41
|
const candidates = [
|
|
43
42
|
base,
|
|
44
43
|
base + '.ts', base + '.tsx',
|
|
45
44
|
base + '.js', base + '.jsx', base + '.mjs', base + '.cjs',
|
|
46
|
-
path.join(base, 'index.ts'),
|
|
47
|
-
path.join(base, 'index.js'),
|
|
45
|
+
path.join(base, 'index.ts'), path.join(base, 'index.tsx'),
|
|
46
|
+
path.join(base, 'index.js'), path.join(base, 'index.jsx'),
|
|
48
47
|
];
|
|
49
48
|
for (const c of candidates) {
|
|
50
49
|
const normC = normalizePath(c);
|
|
@@ -53,6 +52,103 @@ function resolveJsPath(dir, importStr, fileSet) {
|
|
|
53
52
|
return null;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a JS/TS relative import string to an absolute path in fileSet.
|
|
57
|
+
* @param {string} dir - directory of the importing file
|
|
58
|
+
* @param {string} importStr - raw import string (e.g. './utils', '../store')
|
|
59
|
+
* @param {Set<string>} fileSet
|
|
60
|
+
* @returns {string|null}
|
|
61
|
+
*/
|
|
62
|
+
function resolveJsPath(dir, importStr, fileSet) {
|
|
63
|
+
return probeJs(path.resolve(dir, importStr), fileSet);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Strip comments and trailing commas so a tsconfig/jsconfig (JSONC) parses.
|
|
68
|
+
* Deliberately conservative — leaves string contents alone.
|
|
69
|
+
*/
|
|
70
|
+
function stripJsonc(src) {
|
|
71
|
+
let out = '';
|
|
72
|
+
let inStr = false, quote = '', inLine = false, inBlock = false;
|
|
73
|
+
for (let i = 0; i < src.length; i++) {
|
|
74
|
+
const c = src[i], n = src[i + 1];
|
|
75
|
+
if (inLine) { if (c === '\n') { inLine = false; out += c; } continue; }
|
|
76
|
+
if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++; } continue; }
|
|
77
|
+
if (inStr) { out += c; if (c === '\\') { out += (n || ''); i++; } else if (c === quote) inStr = false; continue; }
|
|
78
|
+
if (c === '"' || c === "'") { inStr = true; quote = c; out += c; continue; }
|
|
79
|
+
if (c === '/' && n === '/') { inLine = true; i++; continue; }
|
|
80
|
+
if (c === '/' && n === '*') { inBlock = true; i++; continue; }
|
|
81
|
+
out += c;
|
|
82
|
+
}
|
|
83
|
+
// remove trailing commas before } or ]
|
|
84
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load the JS/TS path-alias map from tsconfig.json / jsconfig.json.
|
|
89
|
+
* Resolves `compilerOptions.paths` and `baseUrl` into absolute target bases so
|
|
90
|
+
* bare/aliased imports (e.g. `@/utils`, `components/Button`) can be resolved to
|
|
91
|
+
* on-disk files. Returns null when no config or no aliasing is configured.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} cwd
|
|
94
|
+
* @returns {{ baseUrl: string|null, entries: Array<{prefix:string,wildcard:boolean,targets:string[]}> }|null}
|
|
95
|
+
*/
|
|
96
|
+
function loadAliasMap(cwd) {
|
|
97
|
+
if (!cwd) return null;
|
|
98
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
99
|
+
let json;
|
|
100
|
+
try { json = JSON.parse(stripJsonc(fs.readFileSync(path.join(cwd, name), 'utf8'))); }
|
|
101
|
+
catch (_) { continue; }
|
|
102
|
+
const co = (json && json.compilerOptions) || {};
|
|
103
|
+
const baseUrl = co.baseUrl ? path.resolve(cwd, co.baseUrl) : null;
|
|
104
|
+
const base = baseUrl || cwd;
|
|
105
|
+
const entries = [];
|
|
106
|
+
for (const [pattern, targets] of Object.entries(co.paths || {})) {
|
|
107
|
+
const wildcard = pattern.includes('*');
|
|
108
|
+
const prefix = pattern.replace(/\*.*$/, '');
|
|
109
|
+
const tgs = (Array.isArray(targets) ? targets : [])
|
|
110
|
+
.map((t) => path.resolve(base, String(t).replace(/\*.*$/, '')));
|
|
111
|
+
if (tgs.length) entries.push({ prefix, wildcard, targets: tgs });
|
|
112
|
+
}
|
|
113
|
+
if (baseUrl || entries.length) return { baseUrl, entries };
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a non-relative JS/TS import specifier through the alias map.
|
|
121
|
+
* @param {string} spec - e.g. '@/utils', '@app/Button', 'components/Nav'
|
|
122
|
+
* @param {object|null} aliasMap - from loadAliasMap
|
|
123
|
+
* @param {Set<string>} fileSet
|
|
124
|
+
* @returns {string|null}
|
|
125
|
+
*/
|
|
126
|
+
function resolveAlias(spec, aliasMap, fileSet) {
|
|
127
|
+
if (!aliasMap) return null;
|
|
128
|
+
for (const e of aliasMap.entries) {
|
|
129
|
+
if (e.wildcard) {
|
|
130
|
+
if (spec.startsWith(e.prefix)) {
|
|
131
|
+
const rest = spec.slice(e.prefix.length);
|
|
132
|
+
for (const t of e.targets) {
|
|
133
|
+
const r = probeJs(rest ? path.join(t, rest) : t, fileSet);
|
|
134
|
+
if (r) return r;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else if (spec === e.prefix) {
|
|
138
|
+
for (const t of e.targets) {
|
|
139
|
+
const r = probeJs(t, fileSet);
|
|
140
|
+
if (r) return r;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Bare import resolved from baseUrl (tsconfig baseUrl without an explicit alias).
|
|
145
|
+
if (aliasMap.baseUrl) {
|
|
146
|
+
const r = probeJs(path.join(aliasMap.baseUrl, spec), fileSet);
|
|
147
|
+
if (r) return r;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
56
152
|
/**
|
|
57
153
|
* Resolve an R `source(...)` argument to an absolute path in fileSet.
|
|
58
154
|
* Tries the dir-relative path first, then a cwd-relative path so that
|
|
@@ -98,21 +194,29 @@ function extractFileDeps(filePath, content, fileSet, cwd, ctx) {
|
|
|
98
194
|
|
|
99
195
|
// ── JS / TS ───────────────────────────────────────────────────────────────
|
|
100
196
|
if (JS_EXTS.has(ext)) {
|
|
197
|
+
const aliasMap = ctx && ctx.aliasMap;
|
|
198
|
+
// Resolve any specifier: relative → dir-relative; otherwise via tsconfig/
|
|
199
|
+
// jsconfig path aliases + baseUrl. Bare npm packages (react, lodash) fall
|
|
200
|
+
// through to null because they are not in fileSet, so no false edges.
|
|
201
|
+
const resolveSpec = (spec) => spec.startsWith('.')
|
|
202
|
+
? resolveJsPath(dir, spec, fileSet)
|
|
203
|
+
: resolveAlias(spec, aliasMap, fileSet);
|
|
204
|
+
|
|
101
205
|
const stripped = content
|
|
102
206
|
.replace(/\/\/.*$/gm, '')
|
|
103
207
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
104
208
|
|
|
105
|
-
// ES imports: import ... from './foo' or import './side-effect'
|
|
106
|
-
const reEs = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
|
|
107
209
|
let m;
|
|
210
|
+
// ES imports: import ... from 'x' | import 'x' | export ... from 'x'
|
|
211
|
+
const reEs = /(?:^|[\r\n])\s*(?:import|export)\s+(?:[^'";\r\n]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
108
212
|
while ((m = reEs.exec(stripped)) !== null) {
|
|
109
|
-
const r =
|
|
213
|
+
const r = resolveSpec(m[1]);
|
|
110
214
|
if (r) found.push(r);
|
|
111
215
|
}
|
|
112
|
-
// CommonJS
|
|
113
|
-
const
|
|
114
|
-
while ((m =
|
|
115
|
-
const r =
|
|
216
|
+
// CommonJS require('x') and dynamic import('x').
|
|
217
|
+
const reCall = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
218
|
+
while ((m = reCall.exec(stripped)) !== null) {
|
|
219
|
+
const r = resolveSpec(m[1]);
|
|
116
220
|
if (r) found.push(r);
|
|
117
221
|
}
|
|
118
222
|
}
|
|
@@ -284,6 +388,10 @@ function build(files, cwd, ctx) {
|
|
|
284
388
|
const fileSet = new Set(files.map((f) => path.resolve(f)));
|
|
285
389
|
// Create a normalized version for cross-platform case-insensitive lookups
|
|
286
390
|
const fileSetNormalized = new Set([...fileSet].map(normalizePath));
|
|
391
|
+
// Resolve the JS/TS path-alias map once (tsconfig/jsconfig paths + baseUrl),
|
|
392
|
+
// unless a caller supplied one explicitly via ctx.
|
|
393
|
+
const aliasMap = (ctx && 'aliasMap' in ctx) ? ctx.aliasMap : loadAliasMap(cwd);
|
|
394
|
+
const effectiveCtx = Object.assign({}, ctx, { aliasMap });
|
|
287
395
|
const forward = new Map();
|
|
288
396
|
const reverse = new Map();
|
|
289
397
|
|
|
@@ -304,7 +412,7 @@ function build(files, cwd, ctx) {
|
|
|
304
412
|
}
|
|
305
413
|
|
|
306
414
|
const normFilePath = normalizePath(filePath);
|
|
307
|
-
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd,
|
|
415
|
+
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, effectiveCtx);
|
|
308
416
|
if (deps.length > 0) {
|
|
309
417
|
forward.set(normFilePath, deps);
|
|
310
418
|
for (const dep of deps) {
|
|
@@ -383,4 +491,4 @@ function buildFromCwd(cwd, opts) {
|
|
|
383
491
|
return build(files, cwd, ctx);
|
|
384
492
|
}
|
|
385
493
|
|
|
386
|
-
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath };
|
|
494
|
+
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
|
package/src/graph/impact.js
CHANGED
|
@@ -186,11 +186,11 @@ function formatImpact(result) {
|
|
|
186
186
|
lines.push('');
|
|
187
187
|
|
|
188
188
|
if (result.direct.length === 0 && result.transitive.length === 0) {
|
|
189
|
-
lines.push('_No
|
|
189
|
+
lines.push('_No importers found via relative + aliased imports (lower bound — dynamic/computed imports are not tracked)._');
|
|
190
190
|
return lines.join('\n');
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
lines.push(`**Total impacted files:** ${result.totalImpact}`);
|
|
193
|
+
lines.push(`**Total impacted files:** ${result.totalImpact} _(lower bound — resolves relative + tsconfig/jsconfig-aliased imports)_`);
|
|
194
194
|
lines.push('');
|
|
195
195
|
|
|
196
196
|
if (result.direct.length > 0) {
|