trace-mcp 1.9.0 → 1.10.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/dist/cli.js +612 -486
- package/dist/cli.js.map +1 -1
- package/dist/index.js +264 -469
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2140,7 +2140,7 @@ var logger = pino({
|
|
|
2140
2140
|
}, process.stderr);
|
|
2141
2141
|
|
|
2142
2142
|
// src/db/schema.ts
|
|
2143
|
-
var SCHEMA_VERSION =
|
|
2143
|
+
var SCHEMA_VERSION = 18;
|
|
2144
2144
|
var DDL = `
|
|
2145
2145
|
-- ============================================================
|
|
2146
2146
|
-- UNIFIED ADDRESS SPACE
|
|
@@ -6585,57 +6585,41 @@ var FilePersister = class {
|
|
|
6585
6585
|
if (ext.gitignored) {
|
|
6586
6586
|
store.updateFileGitignored(fileId, true);
|
|
6587
6587
|
}
|
|
6588
|
-
|
|
6589
|
-
const insertedIds = store.insertSymbols(fileId, ext.symbols);
|
|
6590
|
-
const trigramBySymbolId = /* @__PURE__ */ new Map();
|
|
6591
|
-
for (let i = 0; i < ext.symbols.length; i++) {
|
|
6592
|
-
trigramBySymbolId.set(ext.symbols[i].symbolId, {
|
|
6593
|
-
id: insertedIds[i],
|
|
6594
|
-
name: ext.symbols[i].name,
|
|
6595
|
-
fqn: ext.symbols[i].fqn ?? null
|
|
6596
|
-
});
|
|
6597
|
-
}
|
|
6598
|
-
indexTrigramsBatch(store.db, [...trigramBySymbolId.values()]);
|
|
6599
|
-
}
|
|
6600
|
-
if (ext.otherEdges.length > 0) this.storeRawEdges(ext.otherEdges);
|
|
6588
|
+
this.persistSymbolsAndEntities(fileId, ext.symbols, ext.otherEdges, ext);
|
|
6601
6589
|
if (ext.importEdges.length > 0) {
|
|
6602
6590
|
this.state.pendingImports.set(fileId, ext.importEdges);
|
|
6603
6591
|
}
|
|
6604
|
-
for (const r of ext.routes) store.insertRoute(r, fileId);
|
|
6605
|
-
for (const c of ext.components) store.insertComponent(c, fileId);
|
|
6606
|
-
for (const m of ext.migrations) store.insertMigration(m, fileId);
|
|
6607
|
-
if (ext.ormModels.length > 0) {
|
|
6608
|
-
this.storeOrmResults(ext.ormModels, ext.ormAssociations, fileId);
|
|
6609
|
-
}
|
|
6610
|
-
for (const s of ext.rnScreens) store.insertRnScreen(s, fileId);
|
|
6611
6592
|
for (const fwResult of ext.frameworkExtracts) {
|
|
6612
|
-
|
|
6613
|
-
const fwIds = store.insertSymbols(fileId, fwResult.symbols);
|
|
6614
|
-
const fwTrigramBySymbolId = /* @__PURE__ */ new Map();
|
|
6615
|
-
for (let i = 0; i < fwResult.symbols.length; i++) {
|
|
6616
|
-
fwTrigramBySymbolId.set(fwResult.symbols[i].symbolId, {
|
|
6617
|
-
id: fwIds[i],
|
|
6618
|
-
name: fwResult.symbols[i].name,
|
|
6619
|
-
fqn: fwResult.symbols[i].fqn ?? null
|
|
6620
|
-
});
|
|
6621
|
-
}
|
|
6622
|
-
indexTrigramsBatch(store.db, [...fwTrigramBySymbolId.values()]);
|
|
6623
|
-
}
|
|
6624
|
-
if (fwResult.edges?.length) {
|
|
6625
|
-
this.storeRawEdges(fwResult.edges);
|
|
6626
|
-
}
|
|
6627
|
-
for (const r of fwResult.routes ?? []) store.insertRoute(r, fileId);
|
|
6628
|
-
for (const c of fwResult.components ?? []) store.insertComponent(c, fileId);
|
|
6629
|
-
for (const m of fwResult.migrations ?? []) store.insertMigration(m, fileId);
|
|
6630
|
-
if (fwResult.ormModels?.length) {
|
|
6631
|
-
this.storeOrmResults(fwResult.ormModels, fwResult.ormAssociations ?? [], fileId);
|
|
6632
|
-
}
|
|
6633
|
-
for (const s of fwResult.rnScreens ?? []) store.insertRnScreen(s, fileId);
|
|
6593
|
+
this.persistSymbolsAndEntities(fileId, fwResult.symbols, fwResult.edges ?? [], fwResult);
|
|
6634
6594
|
if (fwResult.frameworkRole) {
|
|
6635
6595
|
store.updateFileStatus(fileId, fwResult.status, fwResult.frameworkRole);
|
|
6636
6596
|
}
|
|
6637
6597
|
}
|
|
6638
6598
|
}
|
|
6599
|
+
/** Insert symbols+trigrams, edges, and entities (routes/components/migrations/ORM/screens). */
|
|
6600
|
+
persistSymbolsAndEntities(fileId, symbols, edges, entities) {
|
|
6601
|
+
const store = this.state.store;
|
|
6602
|
+
if (symbols.length > 0) {
|
|
6603
|
+
const insertedIds = store.insertSymbols(fileId, symbols);
|
|
6604
|
+
const trigramBySymbolId = /* @__PURE__ */ new Map();
|
|
6605
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
6606
|
+
trigramBySymbolId.set(symbols[i].symbolId, {
|
|
6607
|
+
id: insertedIds[i],
|
|
6608
|
+
name: symbols[i].name,
|
|
6609
|
+
fqn: symbols[i].fqn ?? null
|
|
6610
|
+
});
|
|
6611
|
+
}
|
|
6612
|
+
indexTrigramsBatch(store.db, [...trigramBySymbolId.values()]);
|
|
6613
|
+
}
|
|
6614
|
+
if (edges.length > 0) this.storeRawEdges(edges);
|
|
6615
|
+
for (const r of entities.routes ?? []) store.insertRoute(r, fileId);
|
|
6616
|
+
for (const c of entities.components ?? []) store.insertComponent(c, fileId);
|
|
6617
|
+
for (const m of entities.migrations ?? []) store.insertMigration(m, fileId);
|
|
6618
|
+
if (entities.ormModels?.length) {
|
|
6619
|
+
this.storeOrmResults(entities.ormModels, entities.ormAssociations ?? [], fileId);
|
|
6620
|
+
}
|
|
6621
|
+
for (const s of entities.rnScreens ?? []) store.insertRnScreen(s, fileId);
|
|
6622
|
+
}
|
|
6639
6623
|
/**
|
|
6640
6624
|
* Fast path for incremental re-indexing: if the set of symbols is structurally
|
|
6641
6625
|
* identical (same symbolIds, names, kinds, fqns, signatures), only update byte
|
|
@@ -7795,58 +7779,10 @@ var IndexingPipeline = class _IndexingPipeline {
|
|
|
7795
7779
|
this._gitignore = new GitignoreMatcher(this.rootPath);
|
|
7796
7780
|
this._traceignore = new TraceignoreMatcher(this.rootPath, this.config.ignore);
|
|
7797
7781
|
this.registerFrameworkEdgeTypes();
|
|
7798
|
-
const extractor = new FileExtractor({
|
|
7799
|
-
store: this.store,
|
|
7800
|
-
registry: this.registry,
|
|
7801
|
-
rootPath: this.rootPath,
|
|
7802
|
-
workspaces: this.workspaces,
|
|
7803
|
-
gitignore: this._gitignore,
|
|
7804
|
-
fileContentCache: this._fileContentCache,
|
|
7805
|
-
buildProjectContext: () => this.buildProjectContext()
|
|
7806
|
-
});
|
|
7807
7782
|
try {
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
for (let i = 0; i < relPaths.length; i += BATCH_SIZE3) {
|
|
7812
|
-
const batch = relPaths.slice(i, i + BATCH_SIZE3);
|
|
7813
|
-
const extractions = [];
|
|
7814
|
-
for (let c = 0; c < batch.length; c += CONCURRENCY) {
|
|
7815
|
-
const chunk = batch.slice(c, c + CONCURRENCY);
|
|
7816
|
-
const results = await Promise.all(
|
|
7817
|
-
chunk.map((relPath) => extractor.extract(relPath, force))
|
|
7818
|
-
);
|
|
7819
|
-
for (const ext of results) {
|
|
7820
|
-
if (ext === "skipped") {
|
|
7821
|
-
result.skipped++;
|
|
7822
|
-
continue;
|
|
7823
|
-
}
|
|
7824
|
-
if (ext === "error") {
|
|
7825
|
-
result.errors++;
|
|
7826
|
-
continue;
|
|
7827
|
-
}
|
|
7828
|
-
extractions.push(ext);
|
|
7829
|
-
}
|
|
7830
|
-
}
|
|
7831
|
-
if (extractions.length > 0) {
|
|
7832
|
-
const state = this.getPipelineState();
|
|
7833
|
-
const persistEdgeResolver = new EdgeResolver(state);
|
|
7834
|
-
const persister = new FilePersister(state, (edges) => persistEdgeResolver.storeRawEdges(edges));
|
|
7835
|
-
persister.persistBatch(extractions);
|
|
7836
|
-
result.indexed += extractions.length;
|
|
7837
|
-
}
|
|
7838
|
-
const processed = result.indexed + result.skipped + result.errors;
|
|
7839
|
-
this.progress?.update("indexing", { processed });
|
|
7840
|
-
}
|
|
7841
|
-
enableFts5Triggers(this.store.db);
|
|
7842
|
-
const edgeResolver = new EdgeResolver(this.getPipelineState());
|
|
7843
|
-
await edgeResolver.resolveEdges(this.buildProjectContext(), this.buildResolveContext());
|
|
7844
|
-
edgeResolver.resolveOrmAssociationEdges();
|
|
7845
|
-
edgeResolver.resolveTypeScriptHeritageEdges();
|
|
7846
|
-
edgeResolver.resolveEsmImportEdges();
|
|
7847
|
-
edgeResolver.resolveTestCoversEdges();
|
|
7848
|
-
const envIndexer = new EnvIndexer(this.store, this.config, this.rootPath, this._traceignore);
|
|
7849
|
-
await envIndexer.indexEnvFiles(force);
|
|
7783
|
+
await this.extractAndPersist(relPaths, force, result);
|
|
7784
|
+
await this.resolveAllEdges();
|
|
7785
|
+
await this.indexEnvFiles(force);
|
|
7850
7786
|
} finally {
|
|
7851
7787
|
this._fileContentCache.clear();
|
|
7852
7788
|
this._pendingImports.clear();
|
|
@@ -7870,6 +7806,66 @@ var IndexingPipeline = class _IndexingPipeline {
|
|
|
7870
7806
|
logger.info(result, "Indexing pipeline completed");
|
|
7871
7807
|
return result;
|
|
7872
7808
|
}
|
|
7809
|
+
/** Pass 1: extract symbols from files and persist in batched transactions. */
|
|
7810
|
+
async extractAndPersist(relPaths, force, result) {
|
|
7811
|
+
const extractor = new FileExtractor({
|
|
7812
|
+
store: this.store,
|
|
7813
|
+
registry: this.registry,
|
|
7814
|
+
rootPath: this.rootPath,
|
|
7815
|
+
workspaces: this.workspaces,
|
|
7816
|
+
gitignore: this._gitignore,
|
|
7817
|
+
fileContentCache: this._fileContentCache,
|
|
7818
|
+
buildProjectContext: () => this.buildProjectContext()
|
|
7819
|
+
});
|
|
7820
|
+
disableFts5Triggers(this.store.db);
|
|
7821
|
+
const BATCH_SIZE3 = Math.min(500, Math.max(100, Math.ceil(relPaths.length / 20)));
|
|
7822
|
+
const CONCURRENCY = Math.min(8, cpus().length);
|
|
7823
|
+
for (let i = 0; i < relPaths.length; i += BATCH_SIZE3) {
|
|
7824
|
+
const batch = relPaths.slice(i, i + BATCH_SIZE3);
|
|
7825
|
+
const extractions = [];
|
|
7826
|
+
for (let c = 0; c < batch.length; c += CONCURRENCY) {
|
|
7827
|
+
const chunk = batch.slice(c, c + CONCURRENCY);
|
|
7828
|
+
const results = await Promise.all(
|
|
7829
|
+
chunk.map((relPath) => extractor.extract(relPath, force))
|
|
7830
|
+
);
|
|
7831
|
+
for (const ext of results) {
|
|
7832
|
+
if (ext === "skipped") {
|
|
7833
|
+
result.skipped++;
|
|
7834
|
+
continue;
|
|
7835
|
+
}
|
|
7836
|
+
if (ext === "error") {
|
|
7837
|
+
result.errors++;
|
|
7838
|
+
continue;
|
|
7839
|
+
}
|
|
7840
|
+
extractions.push(ext);
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
if (extractions.length > 0) {
|
|
7844
|
+
const state = this.getPipelineState();
|
|
7845
|
+
const persistEdgeResolver = new EdgeResolver(state);
|
|
7846
|
+
const persister = new FilePersister(state, (edges) => persistEdgeResolver.storeRawEdges(edges));
|
|
7847
|
+
persister.persistBatch(extractions);
|
|
7848
|
+
result.indexed += extractions.length;
|
|
7849
|
+
}
|
|
7850
|
+
const processed = result.indexed + result.skipped + result.errors;
|
|
7851
|
+
this.progress?.update("indexing", { processed });
|
|
7852
|
+
}
|
|
7853
|
+
enableFts5Triggers(this.store.db);
|
|
7854
|
+
}
|
|
7855
|
+
/** Pass 2: resolve all edge types (imports, heritage, ORM, tests). */
|
|
7856
|
+
async resolveAllEdges() {
|
|
7857
|
+
const edgeResolver = new EdgeResolver(this.getPipelineState());
|
|
7858
|
+
await edgeResolver.resolveEdges(this.buildProjectContext(), this.buildResolveContext());
|
|
7859
|
+
edgeResolver.resolveOrmAssociationEdges();
|
|
7860
|
+
edgeResolver.resolveTypeScriptHeritageEdges();
|
|
7861
|
+
edgeResolver.resolveEsmImportEdges();
|
|
7862
|
+
edgeResolver.resolveTestCoversEdges();
|
|
7863
|
+
}
|
|
7864
|
+
/** Pass 3: index .env files for environment variable tracking. */
|
|
7865
|
+
async indexEnvFiles(force) {
|
|
7866
|
+
const envIndexer = new EnvIndexer(this.store, this.config, this.rootPath, this._traceignore);
|
|
7867
|
+
await envIndexer.indexEnvFiles(force);
|
|
7868
|
+
}
|
|
7873
7869
|
buildProjectContext() {
|
|
7874
7870
|
if (!this._projectContext) {
|
|
7875
7871
|
this._projectContext = buildProjectContext(this.rootPath);
|
|
@@ -10420,186 +10416,6 @@ function getProjectMap(store, registry, summaryOnly, projectContext) {
|
|
|
10420
10416
|
}
|
|
10421
10417
|
|
|
10422
10418
|
// src/tools/analysis/duplication.ts
|
|
10423
|
-
var CHECKABLE_KINDS = /* @__PURE__ */ new Set([
|
|
10424
|
-
"function",
|
|
10425
|
-
"class",
|
|
10426
|
-
"method",
|
|
10427
|
-
"interface",
|
|
10428
|
-
"type_alias",
|
|
10429
|
-
"enum"
|
|
10430
|
-
]);
|
|
10431
|
-
var TRIVIAL_NAMES = /* @__PURE__ */ new Set([
|
|
10432
|
-
"constructor",
|
|
10433
|
-
"toString",
|
|
10434
|
-
"toJSON",
|
|
10435
|
-
"valueOf",
|
|
10436
|
-
"render",
|
|
10437
|
-
"setup",
|
|
10438
|
-
"main",
|
|
10439
|
-
"index",
|
|
10440
|
-
"default",
|
|
10441
|
-
"init",
|
|
10442
|
-
"create",
|
|
10443
|
-
"get",
|
|
10444
|
-
"set",
|
|
10445
|
-
"delete",
|
|
10446
|
-
"update",
|
|
10447
|
-
"handle",
|
|
10448
|
-
"process",
|
|
10449
|
-
"run",
|
|
10450
|
-
"start",
|
|
10451
|
-
"stop",
|
|
10452
|
-
"reset",
|
|
10453
|
-
"configure",
|
|
10454
|
-
"register",
|
|
10455
|
-
"execute",
|
|
10456
|
-
"validate",
|
|
10457
|
-
"transform",
|
|
10458
|
-
"apply",
|
|
10459
|
-
"call",
|
|
10460
|
-
"bind",
|
|
10461
|
-
"map",
|
|
10462
|
-
"filter",
|
|
10463
|
-
"reduce",
|
|
10464
|
-
"forEach",
|
|
10465
|
-
"connect",
|
|
10466
|
-
"disconnect",
|
|
10467
|
-
"open",
|
|
10468
|
-
"close",
|
|
10469
|
-
"build",
|
|
10470
|
-
"destroy",
|
|
10471
|
-
"mount",
|
|
10472
|
-
"unmount",
|
|
10473
|
-
"dispose",
|
|
10474
|
-
"serialize",
|
|
10475
|
-
"deserialize"
|
|
10476
|
-
]);
|
|
10477
|
-
var TEST_PATH_RE2 = /(?:^|[/\\])(?:tests?|__tests__|spec)[/\\]|\.(?:test|spec)\.[jt]sx?$/i;
|
|
10478
|
-
var MIN_NAME_LENGTH = 4;
|
|
10479
|
-
var MAX_SYMBOLS_PER_FILE2 = 30;
|
|
10480
|
-
var W_NAME = 0.45;
|
|
10481
|
-
var W_KIND = 0.15;
|
|
10482
|
-
var W_SIGNATURE = 0.25;
|
|
10483
|
-
var W_TOKEN = 0.15;
|
|
10484
|
-
function tokenizeName(name) {
|
|
10485
|
-
const parts = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split(/[_\-./:]+/).filter((p5) => p5.length > 1);
|
|
10486
|
-
return new Set(parts);
|
|
10487
|
-
}
|
|
10488
|
-
function jaccard(a, b) {
|
|
10489
|
-
if (a.size === 0 && b.size === 0) return 0;
|
|
10490
|
-
let intersection = 0;
|
|
10491
|
-
for (const t of a) {
|
|
10492
|
-
if (b.has(t)) intersection++;
|
|
10493
|
-
}
|
|
10494
|
-
const union = a.size + b.size - intersection;
|
|
10495
|
-
return union === 0 ? 0 : intersection / union;
|
|
10496
|
-
}
|
|
10497
|
-
function signatureSimilarity(srcParamCount, candParamCount) {
|
|
10498
|
-
if (srcParamCount == null && candParamCount == null) return 0.5;
|
|
10499
|
-
if (srcParamCount == null || candParamCount == null) return 0.3;
|
|
10500
|
-
const max = Math.max(srcParamCount, candParamCount, 1);
|
|
10501
|
-
return 1 - Math.abs(srcParamCount - candParamCount) / max;
|
|
10502
|
-
}
|
|
10503
|
-
function getHeritageNames(metadata) {
|
|
10504
|
-
if (!metadata) return /* @__PURE__ */ new Set();
|
|
10505
|
-
try {
|
|
10506
|
-
const parsed = JSON.parse(metadata);
|
|
10507
|
-
const names = /* @__PURE__ */ new Set();
|
|
10508
|
-
if (typeof parsed.extends === "string") names.add(parsed.extends);
|
|
10509
|
-
if (Array.isArray(parsed.extends)) {
|
|
10510
|
-
for (const e of parsed.extends) if (typeof e === "string") names.add(e);
|
|
10511
|
-
}
|
|
10512
|
-
if (typeof parsed.implements === "string") names.add(parsed.implements);
|
|
10513
|
-
if (Array.isArray(parsed.implements)) {
|
|
10514
|
-
for (const i of parsed.implements) if (typeof i === "string") names.add(i);
|
|
10515
|
-
}
|
|
10516
|
-
return names;
|
|
10517
|
-
} catch {
|
|
10518
|
-
return /* @__PURE__ */ new Set();
|
|
10519
|
-
}
|
|
10520
|
-
}
|
|
10521
|
-
function findDuplicateSymbols(store, db, sourceSymbols, sourceFileId, sourceFilePath, options) {
|
|
10522
|
-
const threshold = options?.threshold ?? 0.7;
|
|
10523
|
-
const maxResults = options?.maxResults ?? 10;
|
|
10524
|
-
const sourceIsTest = TEST_PATH_RE2.test(sourceFilePath);
|
|
10525
|
-
const heritageNames = /* @__PURE__ */ new Set();
|
|
10526
|
-
for (const sym of sourceSymbols) {
|
|
10527
|
-
const h = getHeritageNames(sym.metadata);
|
|
10528
|
-
for (const n of h) heritageNames.add(n);
|
|
10529
|
-
}
|
|
10530
|
-
const checkable = sourceSymbols.filter(
|
|
10531
|
-
(s) => CHECKABLE_KINDS.has(s.kind) && s.name.length >= MIN_NAME_LENGTH && !TRIVIAL_NAMES.has(s.name)
|
|
10532
|
-
).slice(0, MAX_SYMBOLS_PER_FILE2);
|
|
10533
|
-
const allWarnings = [];
|
|
10534
|
-
const paramCountStmt = db.prepare(
|
|
10535
|
-
"SELECT param_count FROM symbols WHERE id = ?"
|
|
10536
|
-
);
|
|
10537
|
-
for (const src of checkable) {
|
|
10538
|
-
const srcParamCount = src.param_count ?? paramCountStmt.get(src.id)?.param_count ?? null;
|
|
10539
|
-
const srcTokens = tokenizeName(src.fqn ?? src.name);
|
|
10540
|
-
const candidates = fuzzySearch(db, src.name, {
|
|
10541
|
-
threshold: 0.25,
|
|
10542
|
-
limit: 30,
|
|
10543
|
-
kind: src.kind
|
|
10544
|
-
});
|
|
10545
|
-
for (const cand of candidates) {
|
|
10546
|
-
if (cand.fileId === sourceFileId) continue;
|
|
10547
|
-
if (cand.symbolIdStr === src.symbol_id) continue;
|
|
10548
|
-
const candFile = store.getFileById(cand.fileId);
|
|
10549
|
-
if (!candFile) continue;
|
|
10550
|
-
const candIsTest = TEST_PATH_RE2.test(candFile.path);
|
|
10551
|
-
if (sourceIsTest !== candIsTest) continue;
|
|
10552
|
-
if (heritageNames.has(cand.name)) continue;
|
|
10553
|
-
const candSymbol = getCandidateSymbol(db, cand.symbolId);
|
|
10554
|
-
if (candSymbol) {
|
|
10555
|
-
const candHeritage = getHeritageNames(candSymbol.metadata);
|
|
10556
|
-
for (const h of candHeritage) {
|
|
10557
|
-
if (heritageNames.has(h)) continue;
|
|
10558
|
-
}
|
|
10559
|
-
}
|
|
10560
|
-
const nameSim = cand.similarity;
|
|
10561
|
-
const kindMatch = cand.kind === src.kind ? 1 : 0;
|
|
10562
|
-
const candParamCount = candSymbol?.param_count ?? paramCountStmt.get(cand.symbolId)?.param_count ?? null;
|
|
10563
|
-
const sigSim = signatureSimilarity(srcParamCount, candParamCount);
|
|
10564
|
-
const candTokens = tokenizeName(cand.fqn ?? cand.name);
|
|
10565
|
-
const tokenOvl = jaccard(srcTokens, candTokens);
|
|
10566
|
-
const score = W_NAME * nameSim + W_KIND * kindMatch + W_SIGNATURE * sigSim + W_TOKEN * tokenOvl;
|
|
10567
|
-
if (score >= threshold) {
|
|
10568
|
-
allWarnings.push({
|
|
10569
|
-
source_symbol_id: src.symbol_id,
|
|
10570
|
-
source_name: src.name,
|
|
10571
|
-
source_file: sourceFilePath,
|
|
10572
|
-
duplicate_symbol_id: cand.symbolIdStr,
|
|
10573
|
-
duplicate_name: cand.name,
|
|
10574
|
-
duplicate_file: candFile.path,
|
|
10575
|
-
duplicate_line: candSymbol?.line_start ?? null,
|
|
10576
|
-
score: Math.round(score * 1e3) / 1e3,
|
|
10577
|
-
signals: {
|
|
10578
|
-
name_similarity: Math.round(nameSim * 1e3) / 1e3,
|
|
10579
|
-
kind_match: kindMatch,
|
|
10580
|
-
signature_similarity: Math.round(sigSim * 1e3) / 1e3,
|
|
10581
|
-
token_overlap: Math.round(tokenOvl * 1e3) / 1e3
|
|
10582
|
-
}
|
|
10583
|
-
});
|
|
10584
|
-
}
|
|
10585
|
-
}
|
|
10586
|
-
}
|
|
10587
|
-
allWarnings.sort((a, b) => b.score - a.score);
|
|
10588
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10589
|
-
const deduped = [];
|
|
10590
|
-
for (const w of allWarnings) {
|
|
10591
|
-
const key = `${w.source_symbol_id}::${w.duplicate_symbol_id}`;
|
|
10592
|
-
if (seen.has(key)) continue;
|
|
10593
|
-
seen.add(key);
|
|
10594
|
-
deduped.push(w);
|
|
10595
|
-
if (deduped.length >= maxResults) break;
|
|
10596
|
-
}
|
|
10597
|
-
return {
|
|
10598
|
-
warnings: deduped,
|
|
10599
|
-
symbols_checked: checkable.length,
|
|
10600
|
-
threshold
|
|
10601
|
-
};
|
|
10602
|
-
}
|
|
10603
10419
|
function checkFileForDuplicates(store, db, filePath, options) {
|
|
10604
10420
|
const file = store.getFile(filePath);
|
|
10605
10421
|
if (!file) {
|
|
@@ -10642,11 +10458,6 @@ function checkSymbolForDuplicates(store, db, query, options) {
|
|
|
10642
10458
|
}
|
|
10643
10459
|
return { warnings: [], symbols_checked: 0, threshold };
|
|
10644
10460
|
}
|
|
10645
|
-
function getCandidateSymbol(db, symbolId) {
|
|
10646
|
-
return db.prepare(
|
|
10647
|
-
"SELECT * FROM symbols WHERE id = ?"
|
|
10648
|
-
).get(symbolId);
|
|
10649
|
-
}
|
|
10650
10461
|
|
|
10651
10462
|
// src/tools/register/core.ts
|
|
10652
10463
|
function registerCoreTools(server, ctx) {
|
|
@@ -12488,11 +12299,11 @@ function suggestQueries(store) {
|
|
|
12488
12299
|
|
|
12489
12300
|
// src/tools/navigation/related.ts
|
|
12490
12301
|
import { ok as ok4, err as err5 } from "neverthrow";
|
|
12491
|
-
function
|
|
12302
|
+
function tokenizeName(name) {
|
|
12492
12303
|
const parts = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split(/[_\-.]/).filter((p5) => p5.length > 1);
|
|
12493
12304
|
return new Set(parts);
|
|
12494
12305
|
}
|
|
12495
|
-
function
|
|
12306
|
+
function jaccard(a, b) {
|
|
12496
12307
|
if (a.size === 0 && b.size === 0) return 0;
|
|
12497
12308
|
let intersection = 0;
|
|
12498
12309
|
for (const t of a) {
|
|
@@ -12581,13 +12392,13 @@ function getRelatedSymbols(store, opts) {
|
|
|
12581
12392
|
}
|
|
12582
12393
|
}
|
|
12583
12394
|
}
|
|
12584
|
-
const targetTokens =
|
|
12395
|
+
const targetTokens = tokenizeName(target.name);
|
|
12585
12396
|
const candidateIds = [...scores.keys()];
|
|
12586
12397
|
const candidateSymbols = store.getSymbolsByIds(candidateIds);
|
|
12587
12398
|
const candidateFileIds = [...new Set([...candidateSymbols.values()].map((s) => s.file_id))];
|
|
12588
12399
|
const fileMap = store.getFilesByIds(candidateFileIds);
|
|
12589
12400
|
for (const [symId4, sym] of candidateSymbols) {
|
|
12590
|
-
const nameScore =
|
|
12401
|
+
const nameScore = jaccard(targetTokens, tokenizeName(sym.name));
|
|
12591
12402
|
if (nameScore > 0) {
|
|
12592
12403
|
ensureEntry(symId4).name_overlap = nameScore;
|
|
12593
12404
|
}
|
|
@@ -13684,63 +13495,9 @@ var FederationManager = class {
|
|
|
13684
13495
|
detectionSource: svc.detectionSource,
|
|
13685
13496
|
metadata: svc.metadata
|
|
13686
13497
|
});
|
|
13687
|
-
|
|
13688
|
-
if (opts?.contractPaths) {
|
|
13689
|
-
for (const cp of opts.contractPaths) {
|
|
13690
|
-
const absContract = path29.resolve(absRoot, cp);
|
|
13691
|
-
if (fs25.existsSync(absContract)) {
|
|
13692
|
-
const additionalContracts = parseContracts(path29.dirname(absContract));
|
|
13693
|
-
contracts.push(...additionalContracts.filter(
|
|
13694
|
-
(c) => path29.resolve(absRoot, c.specPath) === absContract
|
|
13695
|
-
));
|
|
13696
|
-
}
|
|
13697
|
-
}
|
|
13698
|
-
}
|
|
13699
|
-
for (const contract of contracts) {
|
|
13700
|
-
const contractId = this.topoStore.insertContract(serviceId, {
|
|
13701
|
-
contractType: contract.type,
|
|
13702
|
-
specPath: contract.specPath,
|
|
13703
|
-
version: contract.version,
|
|
13704
|
-
parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
|
|
13705
|
-
});
|
|
13706
|
-
this.topoStore.insertEndpoints(
|
|
13707
|
-
contractId,
|
|
13708
|
-
serviceId,
|
|
13709
|
-
contract.endpoints.map((e) => ({
|
|
13710
|
-
method: e.method ?? void 0,
|
|
13711
|
-
path: e.path,
|
|
13712
|
-
operationId: e.operationId,
|
|
13713
|
-
requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
|
|
13714
|
-
responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
|
|
13715
|
-
}))
|
|
13716
|
-
);
|
|
13717
|
-
if (contract.events.length > 0) {
|
|
13718
|
-
this.topoStore.insertEventChannels(
|
|
13719
|
-
contractId,
|
|
13720
|
-
serviceId,
|
|
13721
|
-
contract.events.map((e) => ({
|
|
13722
|
-
channelName: e.channelName,
|
|
13723
|
-
direction: e.direction
|
|
13724
|
-
}))
|
|
13725
|
-
);
|
|
13726
|
-
}
|
|
13727
|
-
}
|
|
13498
|
+
this.registerContracts(serviceId, svc.repoRoot, absRoot, opts?.contractPaths);
|
|
13728
13499
|
}
|
|
13729
|
-
this.
|
|
13730
|
-
const clientCalls = scanClientCalls(absRoot);
|
|
13731
|
-
if (clientCalls.length > 0) {
|
|
13732
|
-
this.topoStore.insertClientCalls(clientCalls.map((c) => ({
|
|
13733
|
-
sourceRepoId: repoId,
|
|
13734
|
-
filePath: c.filePath,
|
|
13735
|
-
line: c.line,
|
|
13736
|
-
callType: c.callType,
|
|
13737
|
-
method: c.method,
|
|
13738
|
-
urlPattern: c.urlPattern,
|
|
13739
|
-
confidence: c.confidence
|
|
13740
|
-
})));
|
|
13741
|
-
}
|
|
13742
|
-
const linkedCount = this.topoStore.linkClientCallsToEndpoints();
|
|
13743
|
-
this.buildCrossServiceEdges();
|
|
13500
|
+
const clientCalls = this.scanAndLinkClientCalls(repoId, absRoot);
|
|
13744
13501
|
this.topoStore.updateFederatedRepoSyncTime(repoId);
|
|
13745
13502
|
const stats = this.topoStore.getTopologyStats();
|
|
13746
13503
|
return {
|
|
@@ -13749,8 +13506,8 @@ var FederationManager = class {
|
|
|
13749
13506
|
services: detected.length,
|
|
13750
13507
|
contracts: stats.contracts,
|
|
13751
13508
|
endpoints: stats.endpoints,
|
|
13752
|
-
clientCalls: clientCalls.
|
|
13753
|
-
linkedCalls:
|
|
13509
|
+
clientCalls: clientCalls.scanned,
|
|
13510
|
+
linkedCalls: clientCalls.linked
|
|
13754
13511
|
};
|
|
13755
13512
|
}
|
|
13756
13513
|
/**
|
|
@@ -13854,75 +13611,26 @@ var FederationManager = class {
|
|
|
13854
13611
|
detectionSource: svc.detectionSource,
|
|
13855
13612
|
metadata: svc.metadata
|
|
13856
13613
|
});
|
|
13857
|
-
|
|
13858
|
-
for (const ec of existingContracts) {
|
|
13859
|
-
this.topoStore.insertContractSnapshot(ec.id, serviceId, {
|
|
13860
|
-
version: ec.version,
|
|
13861
|
-
specPath: ec.spec_path,
|
|
13862
|
-
contentHash: ec.content_hash ?? "",
|
|
13863
|
-
endpointsJson: ec.parsed_spec,
|
|
13864
|
-
eventsJson: "[]"
|
|
13865
|
-
});
|
|
13866
|
-
}
|
|
13614
|
+
this.snapshotContracts(serviceId);
|
|
13867
13615
|
this.topoStore.deleteContractsByService(serviceId);
|
|
13868
13616
|
const contracts = parseContracts(svc.repoRoot);
|
|
13869
13617
|
contractsUpdated += contracts.length;
|
|
13870
13618
|
for (const contract of contracts) {
|
|
13871
|
-
const contractId = this.topoStore.insertContract(serviceId, {
|
|
13872
|
-
contractType: contract.type,
|
|
13873
|
-
specPath: contract.specPath,
|
|
13874
|
-
version: contract.version,
|
|
13875
|
-
parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
|
|
13876
|
-
});
|
|
13877
|
-
this.topoStore.insertEndpoints(
|
|
13878
|
-
contractId,
|
|
13879
|
-
serviceId,
|
|
13880
|
-
contract.endpoints.map((e) => ({
|
|
13881
|
-
method: e.method ?? void 0,
|
|
13882
|
-
path: e.path,
|
|
13883
|
-
operationId: e.operationId,
|
|
13884
|
-
requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
|
|
13885
|
-
responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
|
|
13886
|
-
}))
|
|
13887
|
-
);
|
|
13888
13619
|
endpointsUpdated += contract.endpoints.length;
|
|
13889
|
-
if (contract.events.length > 0) {
|
|
13890
|
-
this.topoStore.insertEventChannels(
|
|
13891
|
-
contractId,
|
|
13892
|
-
serviceId,
|
|
13893
|
-
contract.events.map((e) => ({
|
|
13894
|
-
channelName: e.channelName,
|
|
13895
|
-
direction: e.direction
|
|
13896
|
-
}))
|
|
13897
|
-
);
|
|
13898
|
-
}
|
|
13899
13620
|
}
|
|
13621
|
+
this.registerContracts(serviceId, svc.repoRoot);
|
|
13900
13622
|
}
|
|
13901
|
-
this.
|
|
13902
|
-
|
|
13903
|
-
clientCallsScanned += calls.length;
|
|
13904
|
-
if (calls.length > 0) {
|
|
13905
|
-
this.topoStore.insertClientCalls(calls.map((c) => ({
|
|
13906
|
-
sourceRepoId: repo.id,
|
|
13907
|
-
filePath: c.filePath,
|
|
13908
|
-
line: c.line,
|
|
13909
|
-
callType: c.callType,
|
|
13910
|
-
method: c.method,
|
|
13911
|
-
urlPattern: c.urlPattern,
|
|
13912
|
-
confidence: c.confidence
|
|
13913
|
-
})));
|
|
13914
|
-
}
|
|
13623
|
+
const calls = this.scanAndLinkClientCalls(repo.id, repo.repo_root);
|
|
13624
|
+
clientCallsScanned += calls.scanned;
|
|
13915
13625
|
this.topoStore.updateFederatedRepoSyncTime(repo.id);
|
|
13916
13626
|
}
|
|
13917
|
-
const newlyLinked = this.topoStore.linkClientCallsToEndpoints();
|
|
13918
|
-
this.buildCrossServiceEdges();
|
|
13919
13627
|
return {
|
|
13920
13628
|
repos: repos.length,
|
|
13921
13629
|
servicesUpdated,
|
|
13922
13630
|
contractsUpdated,
|
|
13923
13631
|
endpointsUpdated,
|
|
13924
13632
|
clientCallsScanned,
|
|
13925
|
-
newlyLinked,
|
|
13633
|
+
newlyLinked: this.topoStore.linkClientCallsToEndpoints(),
|
|
13926
13634
|
crossRepoEdges: this.topoStore.getTopologyStats().crossEdges
|
|
13927
13635
|
};
|
|
13928
13636
|
}
|
|
@@ -13932,82 +13640,18 @@ var FederationManager = class {
|
|
|
13932
13640
|
* If per-repo DBs exist, resolves down to symbol level.
|
|
13933
13641
|
*/
|
|
13934
13642
|
getImpact(opts) {
|
|
13643
|
+
const matchingEndpoints = this.filterEndpoints(opts);
|
|
13935
13644
|
const results = [];
|
|
13936
|
-
const allEndpoints = this.topoStore.getAllEndpoints();
|
|
13937
|
-
let matchingEndpoints = allEndpoints;
|
|
13938
|
-
if (opts.endpoint) {
|
|
13939
|
-
const normalized = opts.endpoint.toLowerCase();
|
|
13940
|
-
matchingEndpoints = allEndpoints.filter(
|
|
13941
|
-
(ep) => ep.path.toLowerCase().includes(normalized)
|
|
13942
|
-
);
|
|
13943
|
-
}
|
|
13944
|
-
if (opts.method) {
|
|
13945
|
-
matchingEndpoints = matchingEndpoints.filter(
|
|
13946
|
-
(ep) => ep.method?.toUpperCase() === opts.method.toUpperCase()
|
|
13947
|
-
);
|
|
13948
|
-
}
|
|
13949
|
-
if (opts.service) {
|
|
13950
|
-
matchingEndpoints = matchingEndpoints.filter(
|
|
13951
|
-
(ep) => ep.service_name.toLowerCase() === opts.service.toLowerCase()
|
|
13952
|
-
);
|
|
13953
|
-
}
|
|
13954
13645
|
for (const ep of matchingEndpoints) {
|
|
13955
13646
|
const clientCalls = this.topoStore.getClientCallsByEndpoint(ep.id);
|
|
13956
13647
|
if (clientCalls.length === 0) continue;
|
|
13957
|
-
const
|
|
13958
|
-
for (const call of clientCalls) {
|
|
13959
|
-
const repo2 = call.source_repo_name;
|
|
13960
|
-
if (!byRepo.has(repo2)) byRepo.set(repo2, []);
|
|
13961
|
-
byRepo.get(repo2).push(call);
|
|
13962
|
-
}
|
|
13963
|
-
const clients = [];
|
|
13964
|
-
for (const [repoName, calls] of byRepo) {
|
|
13965
|
-
const repo2 = this.topoStore.getFederatedRepo(repoName);
|
|
13966
|
-
for (const call of calls) {
|
|
13967
|
-
const symbols = repo2?.db_path && fs25.existsSync(repo2.db_path) ? resolveSymbolsAtLocation(repo2.db_path, call.file_path, call.line) : [];
|
|
13968
|
-
clients.push({
|
|
13969
|
-
repo: repoName,
|
|
13970
|
-
filePath: call.file_path,
|
|
13971
|
-
line: call.line,
|
|
13972
|
-
callType: call.call_type,
|
|
13973
|
-
confidence: call.confidence,
|
|
13974
|
-
symbols
|
|
13975
|
-
});
|
|
13976
|
-
}
|
|
13977
|
-
}
|
|
13648
|
+
const clients = this.collectEndpointClients(clientCalls);
|
|
13978
13649
|
const uniqueRepos = new Set(clients.map((c) => c.repo));
|
|
13979
|
-
const
|
|
13650
|
+
const baseRisk = computeRiskLevel(uniqueRepos.size, clients.length);
|
|
13980
13651
|
const svc = this.topoStore.getAllServices().find((s) => s.id === ep.service_id);
|
|
13981
13652
|
const repo = svc ? this.topoStore.getFederatedRepo(svc.repo_root) : void 0;
|
|
13982
|
-
|
|
13983
|
-
const
|
|
13984
|
-
for (const contract of contracts) {
|
|
13985
|
-
const snapshot = this.topoStore.getLatestSnapshot(contract.id);
|
|
13986
|
-
if (!snapshot) continue;
|
|
13987
|
-
let oldEndpoints = [];
|
|
13988
|
-
try {
|
|
13989
|
-
const parsed = JSON.parse(snapshot.endpoints_json);
|
|
13990
|
-
oldEndpoints = (parsed.endpoints ?? []).map((e) => ({ method: e.method ?? null, path: e.path, requestSchema: e.requestSchema, responseSchema: e.responseSchema }));
|
|
13991
|
-
} catch {
|
|
13992
|
-
continue;
|
|
13993
|
-
}
|
|
13994
|
-
const currentEndpoints = this.topoStore.getEndpointsByService(ep.service_id).map((e) => ({
|
|
13995
|
-
method: e.method,
|
|
13996
|
-
path: e.path,
|
|
13997
|
-
requestSchema: e.request_schema,
|
|
13998
|
-
responseSchema: e.response_schema
|
|
13999
|
-
}));
|
|
14000
|
-
const epDiffs = diffEndpoints(oldEndpoints, currentEndpoints).filter((d) => d.endpoint.path === ep.path && (d.endpoint.method ?? "*") === (ep.method ?? "*"));
|
|
14001
|
-
if (epDiffs.length > 0 && epDiffs.some((d) => d.breaking)) {
|
|
14002
|
-
breakingChanges = epDiffs;
|
|
14003
|
-
}
|
|
14004
|
-
}
|
|
14005
|
-
let finalRiskLevel = riskLevel3;
|
|
14006
|
-
if (breakingChanges?.some((d) => d.breaking)) {
|
|
14007
|
-
const riskLevels = ["low", "medium", "high", "critical"];
|
|
14008
|
-
const idx = riskLevels.indexOf(riskLevel3);
|
|
14009
|
-
if (idx < riskLevels.length - 1) finalRiskLevel = riskLevels[idx + 1];
|
|
14010
|
-
}
|
|
13653
|
+
const breakingChanges = this.detectBreakingChanges(ep);
|
|
13654
|
+
const riskLevel3 = upgradeRiskIfBreaking(baseRisk, breakingChanges);
|
|
14011
13655
|
results.push({
|
|
14012
13656
|
endpoint: {
|
|
14013
13657
|
method: ep.method,
|
|
@@ -14016,13 +13660,152 @@ var FederationManager = class {
|
|
|
14016
13660
|
repo: repo?.name ?? svc?.repo_root ?? "unknown"
|
|
14017
13661
|
},
|
|
14018
13662
|
clients,
|
|
14019
|
-
riskLevel:
|
|
13663
|
+
riskLevel: riskLevel3,
|
|
14020
13664
|
summary: `${ep.method ?? "*"} ${ep.path} is called by ${clients.length} client(s) in ${uniqueRepos.size} repo(s)${breakingChanges ? " \u26A0 BREAKING SCHEMA CHANGES" : ""}`,
|
|
14021
13665
|
breakingChanges
|
|
14022
13666
|
});
|
|
14023
13667
|
}
|
|
14024
13668
|
return results;
|
|
14025
13669
|
}
|
|
13670
|
+
/** Register contracts for a service, including explicitly provided paths. */
|
|
13671
|
+
registerContracts(serviceId, serviceRoot, repoRoot, explicitPaths) {
|
|
13672
|
+
const contracts = parseContracts(serviceRoot);
|
|
13673
|
+
if (explicitPaths && repoRoot) {
|
|
13674
|
+
for (const cp of explicitPaths) {
|
|
13675
|
+
const absContract = path29.resolve(repoRoot, cp);
|
|
13676
|
+
if (fs25.existsSync(absContract)) {
|
|
13677
|
+
const additional = parseContracts(path29.dirname(absContract));
|
|
13678
|
+
contracts.push(...additional.filter(
|
|
13679
|
+
(c) => path29.resolve(repoRoot, c.specPath) === absContract
|
|
13680
|
+
));
|
|
13681
|
+
}
|
|
13682
|
+
}
|
|
13683
|
+
}
|
|
13684
|
+
for (const contract of contracts) {
|
|
13685
|
+
const contractId = this.topoStore.insertContract(serviceId, {
|
|
13686
|
+
contractType: contract.type,
|
|
13687
|
+
specPath: contract.specPath,
|
|
13688
|
+
version: contract.version,
|
|
13689
|
+
parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
|
|
13690
|
+
});
|
|
13691
|
+
this.topoStore.insertEndpoints(
|
|
13692
|
+
contractId,
|
|
13693
|
+
serviceId,
|
|
13694
|
+
contract.endpoints.map((e) => ({
|
|
13695
|
+
method: e.method ?? void 0,
|
|
13696
|
+
path: e.path,
|
|
13697
|
+
operationId: e.operationId,
|
|
13698
|
+
requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
|
|
13699
|
+
responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
|
|
13700
|
+
}))
|
|
13701
|
+
);
|
|
13702
|
+
if (contract.events.length > 0) {
|
|
13703
|
+
this.topoStore.insertEventChannels(
|
|
13704
|
+
contractId,
|
|
13705
|
+
serviceId,
|
|
13706
|
+
contract.events.map((e) => ({
|
|
13707
|
+
channelName: e.channelName,
|
|
13708
|
+
direction: e.direction
|
|
13709
|
+
}))
|
|
13710
|
+
);
|
|
13711
|
+
}
|
|
13712
|
+
}
|
|
13713
|
+
}
|
|
13714
|
+
/** Snapshot existing contracts before replacing them (for drift detection). */
|
|
13715
|
+
snapshotContracts(serviceId) {
|
|
13716
|
+
const existing = this.topoStore.getContractsByService(serviceId);
|
|
13717
|
+
for (const ec of existing) {
|
|
13718
|
+
this.topoStore.insertContractSnapshot(ec.id, serviceId, {
|
|
13719
|
+
version: ec.version,
|
|
13720
|
+
specPath: ec.spec_path,
|
|
13721
|
+
contentHash: ec.content_hash ?? "",
|
|
13722
|
+
endpointsJson: ec.parsed_spec,
|
|
13723
|
+
eventsJson: "[]"
|
|
13724
|
+
});
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
/** Scan repo for client calls, insert them, link to endpoints, and build edges. */
|
|
13728
|
+
scanAndLinkClientCalls(repoId, repoRoot) {
|
|
13729
|
+
this.topoStore.deleteClientCallsByRepo(repoId);
|
|
13730
|
+
const clientCalls = scanClientCalls(repoRoot);
|
|
13731
|
+
if (clientCalls.length > 0) {
|
|
13732
|
+
this.topoStore.insertClientCalls(clientCalls.map((c) => ({
|
|
13733
|
+
sourceRepoId: repoId,
|
|
13734
|
+
filePath: c.filePath,
|
|
13735
|
+
line: c.line,
|
|
13736
|
+
callType: c.callType,
|
|
13737
|
+
method: c.method,
|
|
13738
|
+
urlPattern: c.urlPattern,
|
|
13739
|
+
confidence: c.confidence
|
|
13740
|
+
})));
|
|
13741
|
+
}
|
|
13742
|
+
const linked = this.topoStore.linkClientCallsToEndpoints();
|
|
13743
|
+
this.buildCrossServiceEdges();
|
|
13744
|
+
return { scanned: clientCalls.length, linked };
|
|
13745
|
+
}
|
|
13746
|
+
filterEndpoints(opts) {
|
|
13747
|
+
let endpoints = this.topoStore.getAllEndpoints();
|
|
13748
|
+
if (opts.endpoint) {
|
|
13749
|
+
const normalized = opts.endpoint.toLowerCase();
|
|
13750
|
+
endpoints = endpoints.filter((ep) => ep.path.toLowerCase().includes(normalized));
|
|
13751
|
+
}
|
|
13752
|
+
if (opts.method) {
|
|
13753
|
+
endpoints = endpoints.filter((ep) => ep.method?.toUpperCase() === opts.method.toUpperCase());
|
|
13754
|
+
}
|
|
13755
|
+
if (opts.service) {
|
|
13756
|
+
endpoints = endpoints.filter((ep) => ep.service_name.toLowerCase() === opts.service.toLowerCase());
|
|
13757
|
+
}
|
|
13758
|
+
return endpoints;
|
|
13759
|
+
}
|
|
13760
|
+
collectEndpointClients(clientCalls) {
|
|
13761
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
13762
|
+
for (const call of clientCalls) {
|
|
13763
|
+
const repo = call.source_repo_name;
|
|
13764
|
+
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
13765
|
+
byRepo.get(repo).push(call);
|
|
13766
|
+
}
|
|
13767
|
+
const clients = [];
|
|
13768
|
+
for (const [repoName, calls] of byRepo) {
|
|
13769
|
+
const repo = this.topoStore.getFederatedRepo(repoName);
|
|
13770
|
+
for (const call of calls) {
|
|
13771
|
+
const symbols = repo?.db_path && fs25.existsSync(repo.db_path) ? resolveSymbolsAtLocation(repo.db_path, call.file_path, call.line) : [];
|
|
13772
|
+
clients.push({
|
|
13773
|
+
repo: repoName,
|
|
13774
|
+
filePath: call.file_path,
|
|
13775
|
+
line: call.line,
|
|
13776
|
+
callType: call.call_type,
|
|
13777
|
+
confidence: call.confidence,
|
|
13778
|
+
symbols
|
|
13779
|
+
});
|
|
13780
|
+
}
|
|
13781
|
+
}
|
|
13782
|
+
return clients;
|
|
13783
|
+
}
|
|
13784
|
+
detectBreakingChanges(ep) {
|
|
13785
|
+
const contracts = this.topoStore.getContractsByService(ep.service_id);
|
|
13786
|
+
for (const contract of contracts) {
|
|
13787
|
+
const snapshot = this.topoStore.getLatestSnapshot(contract.id);
|
|
13788
|
+
if (!snapshot) continue;
|
|
13789
|
+
let oldEndpoints = [];
|
|
13790
|
+
try {
|
|
13791
|
+
const parsed = JSON.parse(snapshot.endpoints_json);
|
|
13792
|
+
oldEndpoints = (parsed.endpoints ?? []).map((e) => ({ method: e.method ?? null, path: e.path, requestSchema: e.requestSchema, responseSchema: e.responseSchema }));
|
|
13793
|
+
} catch {
|
|
13794
|
+
continue;
|
|
13795
|
+
}
|
|
13796
|
+
const currentEndpoints = this.topoStore.getEndpointsByService(ep.service_id).map((e) => ({
|
|
13797
|
+
method: e.method,
|
|
13798
|
+
path: e.path,
|
|
13799
|
+
requestSchema: e.request_schema,
|
|
13800
|
+
responseSchema: e.response_schema
|
|
13801
|
+
}));
|
|
13802
|
+
const epDiffs = diffEndpoints(oldEndpoints, currentEndpoints).filter((d) => d.endpoint.path === ep.path && (d.endpoint.method ?? "*") === (ep.method ?? "*"));
|
|
13803
|
+
if (epDiffs.length > 0 && epDiffs.some((d) => d.breaking)) {
|
|
13804
|
+
return epDiffs;
|
|
13805
|
+
}
|
|
13806
|
+
}
|
|
13807
|
+
return void 0;
|
|
13808
|
+
}
|
|
14026
13809
|
/**
|
|
14027
13810
|
* Search across all federated repos. Opens each per-repo DB readonly,
|
|
14028
13811
|
* runs FTS search, normalizes scores, merges results.
|
|
@@ -14108,6 +13891,18 @@ var FederationManager = class {
|
|
|
14108
13891
|
}
|
|
14109
13892
|
}
|
|
14110
13893
|
};
|
|
13894
|
+
function computeRiskLevel(uniqueRepoCount, clientCount) {
|
|
13895
|
+
if (uniqueRepoCount >= 3) return "critical";
|
|
13896
|
+
if (uniqueRepoCount >= 2) return "high";
|
|
13897
|
+
if (clientCount >= 3) return "medium";
|
|
13898
|
+
return "low";
|
|
13899
|
+
}
|
|
13900
|
+
function upgradeRiskIfBreaking(risk, breakingChanges) {
|
|
13901
|
+
if (!breakingChanges?.some((d) => d.breaking)) return risk;
|
|
13902
|
+
const levels = ["low", "medium", "high", "critical"];
|
|
13903
|
+
const idx = levels.indexOf(risk);
|
|
13904
|
+
return idx < levels.length - 1 ? levels[idx + 1] : risk;
|
|
13905
|
+
}
|
|
14111
13906
|
function resolveSymbolsAtLocation(dbPath, filePath, line) {
|
|
14112
13907
|
if (!line) return [];
|
|
14113
13908
|
try {
|
|
@@ -24534,13 +24329,13 @@ function detectDrift(store, cwd, options = {}) {
|
|
|
24534
24329
|
const commitsB = fileCommitCount.get(fileB) ?? 0;
|
|
24535
24330
|
const denominator = commitsA + commitsB - count;
|
|
24536
24331
|
if (denominator <= 0) continue;
|
|
24537
|
-
const
|
|
24538
|
-
if (
|
|
24332
|
+
const jaccard2 = count / denominator;
|
|
24333
|
+
if (jaccard2 < minConfidence) continue;
|
|
24539
24334
|
anomalies.push({
|
|
24540
24335
|
file_a: fileA,
|
|
24541
24336
|
file_b: fileB,
|
|
24542
24337
|
co_change_count: count,
|
|
24543
|
-
confidence: round2(
|
|
24338
|
+
confidence: round2(jaccard2),
|
|
24544
24339
|
module_a: moduleA,
|
|
24545
24340
|
module_b: moduleB
|
|
24546
24341
|
});
|
|
@@ -31074,7 +30869,7 @@ var TopologyStore = class {
|
|
|
31074
30869
|
};
|
|
31075
30870
|
|
|
31076
30871
|
// src/server/server.ts
|
|
31077
|
-
var PKG_VERSION = true ? "1.
|
|
30872
|
+
var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
31078
30873
|
function j2(value) {
|
|
31079
30874
|
return JSON.stringify(value, (_key, val) => val === null || val === void 0 ? void 0 : val);
|
|
31080
30875
|
}
|
|
@@ -63089,18 +62884,6 @@ function shortPath2(p5) {
|
|
|
63089
62884
|
// src/project-root.ts
|
|
63090
62885
|
import fs90 from "fs";
|
|
63091
62886
|
import path102 from "path";
|
|
63092
|
-
var ROOT_MARKERS = [
|
|
63093
|
-
".git",
|
|
63094
|
-
"package.json",
|
|
63095
|
-
"go.mod",
|
|
63096
|
-
"Cargo.toml",
|
|
63097
|
-
"composer.json",
|
|
63098
|
-
"pyproject.toml",
|
|
63099
|
-
"Gemfile",
|
|
63100
|
-
"pom.xml",
|
|
63101
|
-
"build.gradle",
|
|
63102
|
-
"build.gradle.kts"
|
|
63103
|
-
];
|
|
63104
62887
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "vendor", ".svn", "__pycache__", ".tox"]);
|
|
63105
62888
|
function discoverChildProjects(parentDir) {
|
|
63106
62889
|
const absParent = path102.resolve(parentDir);
|
|
@@ -64451,13 +64234,35 @@ function generateReport(input) {
|
|
|
64451
64234
|
const riskAnalysis = computeRiskScores(store, rootPath, changedFileInfos, blastRadius);
|
|
64452
64235
|
const architectureViolations = computeArchViolations(store, changedFiles, layers);
|
|
64453
64236
|
const deadCode = computeDeadCode(store, changedFiles);
|
|
64237
|
+
let domainAnalysis;
|
|
64238
|
+
let ownershipAnalysis;
|
|
64239
|
+
let deploymentImpact;
|
|
64240
|
+
if (input.enableProjectAware !== false) {
|
|
64241
|
+
try {
|
|
64242
|
+
domainAnalysis = computeDomainAnalysis(store, changedFiles, blastRadius.entries);
|
|
64243
|
+
} catch (e) {
|
|
64244
|
+
logger.debug({ error: e }, "CI report: domain analysis skipped");
|
|
64245
|
+
}
|
|
64246
|
+
try {
|
|
64247
|
+
ownershipAnalysis = computeOwnershipAnalysis(rootPath, changedFiles);
|
|
64248
|
+
} catch (e) {
|
|
64249
|
+
logger.debug({ error: e }, "CI report: ownership analysis skipped");
|
|
64250
|
+
}
|
|
64251
|
+
try {
|
|
64252
|
+
deploymentImpact = computeDeploymentImpact(changedFiles, rootPath);
|
|
64253
|
+
} catch (e) {
|
|
64254
|
+
logger.debug({ error: e }, "CI report: deployment impact skipped");
|
|
64255
|
+
}
|
|
64256
|
+
}
|
|
64454
64257
|
const summary = {
|
|
64455
64258
|
changedFileCount: changedFiles.length,
|
|
64456
64259
|
affectedFileCount: blastRadius.totalAffected,
|
|
64457
64260
|
riskLevel: riskAnalysis.overallLevel,
|
|
64458
64261
|
untestedGaps: testCoverage.gaps.length,
|
|
64459
64262
|
violations: architectureViolations.totalViolations,
|
|
64460
|
-
deadExports: deadCode.totalDead
|
|
64263
|
+
deadExports: deadCode.totalDead,
|
|
64264
|
+
...domainAnalysis ? { domainsCrossed: domainAnalysis.domainsAffected.length } : {},
|
|
64265
|
+
...deploymentImpact ? { servicesAffected: deploymentImpact.servicesAffected.length } : {}
|
|
64461
64266
|
};
|
|
64462
64267
|
return {
|
|
64463
64268
|
changedFiles: changedFileInfos,
|
|
@@ -64466,6 +64271,9 @@ function generateReport(input) {
|
|
|
64466
64271
|
riskAnalysis,
|
|
64467
64272
|
architectureViolations,
|
|
64468
64273
|
deadCode,
|
|
64274
|
+
...domainAnalysis ? { domainAnalysis } : {},
|
|
64275
|
+
...ownershipAnalysis ? { ownershipAnalysis } : {},
|
|
64276
|
+
...deploymentImpact ? { deploymentImpact } : {},
|
|
64469
64277
|
summary
|
|
64470
64278
|
};
|
|
64471
64279
|
}
|
|
@@ -64634,6 +64442,98 @@ function scoreToLevel(score) {
|
|
|
64634
64442
|
if (score >= 0.25) return "medium";
|
|
64635
64443
|
return "low";
|
|
64636
64444
|
}
|
|
64445
|
+
function computeDomainAnalysis(store, changedFiles, blastEntries) {
|
|
64446
|
+
const domainStore = new DomainStore(store.db);
|
|
64447
|
+
const allDomains = domainStore.getAllDomains();
|
|
64448
|
+
if (allDomains.length === 0) return void 0;
|
|
64449
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
64450
|
+
for (const filePath of changedFiles) {
|
|
64451
|
+
const file = store.getFile(filePath);
|
|
64452
|
+
if (!file) continue;
|
|
64453
|
+
const domains = domainStore.getDomainsForFile(file.id);
|
|
64454
|
+
for (const d of domains) {
|
|
64455
|
+
const existing = domainCounts.get(d.name) ?? { filesChanged: 0, filesImpacted: 0 };
|
|
64456
|
+
existing.filesChanged++;
|
|
64457
|
+
domainCounts.set(d.name, existing);
|
|
64458
|
+
}
|
|
64459
|
+
}
|
|
64460
|
+
for (const entry of blastEntries) {
|
|
64461
|
+
const file = store.getFile(entry.path);
|
|
64462
|
+
if (!file) continue;
|
|
64463
|
+
const domains = domainStore.getDomainsForFile(file.id);
|
|
64464
|
+
for (const d of domains) {
|
|
64465
|
+
const existing = domainCounts.get(d.name) ?? { filesChanged: 0, filesImpacted: 0 };
|
|
64466
|
+
existing.filesImpacted++;
|
|
64467
|
+
domainCounts.set(d.name, existing);
|
|
64468
|
+
}
|
|
64469
|
+
}
|
|
64470
|
+
if (domainCounts.size === 0) return void 0;
|
|
64471
|
+
const domainsAffected = [...domainCounts.entries()].map(([name, counts]) => ({ name, ...counts })).sort((a, b) => b.filesChanged + b.filesImpacted - (a.filesChanged + a.filesImpacted));
|
|
64472
|
+
const crossDomainDeps = domainStore.getCrossDomainDependencies();
|
|
64473
|
+
const affectedDomainNames = new Set(domainsAffected.map((d) => d.name));
|
|
64474
|
+
const crossDomainChanges = crossDomainDeps.filter((dep) => affectedDomainNames.has(dep.source_domain) || affectedDomainNames.has(dep.target_domain)).map((dep) => ({ from: dep.source_domain, to: dep.target_domain, edgeCount: dep.edge_count }));
|
|
64475
|
+
const reviewTeams = domainsAffected.filter((d) => d.filesChanged > 0).map((d) => d.name);
|
|
64476
|
+
return { domainsAffected, crossDomainChanges, reviewTeams };
|
|
64477
|
+
}
|
|
64478
|
+
function computeOwnershipAnalysis(rootPath, changedFiles) {
|
|
64479
|
+
if (changedFiles.length === 0) return void 0;
|
|
64480
|
+
const ownerships = getFileOwnership(rootPath, changedFiles);
|
|
64481
|
+
if (ownerships.length === 0) return void 0;
|
|
64482
|
+
const owners = [];
|
|
64483
|
+
const allAuthors = /* @__PURE__ */ new Set();
|
|
64484
|
+
for (const ownership of ownerships) {
|
|
64485
|
+
if (ownership.owners.length === 0) continue;
|
|
64486
|
+
const primary = ownership.owners[0];
|
|
64487
|
+
owners.push({
|
|
64488
|
+
file: ownership.file,
|
|
64489
|
+
primaryOwner: primary.author,
|
|
64490
|
+
percentage: primary.percentage
|
|
64491
|
+
});
|
|
64492
|
+
for (const o of ownership.owners) {
|
|
64493
|
+
allAuthors.add(o.author);
|
|
64494
|
+
}
|
|
64495
|
+
}
|
|
64496
|
+
if (owners.length === 0) return void 0;
|
|
64497
|
+
return {
|
|
64498
|
+
owners: owners.sort((a, b) => a.file.localeCompare(b.file)),
|
|
64499
|
+
teamsCrossed: [...allAuthors].sort()
|
|
64500
|
+
};
|
|
64501
|
+
}
|
|
64502
|
+
function computeDeploymentImpact(changedFiles, rootPath) {
|
|
64503
|
+
if (changedFiles.length === 0) return void 0;
|
|
64504
|
+
let topoStore;
|
|
64505
|
+
try {
|
|
64506
|
+
topoStore = new TopologyStore(TOPOLOGY_DB_PATH);
|
|
64507
|
+
} catch {
|
|
64508
|
+
return void 0;
|
|
64509
|
+
}
|
|
64510
|
+
try {
|
|
64511
|
+
const services = topoStore.getAllServices();
|
|
64512
|
+
if (services.length === 0) return void 0;
|
|
64513
|
+
const serviceCounts = /* @__PURE__ */ new Map();
|
|
64514
|
+
for (const filePath of changedFiles) {
|
|
64515
|
+
for (const svc of services) {
|
|
64516
|
+
if (svc.repo_root && (rootPath === svc.repo_root || rootPath.startsWith(svc.repo_root + "/"))) {
|
|
64517
|
+
const key = svc.name;
|
|
64518
|
+
const existing = serviceCounts.get(key) ?? { name: svc.name, type: svc.service_type ?? "unknown", count: 0 };
|
|
64519
|
+
existing.count++;
|
|
64520
|
+
serviceCounts.set(key, existing);
|
|
64521
|
+
break;
|
|
64522
|
+
}
|
|
64523
|
+
}
|
|
64524
|
+
}
|
|
64525
|
+
if (serviceCounts.size === 0) return void 0;
|
|
64526
|
+
const servicesAffected = [...serviceCounts.values()].map((s) => ({ name: s.name, type: s.type, filesChanged: s.count })).sort((a, b) => b.filesChanged - a.filesChanged);
|
|
64527
|
+
const affectedServiceNames = new Set(servicesAffected.map((s) => s.name));
|
|
64528
|
+
const allEdges = topoStore.getAllCrossServiceEdges();
|
|
64529
|
+
const crossServiceChanges = allEdges.filter(
|
|
64530
|
+
(e) => affectedServiceNames.has(e.source_name) || affectedServiceNames.has(e.target_name)
|
|
64531
|
+
).length;
|
|
64532
|
+
return { servicesAffected, crossServiceChanges };
|
|
64533
|
+
} finally {
|
|
64534
|
+
topoStore.close();
|
|
64535
|
+
}
|
|
64536
|
+
}
|
|
64637
64537
|
|
|
64638
64538
|
// src/ci/markdown-formatter.ts
|
|
64639
64539
|
function formatMarkdown(report) {
|
|
@@ -64649,6 +64549,12 @@ function formatMarkdown(report) {
|
|
|
64649
64549
|
lines.push(`| Untested affected paths | ${report.summary.untestedGaps} |`);
|
|
64650
64550
|
lines.push(`| Architecture violations | ${report.summary.violations} |`);
|
|
64651
64551
|
lines.push(`| Dead exports introduced | ${report.summary.deadExports} |`);
|
|
64552
|
+
if (report.summary.domainsCrossed != null) {
|
|
64553
|
+
lines.push(`| Domains crossed | ${report.summary.domainsCrossed} |`);
|
|
64554
|
+
}
|
|
64555
|
+
if (report.summary.servicesAffected != null) {
|
|
64556
|
+
lines.push(`| Services affected | ${report.summary.servicesAffected} |`);
|
|
64557
|
+
}
|
|
64652
64558
|
lines.push("");
|
|
64653
64559
|
if (report.changedFiles.length > 0) {
|
|
64654
64560
|
const codeFiles = report.changedFiles.filter((f) => f.symbolCount > 0);
|
|
@@ -64746,6 +64652,84 @@ function formatMarkdown(report) {
|
|
|
64746
64652
|
lines.push("</details>");
|
|
64747
64653
|
lines.push("");
|
|
64748
64654
|
}
|
|
64655
|
+
if (report.domainAnalysis) {
|
|
64656
|
+
const da = report.domainAnalysis;
|
|
64657
|
+
lines.push(`<details><summary>Domain Boundaries (${da.domainsAffected.length} domains)</summary>`);
|
|
64658
|
+
lines.push("");
|
|
64659
|
+
if (da.reviewTeams.length > 0) {
|
|
64660
|
+
lines.push(`**Review needed from:** ${da.reviewTeams.join(", ")}`);
|
|
64661
|
+
lines.push("");
|
|
64662
|
+
}
|
|
64663
|
+
lines.push("| Domain | Changed Files | Impacted Files |");
|
|
64664
|
+
lines.push("|--------|--------------|----------------|");
|
|
64665
|
+
for (const d of da.domainsAffected) {
|
|
64666
|
+
lines.push(`| ${d.name} | ${d.filesChanged} | ${d.filesImpacted} |`);
|
|
64667
|
+
}
|
|
64668
|
+
if (da.crossDomainChanges.length > 0) {
|
|
64669
|
+
lines.push("");
|
|
64670
|
+
lines.push("**Cross-domain dependencies:**");
|
|
64671
|
+
lines.push("");
|
|
64672
|
+
lines.push("| From | To | Edges |");
|
|
64673
|
+
lines.push("|------|----|-------|");
|
|
64674
|
+
for (const cd of da.crossDomainChanges) {
|
|
64675
|
+
lines.push(`| ${cd.from} | ${cd.to} | ${cd.edgeCount} |`);
|
|
64676
|
+
}
|
|
64677
|
+
}
|
|
64678
|
+
lines.push("");
|
|
64679
|
+
lines.push("</details>");
|
|
64680
|
+
lines.push("");
|
|
64681
|
+
}
|
|
64682
|
+
if (report.ownershipAnalysis) {
|
|
64683
|
+
const oa = report.ownershipAnalysis;
|
|
64684
|
+
lines.push(`<details><summary>Code Ownership (${oa.teamsCrossed.length} contributors)</summary>`);
|
|
64685
|
+
lines.push("");
|
|
64686
|
+
lines.push(`**Teams involved:** ${oa.teamsCrossed.join(", ")}`);
|
|
64687
|
+
lines.push("");
|
|
64688
|
+
lines.push("| File | Primary Owner | Ownership % |");
|
|
64689
|
+
lines.push("|------|--------------|-------------|");
|
|
64690
|
+
for (const o of oa.owners) {
|
|
64691
|
+
lines.push(`| \`${o.file}\` | ${o.primaryOwner} | ${o.percentage}% |`);
|
|
64692
|
+
}
|
|
64693
|
+
lines.push("");
|
|
64694
|
+
lines.push("</details>");
|
|
64695
|
+
lines.push("");
|
|
64696
|
+
}
|
|
64697
|
+
if (report.deploymentImpact) {
|
|
64698
|
+
const di = report.deploymentImpact;
|
|
64699
|
+
lines.push(`<details><summary>Deployment Impact (${di.servicesAffected.length} services)</summary>`);
|
|
64700
|
+
lines.push("");
|
|
64701
|
+
lines.push("| Service | Type | Changed Files |");
|
|
64702
|
+
lines.push("|---------|------|--------------|");
|
|
64703
|
+
for (const s of di.servicesAffected) {
|
|
64704
|
+
lines.push(`| ${s.name} | ${s.type} | ${s.filesChanged} |`);
|
|
64705
|
+
}
|
|
64706
|
+
if (di.crossServiceChanges > 0) {
|
|
64707
|
+
lines.push("");
|
|
64708
|
+
lines.push(`**Cross-service edges affected:** ${di.crossServiceChanges}`);
|
|
64709
|
+
}
|
|
64710
|
+
lines.push("");
|
|
64711
|
+
lines.push("</details>");
|
|
64712
|
+
lines.push("");
|
|
64713
|
+
}
|
|
64714
|
+
if (report.baseline) {
|
|
64715
|
+
const b = report.baseline;
|
|
64716
|
+
const commitRef = b.baselineCommit ? ` (vs ${b.baselineCommit})` : "";
|
|
64717
|
+
lines.push(`<details open><summary>Trend vs Baseline${commitRef}</summary>`);
|
|
64718
|
+
lines.push("");
|
|
64719
|
+
lines.push("| Metric | Delta | |");
|
|
64720
|
+
lines.push("|--------|-------|-|");
|
|
64721
|
+
lines.push(`| Risk score | ${formatDelta(b.riskDelta)} | ${arrow(b.riskDelta, true)} |`);
|
|
64722
|
+
lines.push(`| Untested gaps | ${formatDeltaInt(b.untestedDelta)} | ${arrow(b.untestedDelta, true)} |`);
|
|
64723
|
+
lines.push(`| Violations | ${formatDeltaInt(b.violationsDelta)} | ${arrow(b.violationsDelta, true)} |`);
|
|
64724
|
+
lines.push(`| Dead exports | ${formatDeltaInt(b.deadExportsDelta)} | ${arrow(b.deadExportsDelta, true)} |`);
|
|
64725
|
+
if (b.regressionDetected) {
|
|
64726
|
+
lines.push("");
|
|
64727
|
+
lines.push("**Warning: quality regression detected**");
|
|
64728
|
+
}
|
|
64729
|
+
lines.push("");
|
|
64730
|
+
lines.push("</details>");
|
|
64731
|
+
lines.push("");
|
|
64732
|
+
}
|
|
64749
64733
|
lines.push("---");
|
|
64750
64734
|
lines.push("_Generated by [trace-mcp](https://github.com/nikolai-vysotskyi/trace-mcp) CI Report_");
|
|
64751
64735
|
return lines.join("\n");
|
|
@@ -64753,6 +64737,117 @@ function formatMarkdown(report) {
|
|
|
64753
64737
|
function formatJson(report) {
|
|
64754
64738
|
return JSON.stringify(report, null, 2);
|
|
64755
64739
|
}
|
|
64740
|
+
function formatDelta(n) {
|
|
64741
|
+
return n >= 0 ? `+${Math.round(n * 100) / 100}` : `${Math.round(n * 100) / 100}`;
|
|
64742
|
+
}
|
|
64743
|
+
function formatDeltaInt(n) {
|
|
64744
|
+
return n >= 0 ? `+${n}` : `${n}`;
|
|
64745
|
+
}
|
|
64746
|
+
function arrow(n, higherIsWorse) {
|
|
64747
|
+
if (n === 0) return "-";
|
|
64748
|
+
const up = n > 0;
|
|
64749
|
+
if (higherIsWorse) return up ? "\u25B2 worse" : "\u25BC better";
|
|
64750
|
+
return up ? "\u25B2 better" : "\u25BC worse";
|
|
64751
|
+
}
|
|
64752
|
+
|
|
64753
|
+
// src/ci/baseline.ts
|
|
64754
|
+
var SNAPSHOT_TYPE = "ci_quality_baseline";
|
|
64755
|
+
var REGRESSION_THRESHOLD = 0.15;
|
|
64756
|
+
function captureBaseline(store, report, commitHash) {
|
|
64757
|
+
store.insertGraphSnapshot(SNAPSHOT_TYPE, {
|
|
64758
|
+
riskScore: report.riskAnalysis.overallScore,
|
|
64759
|
+
riskLevel: report.riskAnalysis.overallLevel,
|
|
64760
|
+
untestedGaps: report.testCoverage.totalUntested,
|
|
64761
|
+
violations: report.architectureViolations.totalViolations,
|
|
64762
|
+
deadExports: report.deadCode.totalDead,
|
|
64763
|
+
changedFiles: report.summary.changedFileCount,
|
|
64764
|
+
affectedFiles: report.summary.affectedFileCount
|
|
64765
|
+
}, commitHash);
|
|
64766
|
+
}
|
|
64767
|
+
function compareWithBaseline(store, current) {
|
|
64768
|
+
const snapshots = store.getGraphSnapshots(SNAPSHOT_TYPE, { limit: 1 });
|
|
64769
|
+
if (snapshots.length === 0) return null;
|
|
64770
|
+
const snapshot = snapshots[0];
|
|
64771
|
+
const baseline = JSON.parse(snapshot.data);
|
|
64772
|
+
const riskDelta = current.riskAnalysis.overallScore - baseline.riskScore;
|
|
64773
|
+
return {
|
|
64774
|
+
riskDelta: Math.round(riskDelta * 100) / 100,
|
|
64775
|
+
untestedDelta: current.testCoverage.totalUntested - baseline.untestedGaps,
|
|
64776
|
+
violationsDelta: current.architectureViolations.totalViolations - baseline.violations,
|
|
64777
|
+
deadExportsDelta: current.deadCode.totalDead - baseline.deadExports,
|
|
64778
|
+
regressionDetected: riskDelta > REGRESSION_THRESHOLD,
|
|
64779
|
+
baselineCommit: snapshot.commit_hash,
|
|
64780
|
+
baselineDate: snapshot.created_at
|
|
64781
|
+
};
|
|
64782
|
+
}
|
|
64783
|
+
|
|
64784
|
+
// src/ci/github-annotations.ts
|
|
64785
|
+
function generateAnnotations(report) {
|
|
64786
|
+
const annotations = [];
|
|
64787
|
+
for (const v of report.architectureViolations.violations) {
|
|
64788
|
+
annotations.push({
|
|
64789
|
+
path: v.source_file,
|
|
64790
|
+
start_line: 1,
|
|
64791
|
+
end_line: 1,
|
|
64792
|
+
annotation_level: "failure",
|
|
64793
|
+
title: "Architecture violation",
|
|
64794
|
+
message: `${v.source_layer} \u2192 ${v.target_layer}: ${v.rule}`
|
|
64795
|
+
});
|
|
64796
|
+
}
|
|
64797
|
+
for (const f of report.riskAnalysis.files.filter((f2) => f2.score >= 0.5)) {
|
|
64798
|
+
annotations.push({
|
|
64799
|
+
path: f.file,
|
|
64800
|
+
start_line: 1,
|
|
64801
|
+
end_line: 1,
|
|
64802
|
+
annotation_level: "warning",
|
|
64803
|
+
title: "High risk file",
|
|
64804
|
+
message: `Risk score ${f.score} (complexity=${f.complexity}, churn=${f.churn}, coupling=${f.coupling})`
|
|
64805
|
+
});
|
|
64806
|
+
}
|
|
64807
|
+
for (const gap of report.testCoverage.gaps.slice(0, 10)) {
|
|
64808
|
+
annotations.push({
|
|
64809
|
+
path: gap.file,
|
|
64810
|
+
start_line: 1,
|
|
64811
|
+
end_line: 1,
|
|
64812
|
+
annotation_level: "notice",
|
|
64813
|
+
title: "Untested export",
|
|
64814
|
+
message: `${gap.kind} "${gap.name}" has no test coverage`
|
|
64815
|
+
});
|
|
64816
|
+
}
|
|
64817
|
+
if (report.domainAnalysis) {
|
|
64818
|
+
for (const cd of report.domainAnalysis.crossDomainChanges.slice(0, 5)) {
|
|
64819
|
+
annotations.push({
|
|
64820
|
+
path: "",
|
|
64821
|
+
start_line: 0,
|
|
64822
|
+
end_line: 0,
|
|
64823
|
+
annotation_level: "notice",
|
|
64824
|
+
title: "Cross-domain dependency",
|
|
64825
|
+
message: `${cd.from} \u2192 ${cd.to} (${cd.edgeCount} edges)`
|
|
64826
|
+
});
|
|
64827
|
+
}
|
|
64828
|
+
}
|
|
64829
|
+
if (report.baseline?.regressionDetected) {
|
|
64830
|
+
annotations.push({
|
|
64831
|
+
path: "",
|
|
64832
|
+
start_line: 0,
|
|
64833
|
+
end_line: 0,
|
|
64834
|
+
annotation_level: "warning",
|
|
64835
|
+
title: "Quality regression",
|
|
64836
|
+
message: `Risk score increased by ${report.baseline.riskDelta} vs baseline (commit ${report.baseline.baselineCommit ?? "unknown"})`
|
|
64837
|
+
});
|
|
64838
|
+
}
|
|
64839
|
+
return annotations;
|
|
64840
|
+
}
|
|
64841
|
+
function formatGitHubActions(annotations) {
|
|
64842
|
+
return annotations.map((a) => {
|
|
64843
|
+
const file = a.path ? `file=${a.path},` : "";
|
|
64844
|
+
const line = a.start_line > 0 ? `line=${a.start_line},` : "";
|
|
64845
|
+
return `::${a.annotation_level} ${file}${line}title=${a.title}::${a.message}`;
|
|
64846
|
+
}).join("\n");
|
|
64847
|
+
}
|
|
64848
|
+
function formatAnnotationsJson(annotations) {
|
|
64849
|
+
return JSON.stringify(annotations, null, 2);
|
|
64850
|
+
}
|
|
64756
64851
|
|
|
64757
64852
|
// src/cli/ci.ts
|
|
64758
64853
|
function resolveDbPath(projectRoot) {
|
|
@@ -64760,7 +64855,7 @@ function resolveDbPath(projectRoot) {
|
|
|
64760
64855
|
if (entry) return entry.dbPath;
|
|
64761
64856
|
return getDbPath(projectRoot);
|
|
64762
64857
|
}
|
|
64763
|
-
var ciReportCommand = new Command5("ci-report").description("Generate a change impact report for a PR/branch").option("--base <ref>", "Base git ref (default: main)", "main").option("--head <ref>", "Head git ref (default: HEAD)", "HEAD").option("--format <fmt>", "Output format: markdown | json (default: markdown)", "markdown").option("--output <path>", "Output file path (default: stdout, use - for stdout)", "-").option("--fail-on <level>", "Exit with code 1 if risk >= level: critical | high | medium", "").option("--index", "Index the project before generating the report", false).action(async (opts) => {
|
|
64858
|
+
var ciReportCommand = new Command5("ci-report").description("Generate a change impact report for a PR/branch").option("--base <ref>", "Base git ref (default: main)", "main").option("--head <ref>", "Head git ref (default: HEAD)", "HEAD").option("--format <fmt>", "Output format: markdown | json (default: markdown)", "markdown").option("--output <path>", "Output file path (default: stdout, use - for stdout)", "-").option("--fail-on <level>", "Exit with code 1 if risk >= level: critical | high | medium", "").option("--index", "Index the project before generating the report", false).option("--no-project-aware", "Disable domain/ownership/deployment analysis").option("--save-baseline", "Save current scores as quality baseline", false).option("--fail-regression", "Exit 1 if quality regressed vs baseline", false).option("--annotations <format>", "Output annotations: github-actions | json").action(async (opts) => {
|
|
64764
64859
|
let projectRoot;
|
|
64765
64860
|
try {
|
|
64766
64861
|
projectRoot = findProjectRoot(process.cwd());
|
|
@@ -64787,7 +64882,9 @@ ${msg}
|
|
|
64787
64882
|
include: ["**/*"],
|
|
64788
64883
|
exclude: ["vendor/**", "node_modules/**", ".git/**"],
|
|
64789
64884
|
db: { path: "" },
|
|
64790
|
-
plugins: []
|
|
64885
|
+
plugins: [],
|
|
64886
|
+
ignore: { directories: [], patterns: [] },
|
|
64887
|
+
watch: { enabled: false, debounceMs: 2e3 }
|
|
64791
64888
|
};
|
|
64792
64889
|
const dbPath = resolveDbPath(projectRoot);
|
|
64793
64890
|
ensureGlobalDirs();
|
|
@@ -64805,10 +64902,35 @@ ${msg}
|
|
|
64805
64902
|
const report = generateReport({
|
|
64806
64903
|
changedFiles,
|
|
64807
64904
|
store,
|
|
64808
|
-
rootPath: projectRoot
|
|
64905
|
+
rootPath: projectRoot,
|
|
64906
|
+
enableProjectAware: opts.projectAware
|
|
64809
64907
|
});
|
|
64908
|
+
const baseline = compareWithBaseline(store, report);
|
|
64909
|
+
if (baseline) {
|
|
64910
|
+
report.baseline = baseline;
|
|
64911
|
+
}
|
|
64912
|
+
if (opts.saveBaseline) {
|
|
64913
|
+
let commitHash = "unknown";
|
|
64914
|
+
try {
|
|
64915
|
+
commitHash = execFileSync7("git", ["rev-parse", "--short", "HEAD"], {
|
|
64916
|
+
cwd: projectRoot,
|
|
64917
|
+
encoding: "utf-8",
|
|
64918
|
+
timeout: 5e3
|
|
64919
|
+
}).trim();
|
|
64920
|
+
} catch {
|
|
64921
|
+
}
|
|
64922
|
+
captureBaseline(store, report, commitHash);
|
|
64923
|
+
logger.info({ commit: commitHash }, "CI report: baseline saved");
|
|
64924
|
+
}
|
|
64810
64925
|
const output = opts.format === "json" ? formatJson(report) : formatMarkdown(report);
|
|
64811
64926
|
writeOutput(opts.output, output);
|
|
64927
|
+
if (opts.annotations) {
|
|
64928
|
+
const annotations = generateAnnotations(report);
|
|
64929
|
+
if (annotations.length > 0) {
|
|
64930
|
+
const annotationOutput = opts.annotations === "json" ? formatAnnotationsJson(annotations) : formatGitHubActions(annotations);
|
|
64931
|
+
writeOutput("-", annotationOutput);
|
|
64932
|
+
}
|
|
64933
|
+
}
|
|
64812
64934
|
db.close();
|
|
64813
64935
|
if (opts.failOn) {
|
|
64814
64936
|
const levels = ["low", "medium", "high", "critical"];
|
|
@@ -64819,6 +64941,10 @@ ${msg}
|
|
|
64819
64941
|
process.exit(1);
|
|
64820
64942
|
}
|
|
64821
64943
|
}
|
|
64944
|
+
if (opts.failRegression && baseline?.regressionDetected) {
|
|
64945
|
+
logger.warn({ riskDelta: baseline.riskDelta }, "CI report: quality regression detected");
|
|
64946
|
+
process.exit(1);
|
|
64947
|
+
}
|
|
64822
64948
|
});
|
|
64823
64949
|
function getChangedFiles(cwd, base, head) {
|
|
64824
64950
|
try {
|
|
@@ -65666,7 +65792,7 @@ trace-mcp status \u2014 ${projectRoot}
|
|
|
65666
65792
|
});
|
|
65667
65793
|
|
|
65668
65794
|
// src/cli.ts
|
|
65669
|
-
var PKG_VERSION2 = true ? "1.
|
|
65795
|
+
var PKG_VERSION2 = true ? "1.10.0" : "0.0.0-dev";
|
|
65670
65796
|
function registerDefaultPlugins(registry) {
|
|
65671
65797
|
for (const p5 of createAllLanguagePlugins()) registry.registerLanguagePlugin(p5);
|
|
65672
65798
|
for (const p5 of createAllIntegrationPlugins()) registry.registerFrameworkPlugin(p5);
|