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/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.9.1 | Benchmark: sigmap-v8.9-main (2026-07-06)
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.9-main, 2026-07-06)
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.9.1 | Benchmark: sigmap-v8.9-main (2026-07-06)
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.9-main, 2026-07-06)
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.9.1",
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.",
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
- "context-compression",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.9.1",
3
+ "version": "8.11.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.9.1",
3
+ "version": "8.11.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 'camelCase'; // single lowercase word
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
- if (l == null || l === 'other') continue;
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) {
@@ -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
- if (style === 'other' || style === dominant) continue;
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
- return sigs
84
- .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
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.slice(0, 8);
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
- return sigs
156
- .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
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.slice(0, 8);
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.slice(0, 8);
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 };
@@ -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
- * Resolve a JS/TS relative import string to an absolute path in fileSet.
35
- * @param {string} dir - directory of the importing file
36
- * @param {string} importStr - raw import string (e.g. './utils', '../store')
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 resolveJsPath(dir, importStr, fileSet) {
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 = resolveJsPath(dir, m[1], fileSet);
213
+ const r = resolveSpec(m[1]);
110
214
  if (r) found.push(r);
111
215
  }
112
- // CommonJS: require('./foo')
113
- const reCjs = /\brequire\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
114
- while ((m = reCjs.exec(stripped)) !== null) {
115
- const r = resolveJsPath(dir, m[1], fileSet);
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, ctx);
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 };
@@ -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 files import this filezero blast radius._');
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) {