sigmap 8.32.1 → 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/AGENTS.md +317 -293
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/gen-context.js +99 -63
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/eval/analyzer.js +5 -39
- package/src/extractors/dispatch.js +16 -1
- package/src/map/route-table.js +46 -9
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ 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
|
+
|
|
13
27
|
## [8.32.1] — 2026-09-13
|
|
14
28
|
|
|
15
29
|
|
package/README.md
CHANGED
|
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-09-
|
|
125
|
+
Benchmark : sigmap-v8.33-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-09-13
|
|
127
127
|
|
|
128
128
|
Hit@5 : 78.9% (grep-agent baseline 44.0% — 1.73× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -4151,47 +4151,13 @@ __factories["./src/eval/analyzer"] = function(module, exports) {
|
|
|
4151
4151
|
const path = require('path');
|
|
4152
4152
|
|
|
4153
4153
|
// Extension → extractor name (mirrors EXT_MAP in gen-context.js)
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
'.java': 'java',
|
|
4159
|
-
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
4160
|
-
'.go': 'go',
|
|
4161
|
-
'.rs': 'rust',
|
|
4162
|
-
'.cs': 'csharp',
|
|
4163
|
-
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
4164
|
-
'.rb': 'ruby', '.rake': 'ruby',
|
|
4165
|
-
'.php': 'php',
|
|
4166
|
-
'.swift': 'swift',
|
|
4167
|
-
'.dart': 'dart',
|
|
4168
|
-
'.scala': 'scala', '.sc': 'scala',
|
|
4169
|
-
'.gd': 'gdscript',
|
|
4170
|
-
'.r': 'r', '.R': 'r',
|
|
4171
|
-
'.svelte': 'svelte',
|
|
4172
|
-
'.html': 'html', '.htm': 'html',
|
|
4173
|
-
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
4174
|
-
'.yml': 'yaml', '.yaml': 'yaml',
|
|
4175
|
-
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
4176
|
-
'.toml': 'toml',
|
|
4177
|
-
'.properties': 'properties',
|
|
4178
|
-
'.xml': 'xml',
|
|
4179
|
-
'.md': 'markdown',
|
|
4180
|
-
// Phase C specialized extractors
|
|
4181
|
-
'.tsx': 'typescript_react',
|
|
4182
|
-
'.vue': 'vue_sfc',
|
|
4183
|
-
};
|
|
4184
|
-
|
|
4185
|
-
function isDockerfile(name) {
|
|
4186
|
-
return name === 'Dockerfile' || name.startsWith('Dockerfile.');
|
|
4187
|
-
}
|
|
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');
|
|
4188
4158
|
|
|
4189
4159
|
function getExtractorName(filePath) {
|
|
4190
|
-
|
|
4191
|
-
const ext = path.extname(base).toLowerCase();
|
|
4192
|
-
if (EXT_MAP[ext]) return EXT_MAP[ext];
|
|
4193
|
-
if (isDockerfile(base)) return 'dockerfile';
|
|
4194
|
-
return null;
|
|
4160
|
+
return langFor(filePath);
|
|
4195
4161
|
}
|
|
4196
4162
|
|
|
4197
4163
|
/** Rough token estimate: chars / 4 */
|
|
@@ -5989,6 +5955,21 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
|
5989
5955
|
generic: __require('./src/extractors/generic'),
|
|
5990
5956
|
};
|
|
5991
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
|
+
*/
|
|
5992
5973
|
const EXT_MAP = {
|
|
5993
5974
|
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
5994
5975
|
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
@@ -6049,7 +6030,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
|
6049
6030
|
}
|
|
6050
6031
|
}
|
|
6051
6032
|
|
|
6052
|
-
module.exports = { extractFile, langFor };
|
|
6033
|
+
module.exports = { extractFile, langFor, EXT_MAP };
|
|
6053
6034
|
|
|
6054
6035
|
};
|
|
6055
6036
|
|
|
@@ -14453,6 +14434,45 @@ __factories["./src/map/route-table"] = function(module, exports) {
|
|
|
14453
14434
|
return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
|
|
14454
14435
|
}
|
|
14455
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
|
+
|
|
14456
14476
|
/**
|
|
14457
14477
|
* Structured route rows across the supported frameworks — the data behind
|
|
14458
14478
|
* `analyze`, exposed for retrieval surface-enrichment (#488).
|
|
@@ -14481,16 +14501,14 @@ __factories["./src/map/route-table"] = function(module, exports) {
|
|
|
14481
14501
|
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
14482
14502
|
}
|
|
14483
14503
|
|
|
14484
|
-
// NestJS
|
|
14485
|
-
|
|
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;
|
|
14486
14509
|
while ((m = re2.exec(content)) !== null) {
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
// NestJS: @Get() with no path
|
|
14491
|
-
const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
|
|
14492
|
-
while ((m = re3.exec(content)) !== null) {
|
|
14493
|
-
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 });
|
|
14494
14512
|
}
|
|
14495
14513
|
}
|
|
14496
14514
|
|
|
@@ -15788,7 +15806,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15788
15806
|
|
|
15789
15807
|
const SERVER_INFO = {
|
|
15790
15808
|
name: 'sigmap',
|
|
15791
|
-
version: '8.
|
|
15809
|
+
version: '8.33.0',
|
|
15792
15810
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15793
15811
|
};
|
|
15794
15812
|
|
|
@@ -22082,7 +22100,7 @@ function __tryGit(args, opts = {}) {
|
|
|
22082
22100
|
catch (_) { return ''; }
|
|
22083
22101
|
}
|
|
22084
22102
|
|
|
22085
|
-
const VERSION = '8.
|
|
22103
|
+
const VERSION = '8.33.0';
|
|
22086
22104
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
22087
22105
|
|
|
22088
22106
|
function requireSourceOrBundled(key) {
|
|
@@ -22532,6 +22550,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
22532
22550
|
// Restore the original file order for stable output.
|
|
22533
22551
|
const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
|
|
22534
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
|
+
|
|
22535
22564
|
if (verboseDropped.length > 0 || collapsedCount > 0) {
|
|
22536
22565
|
const parts = [];
|
|
22537
22566
|
if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
|
|
@@ -22860,6 +22889,20 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
|
|
|
22860
22889
|
}
|
|
22861
22890
|
}
|
|
22862
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
|
+
|
|
22863
22906
|
return lines.join('\n');
|
|
22864
22907
|
}
|
|
22865
22908
|
|
|
@@ -26849,23 +26892,16 @@ function main() {
|
|
|
26849
26892
|
process.exit(1);
|
|
26850
26893
|
}
|
|
26851
26894
|
|
|
26852
|
-
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
26856
|
-
'.swift': 'swift', '.dart': 'dart', '.scala': 'scala',
|
|
26857
|
-
'.r': 'r', '.R': 'r',
|
|
26858
|
-
'.vue': 'vue_sfc', '.svelte': 'svelte', '.html': 'html',
|
|
26859
|
-
'.css': 'css', '.yml': 'yaml', '.sh': 'shell',
|
|
26860
|
-
};
|
|
26861
|
-
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');
|
|
26862
26899
|
|
|
26863
26900
|
let passed = 0; let failed = 0;
|
|
26864
26901
|
const entries = fs.readdirSync(fixturesDir).sort();
|
|
26865
26902
|
|
|
26866
26903
|
for (const filename of entries) {
|
|
26867
|
-
const
|
|
26868
|
-
const lang = EXT_TO_LANG[ext] || SPECIAL[filename];
|
|
26904
|
+
const lang = langFor(filename);
|
|
26869
26905
|
if (!lang) continue;
|
|
26870
26906
|
|
|
26871
26907
|
const fixturePath = path.join(fixturesDir, filename);
|
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.33.0 | Benchmark: sigmap-v8.33-main (2026-09-13)
|
|
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.33-main, 2026-09-13)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.33.0 | Benchmark: sigmap-v8.33-main (2026-09-13)
|
|
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.33-main, 2026-09-13)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 78.9% vs 44.0% single-shot grep baseline (1.73× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.33.0",
|
|
4
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": {
|
package/src/eval/analyzer.js
CHANGED
|
@@ -14,47 +14,13 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
16
|
// Extension → extractor name (mirrors EXT_MAP in gen-context.js)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'.java': 'java',
|
|
22
|
-
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
23
|
-
'.go': 'go',
|
|
24
|
-
'.rs': 'rust',
|
|
25
|
-
'.cs': 'csharp',
|
|
26
|
-
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
27
|
-
'.rb': 'ruby', '.rake': 'ruby',
|
|
28
|
-
'.php': 'php',
|
|
29
|
-
'.swift': 'swift',
|
|
30
|
-
'.dart': 'dart',
|
|
31
|
-
'.scala': 'scala', '.sc': 'scala',
|
|
32
|
-
'.gd': 'gdscript',
|
|
33
|
-
'.r': 'r', '.R': 'r',
|
|
34
|
-
'.svelte': 'svelte',
|
|
35
|
-
'.html': 'html', '.htm': 'html',
|
|
36
|
-
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
37
|
-
'.yml': 'yaml', '.yaml': 'yaml',
|
|
38
|
-
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
39
|
-
'.toml': 'toml',
|
|
40
|
-
'.properties': 'properties',
|
|
41
|
-
'.xml': 'xml',
|
|
42
|
-
'.md': 'markdown',
|
|
43
|
-
// Phase C specialized extractors
|
|
44
|
-
'.tsx': 'typescript_react',
|
|
45
|
-
'.vue': 'vue_sfc',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function isDockerfile(name) {
|
|
49
|
-
return name === 'Dockerfile' || name.startsWith('Dockerfile.');
|
|
50
|
-
}
|
|
17
|
+
// Extractor resolution goes through the dispatcher — the single source of
|
|
18
|
+
// truth (#591). This file previously kept its own copy, which had drifted to
|
|
19
|
+
// a dead duplicate `.vue` key.
|
|
20
|
+
const { langFor } = require('../extractors/dispatch');
|
|
51
21
|
|
|
52
22
|
function getExtractorName(filePath) {
|
|
53
|
-
|
|
54
|
-
const ext = path.extname(base).toLowerCase();
|
|
55
|
-
if (EXT_MAP[ext]) return EXT_MAP[ext];
|
|
56
|
-
if (isDockerfile(base)) return 'dockerfile';
|
|
57
|
-
return null;
|
|
23
|
+
return langFor(filePath);
|
|
58
24
|
}
|
|
59
25
|
|
|
60
26
|
/** Rough token estimate: chars / 4 */
|
|
@@ -48,6 +48,21 @@ const EXTRACTORS = {
|
|
|
48
48
|
generic: require('./generic'),
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Extension → extractor module name. **The single source of truth for
|
|
53
|
+
* extractor resolution** (#591).
|
|
54
|
+
*
|
|
55
|
+
* Anything that decides *which extractor module to load* must go through
|
|
56
|
+
* `langFor` rather than declaring its own copy. Three copies existed and two
|
|
57
|
+
* had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
|
|
58
|
+
* the `--diagnose-extractors` map pointed at `vue.js` after that module was
|
|
59
|
+
* deleted — which is how an unreachable extractor survived unnoticed (#582).
|
|
60
|
+
*
|
|
61
|
+
* Not every extension map in the codebase belongs here. `language-detector.js`
|
|
62
|
+
* maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
|
|
63
|
+
* short display *labels*. Both are correct for their purpose and deliberately
|
|
64
|
+
* differ from resolution — folding them in would miscount languages.
|
|
65
|
+
*/
|
|
51
66
|
const EXT_MAP = {
|
|
52
67
|
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
53
68
|
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
@@ -108,4 +123,4 @@ function extractFile(filePathOrName, src) {
|
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
|
|
111
|
-
module.exports = { extractFile, langFor };
|
|
126
|
+
module.exports = { extractFile, langFor, EXT_MAP };
|
package/src/map/route-table.js
CHANGED
|
@@ -20,6 +20,45 @@ function shouldSkipFile(rel) {
|
|
|
20
20
|
return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Byte offsets and prefixes of every `@Controller(...)` in a file.
|
|
25
|
+
* A file may declare several controllers, so each route is attributed to the
|
|
26
|
+
* nearest one above it rather than to a single file-wide prefix.
|
|
27
|
+
* @param {string} content
|
|
28
|
+
* @returns {Array<{index:number, prefix:string}>} ascending by index
|
|
29
|
+
*/
|
|
30
|
+
function nestControllerPrefixes(content) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
33
|
+
let m;
|
|
34
|
+
while ((m = re.exec(content)) !== null) {
|
|
35
|
+
out.push({ index: m.index, prefix: m[1] || '' });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
|
|
41
|
+
function prefixBefore(controllers, index) {
|
|
42
|
+
let prefix = '';
|
|
43
|
+
for (const c of controllers) {
|
|
44
|
+
if (c.index > index) break;
|
|
45
|
+
prefix = c.prefix;
|
|
46
|
+
}
|
|
47
|
+
return prefix;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Join a controller prefix and a method path into one route path.
|
|
52
|
+
* Either side may be empty, absent, or carry its own slashes.
|
|
53
|
+
* @returns {string} always slash-prefixed; never a trailing slash except '/'
|
|
54
|
+
*/
|
|
55
|
+
function joinRoute(prefix, methodPath) {
|
|
56
|
+
const parts = [prefix, methodPath]
|
|
57
|
+
.map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
return parts.length ? '/' + parts.join('/') : '/';
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
/**
|
|
24
63
|
* Structured route rows across the supported frameworks — the data behind
|
|
25
64
|
* `analyze`, exposed for retrieval surface-enrichment (#488).
|
|
@@ -48,16 +87,14 @@ function collectRoutes(files, cwd) {
|
|
|
48
87
|
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
49
88
|
}
|
|
50
89
|
|
|
51
|
-
// NestJS
|
|
52
|
-
|
|
90
|
+
// NestJS: @Get(':id') / @Post() — composed with the enclosing
|
|
91
|
+
// @Controller('prefix'). Without the prefix the emitted path matches
|
|
92
|
+
// nothing real, which defeats the point of route pseudo-signatures (#585).
|
|
93
|
+
const controllers = nestControllerPrefixes(content);
|
|
94
|
+
const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
|
|
53
95
|
while ((m = re2.exec(content)) !== null) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// NestJS: @Get() with no path
|
|
58
|
-
const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
|
|
59
|
-
while ((m = re3.exec(content)) !== null) {
|
|
60
|
-
routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
|
|
96
|
+
const prefix = prefixBefore(controllers, m.index);
|
|
97
|
+
routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
|
|
61
98
|
}
|
|
62
99
|
}
|
|
63
100
|
|
package/src/mcp/server.js
CHANGED