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/index.js CHANGED
@@ -3650,7 +3650,7 @@ function captureGraphSnapshots(store, cwd) {
3650
3650
 
3651
3651
  // src/db/schema.ts
3652
3652
  import Database from "better-sqlite3";
3653
- var SCHEMA_VERSION = 17;
3653
+ var SCHEMA_VERSION = 18;
3654
3654
  var DDL = `
3655
3655
  -- ============================================================
3656
3656
  -- UNIFIED ADDRESS SPACE
@@ -4830,57 +4830,41 @@ var FilePersister = class {
4830
4830
  if (ext.gitignored) {
4831
4831
  store.updateFileGitignored(fileId, true);
4832
4832
  }
4833
- if (ext.symbols.length > 0) {
4834
- const insertedIds = store.insertSymbols(fileId, ext.symbols);
4835
- const trigramBySymbolId = /* @__PURE__ */ new Map();
4836
- for (let i = 0; i < ext.symbols.length; i++) {
4837
- trigramBySymbolId.set(ext.symbols[i].symbolId, {
4838
- id: insertedIds[i],
4839
- name: ext.symbols[i].name,
4840
- fqn: ext.symbols[i].fqn ?? null
4841
- });
4842
- }
4843
- indexTrigramsBatch(store.db, [...trigramBySymbolId.values()]);
4844
- }
4845
- if (ext.otherEdges.length > 0) this.storeRawEdges(ext.otherEdges);
4833
+ this.persistSymbolsAndEntities(fileId, ext.symbols, ext.otherEdges, ext);
4846
4834
  if (ext.importEdges.length > 0) {
4847
4835
  this.state.pendingImports.set(fileId, ext.importEdges);
4848
4836
  }
4849
- for (const r of ext.routes) store.insertRoute(r, fileId);
4850
- for (const c of ext.components) store.insertComponent(c, fileId);
4851
- for (const m of ext.migrations) store.insertMigration(m, fileId);
4852
- if (ext.ormModels.length > 0) {
4853
- this.storeOrmResults(ext.ormModels, ext.ormAssociations, fileId);
4854
- }
4855
- for (const s of ext.rnScreens) store.insertRnScreen(s, fileId);
4856
4837
  for (const fwResult of ext.frameworkExtracts) {
4857
- if (fwResult.symbols.length > 0) {
4858
- const fwIds = store.insertSymbols(fileId, fwResult.symbols);
4859
- const fwTrigramBySymbolId = /* @__PURE__ */ new Map();
4860
- for (let i = 0; i < fwResult.symbols.length; i++) {
4861
- fwTrigramBySymbolId.set(fwResult.symbols[i].symbolId, {
4862
- id: fwIds[i],
4863
- name: fwResult.symbols[i].name,
4864
- fqn: fwResult.symbols[i].fqn ?? null
4865
- });
4866
- }
4867
- indexTrigramsBatch(store.db, [...fwTrigramBySymbolId.values()]);
4868
- }
4869
- if (fwResult.edges?.length) {
4870
- this.storeRawEdges(fwResult.edges);
4871
- }
4872
- for (const r of fwResult.routes ?? []) store.insertRoute(r, fileId);
4873
- for (const c of fwResult.components ?? []) store.insertComponent(c, fileId);
4874
- for (const m of fwResult.migrations ?? []) store.insertMigration(m, fileId);
4875
- if (fwResult.ormModels?.length) {
4876
- this.storeOrmResults(fwResult.ormModels, fwResult.ormAssociations ?? [], fileId);
4877
- }
4878
- for (const s of fwResult.rnScreens ?? []) store.insertRnScreen(s, fileId);
4838
+ this.persistSymbolsAndEntities(fileId, fwResult.symbols, fwResult.edges ?? [], fwResult);
4879
4839
  if (fwResult.frameworkRole) {
4880
4840
  store.updateFileStatus(fileId, fwResult.status, fwResult.frameworkRole);
4881
4841
  }
4882
4842
  }
4883
4843
  }
4844
+ /** Insert symbols+trigrams, edges, and entities (routes/components/migrations/ORM/screens). */
4845
+ persistSymbolsAndEntities(fileId, symbols, edges, entities) {
4846
+ const store = this.state.store;
4847
+ if (symbols.length > 0) {
4848
+ const insertedIds = store.insertSymbols(fileId, symbols);
4849
+ const trigramBySymbolId = /* @__PURE__ */ new Map();
4850
+ for (let i = 0; i < symbols.length; i++) {
4851
+ trigramBySymbolId.set(symbols[i].symbolId, {
4852
+ id: insertedIds[i],
4853
+ name: symbols[i].name,
4854
+ fqn: symbols[i].fqn ?? null
4855
+ });
4856
+ }
4857
+ indexTrigramsBatch(store.db, [...trigramBySymbolId.values()]);
4858
+ }
4859
+ if (edges.length > 0) this.storeRawEdges(edges);
4860
+ for (const r of entities.routes ?? []) store.insertRoute(r, fileId);
4861
+ for (const c of entities.components ?? []) store.insertComponent(c, fileId);
4862
+ for (const m of entities.migrations ?? []) store.insertMigration(m, fileId);
4863
+ if (entities.ormModels?.length) {
4864
+ this.storeOrmResults(entities.ormModels, entities.ormAssociations ?? [], fileId);
4865
+ }
4866
+ for (const s of entities.rnScreens ?? []) store.insertRnScreen(s, fileId);
4867
+ }
4884
4868
  /**
4885
4869
  * Fast path for incremental re-indexing: if the set of symbols is structurally
4886
4870
  * identical (same symbolIds, names, kinds, fqns, signatures), only update byte
@@ -6040,58 +6024,10 @@ var IndexingPipeline = class _IndexingPipeline {
6040
6024
  this._gitignore = new GitignoreMatcher(this.rootPath);
6041
6025
  this._traceignore = new TraceignoreMatcher(this.rootPath, this.config.ignore);
6042
6026
  this.registerFrameworkEdgeTypes();
6043
- const extractor = new FileExtractor({
6044
- store: this.store,
6045
- registry: this.registry,
6046
- rootPath: this.rootPath,
6047
- workspaces: this.workspaces,
6048
- gitignore: this._gitignore,
6049
- fileContentCache: this._fileContentCache,
6050
- buildProjectContext: () => this.buildProjectContext()
6051
- });
6052
6027
  try {
6053
- disableFts5Triggers(this.store.db);
6054
- const BATCH_SIZE3 = Math.min(500, Math.max(100, Math.ceil(relPaths.length / 20)));
6055
- const CONCURRENCY = Math.min(8, cpus().length);
6056
- for (let i = 0; i < relPaths.length; i += BATCH_SIZE3) {
6057
- const batch = relPaths.slice(i, i + BATCH_SIZE3);
6058
- const extractions = [];
6059
- for (let c = 0; c < batch.length; c += CONCURRENCY) {
6060
- const chunk = batch.slice(c, c + CONCURRENCY);
6061
- const results = await Promise.all(
6062
- chunk.map((relPath) => extractor.extract(relPath, force))
6063
- );
6064
- for (const ext of results) {
6065
- if (ext === "skipped") {
6066
- result.skipped++;
6067
- continue;
6068
- }
6069
- if (ext === "error") {
6070
- result.errors++;
6071
- continue;
6072
- }
6073
- extractions.push(ext);
6074
- }
6075
- }
6076
- if (extractions.length > 0) {
6077
- const state = this.getPipelineState();
6078
- const persistEdgeResolver = new EdgeResolver(state);
6079
- const persister = new FilePersister(state, (edges) => persistEdgeResolver.storeRawEdges(edges));
6080
- persister.persistBatch(extractions);
6081
- result.indexed += extractions.length;
6082
- }
6083
- const processed = result.indexed + result.skipped + result.errors;
6084
- this.progress?.update("indexing", { processed });
6085
- }
6086
- enableFts5Triggers(this.store.db);
6087
- const edgeResolver = new EdgeResolver(this.getPipelineState());
6088
- await edgeResolver.resolveEdges(this.buildProjectContext(), this.buildResolveContext());
6089
- edgeResolver.resolveOrmAssociationEdges();
6090
- edgeResolver.resolveTypeScriptHeritageEdges();
6091
- edgeResolver.resolveEsmImportEdges();
6092
- edgeResolver.resolveTestCoversEdges();
6093
- const envIndexer = new EnvIndexer(this.store, this.config, this.rootPath, this._traceignore);
6094
- await envIndexer.indexEnvFiles(force);
6028
+ await this.extractAndPersist(relPaths, force, result);
6029
+ await this.resolveAllEdges();
6030
+ await this.indexEnvFiles(force);
6095
6031
  } finally {
6096
6032
  this._fileContentCache.clear();
6097
6033
  this._pendingImports.clear();
@@ -6115,6 +6051,66 @@ var IndexingPipeline = class _IndexingPipeline {
6115
6051
  logger.info(result, "Indexing pipeline completed");
6116
6052
  return result;
6117
6053
  }
6054
+ /** Pass 1: extract symbols from files and persist in batched transactions. */
6055
+ async extractAndPersist(relPaths, force, result) {
6056
+ const extractor = new FileExtractor({
6057
+ store: this.store,
6058
+ registry: this.registry,
6059
+ rootPath: this.rootPath,
6060
+ workspaces: this.workspaces,
6061
+ gitignore: this._gitignore,
6062
+ fileContentCache: this._fileContentCache,
6063
+ buildProjectContext: () => this.buildProjectContext()
6064
+ });
6065
+ disableFts5Triggers(this.store.db);
6066
+ const BATCH_SIZE3 = Math.min(500, Math.max(100, Math.ceil(relPaths.length / 20)));
6067
+ const CONCURRENCY = Math.min(8, cpus().length);
6068
+ for (let i = 0; i < relPaths.length; i += BATCH_SIZE3) {
6069
+ const batch = relPaths.slice(i, i + BATCH_SIZE3);
6070
+ const extractions = [];
6071
+ for (let c = 0; c < batch.length; c += CONCURRENCY) {
6072
+ const chunk = batch.slice(c, c + CONCURRENCY);
6073
+ const results = await Promise.all(
6074
+ chunk.map((relPath) => extractor.extract(relPath, force))
6075
+ );
6076
+ for (const ext of results) {
6077
+ if (ext === "skipped") {
6078
+ result.skipped++;
6079
+ continue;
6080
+ }
6081
+ if (ext === "error") {
6082
+ result.errors++;
6083
+ continue;
6084
+ }
6085
+ extractions.push(ext);
6086
+ }
6087
+ }
6088
+ if (extractions.length > 0) {
6089
+ const state = this.getPipelineState();
6090
+ const persistEdgeResolver = new EdgeResolver(state);
6091
+ const persister = new FilePersister(state, (edges) => persistEdgeResolver.storeRawEdges(edges));
6092
+ persister.persistBatch(extractions);
6093
+ result.indexed += extractions.length;
6094
+ }
6095
+ const processed = result.indexed + result.skipped + result.errors;
6096
+ this.progress?.update("indexing", { processed });
6097
+ }
6098
+ enableFts5Triggers(this.store.db);
6099
+ }
6100
+ /** Pass 2: resolve all edge types (imports, heritage, ORM, tests). */
6101
+ async resolveAllEdges() {
6102
+ const edgeResolver = new EdgeResolver(this.getPipelineState());
6103
+ await edgeResolver.resolveEdges(this.buildProjectContext(), this.buildResolveContext());
6104
+ edgeResolver.resolveOrmAssociationEdges();
6105
+ edgeResolver.resolveTypeScriptHeritageEdges();
6106
+ edgeResolver.resolveEsmImportEdges();
6107
+ edgeResolver.resolveTestCoversEdges();
6108
+ }
6109
+ /** Pass 3: index .env files for environment variable tracking. */
6110
+ async indexEnvFiles(force) {
6111
+ const envIndexer = new EnvIndexer(this.store, this.config, this.rootPath, this._traceignore);
6112
+ await envIndexer.indexEnvFiles(force);
6113
+ }
6118
6114
  buildProjectContext() {
6119
6115
  if (!this._projectContext) {
6120
6116
  this._projectContext = buildProjectContext(this.rootPath);
@@ -8589,186 +8585,6 @@ function getProjectMap(store, registry, summaryOnly, projectContext) {
8589
8585
  }
8590
8586
 
8591
8587
  // src/tools/analysis/duplication.ts
8592
- var CHECKABLE_KINDS = /* @__PURE__ */ new Set([
8593
- "function",
8594
- "class",
8595
- "method",
8596
- "interface",
8597
- "type_alias",
8598
- "enum"
8599
- ]);
8600
- var TRIVIAL_NAMES = /* @__PURE__ */ new Set([
8601
- "constructor",
8602
- "toString",
8603
- "toJSON",
8604
- "valueOf",
8605
- "render",
8606
- "setup",
8607
- "main",
8608
- "index",
8609
- "default",
8610
- "init",
8611
- "create",
8612
- "get",
8613
- "set",
8614
- "delete",
8615
- "update",
8616
- "handle",
8617
- "process",
8618
- "run",
8619
- "start",
8620
- "stop",
8621
- "reset",
8622
- "configure",
8623
- "register",
8624
- "execute",
8625
- "validate",
8626
- "transform",
8627
- "apply",
8628
- "call",
8629
- "bind",
8630
- "map",
8631
- "filter",
8632
- "reduce",
8633
- "forEach",
8634
- "connect",
8635
- "disconnect",
8636
- "open",
8637
- "close",
8638
- "build",
8639
- "destroy",
8640
- "mount",
8641
- "unmount",
8642
- "dispose",
8643
- "serialize",
8644
- "deserialize"
8645
- ]);
8646
- var TEST_PATH_RE2 = /(?:^|[/\\])(?:tests?|__tests__|spec)[/\\]|\.(?:test|spec)\.[jt]sx?$/i;
8647
- var MIN_NAME_LENGTH = 4;
8648
- var MAX_SYMBOLS_PER_FILE2 = 30;
8649
- var W_NAME = 0.45;
8650
- var W_KIND = 0.15;
8651
- var W_SIGNATURE = 0.25;
8652
- var W_TOKEN = 0.15;
8653
- function tokenizeName(name) {
8654
- const parts = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split(/[_\-./:]+/).filter((p) => p.length > 1);
8655
- return new Set(parts);
8656
- }
8657
- function jaccard(a, b) {
8658
- if (a.size === 0 && b.size === 0) return 0;
8659
- let intersection = 0;
8660
- for (const t of a) {
8661
- if (b.has(t)) intersection++;
8662
- }
8663
- const union = a.size + b.size - intersection;
8664
- return union === 0 ? 0 : intersection / union;
8665
- }
8666
- function signatureSimilarity(srcParamCount, candParamCount) {
8667
- if (srcParamCount == null && candParamCount == null) return 0.5;
8668
- if (srcParamCount == null || candParamCount == null) return 0.3;
8669
- const max = Math.max(srcParamCount, candParamCount, 1);
8670
- return 1 - Math.abs(srcParamCount - candParamCount) / max;
8671
- }
8672
- function getHeritageNames(metadata) {
8673
- if (!metadata) return /* @__PURE__ */ new Set();
8674
- try {
8675
- const parsed = JSON.parse(metadata);
8676
- const names = /* @__PURE__ */ new Set();
8677
- if (typeof parsed.extends === "string") names.add(parsed.extends);
8678
- if (Array.isArray(parsed.extends)) {
8679
- for (const e of parsed.extends) if (typeof e === "string") names.add(e);
8680
- }
8681
- if (typeof parsed.implements === "string") names.add(parsed.implements);
8682
- if (Array.isArray(parsed.implements)) {
8683
- for (const i of parsed.implements) if (typeof i === "string") names.add(i);
8684
- }
8685
- return names;
8686
- } catch {
8687
- return /* @__PURE__ */ new Set();
8688
- }
8689
- }
8690
- function findDuplicateSymbols(store, db, sourceSymbols, sourceFileId, sourceFilePath, options) {
8691
- const threshold = options?.threshold ?? 0.7;
8692
- const maxResults = options?.maxResults ?? 10;
8693
- const sourceIsTest = TEST_PATH_RE2.test(sourceFilePath);
8694
- const heritageNames = /* @__PURE__ */ new Set();
8695
- for (const sym of sourceSymbols) {
8696
- const h = getHeritageNames(sym.metadata);
8697
- for (const n of h) heritageNames.add(n);
8698
- }
8699
- const checkable = sourceSymbols.filter(
8700
- (s) => CHECKABLE_KINDS.has(s.kind) && s.name.length >= MIN_NAME_LENGTH && !TRIVIAL_NAMES.has(s.name)
8701
- ).slice(0, MAX_SYMBOLS_PER_FILE2);
8702
- const allWarnings = [];
8703
- const paramCountStmt = db.prepare(
8704
- "SELECT param_count FROM symbols WHERE id = ?"
8705
- );
8706
- for (const src of checkable) {
8707
- const srcParamCount = src.param_count ?? paramCountStmt.get(src.id)?.param_count ?? null;
8708
- const srcTokens = tokenizeName(src.fqn ?? src.name);
8709
- const candidates = fuzzySearch(db, src.name, {
8710
- threshold: 0.25,
8711
- limit: 30,
8712
- kind: src.kind
8713
- });
8714
- for (const cand of candidates) {
8715
- if (cand.fileId === sourceFileId) continue;
8716
- if (cand.symbolIdStr === src.symbol_id) continue;
8717
- const candFile = store.getFileById(cand.fileId);
8718
- if (!candFile) continue;
8719
- const candIsTest = TEST_PATH_RE2.test(candFile.path);
8720
- if (sourceIsTest !== candIsTest) continue;
8721
- if (heritageNames.has(cand.name)) continue;
8722
- const candSymbol = getCandidateSymbol(db, cand.symbolId);
8723
- if (candSymbol) {
8724
- const candHeritage = getHeritageNames(candSymbol.metadata);
8725
- for (const h of candHeritage) {
8726
- if (heritageNames.has(h)) continue;
8727
- }
8728
- }
8729
- const nameSim = cand.similarity;
8730
- const kindMatch = cand.kind === src.kind ? 1 : 0;
8731
- const candParamCount = candSymbol?.param_count ?? paramCountStmt.get(cand.symbolId)?.param_count ?? null;
8732
- const sigSim = signatureSimilarity(srcParamCount, candParamCount);
8733
- const candTokens = tokenizeName(cand.fqn ?? cand.name);
8734
- const tokenOvl = jaccard(srcTokens, candTokens);
8735
- const score = W_NAME * nameSim + W_KIND * kindMatch + W_SIGNATURE * sigSim + W_TOKEN * tokenOvl;
8736
- if (score >= threshold) {
8737
- allWarnings.push({
8738
- source_symbol_id: src.symbol_id,
8739
- source_name: src.name,
8740
- source_file: sourceFilePath,
8741
- duplicate_symbol_id: cand.symbolIdStr,
8742
- duplicate_name: cand.name,
8743
- duplicate_file: candFile.path,
8744
- duplicate_line: candSymbol?.line_start ?? null,
8745
- score: Math.round(score * 1e3) / 1e3,
8746
- signals: {
8747
- name_similarity: Math.round(nameSim * 1e3) / 1e3,
8748
- kind_match: kindMatch,
8749
- signature_similarity: Math.round(sigSim * 1e3) / 1e3,
8750
- token_overlap: Math.round(tokenOvl * 1e3) / 1e3
8751
- }
8752
- });
8753
- }
8754
- }
8755
- }
8756
- allWarnings.sort((a, b) => b.score - a.score);
8757
- const seen = /* @__PURE__ */ new Set();
8758
- const deduped = [];
8759
- for (const w of allWarnings) {
8760
- const key = `${w.source_symbol_id}::${w.duplicate_symbol_id}`;
8761
- if (seen.has(key)) continue;
8762
- seen.add(key);
8763
- deduped.push(w);
8764
- if (deduped.length >= maxResults) break;
8765
- }
8766
- return {
8767
- warnings: deduped,
8768
- symbols_checked: checkable.length,
8769
- threshold
8770
- };
8771
- }
8772
8588
  function checkFileForDuplicates(store, db, filePath, options) {
8773
8589
  const file = store.getFile(filePath);
8774
8590
  if (!file) {
@@ -8811,11 +8627,6 @@ function checkSymbolForDuplicates(store, db, query, options) {
8811
8627
  }
8812
8628
  return { warnings: [], symbols_checked: 0, threshold };
8813
8629
  }
8814
- function getCandidateSymbol(db, symbolId) {
8815
- return db.prepare(
8816
- "SELECT * FROM symbols WHERE id = ?"
8817
- ).get(symbolId);
8818
- }
8819
8630
 
8820
8631
  // src/tools/register/core.ts
8821
8632
  function registerCoreTools(server, ctx) {
@@ -10657,11 +10468,11 @@ function suggestQueries(store) {
10657
10468
 
10658
10469
  // src/tools/navigation/related.ts
10659
10470
  import { ok as ok4, err as err5 } from "neverthrow";
10660
- function tokenizeName2(name) {
10471
+ function tokenizeName(name) {
10661
10472
  const parts = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split(/[_\-.]/).filter((p) => p.length > 1);
10662
10473
  return new Set(parts);
10663
10474
  }
10664
- function jaccard2(a, b) {
10475
+ function jaccard(a, b) {
10665
10476
  if (a.size === 0 && b.size === 0) return 0;
10666
10477
  let intersection = 0;
10667
10478
  for (const t of a) {
@@ -10750,13 +10561,13 @@ function getRelatedSymbols(store, opts) {
10750
10561
  }
10751
10562
  }
10752
10563
  }
10753
- const targetTokens = tokenizeName2(target.name);
10564
+ const targetTokens = tokenizeName(target.name);
10754
10565
  const candidateIds = [...scores.keys()];
10755
10566
  const candidateSymbols = store.getSymbolsByIds(candidateIds);
10756
10567
  const candidateFileIds = [...new Set([...candidateSymbols.values()].map((s) => s.file_id))];
10757
10568
  const fileMap = store.getFilesByIds(candidateFileIds);
10758
10569
  for (const [symId, sym] of candidateSymbols) {
10759
- const nameScore = jaccard2(targetTokens, tokenizeName2(sym.name));
10570
+ const nameScore = jaccard(targetTokens, tokenizeName(sym.name));
10760
10571
  if (nameScore > 0) {
10761
10572
  ensureEntry(symId).name_overlap = nameScore;
10762
10573
  }
@@ -11853,63 +11664,9 @@ var FederationManager = class {
11853
11664
  detectionSource: svc.detectionSource,
11854
11665
  metadata: svc.metadata
11855
11666
  });
11856
- const contracts = parseContracts(svc.repoRoot);
11857
- if (opts?.contractPaths) {
11858
- for (const cp of opts.contractPaths) {
11859
- const absContract = path29.resolve(absRoot, cp);
11860
- if (fs23.existsSync(absContract)) {
11861
- const additionalContracts = parseContracts(path29.dirname(absContract));
11862
- contracts.push(...additionalContracts.filter(
11863
- (c) => path29.resolve(absRoot, c.specPath) === absContract
11864
- ));
11865
- }
11866
- }
11867
- }
11868
- for (const contract of contracts) {
11869
- const contractId = this.topoStore.insertContract(serviceId, {
11870
- contractType: contract.type,
11871
- specPath: contract.specPath,
11872
- version: contract.version,
11873
- parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
11874
- });
11875
- this.topoStore.insertEndpoints(
11876
- contractId,
11877
- serviceId,
11878
- contract.endpoints.map((e) => ({
11879
- method: e.method ?? void 0,
11880
- path: e.path,
11881
- operationId: e.operationId,
11882
- requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
11883
- responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
11884
- }))
11885
- );
11886
- if (contract.events.length > 0) {
11887
- this.topoStore.insertEventChannels(
11888
- contractId,
11889
- serviceId,
11890
- contract.events.map((e) => ({
11891
- channelName: e.channelName,
11892
- direction: e.direction
11893
- }))
11894
- );
11895
- }
11896
- }
11667
+ this.registerContracts(serviceId, svc.repoRoot, absRoot, opts?.contractPaths);
11897
11668
  }
11898
- this.topoStore.deleteClientCallsByRepo(repoId);
11899
- const clientCalls = scanClientCalls(absRoot);
11900
- if (clientCalls.length > 0) {
11901
- this.topoStore.insertClientCalls(clientCalls.map((c) => ({
11902
- sourceRepoId: repoId,
11903
- filePath: c.filePath,
11904
- line: c.line,
11905
- callType: c.callType,
11906
- method: c.method,
11907
- urlPattern: c.urlPattern,
11908
- confidence: c.confidence
11909
- })));
11910
- }
11911
- const linkedCount = this.topoStore.linkClientCallsToEndpoints();
11912
- this.buildCrossServiceEdges();
11669
+ const clientCalls = this.scanAndLinkClientCalls(repoId, absRoot);
11913
11670
  this.topoStore.updateFederatedRepoSyncTime(repoId);
11914
11671
  const stats = this.topoStore.getTopologyStats();
11915
11672
  return {
@@ -11918,8 +11675,8 @@ var FederationManager = class {
11918
11675
  services: detected.length,
11919
11676
  contracts: stats.contracts,
11920
11677
  endpoints: stats.endpoints,
11921
- clientCalls: clientCalls.length,
11922
- linkedCalls: linkedCount
11678
+ clientCalls: clientCalls.scanned,
11679
+ linkedCalls: clientCalls.linked
11923
11680
  };
11924
11681
  }
11925
11682
  /**
@@ -12023,75 +11780,26 @@ var FederationManager = class {
12023
11780
  detectionSource: svc.detectionSource,
12024
11781
  metadata: svc.metadata
12025
11782
  });
12026
- const existingContracts = this.topoStore.getContractsByService(serviceId);
12027
- for (const ec of existingContracts) {
12028
- this.topoStore.insertContractSnapshot(ec.id, serviceId, {
12029
- version: ec.version,
12030
- specPath: ec.spec_path,
12031
- contentHash: ec.content_hash ?? "",
12032
- endpointsJson: ec.parsed_spec,
12033
- eventsJson: "[]"
12034
- });
12035
- }
11783
+ this.snapshotContracts(serviceId);
12036
11784
  this.topoStore.deleteContractsByService(serviceId);
12037
11785
  const contracts = parseContracts(svc.repoRoot);
12038
11786
  contractsUpdated += contracts.length;
12039
11787
  for (const contract of contracts) {
12040
- const contractId = this.topoStore.insertContract(serviceId, {
12041
- contractType: contract.type,
12042
- specPath: contract.specPath,
12043
- version: contract.version,
12044
- parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
12045
- });
12046
- this.topoStore.insertEndpoints(
12047
- contractId,
12048
- serviceId,
12049
- contract.endpoints.map((e) => ({
12050
- method: e.method ?? void 0,
12051
- path: e.path,
12052
- operationId: e.operationId,
12053
- requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
12054
- responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
12055
- }))
12056
- );
12057
11788
  endpointsUpdated += contract.endpoints.length;
12058
- if (contract.events.length > 0) {
12059
- this.topoStore.insertEventChannels(
12060
- contractId,
12061
- serviceId,
12062
- contract.events.map((e) => ({
12063
- channelName: e.channelName,
12064
- direction: e.direction
12065
- }))
12066
- );
12067
- }
12068
11789
  }
11790
+ this.registerContracts(serviceId, svc.repoRoot);
12069
11791
  }
12070
- this.topoStore.deleteClientCallsByRepo(repo.id);
12071
- const calls = scanClientCalls(repo.repo_root);
12072
- clientCallsScanned += calls.length;
12073
- if (calls.length > 0) {
12074
- this.topoStore.insertClientCalls(calls.map((c) => ({
12075
- sourceRepoId: repo.id,
12076
- filePath: c.filePath,
12077
- line: c.line,
12078
- callType: c.callType,
12079
- method: c.method,
12080
- urlPattern: c.urlPattern,
12081
- confidence: c.confidence
12082
- })));
12083
- }
11792
+ const calls = this.scanAndLinkClientCalls(repo.id, repo.repo_root);
11793
+ clientCallsScanned += calls.scanned;
12084
11794
  this.topoStore.updateFederatedRepoSyncTime(repo.id);
12085
11795
  }
12086
- const newlyLinked = this.topoStore.linkClientCallsToEndpoints();
12087
- this.buildCrossServiceEdges();
12088
11796
  return {
12089
11797
  repos: repos.length,
12090
11798
  servicesUpdated,
12091
11799
  contractsUpdated,
12092
11800
  endpointsUpdated,
12093
11801
  clientCallsScanned,
12094
- newlyLinked,
11802
+ newlyLinked: this.topoStore.linkClientCallsToEndpoints(),
12095
11803
  crossRepoEdges: this.topoStore.getTopologyStats().crossEdges
12096
11804
  };
12097
11805
  }
@@ -12101,82 +11809,18 @@ var FederationManager = class {
12101
11809
  * If per-repo DBs exist, resolves down to symbol level.
12102
11810
  */
12103
11811
  getImpact(opts) {
11812
+ const matchingEndpoints = this.filterEndpoints(opts);
12104
11813
  const results = [];
12105
- const allEndpoints = this.topoStore.getAllEndpoints();
12106
- let matchingEndpoints = allEndpoints;
12107
- if (opts.endpoint) {
12108
- const normalized = opts.endpoint.toLowerCase();
12109
- matchingEndpoints = allEndpoints.filter(
12110
- (ep) => ep.path.toLowerCase().includes(normalized)
12111
- );
12112
- }
12113
- if (opts.method) {
12114
- matchingEndpoints = matchingEndpoints.filter(
12115
- (ep) => ep.method?.toUpperCase() === opts.method.toUpperCase()
12116
- );
12117
- }
12118
- if (opts.service) {
12119
- matchingEndpoints = matchingEndpoints.filter(
12120
- (ep) => ep.service_name.toLowerCase() === opts.service.toLowerCase()
12121
- );
12122
- }
12123
11814
  for (const ep of matchingEndpoints) {
12124
11815
  const clientCalls = this.topoStore.getClientCallsByEndpoint(ep.id);
12125
11816
  if (clientCalls.length === 0) continue;
12126
- const byRepo = /* @__PURE__ */ new Map();
12127
- for (const call of clientCalls) {
12128
- const repo2 = call.source_repo_name;
12129
- if (!byRepo.has(repo2)) byRepo.set(repo2, []);
12130
- byRepo.get(repo2).push(call);
12131
- }
12132
- const clients = [];
12133
- for (const [repoName, calls] of byRepo) {
12134
- const repo2 = this.topoStore.getFederatedRepo(repoName);
12135
- for (const call of calls) {
12136
- const symbols = repo2?.db_path && fs23.existsSync(repo2.db_path) ? resolveSymbolsAtLocation(repo2.db_path, call.file_path, call.line) : [];
12137
- clients.push({
12138
- repo: repoName,
12139
- filePath: call.file_path,
12140
- line: call.line,
12141
- callType: call.call_type,
12142
- confidence: call.confidence,
12143
- symbols
12144
- });
12145
- }
12146
- }
11817
+ const clients = this.collectEndpointClients(clientCalls);
12147
11818
  const uniqueRepos = new Set(clients.map((c) => c.repo));
12148
- const riskLevel3 = uniqueRepos.size >= 3 ? "critical" : uniqueRepos.size >= 2 ? "high" : clients.length >= 3 ? "medium" : "low";
11819
+ const baseRisk = computeRiskLevel(uniqueRepos.size, clients.length);
12149
11820
  const svc = this.topoStore.getAllServices().find((s) => s.id === ep.service_id);
12150
11821
  const repo = svc ? this.topoStore.getFederatedRepo(svc.repo_root) : void 0;
12151
- let breakingChanges;
12152
- const contracts = this.topoStore.getContractsByService(ep.service_id);
12153
- for (const contract of contracts) {
12154
- const snapshot = this.topoStore.getLatestSnapshot(contract.id);
12155
- if (!snapshot) continue;
12156
- let oldEndpoints = [];
12157
- try {
12158
- const parsed = JSON.parse(snapshot.endpoints_json);
12159
- oldEndpoints = (parsed.endpoints ?? []).map((e) => ({ method: e.method ?? null, path: e.path, requestSchema: e.requestSchema, responseSchema: e.responseSchema }));
12160
- } catch {
12161
- continue;
12162
- }
12163
- const currentEndpoints = this.topoStore.getEndpointsByService(ep.service_id).map((e) => ({
12164
- method: e.method,
12165
- path: e.path,
12166
- requestSchema: e.request_schema,
12167
- responseSchema: e.response_schema
12168
- }));
12169
- const epDiffs = diffEndpoints(oldEndpoints, currentEndpoints).filter((d) => d.endpoint.path === ep.path && (d.endpoint.method ?? "*") === (ep.method ?? "*"));
12170
- if (epDiffs.length > 0 && epDiffs.some((d) => d.breaking)) {
12171
- breakingChanges = epDiffs;
12172
- }
12173
- }
12174
- let finalRiskLevel = riskLevel3;
12175
- if (breakingChanges?.some((d) => d.breaking)) {
12176
- const riskLevels = ["low", "medium", "high", "critical"];
12177
- const idx = riskLevels.indexOf(riskLevel3);
12178
- if (idx < riskLevels.length - 1) finalRiskLevel = riskLevels[idx + 1];
12179
- }
11822
+ const breakingChanges = this.detectBreakingChanges(ep);
11823
+ const riskLevel3 = upgradeRiskIfBreaking(baseRisk, breakingChanges);
12180
11824
  results.push({
12181
11825
  endpoint: {
12182
11826
  method: ep.method,
@@ -12185,13 +11829,152 @@ var FederationManager = class {
12185
11829
  repo: repo?.name ?? svc?.repo_root ?? "unknown"
12186
11830
  },
12187
11831
  clients,
12188
- riskLevel: finalRiskLevel,
11832
+ riskLevel: riskLevel3,
12189
11833
  summary: `${ep.method ?? "*"} ${ep.path} is called by ${clients.length} client(s) in ${uniqueRepos.size} repo(s)${breakingChanges ? " \u26A0 BREAKING SCHEMA CHANGES" : ""}`,
12190
11834
  breakingChanges
12191
11835
  });
12192
11836
  }
12193
11837
  return results;
12194
11838
  }
11839
+ /** Register contracts for a service, including explicitly provided paths. */
11840
+ registerContracts(serviceId, serviceRoot, repoRoot, explicitPaths) {
11841
+ const contracts = parseContracts(serviceRoot);
11842
+ if (explicitPaths && repoRoot) {
11843
+ for (const cp of explicitPaths) {
11844
+ const absContract = path29.resolve(repoRoot, cp);
11845
+ if (fs23.existsSync(absContract)) {
11846
+ const additional = parseContracts(path29.dirname(absContract));
11847
+ contracts.push(...additional.filter(
11848
+ (c) => path29.resolve(repoRoot, c.specPath) === absContract
11849
+ ));
11850
+ }
11851
+ }
11852
+ }
11853
+ for (const contract of contracts) {
11854
+ const contractId = this.topoStore.insertContract(serviceId, {
11855
+ contractType: contract.type,
11856
+ specPath: contract.specPath,
11857
+ version: contract.version,
11858
+ parsedSpec: JSON.stringify({ endpoints: contract.endpoints, events: contract.events })
11859
+ });
11860
+ this.topoStore.insertEndpoints(
11861
+ contractId,
11862
+ serviceId,
11863
+ contract.endpoints.map((e) => ({
11864
+ method: e.method ?? void 0,
11865
+ path: e.path,
11866
+ operationId: e.operationId,
11867
+ requestSchema: e.requestSchema ? JSON.stringify(e.requestSchema) : void 0,
11868
+ responseSchema: e.responseSchema ? JSON.stringify(e.responseSchema) : void 0
11869
+ }))
11870
+ );
11871
+ if (contract.events.length > 0) {
11872
+ this.topoStore.insertEventChannels(
11873
+ contractId,
11874
+ serviceId,
11875
+ contract.events.map((e) => ({
11876
+ channelName: e.channelName,
11877
+ direction: e.direction
11878
+ }))
11879
+ );
11880
+ }
11881
+ }
11882
+ }
11883
+ /** Snapshot existing contracts before replacing them (for drift detection). */
11884
+ snapshotContracts(serviceId) {
11885
+ const existing = this.topoStore.getContractsByService(serviceId);
11886
+ for (const ec of existing) {
11887
+ this.topoStore.insertContractSnapshot(ec.id, serviceId, {
11888
+ version: ec.version,
11889
+ specPath: ec.spec_path,
11890
+ contentHash: ec.content_hash ?? "",
11891
+ endpointsJson: ec.parsed_spec,
11892
+ eventsJson: "[]"
11893
+ });
11894
+ }
11895
+ }
11896
+ /** Scan repo for client calls, insert them, link to endpoints, and build edges. */
11897
+ scanAndLinkClientCalls(repoId, repoRoot) {
11898
+ this.topoStore.deleteClientCallsByRepo(repoId);
11899
+ const clientCalls = scanClientCalls(repoRoot);
11900
+ if (clientCalls.length > 0) {
11901
+ this.topoStore.insertClientCalls(clientCalls.map((c) => ({
11902
+ sourceRepoId: repoId,
11903
+ filePath: c.filePath,
11904
+ line: c.line,
11905
+ callType: c.callType,
11906
+ method: c.method,
11907
+ urlPattern: c.urlPattern,
11908
+ confidence: c.confidence
11909
+ })));
11910
+ }
11911
+ const linked = this.topoStore.linkClientCallsToEndpoints();
11912
+ this.buildCrossServiceEdges();
11913
+ return { scanned: clientCalls.length, linked };
11914
+ }
11915
+ filterEndpoints(opts) {
11916
+ let endpoints = this.topoStore.getAllEndpoints();
11917
+ if (opts.endpoint) {
11918
+ const normalized = opts.endpoint.toLowerCase();
11919
+ endpoints = endpoints.filter((ep) => ep.path.toLowerCase().includes(normalized));
11920
+ }
11921
+ if (opts.method) {
11922
+ endpoints = endpoints.filter((ep) => ep.method?.toUpperCase() === opts.method.toUpperCase());
11923
+ }
11924
+ if (opts.service) {
11925
+ endpoints = endpoints.filter((ep) => ep.service_name.toLowerCase() === opts.service.toLowerCase());
11926
+ }
11927
+ return endpoints;
11928
+ }
11929
+ collectEndpointClients(clientCalls) {
11930
+ const byRepo = /* @__PURE__ */ new Map();
11931
+ for (const call of clientCalls) {
11932
+ const repo = call.source_repo_name;
11933
+ if (!byRepo.has(repo)) byRepo.set(repo, []);
11934
+ byRepo.get(repo).push(call);
11935
+ }
11936
+ const clients = [];
11937
+ for (const [repoName, calls] of byRepo) {
11938
+ const repo = this.topoStore.getFederatedRepo(repoName);
11939
+ for (const call of calls) {
11940
+ const symbols = repo?.db_path && fs23.existsSync(repo.db_path) ? resolveSymbolsAtLocation(repo.db_path, call.file_path, call.line) : [];
11941
+ clients.push({
11942
+ repo: repoName,
11943
+ filePath: call.file_path,
11944
+ line: call.line,
11945
+ callType: call.call_type,
11946
+ confidence: call.confidence,
11947
+ symbols
11948
+ });
11949
+ }
11950
+ }
11951
+ return clients;
11952
+ }
11953
+ detectBreakingChanges(ep) {
11954
+ const contracts = this.topoStore.getContractsByService(ep.service_id);
11955
+ for (const contract of contracts) {
11956
+ const snapshot = this.topoStore.getLatestSnapshot(contract.id);
11957
+ if (!snapshot) continue;
11958
+ let oldEndpoints = [];
11959
+ try {
11960
+ const parsed = JSON.parse(snapshot.endpoints_json);
11961
+ oldEndpoints = (parsed.endpoints ?? []).map((e) => ({ method: e.method ?? null, path: e.path, requestSchema: e.requestSchema, responseSchema: e.responseSchema }));
11962
+ } catch {
11963
+ continue;
11964
+ }
11965
+ const currentEndpoints = this.topoStore.getEndpointsByService(ep.service_id).map((e) => ({
11966
+ method: e.method,
11967
+ path: e.path,
11968
+ requestSchema: e.request_schema,
11969
+ responseSchema: e.response_schema
11970
+ }));
11971
+ const epDiffs = diffEndpoints(oldEndpoints, currentEndpoints).filter((d) => d.endpoint.path === ep.path && (d.endpoint.method ?? "*") === (ep.method ?? "*"));
11972
+ if (epDiffs.length > 0 && epDiffs.some((d) => d.breaking)) {
11973
+ return epDiffs;
11974
+ }
11975
+ }
11976
+ return void 0;
11977
+ }
12195
11978
  /**
12196
11979
  * Search across all federated repos. Opens each per-repo DB readonly,
12197
11980
  * runs FTS search, normalizes scores, merges results.
@@ -12277,6 +12060,18 @@ var FederationManager = class {
12277
12060
  }
12278
12061
  }
12279
12062
  };
12063
+ function computeRiskLevel(uniqueRepoCount, clientCount) {
12064
+ if (uniqueRepoCount >= 3) return "critical";
12065
+ if (uniqueRepoCount >= 2) return "high";
12066
+ if (clientCount >= 3) return "medium";
12067
+ return "low";
12068
+ }
12069
+ function upgradeRiskIfBreaking(risk, breakingChanges) {
12070
+ if (!breakingChanges?.some((d) => d.breaking)) return risk;
12071
+ const levels = ["low", "medium", "high", "critical"];
12072
+ const idx = levels.indexOf(risk);
12073
+ return idx < levels.length - 1 ? levels[idx + 1] : risk;
12074
+ }
12280
12075
  function resolveSymbolsAtLocation(dbPath, filePath, line) {
12281
12076
  if (!line) return [];
12282
12077
  try {
@@ -22465,13 +22260,13 @@ function detectDrift(store, cwd, options = {}) {
22465
22260
  const commitsB = fileCommitCount.get(fileB) ?? 0;
22466
22261
  const denominator = commitsA + commitsB - count;
22467
22262
  if (denominator <= 0) continue;
22468
- const jaccard3 = count / denominator;
22469
- if (jaccard3 < minConfidence) continue;
22263
+ const jaccard2 = count / denominator;
22264
+ if (jaccard2 < minConfidence) continue;
22470
22265
  anomalies.push({
22471
22266
  file_a: fileA,
22472
22267
  file_b: fileB,
22473
22268
  co_change_count: count,
22474
- confidence: round2(jaccard3),
22269
+ confidence: round2(jaccard2),
22475
22270
  module_a: moduleA,
22476
22271
  module_b: moduleB
22477
22272
  });
@@ -28797,7 +28592,7 @@ var TopologyStore = class {
28797
28592
  };
28798
28593
 
28799
28594
  // src/server/server.ts
28800
- var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
28595
+ var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
28801
28596
  function j2(value) {
28802
28597
  return JSON.stringify(value, (_key, val) => val === null || val === void 0 ? void 0 : val);
28803
28598
  }