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 CHANGED
@@ -2140,7 +2140,7 @@ var logger = pino({
2140
2140
  }, process.stderr);
2141
2141
 
2142
2142
  // src/db/schema.ts
2143
- var SCHEMA_VERSION = 17;
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
- if (ext.symbols.length > 0) {
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
- if (fwResult.symbols.length > 0) {
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
- disableFts5Triggers(this.store.db);
7809
- const BATCH_SIZE3 = Math.min(500, Math.max(100, Math.ceil(relPaths.length / 20)));
7810
- const CONCURRENCY = Math.min(8, cpus().length);
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 tokenizeName2(name) {
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 jaccard2(a, b) {
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 = tokenizeName2(target.name);
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 = jaccard2(targetTokens, tokenizeName2(sym.name));
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
- const contracts = parseContracts(svc.repoRoot);
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.topoStore.deleteClientCallsByRepo(repoId);
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.length,
13753
- linkedCalls: linkedCount
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
- const existingContracts = this.topoStore.getContractsByService(serviceId);
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.topoStore.deleteClientCallsByRepo(repo.id);
13902
- const calls = scanClientCalls(repo.repo_root);
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 byRepo = /* @__PURE__ */ new Map();
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 riskLevel3 = uniqueRepos.size >= 3 ? "critical" : uniqueRepos.size >= 2 ? "high" : clients.length >= 3 ? "medium" : "low";
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
- let breakingChanges;
13983
- const contracts = this.topoStore.getContractsByService(ep.service_id);
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: finalRiskLevel,
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 jaccard3 = count / denominator;
24538
- if (jaccard3 < minConfidence) continue;
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(jaccard3),
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.9.0" : "0.0.0-dev";
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.9.0" : "0.0.0-dev";
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);