sigmap 8.32.0 → 8.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,32 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.33.0] — 2026-09-13
14
+
15
+ ### Added
16
+ - The generated artifact now **says when the token budget left files out**. The `[sigmap] budget: dropped N file(s)` warning only ever went to stderr, which an agent reading the file never sees — on flask that meant 25 of 51 files present with no indication the other 26 existed, indistinguishable from a 25-file repo. `applyTokenBudget` attaches a non-enumerable summary to the array it returns, so all four call sites (full, per-module, hot-cold) gain the footer without a signature change. The wording states that the omitted files are still in the retrieval index and reachable via `sigmap ask`, and a test asserts it does not overstate the loss — replacing a misleading silence with a misleading warning would be no improvement (#587, PR #599)
17
+ - A test fixture for **every** language extractor, plus a guard that fails when one is missing (#588, PR #595). The 8 languages named in the issue were only part of it: the guard immediately found a ninth, `typescript_react` — `.tsx`, every React component, and the extractor that shipped a silent truncation cap in v8.32.0. It also caught a stale `test/expected/vue.txt` that outlived the `vue.js` deleted in v8.32.1, which had been making `--diagnose-extractors` print a silent `SKIP`. That diagnostic went from 21 passing with an unnoticed SKIP to 32 passing with none
18
+
19
+ ### Fixed
20
+ - NestJS route paths now compose the `@Controller` prefix. `@Controller('cats')` + `@Get(':id')` emitted `:id` rather than `/cats/:id`, so the route pseudo-signature matched nothing a user would ask about — defeating the purpose of the feature, which exists so a route-worded query can reach a controller whose signatures never mention the path. The prefix is attributed per controller rather than per file, since a file may declare several. The other six claimed frameworks were verified unchanged (#585, PR #598)
21
+
22
+ ### Changed
23
+ - Extractor resolution has **one source of truth**. Three places decided which extractor module to load and two had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and the `--diagnose-extractors` map still pointed at `vue.js` after that module was deleted — which is how an unreachable extractor survived unnoticed in the first place. The hand-maintained copy was also incomplete, with no `.gd` entry, so gdscript was never actually diagnosed despite having both a fixture and recorded expected output. `language-detector.js` and `dashboard.js` are deliberately **not** merged in: they map `.tsx` for language *statistics* and display *labels*, not resolution, and a test pins that distinction so a future dedup cannot quietly break language counts (#591, PR #597)
24
+
25
+ ---
26
+
27
+ ## [8.32.1] — 2026-09-13
28
+
29
+
30
+ ### Fixed
31
+ - **Retraction of a v8.32.0 claim.** That release stated "every extractor now discloses what a ceiling dropped". It did not. `vue.js` was registered but unreachable (`.vue` dispatches to `vue_sfc`), so disclosure was added to dead code while the live `.vue` handler kept truncating silently; `.tsx` (every React component), `.properties`, `.toml` and `.md` still cut output with a bare `slice()`; and `r.js` called `capWithNotice` but eight inner caps stopped collection at the ceiling so it never fired — and when forced to, reported `+1 more` where 50 signatures were hidden. All fixed, ceilings unchanged, with a test that fails against the pre-fix extractors. The root cause was that three of those languages have no test fixture (#582, #583, #584, #588)
32
+ - The walk depth hid most code in JVM package layouts: `maxDepth: 6` indexed **6 of 47** Java files on spring-petclinic, because Java puts one directory per package segment. #561 had already raised the *graph* walk to 12, so extraction was the shallower half of an inconsistent pair. Depth now rises to 12 only for JVM layouts; an explicit `maxDepth` always wins. Gated JVM corpus 16.4% → 23.0% (#590)
33
+
34
+ ### Changed
35
+ - Published retrieval hit@5 moves 81.1% → 78.9%. One of 18 repos accounts for it: spring-petclinic 100% → 60%, previously measured against an index holding 6 of 47 Java files. The leak-free `mined` corpus is flat and the leak-free `jvm` corpus improves, so the prior figure was inflated by under-indexing rather than this being a ranking regression. The two affected tasks are tracked as a ranking weakness the missing files were concealing (#592)
36
+
37
+ ---
38
+
13
39
  ## [8.32.0] — 2026-09-12
14
40
 
15
41
  ### Added
@@ -18,6 +44,7 @@ Format: [Semantic Versioning](https://semver.org/)
18
44
 
19
45
  ### Fixed
20
46
  - Every extractor now **discloses what a ceiling dropped** instead of truncating silently. 20 extractors gained `… +N more` markers on their member and per-file caps (23 now carry them in total). The ceilings themselves are unchanged — raising them is a separate, measured decision (#578, #576)
47
+ > **Correction (2026-09-13):** "every extractor" was wrong as published. `vue.js` was dead code — `.vue` dispatches to `vue_sfc` — so one of the 20 was unreachable; four reachable extractors (`.tsx`, `.properties`, `.toml`, `.md`) still truncated silently; and `r.js`'s disclosure was defeated by eight inner caps. Corrected in #589 (#582, #583, #584).
21
48
  - The retrieval gate reused a gitignored index, so a "regression" could be pure staleness — this produced three separate false investigations, including one re-baseline. It now regenerates every index it scores, including one per JVM repo driven from `benchmarks/config-overrides.json` rather than a hand-written config (#579)
22
49
  - The `sigmap lines` CLI example nested a fenced block inside a text fence; the inner fence closed early and a brace expression became a Vue interpolation. This failed the Pages build *after* v8.31.0 was tagged and published (#574)
23
50
 
package/README.md CHANGED
@@ -59,12 +59,12 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
59
59
 
60
60
  **Proof it pays off** (full benchmark below):
61
61
  <!--SM:whyMetrics-->
62
- - **81.1% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.73× lift)
62
+ - **78.9% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.73× lift)
63
63
  - **96.8% token reduction** — average across 21 real repos
64
- - **64.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
- - **1.54 prompts per task** — down from 2.84 (45.7% fewer retries, modeled)
64
+ - **62.9% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
+ - **1.58 prompts per task** — down from 2.84 (44.4% fewer retries, modeled)
66
66
  <!--/SM:whyMetrics-->
67
- - **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
67
+ - **<!--SM:languages-->32<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
68
68
  - **No vendor lock-in** — works with any AI assistant or local LLM
69
69
  - **No API costs** — use local models (Ollama, llama.cpp, vLLM) with zero token fees
70
70
  - **Full privacy** — keep your code and context on your machine
@@ -98,7 +98,7 @@ sigmap verify answer.md --report # standalone red/amber/green HTML report
98
98
  | Without SigMap | With SigMap |
99
99
  |---|---|
100
100
  | ❌ Non-reproducible agent guesses | ✅ Deterministic map — same input, same output, every time |
101
- | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->81%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
101
+ | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->79%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
102
102
  | ❌ Embeddings / vector DB required | ✅ Zero deps, no infra, fully offline |
103
103
 
104
104
  ---
@@ -122,13 +122,13 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.32-main (21 repositories, including R language)
126
- Date : 2026-09-12
125
+ Benchmark : sigmap-v8.33-main (21 repositories, including R language)
126
+ Date : 2026-09-13
127
127
 
128
- Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
128
+ Hit@5 : 78.9% (grep-agent baseline 44.0% — 1.73× lift)
129
129
  Token reduction: 96.8% (across 21 repos)
130
- Prompt reduction : 45.7% (2.84 → 1.54 prompts per task, modeled)
131
- Task success : 64.8% (proxy — modeled from retrieval tiers)
130
+ Prompt reduction : 44.4% (2.84 → 1.58 prompts per task, modeled)
131
+ Task success : 62.9% (proxy — modeled from retrieval tiers)
132
132
  Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
133
133
  ```
134
134
  <!--/SM:benchmarkBlock-->
@@ -332,7 +332,7 @@ sigmap --health
332
332
  | Benchmark methodology | [benchmark.html](https://sigmap.io/guide/benchmark.html) |
333
333
  | Config reference | [config.html](https://sigmap.io/guide/config.html) |
334
334
  | Roadmap | [roadmap.html](https://sigmap.io/guide/roadmap.html) |
335
- | <!--SM:languages-->33<!--/SM:languages--> languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
335
+ | <!--SM:languages-->32<!--/SM:languages--> languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
336
336
 
337
337
  ---
338
338
 
@@ -389,7 +389,7 @@ See [.github/PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md) for the
389
389
 
390
390
  ---
391
391
 
392
- ## <!--SM:languages-->33<!--/SM:languages--> languages
392
+ ## <!--SM:languages-->32<!--/SM:languages--> languages
393
393
 
394
394
  TypeScript · JavaScript · Python · Java · Kotlin · Go · Rust · C# · C/C++ · Ruby · PHP · Swift · Dart · Scala · Vue · Svelte · HTML · CSS/SCSS · YAML · Shell · SQL · GraphQL · Terraform · Protobuf · Dockerfile · TOML · XML · Properties · Markdown · R · GDScript
395
395
 
package/gen-context.js CHANGED
@@ -1790,6 +1790,55 @@ __factories["./src/config/loader"] = function(module, exports) {
1790
1790
  });
1791
1791
  }
1792
1792
 
1793
+ /**
1794
+ * Directory depth needed to reach source under a JVM package layout.
1795
+ *
1796
+ * `maxDepth: 6` is right for the JS/Python-shaped trees it was tuned on, but a
1797
+ * JVM project puts one directory per package segment — real code in
1798
+ * `src/main/java/com/company/project/module/Class.java` sits 8-10 levels down.
1799
+ * At depth 6 only the top-level package was indexed: on spring-petclinic, 6 of
1800
+ * 47 Java files (#590).
1801
+ *
1802
+ * #561 already raised the dependency-graph walk to 12 for exactly this reason,
1803
+ * so extraction was the shallower half of an inconsistent pair.
1804
+ *
1805
+ * Deepening globally is not free — it adds candidates to every repo and cost
1806
+ * 2.2pp on the (filename-leaky) 105-task matrix corpus while the unbiased
1807
+ * `mined` corpus stayed flat. So the depth is raised only where the layout
1808
+ * demands it, leaving non-JVM repos byte-identical.
1809
+ *
1810
+ * @param {string} cwd
1811
+ * @returns {boolean} true when the repo looks like a Maven/Gradle/sbt project
1812
+ */
1813
+ function _isJvmLayout(cwd) {
1814
+ const MARKERS = ['pom.xml', 'build.gradle', 'build.gradle.kts', 'build.sbt', 'settings.gradle', 'settings.gradle.kts'];
1815
+ for (const m of MARKERS) {
1816
+ try { if (fs.existsSync(path.join(cwd, m))) return true; } catch { /* unreadable cwd */ }
1817
+ }
1818
+ for (const d of ['src/main/java', 'src/main/kotlin', 'src/main/scala']) {
1819
+ try { if (fs.existsSync(path.join(cwd, d))) return true; } catch { /* ignore */ }
1820
+ }
1821
+ return false;
1822
+ }
1823
+
1824
+ /** Walk depth for a JVM package layout — matches the graph walk from #561. */
1825
+ const JVM_MAX_DEPTH = 12;
1826
+
1827
+ /**
1828
+ * Raise `maxDepth` to the JVM depth when the layout needs it (#590).
1829
+ * An explicit user value always wins, including a deliberately shallow one.
1830
+ * @param {object} cfg resolved config (mutated and returned)
1831
+ * @param {string} cwd
1832
+ * @param {boolean} userSetDepth
1833
+ * @returns {object} cfg
1834
+ */
1835
+ function _applyJvmDepth(cfg, cwd, userSetDepth) {
1836
+ if (!userSetDepth && cfg.maxDepth < JVM_MAX_DEPTH && _isJvmLayout(cwd)) {
1837
+ cfg.maxDepth = JVM_MAX_DEPTH;
1838
+ }
1839
+ return cfg;
1840
+ }
1841
+
1793
1842
  /**
1794
1843
  * Load and merge configuration for a given working directory.
1795
1844
  *
@@ -1802,7 +1851,7 @@ __factories["./src/config/loader"] = function(module, exports) {
1802
1851
  const cfg = deepClone(DEFAULTS);
1803
1852
  const detected = detectAutoSrcDirs(cwd, cfg.exclude);
1804
1853
  if (detected.length > 0) cfg.srcDirs = detected;
1805
- return cfg;
1854
+ return _applyJvmDepth(cfg, cwd, false);
1806
1855
  }
1807
1856
 
1808
1857
  let userConfig;
@@ -1814,7 +1863,7 @@ __factories["./src/config/loader"] = function(module, exports) {
1814
1863
  const cfg = deepClone(DEFAULTS);
1815
1864
  const detected = detectAutoSrcDirs(cwd, cfg.exclude);
1816
1865
  if (detected.length > 0) cfg.srcDirs = detected;
1817
- return cfg;
1866
+ return _applyJvmDepth(cfg, cwd, false);
1818
1867
  }
1819
1868
 
1820
1869
  // Warn on unknown keys (helps catch typos)
@@ -1866,7 +1915,8 @@ __factories["./src/config/loader"] = function(module, exports) {
1866
1915
  } else if (Array.isArray(merged.adapters) && !userConfig.outputs) {
1867
1916
  merged.outputs = merged.adapters.filter((a) => ['copilot','claude','cursor','windsurf'].includes(a));
1868
1917
  }
1869
- return merged;
1918
+
1919
+ return _applyJvmDepth(merged, cwd, userConfig.maxDepth !== undefined);
1870
1920
  }
1871
1921
 
1872
1922
  function deepClone(obj) {
@@ -4101,48 +4151,13 @@ __factories["./src/eval/analyzer"] = function(module, exports) {
4101
4151
  const path = require('path');
4102
4152
 
4103
4153
  // Extension → extractor name (mirrors EXT_MAP in gen-context.js)
4104
- const EXT_MAP = {
4105
- '.ts': 'typescript', '.tsx': 'typescript',
4106
- '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
4107
- '.py': 'python', '.pyw': 'python',
4108
- '.java': 'java',
4109
- '.kt': 'kotlin', '.kts': 'kotlin',
4110
- '.go': 'go',
4111
- '.rs': 'rust',
4112
- '.cs': 'csharp',
4113
- '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
4114
- '.rb': 'ruby', '.rake': 'ruby',
4115
- '.php': 'php',
4116
- '.swift': 'swift',
4117
- '.dart': 'dart',
4118
- '.scala': 'scala', '.sc': 'scala',
4119
- '.gd': 'gdscript',
4120
- '.r': 'r', '.R': 'r',
4121
- '.vue': 'vue',
4122
- '.svelte': 'svelte',
4123
- '.html': 'html', '.htm': 'html',
4124
- '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
4125
- '.yml': 'yaml', '.yaml': 'yaml',
4126
- '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
4127
- '.toml': 'toml',
4128
- '.properties': 'properties',
4129
- '.xml': 'xml',
4130
- '.md': 'markdown',
4131
- // Phase C specialized extractors
4132
- '.tsx': 'typescript_react',
4133
- '.vue': 'vue_sfc',
4134
- };
4135
-
4136
- function isDockerfile(name) {
4137
- return name === 'Dockerfile' || name.startsWith('Dockerfile.');
4138
- }
4154
+ // Extractor resolution goes through the dispatcher — the single source of
4155
+ // truth (#591). This file previously kept its own copy, which had drifted to
4156
+ // a dead duplicate `.vue` key.
4157
+ const { langFor } = __require('./src/extractors/dispatch');
4139
4158
 
4140
4159
  function getExtractorName(filePath) {
4141
- const base = path.basename(filePath);
4142
- const ext = path.extname(base).toLowerCase();
4143
- if (EXT_MAP[ext]) return EXT_MAP[ext];
4144
- if (isDockerfile(base)) return 'dockerfile';
4145
- return null;
4160
+ return langFor(filePath);
4146
4161
  }
4147
4162
 
4148
4163
  /** Rough token estimate: chars / 4 */
@@ -5922,7 +5937,6 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5922
5937
  scala: __require('./src/extractors/scala'),
5923
5938
  gdscript: __require('./src/extractors/gdscript'),
5924
5939
  r: __require('./src/extractors/r'),
5925
- vue: __require('./src/extractors/vue'),
5926
5940
  vue_sfc: __require('./src/extractors/vue_sfc'),
5927
5941
  svelte: __require('./src/extractors/svelte'),
5928
5942
  html: __require('./src/extractors/html'),
@@ -5941,6 +5955,21 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5941
5955
  generic: __require('./src/extractors/generic'),
5942
5956
  };
5943
5957
 
5958
+ /**
5959
+ * Extension → extractor module name. **The single source of truth for
5960
+ * extractor resolution** (#591).
5961
+ *
5962
+ * Anything that decides *which extractor module to load* must go through
5963
+ * `langFor` rather than declaring its own copy. Three copies existed and two
5964
+ * had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
5965
+ * the `--diagnose-extractors` map pointed at `vue.js` after that module was
5966
+ * deleted — which is how an unreachable extractor survived unnoticed (#582).
5967
+ *
5968
+ * Not every extension map in the codebase belongs here. `language-detector.js`
5969
+ * maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
5970
+ * short display *labels*. Both are correct for their purpose and deliberately
5971
+ * differ from resolution — folding them in would miscount languages.
5972
+ */
5944
5973
  const EXT_MAP = {
5945
5974
  '.ts': 'typescript', '.tsx': 'typescript_react',
5946
5975
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
@@ -6001,7 +6030,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
6001
6030
  }
6002
6031
  }
6003
6032
 
6004
- module.exports = { extractFile, langFor };
6033
+ module.exports = { extractFile, langFor, EXT_MAP };
6005
6034
 
6006
6035
  };
6007
6036
 
@@ -6986,6 +7015,11 @@ __factories["./src/extractors/line-anchor"] = function(module, exports) {
6986
7015
  // ── ./src/extractors/markdown ──
6987
7016
  __factories["./src/extractors/markdown"] = function(module, exports) {
6988
7017
 
7018
+ const { capWithNotice } = __require('./src/util/truncate');
7019
+
7020
+ // Ceiling discloses what it drops rather than truncating silently (#583).
7021
+ const PER_FILE_LIMIT = 40;
7022
+
6989
7023
  /**
6990
7024
  * Lightweight markdown technical indexer.
6991
7025
  * Captures headings and fenced code block language hints only.
@@ -7010,7 +7044,7 @@ __factories["./src/extractors/markdown"] = function(module, exports) {
7010
7044
  sigs.push(`code-fence ${lang}`);
7011
7045
  }
7012
7046
 
7013
- return Array.from(new Set(sigs)).slice(0, 40);
7047
+ return capWithNotice(Array.from(new Set(sigs)), PER_FILE_LIMIT, 'headings');
7014
7048
  }
7015
7049
 
7016
7050
  module.exports = { extract };
@@ -7341,6 +7375,11 @@ __factories["./src/extractors/prdiff"] = function(module, exports) {
7341
7375
  // ── ./src/extractors/properties ──
7342
7376
  __factories["./src/extractors/properties"] = function(module, exports) {
7343
7377
 
7378
+ const { capWithNotice } = __require('./src/util/truncate');
7379
+
7380
+ // Ceiling discloses what it drops rather than truncating silently (#583).
7381
+ const PER_FILE_LIMIT = 50;
7382
+
7344
7383
  /**
7345
7384
  * Extract signatures from .properties configuration files.
7346
7385
  * Captures key names, grouped by prefixes where possible.
@@ -7372,7 +7411,7 @@ __factories["./src/extractors/properties"] = function(module, exports) {
7372
7411
  sigs.push(`key ${key}`);
7373
7412
  }
7374
7413
 
7375
- return Array.from(new Set(sigs)).slice(0, 50);
7414
+ return capWithNotice(Array.from(new Set(sigs)), PER_FILE_LIMIT, 'keys');
7376
7415
  }
7377
7416
 
7378
7417
  module.exports = { extract };
@@ -7845,7 +7884,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7845
7884
  // ClassName <- R6::R6Class(...)
7846
7885
  const r6Re = /([\w.]+)\s*(?:<<-|<-|=)\s*(?:R6::)?R6Class\s*\(/g;
7847
7886
  let m;
7848
- while ((m = r6Re.exec(stripped)) !== null && sigs.length < 30) {
7887
+ while ((m = r6Re.exec(stripped)) !== null) {
7849
7888
  const name = m[1];
7850
7889
  if (name.startsWith('.')) continue;
7851
7890
  const openIdx = r6Re.lastIndex - 1;
@@ -7856,7 +7895,6 @@ __factories["./src/extractors/r"] = function(module, exports) {
7856
7895
  sigs.push(`${name} <- R6Class("${classNameLit}")` + applyHint(docHints, name));
7857
7896
  for (const memberSig of extractListMethods(body, 8)) {
7858
7897
  sigs.push(' ' + memberSig);
7859
- if (sigs.length >= 30) break;
7860
7898
  }
7861
7899
  consumedRanges.push([m.index, closeIdx]);
7862
7900
  r6Re.lastIndex = closeIdx;
@@ -7866,7 +7904,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7866
7904
  // ClassName <- new_class("ClassName", properties = list(...))
7867
7905
  const s7Classes = new Set();
7868
7906
  const s7Re = /([\w.]+)\s*(?:<<-|<-|=)\s*(?:S7::)?new_class\s*\(/g;
7869
- while ((m = s7Re.exec(stripped)) !== null && sigs.length < 30) {
7907
+ while ((m = s7Re.exec(stripped)) !== null) {
7870
7908
  const name = m[1];
7871
7909
  if (name.startsWith('.')) continue;
7872
7910
  const openIdx = s7Re.lastIndex - 1;
@@ -7883,7 +7921,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7883
7921
 
7884
7922
  // S7 method dispatch: `method(generic, ClassName) <- function(args)`
7885
7923
  const s7MethodRe = /^[ \t]*method\s*\(\s*([\w.]+)\s*,\s*([\w.]+)\s*\)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
7886
- while ((m = s7MethodRe.exec(stripped)) !== null && sigs.length < 30) {
7924
+ while ((m = s7MethodRe.exec(stripped)) !== null) {
7887
7925
  if (!s7Classes.has(m[2])) continue;
7888
7926
  const argsStart = s7MethodRe.lastIndex - 1;
7889
7927
  const args = readBalancedParens(stripped, argsStart);
@@ -7896,7 +7934,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7896
7934
  // Skip matches whose position falls inside an R6/S7 class body — those have
7897
7935
  // already been emitted as indented members.
7898
7936
  const funcRe = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
7899
- while ((m = funcRe.exec(stripped)) !== null && sigs.length < 30) {
7937
+ while ((m = funcRe.exec(stripped)) !== null) {
7900
7938
  const name = m[1];
7901
7939
  if (name.startsWith('.')) continue;
7902
7940
  if (inAnyRange(m.index, consumedRanges)) continue;
@@ -7908,15 +7946,12 @@ __factories["./src/extractors/r"] = function(module, exports) {
7908
7946
 
7909
7947
  // ── S4 ────────────────────────────────────────────────────────────────────
7910
7948
  for (const sm of stripped.matchAll(/^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm)) {
7911
- if (sigs.length >= 30) break;
7912
7949
  sigs.push(`setGeneric("${sm[1]}")`);
7913
7950
  }
7914
7951
  for (const sm of stripped.matchAll(/^[ \t]*setMethod\s*\(\s*["']([\w.]+)["']\s*,\s*["']([\w.]+)["']/gm)) {
7915
- if (sigs.length >= 30) break;
7916
7952
  sigs.push(`setMethod("${sm[1]}", "${sm[2]}")`);
7917
7953
  }
7918
7954
  for (const sm of stripped.matchAll(/^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm)) {
7919
- if (sigs.length >= 30) break;
7920
7955
  sigs.push(`setClass("${sm[1]}")`);
7921
7956
  }
7922
7957
 
@@ -8925,6 +8960,11 @@ __factories["./src/extractors/todos"] = function(module, exports) {
8925
8960
  // ── ./src/extractors/toml ──
8926
8961
  __factories["./src/extractors/toml"] = function(module, exports) {
8927
8962
 
8963
+ const { capWithNotice } = __require('./src/util/truncate');
8964
+
8965
+ // Ceiling discloses what it drops rather than truncating silently (#583).
8966
+ const PER_FILE_LIMIT = 40;
8967
+
8928
8968
  /**
8929
8969
  * Extract signatures from TOML configuration files.
8930
8970
  * Focuses on section/table names and high-value keys.
@@ -8961,7 +9001,7 @@ __factories["./src/extractors/toml"] = function(module, exports) {
8961
9001
  }
8962
9002
  }
8963
9003
 
8964
- return Array.from(new Set(sigs)).slice(0, 40);
9004
+ return capWithNotice(Array.from(new Set(sigs)), PER_FILE_LIMIT, 'entries');
8965
9005
  }
8966
9006
 
8967
9007
  module.exports = { extract };
@@ -9301,6 +9341,11 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
9301
9341
  // ── ./src/extractors/typescript_react ──
9302
9342
  __factories["./src/extractors/typescript_react"] = function(module, exports) {
9303
9343
 
9344
+ const { capWithNotice } = __require('./src/util/truncate');
9345
+
9346
+ // Ceiling discloses what it drops rather than truncating silently (#583).
9347
+ const PER_FILE_LIMIT = 50;
9348
+
9304
9349
  /**
9305
9350
  * Extract React component signatures from .tsx files.
9306
9351
  * Captures component props interfaces, hooks usage, and exports.
@@ -9355,97 +9400,7 @@ __factories["./src/extractors/typescript_react"] = function(module, exports) {
9355
9400
  sigs.push(`handler on${h}`);
9356
9401
  }
9357
9402
 
9358
- return Array.from(new Set(sigs)).slice(0, 50);
9359
- }
9360
-
9361
- module.exports = { extract };
9362
-
9363
- };
9364
-
9365
- // ── ./src/extractors/vue ──
9366
- __factories["./src/extractors/vue"] = function(module, exports) {
9367
-
9368
- const { capWithNotice } = __require('./src/util/truncate');
9369
-
9370
- // Ceiling sits above the default `maxSigsPerFile` so the configured budget
9371
- // governs output rather than a literal buried here, and omissions are disclosed (#576).
9372
- const PER_FILE_LIMIT = 25;
9373
-
9374
- /**
9375
- * Extract signatures from Vue single-file components.
9376
- * @param {string} src - Raw file content
9377
- * @returns {string[]} Array of signature strings
9378
- */
9379
- function extract(src) {
9380
- if (!src || typeof src !== 'string') return [];
9381
- const sigs = [];
9382
-
9383
- // Extract component name from filename hint if present or defineComponent
9384
- const nameMatch = src.match(/name\s*:\s*['"](\w+)['"]/);
9385
- if (nameMatch) sigs.push(`component ${nameMatch[1]}`);
9386
-
9387
- // Extract <script> block
9388
- const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
9389
- if (!scriptMatch) return sigs;
9390
-
9391
- const script = scriptMatch[1]
9392
- .replace(/\/\/.*$/gm, '')
9393
- .replace(/\/\*[\s\S]*?\*\//g, '');
9394
-
9395
- // Props
9396
- const propsMatch = script.match(/props\s*:\s*(\{[\s\S]*?\})/);
9397
- if (propsMatch) {
9398
- const propNames = [];
9399
- for (const m of propsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
9400
- propNames.push(m[1]);
9401
- }
9402
- if (propNames.length > 0) sigs.push(`props: [${propNames.join(', ')}]`);
9403
- }
9404
-
9405
- // Methods in options API
9406
- const methodsMatch = script.match(/methods\s*:\s*\{([\s\S]*?)\},?\s*(?:computed|watch|mounted|created|data|\})/);
9407
- if (methodsMatch) {
9408
- for (const m of methodsMatch[1].matchAll(/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
9409
- if (m[1].startsWith('_')) continue;
9410
- const asyncKw = m[0].includes('async') ? 'async ' : '';
9411
- const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
9412
- sigs.push(` ${asyncKw}${m[1]}(${normalizeParams(m[2])})${retStr}`);
9413
- }
9414
- }
9415
-
9416
- // Top-level functions in <script> (e.g., composition API helpers)
9417
- for (const m of script.matchAll(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
9418
- if (m[1].startsWith('_')) continue;
9419
- const asyncKw = m[0].includes('async') ? 'async ' : '';
9420
- const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
9421
- sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
9422
- }
9423
-
9424
- // defineProps (Composition API)
9425
- const definePropsMatch = script.match(/defineProps(?:<[^>]*>)?\s*\(\s*(\{[\s\S]*?\})\s*\)/);
9426
- if (definePropsMatch) {
9427
- const propNames = [];
9428
- for (const m of definePropsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
9429
- propNames.push(m[1]);
9430
- }
9431
- if (propNames.length > 0) sigs.push(`defineProps: [${propNames.join(', ')}]`);
9432
- }
9433
-
9434
- // Emits
9435
- const emitsMatch = script.match(/(?:defineEmits|emits)\s*(?::\s*|\(\s*)(\[[\s\S]*?\])/);
9436
- if (emitsMatch) sigs.push(`emits: ${emitsMatch[1].replace(/\s+/g, ' ')}`);
9437
-
9438
- return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
9439
- }
9440
-
9441
- function normalizeParams(params) {
9442
- if (!params) return '';
9443
- return params.trim().replace(/\s+/g, ' ');
9444
- }
9445
-
9446
- function normalizeType(type) {
9447
- if (!type) return '';
9448
- return type.trim().replace(/[;\s]+$/g, '').replace(/\s+/g, ' ').slice(0, 25);
9403
+ return capWithNotice(Array.from(new Set(sigs)), PER_FILE_LIMIT, 'signatures');
9449
9404
  }
9450
9405
 
9451
9406
  module.exports = { extract };
@@ -14479,6 +14434,45 @@ __factories["./src/map/route-table"] = function(module, exports) {
14479
14434
  return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
14480
14435
  }
14481
14436
 
14437
+ /**
14438
+ * Byte offsets and prefixes of every `@Controller(...)` in a file.
14439
+ * A file may declare several controllers, so each route is attributed to the
14440
+ * nearest one above it rather than to a single file-wide prefix.
14441
+ * @param {string} content
14442
+ * @returns {Array<{index:number, prefix:string}>} ascending by index
14443
+ */
14444
+ function nestControllerPrefixes(content) {
14445
+ const out = [];
14446
+ const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
14447
+ let m;
14448
+ while ((m = re.exec(content)) !== null) {
14449
+ out.push({ index: m.index, prefix: m[1] || '' });
14450
+ }
14451
+ return out;
14452
+ }
14453
+
14454
+ /** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
14455
+ function prefixBefore(controllers, index) {
14456
+ let prefix = '';
14457
+ for (const c of controllers) {
14458
+ if (c.index > index) break;
14459
+ prefix = c.prefix;
14460
+ }
14461
+ return prefix;
14462
+ }
14463
+
14464
+ /**
14465
+ * Join a controller prefix and a method path into one route path.
14466
+ * Either side may be empty, absent, or carry its own slashes.
14467
+ * @returns {string} always slash-prefixed; never a trailing slash except '/'
14468
+ */
14469
+ function joinRoute(prefix, methodPath) {
14470
+ const parts = [prefix, methodPath]
14471
+ .map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
14472
+ .filter(Boolean);
14473
+ return parts.length ? '/' + parts.join('/') : '/';
14474
+ }
14475
+
14482
14476
  /**
14483
14477
  * Structured route rows across the supported frameworks — the data behind
14484
14478
  * `analyze`, exposed for retrieval surface-enrichment (#488).
@@ -14507,16 +14501,14 @@ __factories["./src/map/route-table"] = function(module, exports) {
14507
14501
  routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
14508
14502
  }
14509
14503
 
14510
- // NestJS decorators: @Get('/path') @Post('/path')
14511
- const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
14504
+ // NestJS: @Get(':id') / @Post() — composed with the enclosing
14505
+ // @Controller('prefix'). Without the prefix the emitted path matches
14506
+ // nothing real, which defeats the point of route pseudo-signatures (#585).
14507
+ const controllers = nestControllerPrefixes(content);
14508
+ const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
14512
14509
  while ((m = re2.exec(content)) !== null) {
14513
- routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
14514
- }
14515
-
14516
- // NestJS: @Get() with no path
14517
- const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
14518
- while ((m = re3.exec(content)) !== null) {
14519
- routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
14510
+ const prefix = prefixBefore(controllers, m.index);
14511
+ routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
14520
14512
  }
14521
14513
  }
14522
14514
 
@@ -15814,7 +15806,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15814
15806
 
15815
15807
  const SERVER_INFO = {
15816
15808
  name: 'sigmap',
15817
- version: '8.32.0',
15809
+ version: '8.33.0',
15818
15810
  description: 'SigMap MCP server — code signatures on demand',
15819
15811
  };
15820
15812
 
@@ -22108,7 +22100,7 @@ function __tryGit(args, opts = {}) {
22108
22100
  catch (_) { return ''; }
22109
22101
  }
22110
22102
 
22111
- const VERSION = '8.32.0';
22103
+ const VERSION = '8.33.0';
22112
22104
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
22113
22105
 
22114
22106
  function requireSourceOrBundled(key) {
@@ -22558,6 +22550,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
22558
22550
  // Restore the original file order for stable output.
22559
22551
  const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
22560
22552
 
22553
+ // Record what was omitted so the artifact itself can say so (#587). The
22554
+ // stderr warning below is invisible to an agent that only reads the file:
22555
+ // 25 sections with no notice is indistinguishable from a 25-file repo.
22556
+ // Non-enumerable so every existing consumer still sees a plain array.
22557
+ if (verboseDropped.length > 0 || collapsedCount > 0) {
22558
+ Object.defineProperty(kept, '__omissions', {
22559
+ value: { dropped: verboseDropped.length, collapsed: collapsedCount, maxTokens },
22560
+ enumerable: false, writable: false, configurable: true,
22561
+ });
22562
+ }
22563
+
22561
22564
  if (verboseDropped.length > 0 || collapsedCount > 0) {
22562
22565
  const parts = [];
22563
22566
  if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
@@ -22886,6 +22889,20 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
22886
22889
  }
22887
22890
  }
22888
22891
 
22892
+ // Say what the budget left out — an omission the reader cannot see is the
22893
+ // same failure as an undisclosed truncation cap (#587, cf. #576).
22894
+ const omitted = fileEntries && fileEntries.__omissions;
22895
+ if (omitted && (omitted.dropped > 0 || omitted.collapsed > 0)) {
22896
+ const bits = [];
22897
+ if (omitted.dropped) bits.push(`${omitted.dropped} file(s) omitted`);
22898
+ if (omitted.collapsed) bits.push(`${omitted.collapsed} collapsed to anchors`);
22899
+ lines.push('');
22900
+ lines.push(`> **Not everything is here.** ${bits.join(', ')} to stay under the `
22901
+ + `${omitted.maxTokens}-token budget (tests and configs go first). `
22902
+ + 'The retrieval index still has them all — run `sigmap ask "<question>"` '
22903
+ + 'to pull in anything missing.');
22904
+ }
22905
+
22889
22906
  return lines.join('\n');
22890
22907
  }
22891
22908
 
@@ -26875,23 +26892,16 @@ function main() {
26875
26892
  process.exit(1);
26876
26893
  }
26877
26894
 
26878
- const EXT_TO_LANG = {
26879
- '.ts': 'typescript', '.js': 'javascript', '.py': 'python',
26880
- '.java': 'java', '.kt': 'kotlin', '.go': 'go', '.rs': 'rust',
26881
- '.cs': 'csharp', '.cpp': 'cpp', '.rb': 'ruby', '.php': 'php',
26882
- '.swift': 'swift', '.dart': 'dart', '.scala': 'scala',
26883
- '.r': 'r', '.R': 'r',
26884
- '.vue': 'vue', '.svelte': 'svelte', '.html': 'html',
26885
- '.css': 'css', '.yml': 'yaml', '.sh': 'shell',
26886
- };
26887
- const SPECIAL = { 'Dockerfile': 'dockerfile' };
26895
+ // Resolution goes through the dispatcher — the single source of truth
26896
+ // (#591). This map was a third copy, and it still pointed at `vue.js`
26897
+ // after that module was deleted, which is how a dead extractor survived.
26898
+ const { langFor } = requireSourceOrBundled('./src/extractors/dispatch');
26888
26899
 
26889
26900
  let passed = 0; let failed = 0;
26890
26901
  const entries = fs.readdirSync(fixturesDir).sort();
26891
26902
 
26892
26903
  for (const filename of entries) {
26893
- const ext = path.extname(filename).toLowerCase();
26894
- const lang = EXT_TO_LANG[ext] || SPECIAL[filename];
26904
+ const lang = langFor(filename);
26895
26905
  if (!lang) continue;
26896
26906
 
26897
26907
  const fixturePath = path.join(fixturesDir, filename);