sigmap 7.31.0 → 8.0.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 +12 -0
- package/README.md +2 -2
- package/gen-context.js +431 -11
- package/gen-project-map.js +14 -6
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/evidence/pack.js +42 -8
- package/src/map/build-ci.js +91 -0
- package/src/map/config-manifest.js +101 -0
- package/src/map/env-schema.js +90 -0
- package/src/map/migrations.js +84 -0
- package/src/mcp/handlers.js +5 -1
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.0.0] — 2026-07-04
|
|
14
|
+
|
|
15
|
+
Major release — **v8.5 "Repo-Context Coverage & Test Discovery" (C1 + C2 + C3).** Marks the v8 milestone: the signature map now reaches beyond functions/classes/routes into the repo's operational surface, impl→test discovery is measured rather than best-effort, and every Evidence Pack file carries a risk label from a richer, precedence-ordered set. All zero-dependency, deterministic, and in-boundary with the North-Star constraints. **No breaking API changes** — the `8.0.0` bump aligns the published version with the roadmap's v8 framing; existing `riskLabel`/`relatedTests` consumers keep working.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **C1 — Repo-context coverage expansion (#402):** four dedicated zero-dep map analyzers under `src/map/` (mirroring `route-table.js`), wired into `gen-project-map.js` and the MCP `get_map` `MAP_SECTIONS` — `env-schema.js` (**Environment variables** — env reads across JS/TS/Python/Ruby/Go + `.env.example` keys), `build-ci.js` (**Build & CI** — npm/pnpm scripts, GitHub Actions workflows, Makefile targets), `config-manifest.js` (**Config & manifests** — package manifests across npm/Python/Rust/Go/Maven/Gradle/Ruby/PHP + notable config files), and `migrations.js` (**Database migrations** — Rails/Alembic/Prisma/Flyway/timestamped-SQL detection). `PROJECT_MAP.md` and `get_map` now surface all four sections.
|
|
19
|
+
- **C2 — Measured test discovery (#402):** `findRelatedTests` now normalizes cross-language test conventions (`test_x.py`↔`x.py`, `x_test.go`↔`x.go`, `XTest.java`↔`X.java`, `x.spec.ts`↔`x.ts`). New reproducible benchmark `scripts/run-test-discovery-benchmark.mjs` (`npm run benchmark:test-discovery`) scores it against an independent canonical-name gold oracle over `benchmarks/repos` — no LLM, pure string math — measuring **F1 98.0%, hit@1 97.4% across 28 repos / 3,701 gold pairs**. The headline number is surfaced in `benchmarks/latest.json` under `test_discovery`.
|
|
20
|
+
- **C3 — Richer risk labels (#402):** `riskLabelFor` now returns the v8.5 set — `migration | payment | auth | security | public-api | config | test | generated | source` — with strict most-specific-risk precedence (a migration touching auth is still `migration`; payment/auth outrank the generic `security` bucket). `test`/`generated`/`config`/`source` semantics are preserved so `findRelatedTests` and the verifier keep working. Extended coverage in `test/integration/evidence-pack.test.js`, `project-map.test.js`, and `benchmark-latest.test.js`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Comparison-chart correctness multiplier (#399):** corrected the stale answer-correctness multiplier badge in `docs/comparison-chart.svg` (×5.2 → ×6.8) to match the current task-success benchmark.
|
|
24
|
+
|
|
13
25
|
## [7.31.0] — 2026-07-02
|
|
14
26
|
|
|
15
27
|
Minor release — **identifier-aware BM25 re-ranker.** Plain exact-token TF-IDF missed queries whose terms live *inside* code identifiers — `component emit` never surfaced `componentEmits` because that is one token sharing no exact term with the query. This was the dominant retrieval-miss cause. The new ranker splits identifiers, stems lightly, boosts path tokens, and scores with length-normalized BM25. Deterministic, zero new dependencies, no LLM/embeddings.
|
package/README.md
CHANGED
|
@@ -98,8 +98,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
98
98
|
|
|
99
99
|
<!--SM:benchmarkBlock-->
|
|
100
100
|
```
|
|
101
|
-
Benchmark : sigmap-
|
|
102
|
-
Date : 2026-07-
|
|
101
|
+
Benchmark : sigmap-v8.0-main (21 repositories, including R language)
|
|
102
|
+
Date : 2026-07-04
|
|
103
103
|
|
|
104
104
|
Hit@5 : 86.7% (baseline 13.6% — 6.4× lift)
|
|
105
105
|
Token reduction: 97.0% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -4609,7 +4609,14 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4609
4609
|
const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
|
|
4610
4610
|
const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
|
|
4611
4611
|
const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
|
|
4612
|
-
|
|
4612
|
+
// DB migrations: framework dirs (Rails/Alembic/Prisma), Flyway `V1__x.sql`,
|
|
4613
|
+
// timestamped migration files, and `*_migration.*` naming.
|
|
4614
|
+
const MIGRATION_RE = /(^|\/)(migrations?|alembic\/versions|prisma\/migrations)(\/|$)|(^|\/)db\/migrate\/|(^|\/)V\d+(_\d+)*__[^/]+\.(sql|java)$|(^|\/)\d{8,}[_-][^/]+\.(sql|rb|py|js|ts)$|[._-]migration[s]?[._-]/i;
|
|
4615
|
+
const PAYMENT_RE = /(^|\/|[._-])(payment|payments|billing|checkout|invoice|invoicing|subscription|stripe|paypal|braintree|charge|refund|payout)([._-]|\/|$)/i;
|
|
4616
|
+
const AUTH_RE = /(^|\/|[._-])(auth|authn|authz|login|logout|signin|signup|password|passwd|session|oauth|jwt|permission|permissions|acl|rbac|credential|credentials)([._-]|\/|$)/i;
|
|
4617
|
+
const SECURITY_RE = /(^|\/|[._-])(secret|secrets|crypto|cipher|encrypt|decrypt|token|signing|keystore|vault)([._-]|\/|$)/i;
|
|
4618
|
+
// Public API surface: `api/` dirs, `public-api`, and module barrel entrypoints.
|
|
4619
|
+
const PUBLIC_API_RE = /(^|\/)api(\/|$)|(^|\/)public[-_]?api(\/|$)|(^|\/)index\.(js|ts|mjs|cjs)$/i;
|
|
4613
4620
|
|
|
4614
4621
|
/**
|
|
4615
4622
|
* Split a signature's ` :start-end` line anchor from its symbol text.
|
|
@@ -4627,17 +4634,25 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4627
4634
|
}
|
|
4628
4635
|
|
|
4629
4636
|
/**
|
|
4630
|
-
* Classify a file into a
|
|
4631
|
-
*
|
|
4637
|
+
* Classify a file into a risk label (C3, v8.5). Path-based, deterministic.
|
|
4638
|
+
* Precedence is strict, most-specific-risk first: a migration touching payments
|
|
4639
|
+
* is labeled `migration` (a schema change is the dominant risk), payment/auth
|
|
4640
|
+
* outrank the generic `security` bucket, and `config`/`public-api` resolve
|
|
4641
|
+
* before the `source` fallback. `test`/`generated` semantics are preserved so
|
|
4642
|
+
* existing consumers (findRelatedTests, verifier) keep working.
|
|
4632
4643
|
* @param {string} relPath
|
|
4633
|
-
* @returns {'generated'|'test'|'
|
|
4644
|
+
* @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
|
|
4634
4645
|
*/
|
|
4635
4646
|
function riskLabelFor(relPath) {
|
|
4636
4647
|
const p = relPath.replace(/\\/g, '/');
|
|
4637
4648
|
if (GENERATED_RE.test(p)) return 'generated';
|
|
4638
4649
|
if (TEST_RE.test(p)) return 'test';
|
|
4650
|
+
if (MIGRATION_RE.test(p)) return 'migration';
|
|
4651
|
+
if (PAYMENT_RE.test(p)) return 'payment';
|
|
4652
|
+
if (AUTH_RE.test(p)) return 'auth';
|
|
4639
4653
|
if (SECURITY_RE.test(p)) return 'security';
|
|
4640
4654
|
if (CONFIG_RE.test(p)) return 'config';
|
|
4655
|
+
if (PUBLIC_API_RE.test(p)) return 'public-api';
|
|
4641
4656
|
return 'source';
|
|
4642
4657
|
}
|
|
4643
4658
|
|
|
@@ -4648,9 +4663,28 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4648
4663
|
}
|
|
4649
4664
|
|
|
4650
4665
|
/**
|
|
4651
|
-
*
|
|
4652
|
-
*
|
|
4653
|
-
*
|
|
4666
|
+
* Infer the implementation stem a test file targets, by stripping the
|
|
4667
|
+
* conventional test affixes across languages (measured in the C2 benchmark):
|
|
4668
|
+
* foo.test.js / foo.spec.ts → foo (JS/TS)
|
|
4669
|
+
* test_foo.py → foo (Python / pytest)
|
|
4670
|
+
* foo_test.go / foo_test.py → foo (Go, unittest)
|
|
4671
|
+
* FooTest.java / BarSpec.scala → Foo (JVM, PascalCase)
|
|
4672
|
+
* @param {string} relPath
|
|
4673
|
+
* @returns {string}
|
|
4674
|
+
*/
|
|
4675
|
+
function testTargetStem(relPath) {
|
|
4676
|
+
let s = stemOf(relPath); // strips ext + trailing .test/.spec
|
|
4677
|
+
s = s.replace(/^test[_-]/i, ''); // Python: test_foo
|
|
4678
|
+
s = s.replace(/[_-]test$/i, ''); // Go / unittest: foo_test
|
|
4679
|
+
s = s.replace(/(Tests?|Specs?)$/, ''); // JVM PascalCase: FooTest, BarSpec
|
|
4680
|
+
return s;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
/**
|
|
4684
|
+
* Impl→test discovery (C2, v8.5). Matches test files back to their
|
|
4685
|
+
* implementation by normalizing conventional test affixes, so JS/TS, Python,
|
|
4686
|
+
* Go, and JVM naming conventions all resolve. Deterministic; accuracy is
|
|
4687
|
+
* measured by `scripts/run-test-discovery-benchmark.mjs`.
|
|
4654
4688
|
* @param {string} relPath
|
|
4655
4689
|
* @param {string[]} allFiles - universe of indexed files (relative paths)
|
|
4656
4690
|
* @returns {string[]}
|
|
@@ -4663,7 +4697,7 @@ __factories["./src/evidence/pack"] = function(module, exports) {
|
|
|
4663
4697
|
for (const f of allFiles) {
|
|
4664
4698
|
if (f === relPath) continue;
|
|
4665
4699
|
if (riskLabelFor(f) !== 'test') continue;
|
|
4666
|
-
if (
|
|
4700
|
+
if (testTargetStem(f).toLowerCase() === stem) out.push(f);
|
|
4667
4701
|
}
|
|
4668
4702
|
return out.sort();
|
|
4669
4703
|
}
|
|
@@ -11179,6 +11213,101 @@ __factories["./src/learning/weights"] = function(module, exports) {
|
|
|
11179
11213
|
|
|
11180
11214
|
};
|
|
11181
11215
|
|
|
11216
|
+
// ── ./src/map/build-ci ──
|
|
11217
|
+
__factories["./src/map/build-ci"] = function(module, exports) {
|
|
11218
|
+
|
|
11219
|
+
/**
|
|
11220
|
+
* Build & CI extractor (v8.5 C1).
|
|
11221
|
+
*
|
|
11222
|
+
* Surfaces how the project is built and validated: npm/pnpm/yarn scripts
|
|
11223
|
+
* (package.json), GitHub Actions workflows (.github/workflows/*.yml), and
|
|
11224
|
+
* Makefile targets. Pure, zero-dependency, deterministic.
|
|
11225
|
+
*
|
|
11226
|
+
* @param {string[]} files — absolute file paths (unused; roots are read directly)
|
|
11227
|
+
* @param {string} cwd — project root
|
|
11228
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
11229
|
+
*/
|
|
11230
|
+
|
|
11231
|
+
const fs = require('fs');
|
|
11232
|
+
const path = require('path');
|
|
11233
|
+
|
|
11234
|
+
const MAX_ROWS = 120;
|
|
11235
|
+
|
|
11236
|
+
function readJson(p) {
|
|
11237
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
11238
|
+
}
|
|
11239
|
+
|
|
11240
|
+
function npmScripts(cwd, rows) {
|
|
11241
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
11242
|
+
if (!pkg || !pkg.scripts || typeof pkg.scripts !== 'object') return;
|
|
11243
|
+
for (const name of Object.keys(pkg.scripts).sort()) {
|
|
11244
|
+
rows.push({ kind: 'script', name, detail: 'npm run ' + name });
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
|
|
11248
|
+
function ciWorkflows(cwd, rows) {
|
|
11249
|
+
const dir = path.join(cwd, '.github', 'workflows');
|
|
11250
|
+
let entries;
|
|
11251
|
+
try { entries = fs.readdirSync(dir); } catch (_) { return; }
|
|
11252
|
+
for (const file of entries.sort()) {
|
|
11253
|
+
if (!/\.ya?ml$/i.test(file)) continue;
|
|
11254
|
+
let content;
|
|
11255
|
+
try { content = fs.readFileSync(path.join(dir, file), 'utf8'); } catch (_) { continue; }
|
|
11256
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
11257
|
+
const name = nameMatch ? nameMatch[1].trim().replace(/^['"]|['"]$/g, '') : file;
|
|
11258
|
+
// Trigger events from an `on:` mapping or inline form.
|
|
11259
|
+
const onMatch = content.match(/^on:\s*(.*)$/m);
|
|
11260
|
+
let triggers = '';
|
|
11261
|
+
if (onMatch) {
|
|
11262
|
+
if (onMatch[1].trim()) {
|
|
11263
|
+
triggers = onMatch[1].replace(/[[\]{}'"]/g, '').trim();
|
|
11264
|
+
} else {
|
|
11265
|
+
const block = content.slice(onMatch.index);
|
|
11266
|
+
const events = [...block.matchAll(/^\s{2,}([a-z_]+):/gm)].map((m) => m[1]);
|
|
11267
|
+
triggers = [...new Set(events)].slice(0, 6).join(', ');
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
rows.push({ kind: 'ci', name, detail: `${file}${triggers ? ' — ' + triggers : ''}` });
|
|
11271
|
+
}
|
|
11272
|
+
}
|
|
11273
|
+
|
|
11274
|
+
function makeTargets(cwd, rows) {
|
|
11275
|
+
let content;
|
|
11276
|
+
try { content = fs.readFileSync(path.join(cwd, 'Makefile'), 'utf8'); } catch (_) { return; }
|
|
11277
|
+
const targets = [];
|
|
11278
|
+
for (const line of content.split('\n')) {
|
|
11279
|
+
const m = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*)\s*:(?!=)/);
|
|
11280
|
+
if (m && m[1] !== '.PHONY') targets.push(m[1]);
|
|
11281
|
+
}
|
|
11282
|
+
for (const t of [...new Set(targets)].sort()) {
|
|
11283
|
+
rows.push({ kind: 'make', name: t, detail: 'make ' + t });
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
|
|
11287
|
+
function analyze(files, cwd) {
|
|
11288
|
+
const rows = [];
|
|
11289
|
+
npmScripts(cwd, rows);
|
|
11290
|
+
ciWorkflows(cwd, rows);
|
|
11291
|
+
makeTargets(cwd, rows);
|
|
11292
|
+
if (rows.length === 0) return '';
|
|
11293
|
+
|
|
11294
|
+
const lines = [
|
|
11295
|
+
'| Kind | Name | Detail |',
|
|
11296
|
+
'|------|------|--------|',
|
|
11297
|
+
];
|
|
11298
|
+
for (const r of rows.slice(0, MAX_ROWS)) {
|
|
11299
|
+
lines.push(`| ${r.kind} | ${r.name} | ${r.detail} |`);
|
|
11300
|
+
}
|
|
11301
|
+
if (rows.length > MAX_ROWS) {
|
|
11302
|
+
lines.push(`| … | | +${rows.length - MAX_ROWS} more |`);
|
|
11303
|
+
}
|
|
11304
|
+
return lines.join('\n');
|
|
11305
|
+
}
|
|
11306
|
+
|
|
11307
|
+
module.exports = { analyze };
|
|
11308
|
+
|
|
11309
|
+
};
|
|
11310
|
+
|
|
11182
11311
|
// ── ./src/map/class-hierarchy ──
|
|
11183
11312
|
__factories["./src/map/class-hierarchy"] = function(module, exports) {
|
|
11184
11313
|
|
|
@@ -11300,6 +11429,205 @@ __factories["./src/map/class-hierarchy"] = function(module, exports) {
|
|
|
11300
11429
|
|
|
11301
11430
|
};
|
|
11302
11431
|
|
|
11432
|
+
// ── ./src/map/config-manifest ──
|
|
11433
|
+
__factories["./src/map/config-manifest"] = function(module, exports) {
|
|
11434
|
+
|
|
11435
|
+
/**
|
|
11436
|
+
* Config & package-manifest extractor (v8.5 C1).
|
|
11437
|
+
*
|
|
11438
|
+
* Surfaces the project's package manifests (name / version / dependency counts)
|
|
11439
|
+
* across ecosystems and the notable root config files present. Pure,
|
|
11440
|
+
* zero-dependency, deterministic.
|
|
11441
|
+
*
|
|
11442
|
+
* @param {string[]} files — absolute file paths (unused; roots are read directly)
|
|
11443
|
+
* @param {string} cwd — project root
|
|
11444
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
11445
|
+
*/
|
|
11446
|
+
|
|
11447
|
+
const fs = require('fs');
|
|
11448
|
+
const path = require('path');
|
|
11449
|
+
|
|
11450
|
+
const CONFIG_FILES = [
|
|
11451
|
+
'tsconfig.json', 'jsconfig.json', '.eslintrc', '.eslintrc.json', '.eslintrc.js',
|
|
11452
|
+
'.prettierrc', 'babel.config.js', 'jest.config.js', 'vitest.config.ts',
|
|
11453
|
+
'webpack.config.js', 'vite.config.ts', 'rollup.config.js', 'tailwind.config.js',
|
|
11454
|
+
'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', '.editorconfig',
|
|
11455
|
+
];
|
|
11456
|
+
|
|
11457
|
+
function readText(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; } }
|
|
11458
|
+
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
|
|
11459
|
+
function count(obj) { return obj && typeof obj === 'object' ? Object.keys(obj).length : 0; }
|
|
11460
|
+
|
|
11461
|
+
function manifests(cwd, rows) {
|
|
11462
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
11463
|
+
if (pkg) {
|
|
11464
|
+
const deps = count(pkg.dependencies);
|
|
11465
|
+
const dev = count(pkg.devDependencies);
|
|
11466
|
+
const id = [pkg.name, pkg.version].filter(Boolean).join('@') || 'package.json';
|
|
11467
|
+
rows.push({ manifest: 'package.json (npm)', detail: `${id} · ${deps} deps, ${dev} devDeps` });
|
|
11468
|
+
}
|
|
11469
|
+
|
|
11470
|
+
const pyproject = readText(path.join(cwd, 'pyproject.toml'));
|
|
11471
|
+
if (pyproject) {
|
|
11472
|
+
const name = (pyproject.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
11473
|
+
const ver = (pyproject.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
11474
|
+
rows.push({ manifest: 'pyproject.toml (python)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
|
|
11475
|
+
} else if (readText(path.join(cwd, 'setup.py'))) {
|
|
11476
|
+
rows.push({ manifest: 'setup.py (python)', detail: 'present' });
|
|
11477
|
+
}
|
|
11478
|
+
if (readText(path.join(cwd, 'requirements.txt'))) {
|
|
11479
|
+
rows.push({ manifest: 'requirements.txt (python)', detail: 'present' });
|
|
11480
|
+
}
|
|
11481
|
+
|
|
11482
|
+
const cargo = readText(path.join(cwd, 'Cargo.toml'));
|
|
11483
|
+
if (cargo) {
|
|
11484
|
+
const name = (cargo.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
11485
|
+
const ver = (cargo.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
11486
|
+
rows.push({ manifest: 'Cargo.toml (rust)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
|
|
11487
|
+
}
|
|
11488
|
+
|
|
11489
|
+
const gomod = readText(path.join(cwd, 'go.mod'));
|
|
11490
|
+
if (gomod) {
|
|
11491
|
+
const mod = (gomod.match(/^module\s+(\S+)/m) || [])[1];
|
|
11492
|
+
const go = (gomod.match(/^go\s+(\S+)/m) || [])[1];
|
|
11493
|
+
rows.push({ manifest: 'go.mod (go)', detail: [mod, go && 'go ' + go].filter(Boolean).join(' · ') || 'present' });
|
|
11494
|
+
}
|
|
11495
|
+
|
|
11496
|
+
if (readText(path.join(cwd, 'pom.xml'))) rows.push({ manifest: 'pom.xml (maven)', detail: 'present' });
|
|
11497
|
+
if (readText(path.join(cwd, 'build.gradle')) || readText(path.join(cwd, 'build.gradle.kts'))) {
|
|
11498
|
+
rows.push({ manifest: 'build.gradle (gradle)', detail: 'present' });
|
|
11499
|
+
}
|
|
11500
|
+
if (readText(path.join(cwd, 'Gemfile'))) rows.push({ manifest: 'Gemfile (ruby)', detail: 'present' });
|
|
11501
|
+
const composer = readJson(path.join(cwd, 'composer.json'));
|
|
11502
|
+
if (composer) {
|
|
11503
|
+
rows.push({ manifest: 'composer.json (php)', detail: `${composer.name || 'present'} · ${count(composer.require)} deps` });
|
|
11504
|
+
}
|
|
11505
|
+
}
|
|
11506
|
+
|
|
11507
|
+
function configFiles(cwd) {
|
|
11508
|
+
const present = [];
|
|
11509
|
+
for (const f of CONFIG_FILES) {
|
|
11510
|
+
if (fs.existsSync(path.join(cwd, f))) present.push(f);
|
|
11511
|
+
}
|
|
11512
|
+
return present;
|
|
11513
|
+
}
|
|
11514
|
+
|
|
11515
|
+
function analyze(files, cwd) {
|
|
11516
|
+
const rows = [];
|
|
11517
|
+
manifests(cwd, rows);
|
|
11518
|
+
const configs = configFiles(cwd);
|
|
11519
|
+
if (rows.length === 0 && configs.length === 0) return '';
|
|
11520
|
+
|
|
11521
|
+
const lines = [];
|
|
11522
|
+
if (rows.length) {
|
|
11523
|
+
lines.push('| Manifest | Detail |', '|----------|--------|');
|
|
11524
|
+
for (const r of rows) lines.push(`| ${r.manifest} | ${r.detail} |`);
|
|
11525
|
+
}
|
|
11526
|
+
if (configs.length) {
|
|
11527
|
+
if (lines.length) lines.push('');
|
|
11528
|
+
lines.push(`**Config files:** ${configs.map((c) => '`' + c + '`').join(', ')}`);
|
|
11529
|
+
}
|
|
11530
|
+
return lines.join('\n');
|
|
11531
|
+
}
|
|
11532
|
+
|
|
11533
|
+
module.exports = { analyze };
|
|
11534
|
+
|
|
11535
|
+
};
|
|
11536
|
+
|
|
11537
|
+
// ── ./src/map/env-schema ──
|
|
11538
|
+
__factories["./src/map/env-schema"] = function(module, exports) {
|
|
11539
|
+
|
|
11540
|
+
/**
|
|
11541
|
+
* Environment-variable schema extractor (v8.5 C1).
|
|
11542
|
+
*
|
|
11543
|
+
* Surfaces the environment the project actually reads — from source across
|
|
11544
|
+
* JS/TS, Python, Ruby, and Go, plus keys declared in a committed `.env.example`
|
|
11545
|
+
* / `.env.sample` / `.env.template`. Pure, zero-dependency, deterministic.
|
|
11546
|
+
*
|
|
11547
|
+
* @param {string[]} files — absolute file paths to analyze (srcDirs-scoped)
|
|
11548
|
+
* @param {string} cwd — project root
|
|
11549
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
11550
|
+
*/
|
|
11551
|
+
|
|
11552
|
+
const fs = require('fs');
|
|
11553
|
+
const path = require('path');
|
|
11554
|
+
|
|
11555
|
+
const SCAN_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go']);
|
|
11556
|
+
const EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template', '.env.dist'];
|
|
11557
|
+
|
|
11558
|
+
// process.env.X / process.env['X'] / import.meta.env.X / Deno.env.get('X')
|
|
11559
|
+
const JS_RE = /(?:process\.env|import\.meta\.env)(?:\.([A-Z_][A-Z0-9_]*)|\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\])|Deno\.env\.get\(\s*['"]([A-Z_][A-Z0-9_]*)['"]/g;
|
|
11560
|
+
// os.environ['X'] / os.environ.get('X') / os.getenv('X') / getenv('X')
|
|
11561
|
+
const PY_RE = /(?:os\.)?(?:environ(?:\.get)?\[?\s*['"]([A-Z_][A-Z0-9_]*)['"]|getenv\(\s*['"]([A-Z_][A-Z0-9_]*)['"])/g;
|
|
11562
|
+
const RB_RE = /ENV\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\]/g;
|
|
11563
|
+
const GO_RE = /os\.(?:Getenv|LookupEnv)\(\s*["`']([A-Z_][A-Z0-9_]*)["`']/g;
|
|
11564
|
+
|
|
11565
|
+
const MAX_ROWS = 200;
|
|
11566
|
+
|
|
11567
|
+
function collectMatches(re, content, into) {
|
|
11568
|
+
let m;
|
|
11569
|
+
re.lastIndex = 0;
|
|
11570
|
+
while ((m = re.exec(content)) !== null) {
|
|
11571
|
+
const name = m[1] || m[2] || m[3];
|
|
11572
|
+
if (name) into.add(name);
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
|
|
11576
|
+
function readExampleKeys(cwd) {
|
|
11577
|
+
const keys = new Set();
|
|
11578
|
+
for (const name of EXAMPLE_FILES) {
|
|
11579
|
+
let content;
|
|
11580
|
+
try { content = fs.readFileSync(path.join(cwd, name), 'utf8'); } catch (_) { continue; }
|
|
11581
|
+
for (const line of content.split('\n')) {
|
|
11582
|
+
const t = line.trim();
|
|
11583
|
+
if (!t || t.startsWith('#')) continue;
|
|
11584
|
+
const eq = t.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
|
|
11585
|
+
if (eq) keys.add(eq[1]);
|
|
11586
|
+
}
|
|
11587
|
+
}
|
|
11588
|
+
return keys;
|
|
11589
|
+
}
|
|
11590
|
+
|
|
11591
|
+
function analyze(files, cwd) {
|
|
11592
|
+
const fromCode = new Set();
|
|
11593
|
+
|
|
11594
|
+
for (const filePath of files) {
|
|
11595
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
11596
|
+
if (!SCAN_EXTS.has(ext)) continue;
|
|
11597
|
+
let content;
|
|
11598
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
|
|
11599
|
+
|
|
11600
|
+
if (ext === '.py') collectMatches(PY_RE, content, fromCode);
|
|
11601
|
+
else if (ext === '.rb') collectMatches(RB_RE, content, fromCode);
|
|
11602
|
+
else if (ext === '.go') collectMatches(GO_RE, content, fromCode);
|
|
11603
|
+
else collectMatches(JS_RE, content, fromCode);
|
|
11604
|
+
}
|
|
11605
|
+
|
|
11606
|
+
const fromExample = readExampleKeys(cwd);
|
|
11607
|
+
const all = new Set([...fromCode, ...fromExample]);
|
|
11608
|
+
if (all.size === 0) return '';
|
|
11609
|
+
|
|
11610
|
+
const names = [...all].sort();
|
|
11611
|
+
const lines = [
|
|
11612
|
+
'| Variable | Source |',
|
|
11613
|
+
'|----------|--------|',
|
|
11614
|
+
];
|
|
11615
|
+
for (const name of names.slice(0, MAX_ROWS)) {
|
|
11616
|
+
const src = [];
|
|
11617
|
+
if (fromCode.has(name)) src.push('code');
|
|
11618
|
+
if (fromExample.has(name)) src.push('.env.example');
|
|
11619
|
+
lines.push(`| ${name} | ${src.join(', ')} |`);
|
|
11620
|
+
}
|
|
11621
|
+
if (names.length > MAX_ROWS) {
|
|
11622
|
+
lines.push(`| … | +${names.length - MAX_ROWS} more |`);
|
|
11623
|
+
}
|
|
11624
|
+
return lines.join('\n');
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
module.exports = { analyze };
|
|
11628
|
+
|
|
11629
|
+
};
|
|
11630
|
+
|
|
11303
11631
|
// ── ./src/map/import-graph ──
|
|
11304
11632
|
__factories["./src/map/import-graph"] = function(module, exports) {
|
|
11305
11633
|
|
|
@@ -11489,6 +11817,94 @@ __factories["./src/map/import-graph"] = function(module, exports) {
|
|
|
11489
11817
|
|
|
11490
11818
|
};
|
|
11491
11819
|
|
|
11820
|
+
// ── ./src/map/migrations ──
|
|
11821
|
+
__factories["./src/map/migrations"] = function(module, exports) {
|
|
11822
|
+
|
|
11823
|
+
/**
|
|
11824
|
+
* Database-migration extractor (v8.5 C1).
|
|
11825
|
+
*
|
|
11826
|
+
* Detects schema-migration files across the common frameworks — Rails
|
|
11827
|
+
* (db/migrate), Django/Alembic, Prisma, Flyway (`V1__name.sql`), knex/Sequelize,
|
|
11828
|
+
* and timestamped SQL — and surfaces them with a parsed version + name. Pure,
|
|
11829
|
+
* zero-dependency, deterministic.
|
|
11830
|
+
*
|
|
11831
|
+
* @param {string[]} files — absolute file paths (unused; the tree is walked)
|
|
11832
|
+
* @param {string} cwd — project root
|
|
11833
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
11834
|
+
*/
|
|
11835
|
+
|
|
11836
|
+
const fs = require('fs');
|
|
11837
|
+
const path = require('path');
|
|
11838
|
+
|
|
11839
|
+
const MAX_DEPTH = 6;
|
|
11840
|
+
const MAX_ROWS = 200;
|
|
11841
|
+
const SKIP_DIR = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'target', '.venv', 'venv', '__pycache__']);
|
|
11842
|
+
const MIG_EXT = new Set(['.sql', '.rb', '.py', '.js', '.ts']);
|
|
11843
|
+
|
|
11844
|
+
// A directory whose path marks its children as migrations.
|
|
11845
|
+
const MIG_DIR_RE = /(^|\/)(db\/migrate|migrations?|alembic\/versions|prisma\/migrations)$/i;
|
|
11846
|
+
// A filename that is itself a migration regardless of directory.
|
|
11847
|
+
const FLYWAY_RE = /^V\d+(?:[._]\d+)*__(.+)\.(sql|java)$/;
|
|
11848
|
+
const TIMESTAMP_RE = /^(\d{8,})[_-](.+)\.(sql|rb|py|js|ts)$/;
|
|
11849
|
+
const NAMED_RE = /[._-]migrations?[._-]/i;
|
|
11850
|
+
|
|
11851
|
+
function walk(dir, cwd, depth, out) {
|
|
11852
|
+
if (depth > MAX_DEPTH) return;
|
|
11853
|
+
let entries;
|
|
11854
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
11855
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
11856
|
+
|
|
11857
|
+
const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
|
|
11858
|
+
const dirIsMigration = MIG_DIR_RE.test(relDir);
|
|
11859
|
+
|
|
11860
|
+
for (const e of entries) {
|
|
11861
|
+
if (e.isDirectory()) {
|
|
11862
|
+
if (SKIP_DIR.has(e.name)) continue;
|
|
11863
|
+
walk(path.join(dir, e.name), cwd, depth + 1, out);
|
|
11864
|
+
continue;
|
|
11865
|
+
}
|
|
11866
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
11867
|
+
if (!MIG_EXT.has(ext)) continue;
|
|
11868
|
+
|
|
11869
|
+
const rel = path.relative(cwd, path.join(dir, e.name)).replace(/\\/g, '/');
|
|
11870
|
+
let version = null;
|
|
11871
|
+
let name = null;
|
|
11872
|
+
|
|
11873
|
+
let m;
|
|
11874
|
+
if ((m = e.name.match(FLYWAY_RE))) { version = e.name.split('__')[0]; name = m[1].replace(/_/g, ' '); }
|
|
11875
|
+
else if ((m = e.name.match(TIMESTAMP_RE))) { version = m[1]; name = m[2].replace(/[_-]/g, ' '); }
|
|
11876
|
+
else if (dirIsMigration) { version = '—'; name = e.name.replace(ext, ''); }
|
|
11877
|
+
else if (NAMED_RE.test(e.name)) { version = '—'; name = e.name.replace(ext, ''); }
|
|
11878
|
+
else continue;
|
|
11879
|
+
|
|
11880
|
+
out.push({ version, name, file: rel });
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
|
|
11884
|
+
function analyze(files, cwd) {
|
|
11885
|
+
const found = [];
|
|
11886
|
+
walk(cwd, cwd, 0, found);
|
|
11887
|
+
if (found.length === 0) return '';
|
|
11888
|
+
|
|
11889
|
+
found.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
11890
|
+
|
|
11891
|
+
const lines = [
|
|
11892
|
+
'| Version | Migration | File |',
|
|
11893
|
+
'|---------|-----------|------|',
|
|
11894
|
+
];
|
|
11895
|
+
for (const r of found.slice(0, MAX_ROWS)) {
|
|
11896
|
+
lines.push(`| ${r.version} | ${r.name} | ${r.file} |`);
|
|
11897
|
+
}
|
|
11898
|
+
if (found.length > MAX_ROWS) {
|
|
11899
|
+
lines.push(`| … | +${found.length - MAX_ROWS} more | |`);
|
|
11900
|
+
}
|
|
11901
|
+
return lines.join('\n');
|
|
11902
|
+
}
|
|
11903
|
+
|
|
11904
|
+
module.exports = { analyze };
|
|
11905
|
+
|
|
11906
|
+
};
|
|
11907
|
+
|
|
11492
11908
|
// ── ./src/map/route-table ──
|
|
11493
11909
|
__factories["./src/map/route-table"] = function(module, exports) {
|
|
11494
11910
|
|
|
@@ -11644,6 +12060,10 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
11644
12060
|
imports: '### Import graph',
|
|
11645
12061
|
classes: '### Class hierarchy',
|
|
11646
12062
|
routes: '### Route table',
|
|
12063
|
+
env: '### Environment variables',
|
|
12064
|
+
buildci: '### Build & CI',
|
|
12065
|
+
manifests: '### Config & manifests',
|
|
12066
|
+
migrations: '### Database migrations',
|
|
11647
12067
|
};
|
|
11648
12068
|
|
|
11649
12069
|
/**
|
|
@@ -11729,7 +12149,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
11729
12149
|
|
|
11730
12150
|
const header = MAP_SECTIONS[args.type];
|
|
11731
12151
|
if (!header) {
|
|
11732
|
-
return `Unknown map type: "${args.type}". Use:
|
|
12152
|
+
return `Unknown map type: "${args.type}". Use: ${Object.keys(MAP_SECTIONS).join(', ')}`;
|
|
11733
12153
|
}
|
|
11734
12154
|
|
|
11735
12155
|
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
@@ -12643,7 +13063,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
12643
13063
|
|
|
12644
13064
|
const SERVER_INFO = {
|
|
12645
13065
|
name: 'sigmap',
|
|
12646
|
-
version: '
|
|
13066
|
+
version: '8.0.0',
|
|
12647
13067
|
description: 'SigMap MCP server — code signatures on demand',
|
|
12648
13068
|
};
|
|
12649
13069
|
|
|
@@ -16612,7 +17032,7 @@ function __tryGit(args, opts = {}) {
|
|
|
16612
17032
|
catch (_) { return ''; }
|
|
16613
17033
|
}
|
|
16614
17034
|
|
|
16615
|
-
const VERSION = '
|
|
17035
|
+
const VERSION = '8.0.0';
|
|
16616
17036
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
16617
17037
|
|
|
16618
17038
|
function requireSourceOrBundled(key) {
|
package/gen-project-map.js
CHANGED
|
@@ -127,9 +127,13 @@ function formatOutput(sections) {
|
|
|
127
127
|
];
|
|
128
128
|
|
|
129
129
|
const parts = [
|
|
130
|
-
{ key: 'imports',
|
|
131
|
-
{ key: 'classes',
|
|
132
|
-
{ key: 'routes',
|
|
130
|
+
{ key: 'imports', header: '### Import graph', content: sections.imports },
|
|
131
|
+
{ key: 'classes', header: '### Class hierarchy', content: sections.classes },
|
|
132
|
+
{ key: 'routes', header: '### Route table', content: sections.routes },
|
|
133
|
+
{ key: 'env', header: '### Environment variables', content: sections.env },
|
|
134
|
+
{ key: 'buildci', header: '### Build & CI', content: sections.buildci },
|
|
135
|
+
{ key: 'manifests', header: '### Config & manifests', content: sections.manifests },
|
|
136
|
+
{ key: 'migrations', header: '### Database migrations', content: sections.migrations },
|
|
133
137
|
];
|
|
134
138
|
|
|
135
139
|
for (const { header, content } of parts) {
|
|
@@ -165,9 +169,13 @@ function main() {
|
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
const sections = {
|
|
168
|
-
imports:
|
|
169
|
-
classes:
|
|
170
|
-
routes:
|
|
172
|
+
imports: runAnalyzer('import-graph', files, cwd),
|
|
173
|
+
classes: runAnalyzer('class-hierarchy', files, cwd),
|
|
174
|
+
routes: runAnalyzer('route-table', files, cwd),
|
|
175
|
+
env: runAnalyzer('env-schema', files, cwd),
|
|
176
|
+
buildci: runAnalyzer('build-ci', files, cwd),
|
|
177
|
+
manifests: runAnalyzer('config-manifest', files, cwd),
|
|
178
|
+
migrations: runAnalyzer('migrations', files, cwd),
|
|
171
179
|
};
|
|
172
180
|
|
|
173
181
|
const output = formatOutput(sections);
|
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:
|
|
14
|
+
# Version: 8.0.0 | Benchmark: sigmap-v8.0-main (2026-07-04)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.0-main, 2026-07-04)
|
|
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:
|
|
14
|
+
# Version: 8.0.0 | Benchmark: sigmap-v8.0-main (2026-07-04)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,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-
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.0-main, 2026-07-04)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"benchmark:matrix": "node scripts/run-benchmark-matrix.mjs --save --skip-clone",
|
|
28
28
|
"benchmark:verify": "node scripts/run-verify-benchmark.mjs",
|
|
29
29
|
"benchmark:squeeze": "node scripts/run-squeeze-benchmark.mjs --save",
|
|
30
|
+
"benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
|
|
30
31
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
|
31
32
|
"health": "node gen-context.js --health",
|
|
32
33
|
"map": "node gen-project-map.js",
|
package/src/evidence/pack.js
CHANGED
|
@@ -33,7 +33,14 @@ const DEFAULT_TOP = 12;
|
|
|
33
33
|
const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
|
|
34
34
|
const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
|
|
35
35
|
const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
|
|
36
|
-
|
|
36
|
+
// DB migrations: framework dirs (Rails/Alembic/Prisma), Flyway `V1__x.sql`,
|
|
37
|
+
// timestamped migration files, and `*_migration.*` naming.
|
|
38
|
+
const MIGRATION_RE = /(^|\/)(migrations?|alembic\/versions|prisma\/migrations)(\/|$)|(^|\/)db\/migrate\/|(^|\/)V\d+(_\d+)*__[^/]+\.(sql|java)$|(^|\/)\d{8,}[_-][^/]+\.(sql|rb|py|js|ts)$|[._-]migration[s]?[._-]/i;
|
|
39
|
+
const PAYMENT_RE = /(^|\/|[._-])(payment|payments|billing|checkout|invoice|invoicing|subscription|stripe|paypal|braintree|charge|refund|payout)([._-]|\/|$)/i;
|
|
40
|
+
const AUTH_RE = /(^|\/|[._-])(auth|authn|authz|login|logout|signin|signup|password|passwd|session|oauth|jwt|permission|permissions|acl|rbac|credential|credentials)([._-]|\/|$)/i;
|
|
41
|
+
const SECURITY_RE = /(^|\/|[._-])(secret|secrets|crypto|cipher|encrypt|decrypt|token|signing|keystore|vault)([._-]|\/|$)/i;
|
|
42
|
+
// Public API surface: `api/` dirs, `public-api`, and module barrel entrypoints.
|
|
43
|
+
const PUBLIC_API_RE = /(^|\/)api(\/|$)|(^|\/)public[-_]?api(\/|$)|(^|\/)index\.(js|ts|mjs|cjs)$/i;
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* Split a signature's ` :start-end` line anchor from its symbol text.
|
|
@@ -51,17 +58,25 @@ function parseAnchor(sig) {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
|
-
* Classify a file into a
|
|
55
|
-
*
|
|
61
|
+
* Classify a file into a risk label (C3, v8.5). Path-based, deterministic.
|
|
62
|
+
* Precedence is strict, most-specific-risk first: a migration touching payments
|
|
63
|
+
* is labeled `migration` (a schema change is the dominant risk), payment/auth
|
|
64
|
+
* outrank the generic `security` bucket, and `config`/`public-api` resolve
|
|
65
|
+
* before the `source` fallback. `test`/`generated` semantics are preserved so
|
|
66
|
+
* existing consumers (findRelatedTests, verifier) keep working.
|
|
56
67
|
* @param {string} relPath
|
|
57
|
-
* @returns {'generated'|'test'|'
|
|
68
|
+
* @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
|
|
58
69
|
*/
|
|
59
70
|
function riskLabelFor(relPath) {
|
|
60
71
|
const p = relPath.replace(/\\/g, '/');
|
|
61
72
|
if (GENERATED_RE.test(p)) return 'generated';
|
|
62
73
|
if (TEST_RE.test(p)) return 'test';
|
|
74
|
+
if (MIGRATION_RE.test(p)) return 'migration';
|
|
75
|
+
if (PAYMENT_RE.test(p)) return 'payment';
|
|
76
|
+
if (AUTH_RE.test(p)) return 'auth';
|
|
63
77
|
if (SECURITY_RE.test(p)) return 'security';
|
|
64
78
|
if (CONFIG_RE.test(p)) return 'config';
|
|
79
|
+
if (PUBLIC_API_RE.test(p)) return 'public-api';
|
|
65
80
|
return 'source';
|
|
66
81
|
}
|
|
67
82
|
|
|
@@ -72,9 +87,28 @@ function stemOf(relPath) {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
90
|
+
* Infer the implementation stem a test file targets, by stripping the
|
|
91
|
+
* conventional test affixes across languages (measured in the C2 benchmark):
|
|
92
|
+
* foo.test.js / foo.spec.ts → foo (JS/TS)
|
|
93
|
+
* test_foo.py → foo (Python / pytest)
|
|
94
|
+
* foo_test.go / foo_test.py → foo (Go, unittest)
|
|
95
|
+
* FooTest.java / BarSpec.scala → Foo (JVM, PascalCase)
|
|
96
|
+
* @param {string} relPath
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function testTargetStem(relPath) {
|
|
100
|
+
let s = stemOf(relPath); // strips ext + trailing .test/.spec
|
|
101
|
+
s = s.replace(/^test[_-]/i, ''); // Python: test_foo
|
|
102
|
+
s = s.replace(/[_-]test$/i, ''); // Go / unittest: foo_test
|
|
103
|
+
s = s.replace(/(Tests?|Specs?)$/, ''); // JVM PascalCase: FooTest, BarSpec
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Impl→test discovery (C2, v8.5). Matches test files back to their
|
|
109
|
+
* implementation by normalizing conventional test affixes, so JS/TS, Python,
|
|
110
|
+
* Go, and JVM naming conventions all resolve. Deterministic; accuracy is
|
|
111
|
+
* measured by `scripts/run-test-discovery-benchmark.mjs`.
|
|
78
112
|
* @param {string} relPath
|
|
79
113
|
* @param {string[]} allFiles - universe of indexed files (relative paths)
|
|
80
114
|
* @returns {string[]}
|
|
@@ -87,7 +121,7 @@ function findRelatedTests(relPath, allFiles) {
|
|
|
87
121
|
for (const f of allFiles) {
|
|
88
122
|
if (f === relPath) continue;
|
|
89
123
|
if (riskLabelFor(f) !== 'test') continue;
|
|
90
|
-
if (
|
|
124
|
+
if (testTargetStem(f).toLowerCase() === stem) out.push(f);
|
|
91
125
|
}
|
|
92
126
|
return out.sort();
|
|
93
127
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build & CI extractor (v8.5 C1).
|
|
5
|
+
*
|
|
6
|
+
* Surfaces how the project is built and validated: npm/pnpm/yarn scripts
|
|
7
|
+
* (package.json), GitHub Actions workflows (.github/workflows/*.yml), and
|
|
8
|
+
* Makefile targets. Pure, zero-dependency, deterministic.
|
|
9
|
+
*
|
|
10
|
+
* @param {string[]} files — absolute file paths (unused; roots are read directly)
|
|
11
|
+
* @param {string} cwd — project root
|
|
12
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const MAX_ROWS = 120;
|
|
19
|
+
|
|
20
|
+
function readJson(p) {
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function npmScripts(cwd, rows) {
|
|
25
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
26
|
+
if (!pkg || !pkg.scripts || typeof pkg.scripts !== 'object') return;
|
|
27
|
+
for (const name of Object.keys(pkg.scripts).sort()) {
|
|
28
|
+
rows.push({ kind: 'script', name, detail: 'npm run ' + name });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ciWorkflows(cwd, rows) {
|
|
33
|
+
const dir = path.join(cwd, '.github', 'workflows');
|
|
34
|
+
let entries;
|
|
35
|
+
try { entries = fs.readdirSync(dir); } catch (_) { return; }
|
|
36
|
+
for (const file of entries.sort()) {
|
|
37
|
+
if (!/\.ya?ml$/i.test(file)) continue;
|
|
38
|
+
let content;
|
|
39
|
+
try { content = fs.readFileSync(path.join(dir, file), 'utf8'); } catch (_) { continue; }
|
|
40
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
41
|
+
const name = nameMatch ? nameMatch[1].trim().replace(/^['"]|['"]$/g, '') : file;
|
|
42
|
+
// Trigger events from an `on:` mapping or inline form.
|
|
43
|
+
const onMatch = content.match(/^on:\s*(.*)$/m);
|
|
44
|
+
let triggers = '';
|
|
45
|
+
if (onMatch) {
|
|
46
|
+
if (onMatch[1].trim()) {
|
|
47
|
+
triggers = onMatch[1].replace(/[[\]{}'"]/g, '').trim();
|
|
48
|
+
} else {
|
|
49
|
+
const block = content.slice(onMatch.index);
|
|
50
|
+
const events = [...block.matchAll(/^\s{2,}([a-z_]+):/gm)].map((m) => m[1]);
|
|
51
|
+
triggers = [...new Set(events)].slice(0, 6).join(', ');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
rows.push({ kind: 'ci', name, detail: `${file}${triggers ? ' — ' + triggers : ''}` });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeTargets(cwd, rows) {
|
|
59
|
+
let content;
|
|
60
|
+
try { content = fs.readFileSync(path.join(cwd, 'Makefile'), 'utf8'); } catch (_) { return; }
|
|
61
|
+
const targets = [];
|
|
62
|
+
for (const line of content.split('\n')) {
|
|
63
|
+
const m = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*)\s*:(?!=)/);
|
|
64
|
+
if (m && m[1] !== '.PHONY') targets.push(m[1]);
|
|
65
|
+
}
|
|
66
|
+
for (const t of [...new Set(targets)].sort()) {
|
|
67
|
+
rows.push({ kind: 'make', name: t, detail: 'make ' + t });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function analyze(files, cwd) {
|
|
72
|
+
const rows = [];
|
|
73
|
+
npmScripts(cwd, rows);
|
|
74
|
+
ciWorkflows(cwd, rows);
|
|
75
|
+
makeTargets(cwd, rows);
|
|
76
|
+
if (rows.length === 0) return '';
|
|
77
|
+
|
|
78
|
+
const lines = [
|
|
79
|
+
'| Kind | Name | Detail |',
|
|
80
|
+
'|------|------|--------|',
|
|
81
|
+
];
|
|
82
|
+
for (const r of rows.slice(0, MAX_ROWS)) {
|
|
83
|
+
lines.push(`| ${r.kind} | ${r.name} | ${r.detail} |`);
|
|
84
|
+
}
|
|
85
|
+
if (rows.length > MAX_ROWS) {
|
|
86
|
+
lines.push(`| … | | +${rows.length - MAX_ROWS} more |`);
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { analyze };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Config & package-manifest extractor (v8.5 C1).
|
|
5
|
+
*
|
|
6
|
+
* Surfaces the project's package manifests (name / version / dependency counts)
|
|
7
|
+
* across ecosystems and the notable root config files present. Pure,
|
|
8
|
+
* zero-dependency, deterministic.
|
|
9
|
+
*
|
|
10
|
+
* @param {string[]} files — absolute file paths (unused; roots are read directly)
|
|
11
|
+
* @param {string} cwd — project root
|
|
12
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const CONFIG_FILES = [
|
|
19
|
+
'tsconfig.json', 'jsconfig.json', '.eslintrc', '.eslintrc.json', '.eslintrc.js',
|
|
20
|
+
'.prettierrc', 'babel.config.js', 'jest.config.js', 'vitest.config.ts',
|
|
21
|
+
'webpack.config.js', 'vite.config.ts', 'rollup.config.js', 'tailwind.config.js',
|
|
22
|
+
'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', '.editorconfig',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function readText(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; } }
|
|
26
|
+
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
|
|
27
|
+
function count(obj) { return obj && typeof obj === 'object' ? Object.keys(obj).length : 0; }
|
|
28
|
+
|
|
29
|
+
function manifests(cwd, rows) {
|
|
30
|
+
const pkg = readJson(path.join(cwd, 'package.json'));
|
|
31
|
+
if (pkg) {
|
|
32
|
+
const deps = count(pkg.dependencies);
|
|
33
|
+
const dev = count(pkg.devDependencies);
|
|
34
|
+
const id = [pkg.name, pkg.version].filter(Boolean).join('@') || 'package.json';
|
|
35
|
+
rows.push({ manifest: 'package.json (npm)', detail: `${id} · ${deps} deps, ${dev} devDeps` });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const pyproject = readText(path.join(cwd, 'pyproject.toml'));
|
|
39
|
+
if (pyproject) {
|
|
40
|
+
const name = (pyproject.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
41
|
+
const ver = (pyproject.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
42
|
+
rows.push({ manifest: 'pyproject.toml (python)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
|
|
43
|
+
} else if (readText(path.join(cwd, 'setup.py'))) {
|
|
44
|
+
rows.push({ manifest: 'setup.py (python)', detail: 'present' });
|
|
45
|
+
}
|
|
46
|
+
if (readText(path.join(cwd, 'requirements.txt'))) {
|
|
47
|
+
rows.push({ manifest: 'requirements.txt (python)', detail: 'present' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cargo = readText(path.join(cwd, 'Cargo.toml'));
|
|
51
|
+
if (cargo) {
|
|
52
|
+
const name = (cargo.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
53
|
+
const ver = (cargo.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
|
|
54
|
+
rows.push({ manifest: 'Cargo.toml (rust)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const gomod = readText(path.join(cwd, 'go.mod'));
|
|
58
|
+
if (gomod) {
|
|
59
|
+
const mod = (gomod.match(/^module\s+(\S+)/m) || [])[1];
|
|
60
|
+
const go = (gomod.match(/^go\s+(\S+)/m) || [])[1];
|
|
61
|
+
rows.push({ manifest: 'go.mod (go)', detail: [mod, go && 'go ' + go].filter(Boolean).join(' · ') || 'present' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (readText(path.join(cwd, 'pom.xml'))) rows.push({ manifest: 'pom.xml (maven)', detail: 'present' });
|
|
65
|
+
if (readText(path.join(cwd, 'build.gradle')) || readText(path.join(cwd, 'build.gradle.kts'))) {
|
|
66
|
+
rows.push({ manifest: 'build.gradle (gradle)', detail: 'present' });
|
|
67
|
+
}
|
|
68
|
+
if (readText(path.join(cwd, 'Gemfile'))) rows.push({ manifest: 'Gemfile (ruby)', detail: 'present' });
|
|
69
|
+
const composer = readJson(path.join(cwd, 'composer.json'));
|
|
70
|
+
if (composer) {
|
|
71
|
+
rows.push({ manifest: 'composer.json (php)', detail: `${composer.name || 'present'} · ${count(composer.require)} deps` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function configFiles(cwd) {
|
|
76
|
+
const present = [];
|
|
77
|
+
for (const f of CONFIG_FILES) {
|
|
78
|
+
if (fs.existsSync(path.join(cwd, f))) present.push(f);
|
|
79
|
+
}
|
|
80
|
+
return present;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function analyze(files, cwd) {
|
|
84
|
+
const rows = [];
|
|
85
|
+
manifests(cwd, rows);
|
|
86
|
+
const configs = configFiles(cwd);
|
|
87
|
+
if (rows.length === 0 && configs.length === 0) return '';
|
|
88
|
+
|
|
89
|
+
const lines = [];
|
|
90
|
+
if (rows.length) {
|
|
91
|
+
lines.push('| Manifest | Detail |', '|----------|--------|');
|
|
92
|
+
for (const r of rows) lines.push(`| ${r.manifest} | ${r.detail} |`);
|
|
93
|
+
}
|
|
94
|
+
if (configs.length) {
|
|
95
|
+
if (lines.length) lines.push('');
|
|
96
|
+
lines.push(`**Config files:** ${configs.map((c) => '`' + c + '`').join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { analyze };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment-variable schema extractor (v8.5 C1).
|
|
5
|
+
*
|
|
6
|
+
* Surfaces the environment the project actually reads — from source across
|
|
7
|
+
* JS/TS, Python, Ruby, and Go, plus keys declared in a committed `.env.example`
|
|
8
|
+
* / `.env.sample` / `.env.template`. Pure, zero-dependency, deterministic.
|
|
9
|
+
*
|
|
10
|
+
* @param {string[]} files — absolute file paths to analyze (srcDirs-scoped)
|
|
11
|
+
* @param {string} cwd — project root
|
|
12
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const SCAN_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go']);
|
|
19
|
+
const EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template', '.env.dist'];
|
|
20
|
+
|
|
21
|
+
// process.env.X / process.env['X'] / import.meta.env.X / Deno.env.get('X')
|
|
22
|
+
const JS_RE = /(?:process\.env|import\.meta\.env)(?:\.([A-Z_][A-Z0-9_]*)|\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\])|Deno\.env\.get\(\s*['"]([A-Z_][A-Z0-9_]*)['"]/g;
|
|
23
|
+
// os.environ['X'] / os.environ.get('X') / os.getenv('X') / getenv('X')
|
|
24
|
+
const PY_RE = /(?:os\.)?(?:environ(?:\.get)?\[?\s*['"]([A-Z_][A-Z0-9_]*)['"]|getenv\(\s*['"]([A-Z_][A-Z0-9_]*)['"])/g;
|
|
25
|
+
const RB_RE = /ENV\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\]/g;
|
|
26
|
+
const GO_RE = /os\.(?:Getenv|LookupEnv)\(\s*["`']([A-Z_][A-Z0-9_]*)["`']/g;
|
|
27
|
+
|
|
28
|
+
const MAX_ROWS = 200;
|
|
29
|
+
|
|
30
|
+
function collectMatches(re, content, into) {
|
|
31
|
+
let m;
|
|
32
|
+
re.lastIndex = 0;
|
|
33
|
+
while ((m = re.exec(content)) !== null) {
|
|
34
|
+
const name = m[1] || m[2] || m[3];
|
|
35
|
+
if (name) into.add(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readExampleKeys(cwd) {
|
|
40
|
+
const keys = new Set();
|
|
41
|
+
for (const name of EXAMPLE_FILES) {
|
|
42
|
+
let content;
|
|
43
|
+
try { content = fs.readFileSync(path.join(cwd, name), 'utf8'); } catch (_) { continue; }
|
|
44
|
+
for (const line of content.split('\n')) {
|
|
45
|
+
const t = line.trim();
|
|
46
|
+
if (!t || t.startsWith('#')) continue;
|
|
47
|
+
const eq = t.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
|
|
48
|
+
if (eq) keys.add(eq[1]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return keys;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function analyze(files, cwd) {
|
|
55
|
+
const fromCode = new Set();
|
|
56
|
+
|
|
57
|
+
for (const filePath of files) {
|
|
58
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
59
|
+
if (!SCAN_EXTS.has(ext)) continue;
|
|
60
|
+
let content;
|
|
61
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
|
|
62
|
+
|
|
63
|
+
if (ext === '.py') collectMatches(PY_RE, content, fromCode);
|
|
64
|
+
else if (ext === '.rb') collectMatches(RB_RE, content, fromCode);
|
|
65
|
+
else if (ext === '.go') collectMatches(GO_RE, content, fromCode);
|
|
66
|
+
else collectMatches(JS_RE, content, fromCode);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fromExample = readExampleKeys(cwd);
|
|
70
|
+
const all = new Set([...fromCode, ...fromExample]);
|
|
71
|
+
if (all.size === 0) return '';
|
|
72
|
+
|
|
73
|
+
const names = [...all].sort();
|
|
74
|
+
const lines = [
|
|
75
|
+
'| Variable | Source |',
|
|
76
|
+
'|----------|--------|',
|
|
77
|
+
];
|
|
78
|
+
for (const name of names.slice(0, MAX_ROWS)) {
|
|
79
|
+
const src = [];
|
|
80
|
+
if (fromCode.has(name)) src.push('code');
|
|
81
|
+
if (fromExample.has(name)) src.push('.env.example');
|
|
82
|
+
lines.push(`| ${name} | ${src.join(', ')} |`);
|
|
83
|
+
}
|
|
84
|
+
if (names.length > MAX_ROWS) {
|
|
85
|
+
lines.push(`| … | +${names.length - MAX_ROWS} more |`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { analyze };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database-migration extractor (v8.5 C1).
|
|
5
|
+
*
|
|
6
|
+
* Detects schema-migration files across the common frameworks — Rails
|
|
7
|
+
* (db/migrate), Django/Alembic, Prisma, Flyway (`V1__name.sql`), knex/Sequelize,
|
|
8
|
+
* and timestamped SQL — and surfaces them with a parsed version + name. Pure,
|
|
9
|
+
* zero-dependency, deterministic.
|
|
10
|
+
*
|
|
11
|
+
* @param {string[]} files — absolute file paths (unused; the tree is walked)
|
|
12
|
+
* @param {string} cwd — project root
|
|
13
|
+
* @returns {string} formatted markdown table (empty string if none found)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const MAX_DEPTH = 6;
|
|
20
|
+
const MAX_ROWS = 200;
|
|
21
|
+
const SKIP_DIR = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'target', '.venv', 'venv', '__pycache__']);
|
|
22
|
+
const MIG_EXT = new Set(['.sql', '.rb', '.py', '.js', '.ts']);
|
|
23
|
+
|
|
24
|
+
// A directory whose path marks its children as migrations.
|
|
25
|
+
const MIG_DIR_RE = /(^|\/)(db\/migrate|migrations?|alembic\/versions|prisma\/migrations)$/i;
|
|
26
|
+
// A filename that is itself a migration regardless of directory.
|
|
27
|
+
const FLYWAY_RE = /^V\d+(?:[._]\d+)*__(.+)\.(sql|java)$/;
|
|
28
|
+
const TIMESTAMP_RE = /^(\d{8,})[_-](.+)\.(sql|rb|py|js|ts)$/;
|
|
29
|
+
const NAMED_RE = /[._-]migrations?[._-]/i;
|
|
30
|
+
|
|
31
|
+
function walk(dir, cwd, depth, out) {
|
|
32
|
+
if (depth > MAX_DEPTH) return;
|
|
33
|
+
let entries;
|
|
34
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
35
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
36
|
+
|
|
37
|
+
const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
|
|
38
|
+
const dirIsMigration = MIG_DIR_RE.test(relDir);
|
|
39
|
+
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
if (e.isDirectory()) {
|
|
42
|
+
if (SKIP_DIR.has(e.name)) continue;
|
|
43
|
+
walk(path.join(dir, e.name), cwd, depth + 1, out);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
47
|
+
if (!MIG_EXT.has(ext)) continue;
|
|
48
|
+
|
|
49
|
+
const rel = path.relative(cwd, path.join(dir, e.name)).replace(/\\/g, '/');
|
|
50
|
+
let version = null;
|
|
51
|
+
let name = null;
|
|
52
|
+
|
|
53
|
+
let m;
|
|
54
|
+
if ((m = e.name.match(FLYWAY_RE))) { version = e.name.split('__')[0]; name = m[1].replace(/_/g, ' '); }
|
|
55
|
+
else if ((m = e.name.match(TIMESTAMP_RE))) { version = m[1]; name = m[2].replace(/[_-]/g, ' '); }
|
|
56
|
+
else if (dirIsMigration) { version = '—'; name = e.name.replace(ext, ''); }
|
|
57
|
+
else if (NAMED_RE.test(e.name)) { version = '—'; name = e.name.replace(ext, ''); }
|
|
58
|
+
else continue;
|
|
59
|
+
|
|
60
|
+
out.push({ version, name, file: rel });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function analyze(files, cwd) {
|
|
65
|
+
const found = [];
|
|
66
|
+
walk(cwd, cwd, 0, found);
|
|
67
|
+
if (found.length === 0) return '';
|
|
68
|
+
|
|
69
|
+
found.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
70
|
+
|
|
71
|
+
const lines = [
|
|
72
|
+
'| Version | Migration | File |',
|
|
73
|
+
'|---------|-----------|------|',
|
|
74
|
+
];
|
|
75
|
+
for (const r of found.slice(0, MAX_ROWS)) {
|
|
76
|
+
lines.push(`| ${r.version} | ${r.name} | ${r.file} |`);
|
|
77
|
+
}
|
|
78
|
+
if (found.length > MAX_ROWS) {
|
|
79
|
+
lines.push(`| … | +${found.length - MAX_ROWS} more | |`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { analyze };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -21,6 +21,10 @@ const MAP_SECTIONS = {
|
|
|
21
21
|
imports: '### Import graph',
|
|
22
22
|
classes: '### Class hierarchy',
|
|
23
23
|
routes: '### Route table',
|
|
24
|
+
env: '### Environment variables',
|
|
25
|
+
buildci: '### Build & CI',
|
|
26
|
+
manifests: '### Config & manifests',
|
|
27
|
+
migrations: '### Database migrations',
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -106,7 +110,7 @@ function getMap(args, cwd) {
|
|
|
106
110
|
|
|
107
111
|
const header = MAP_SECTIONS[args.type];
|
|
108
112
|
if (!header) {
|
|
109
|
-
return `Unknown map type: "${args.type}". Use:
|
|
113
|
+
return `Unknown map type: "${args.type}". Use: ${Object.keys(MAP_SECTIONS).join(', ')}`;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
package/src/mcp/server.js
CHANGED