gitnexus 1.6.6-rc.79 → 1.6.6-rc.80

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.
@@ -2371,6 +2371,7 @@ export class LocalBackend {
2371
2371
  */
2372
2372
  async _runImpactBFS(repo, sym, symType, direction, opts) {
2373
2373
  const { maxDepth, relationTypes, includeTests, minConfidence } = opts;
2374
+ const skipPerSymbolEnrichment = opts.skipPerSymbolEnrichment ?? false;
2374
2375
  const hasExplicitLimit = typeof opts.limit === 'number' && Number.isFinite(opts.limit);
2375
2376
  const paginationLimit = hasExplicitLimit
2376
2377
  ? Math.max(1, Math.min(Math.trunc(opts.limit), 10000))
@@ -2501,11 +2502,20 @@ export class LocalBackend {
2501
2502
  const directCount = (grouped[1] || []).length;
2502
2503
  let affectedProcesses = [];
2503
2504
  let affectedModules = [];
2505
+ // Per-symbol process membership: maps impacted symbol id -> list of processes
2506
+ // it participates in. Populated by a second chunked Cypher pass below when
2507
+ // any process is affected at all. Surfaced as `processes: [...]` on each
2508
+ // byDepth item so consumers can tell which caller belongs to which cron/
2509
+ // webhook/route without a follow-up query.
2510
+ const perSymbolProcesses = new Map();
2511
+ // Chunking bounds for batched DB round-trips. Declared at function scope so
2512
+ // both the in-block enrichment passes and the post-pagination per-symbol
2513
+ // process enrichment can reference them.
2514
+ const CHUNK_SIZE = 100;
2515
+ // Max number of chunks to process to avoid unbounded DB round-trips.
2516
+ // Configurable via env IMPACT_MAX_CHUNKS, default 10 => max items = 1000
2517
+ const MAX_CHUNKS = parseInt(process.env.IMPACT_MAX_CHUNKS || '10', 10);
2504
2518
  if (impacted.length > 0) {
2505
- const CHUNK_SIZE = 100;
2506
- // Max number of chunks to process to avoid unbounded DB round-trips.
2507
- // Configurable via env IMPACT_MAX_CHUNKS, default 10 => max items = 1000
2508
- const MAX_CHUNKS = parseInt(process.env.IMPACT_MAX_CHUNKS || '10', 10);
2509
2519
  // ── Process enrichment: batched chunking (bounded by MAX_CHUNKS) ─
2510
2520
  // Uses merged Cypher query (WITH + OPTIONAL MATCH) to fetch
2511
2521
  // process + entry point info in 1 round-trip per chunk. Converted to
@@ -2620,6 +2630,9 @@ export class LocalBackend {
2620
2630
  earliest_broken_step: ep.earliest_broken_step === Infinity ? null : ep.earliest_broken_step,
2621
2631
  }))
2622
2632
  .sort((a, b) => b.total_hits - a.total_hits);
2633
+ // Per-symbol process membership is populated post-pagination (see below)
2634
+ // so it covers exactly the symbols returned in byDepth, not a pre-capped
2635
+ // flat slice that could miss depth-2+ symbols when depth-1 is large.
2623
2636
  // ── Module enrichment: use same cap as process enrichment and parameterized queries
2624
2637
  const maxItems = Math.min(impacted.length, MAX_CHUNKS * CHUNK_SIZE);
2625
2638
  const cappedImpacted = impacted.slice(0, maxItems);
@@ -2746,7 +2759,7 @@ export class LocalBackend {
2746
2759
  if (summaryOnly) {
2747
2760
  return base;
2748
2761
  }
2749
- // Apply limit/offset pagination per depth level
2762
+ // Apply limit/offset pagination per depth level.
2750
2763
  const paginatedGrouped = {};
2751
2764
  let anyTruncated = false;
2752
2765
  for (const [depth, items] of Object.entries(grouped)) {
@@ -2757,8 +2770,81 @@ export class LocalBackend {
2757
2770
  anyTruncated = true;
2758
2771
  }
2759
2772
  }
2773
+ // ── Per-symbol process membership enrichment (post-pagination) ───────
2774
+ // Runs after paginatedGrouped is built so we enrich only the IDs that
2775
+ // actually appear in the response. This eliminates the false-empty
2776
+ // processes:[] case where a depth-2+ symbol's flat position in `impacted`
2777
+ // exceeded MAX_CHUNKS*CHUNK_SIZE even though it is returned by byDepth.
2778
+ // Also uses DISTINCT + MIN(r.step) per (symbol, process) pair to avoid
2779
+ // duplicate entries when a symbol has multiple STEP_IN_PROCESS edges.
2780
+ // Skipped entirely when `skipPerSymbolEnrichment` is set (group cross-repo
2781
+ // fan-out, which consumes byDepth but not byDepth[].processes); the
2782
+ // attach-loop below still stamps an empty processes:[] for shape stability.
2783
+ let perSymbolEnrichmentCapped = false;
2784
+ if (affectedProcesses.length > 0 && !skipPerSymbolEnrichment) {
2785
+ // Collect unique IDs from the paginated result in one pass.
2786
+ const pageIds = new Set();
2787
+ for (const items of Object.values(paginatedGrouped)) {
2788
+ for (const it of items) {
2789
+ const id = String(it.id ?? '');
2790
+ if (id)
2791
+ pageIds.add(id);
2792
+ }
2793
+ }
2794
+ // Bound the enrichment to the same ceiling as the aggregation pass
2795
+ // (MAX_CHUNKS * CHUNK_SIZE) so a large paginated page cannot trigger
2796
+ // unbounded DB round-trips (DoD 2.6). When capped, mark the result
2797
+ // partial so callers know some returned symbols may carry an empty
2798
+ // processes:[] that is a cap artifact, not a true absence.
2799
+ const maxPageIds = MAX_CHUNKS * CHUNK_SIZE;
2800
+ let pageIdArr = Array.from(pageIds);
2801
+ if (pageIdArr.length > maxPageIds) {
2802
+ pageIdArr = pageIdArr.slice(0, maxPageIds);
2803
+ perSymbolEnrichmentCapped = true;
2804
+ }
2805
+ for (let i = 0; i < pageIdArr.length; i += CHUNK_SIZE) {
2806
+ const chunkIds = pageIdArr.slice(i, i + CHUNK_SIZE);
2807
+ try {
2808
+ const rows = await executeParameterized(repo.id, `
2809
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
2810
+ WHERE s.id IN $ids
2811
+ RETURN s.id AS sid, p.id AS pid, p.heuristicLabel AS pName,
2812
+ p.processType AS pType, MIN(r.step) AS step
2813
+ `, { ids: chunkIds }).catch(() => []);
2814
+ for (const row of rows) {
2815
+ const sid = row.sid ?? row[0];
2816
+ if (!sid)
2817
+ continue;
2818
+ const procEntry = {
2819
+ id: String(row.pid ?? row[1] ?? ''),
2820
+ label: String(row.pName ?? row[2] ?? ''),
2821
+ processType: String(row.pType ?? row[3] ?? ''),
2822
+ step: Number(row.step ?? row[4] ?? -1),
2823
+ };
2824
+ const list = perSymbolProcesses.get(String(sid));
2825
+ if (list)
2826
+ list.push(procEntry);
2827
+ else
2828
+ perSymbolProcesses.set(String(sid), [procEntry]);
2829
+ }
2830
+ }
2831
+ catch (e) {
2832
+ logQueryError('impact:per-symbol-process-chunk', e);
2833
+ }
2834
+ }
2835
+ }
2836
+ // Attach processes field to each paginated item.
2837
+ for (const items of Object.values(paginatedGrouped)) {
2838
+ for (const it of items) {
2839
+ it.processes = perSymbolProcesses.get(String(it.id)) ?? [];
2840
+ }
2841
+ }
2760
2842
  return {
2761
2843
  ...base,
2844
+ // Surface partial if the per-symbol enrichment was capped, even when the
2845
+ // BFS traversal itself completed — some returned symbols may carry an
2846
+ // empty processes:[] that is a cap artifact rather than a true absence.
2847
+ ...(perSymbolEnrichmentCapped && { partial: true }),
2762
2848
  ...(anyTruncated && {
2763
2849
  pagination: {
2764
2850
  ...(Number.isFinite(paginationLimit) && { limit: paginationLimit }),
@@ -2830,11 +2916,20 @@ export class LocalBackend {
2830
2916
  'METHOD_IMPLEMENTS',
2831
2917
  ];
2832
2918
  try {
2919
+ // skipPerSymbolEnrichment suppresses ONLY the per-symbol STEP_IN_PROCESS
2920
+ // enrichment pass while preserving byDepth. Group-mode cross-repo fan-out
2921
+ // may fan across many repos; the per-symbol pass adds up to MAX_CHUNKS
2922
+ // extra round-trips per repo, which is unacceptable at group scale. But
2923
+ // cross-impact fan-out DOES consume byDepth (cross-impact.ts reads
2924
+ // fan.byDepth to populate group by_depth), so summaryOnly would wrongly
2925
+ // drop it. Group callers do not consume byDepth[].processes, so skipping
2926
+ // only that enrichment is the correct, targeted suppression.
2833
2927
  return await this._runImpactBFS(repo, sym, symType, dir, {
2834
2928
  maxDepth: opts.maxDepth,
2835
2929
  relationTypes,
2836
2930
  includeTests: opts.includeTests,
2837
2931
  minConfidence: opts.minConfidence,
2932
+ skipPerSymbolEnrichment: true,
2838
2933
  });
2839
2934
  }
2840
2935
  catch {
package/dist/mcp/tools.js CHANGED
@@ -299,7 +299,7 @@ Output includes:
299
299
  - summary: direct callers, processes affected, modules affected
300
300
  - affected_processes: which execution flows break and at which step
301
301
  - affected_modules: which functional areas are hit (direct vs indirect)
302
- - byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated)
302
+ - byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated). Each item includes a processes:[{id,label,processType,step}] field listing the execution flows that symbol participates in. Empty when the symbol has no process membership. Can ALSO be empty when partial:true is set — either the process-aggregation pass hit its cap before detecting affected processes, or per-symbol enrichment was capped on a very large page. When partial:true, do NOT treat processes:[] as proof of no participation; cross-check the top-level affected_processes list.
303
303
 
304
304
  Depth groups:
305
305
  - d=1: WILL BREAK (direct callers/importers)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.79",
3
+ "version": "1.6.6-rc.80",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",