gitnexus 1.6.8-rc.55 → 1.6.8-rc.57

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.
Files changed (92) hide show
  1. package/dist/_shared/graph/types.d.ts +17 -1
  2. package/dist/_shared/graph/types.d.ts.map +1 -1
  3. package/dist/cli/ai-context.js +3 -1
  4. package/dist/cli/eval-server.d.ts +8 -0
  5. package/dist/cli/eval-server.js +242 -2
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/tool.d.ts +2 -0
  8. package/dist/cli/tool.js +11 -0
  9. package/dist/core/group/cross-impact.d.ts +2 -1
  10. package/dist/core/group/cross-impact.js +5 -1
  11. package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
  12. package/dist/core/group/extractors/http-route-extractor.js +22 -4
  13. package/dist/core/incremental/subgraph-extract.js +7 -1
  14. package/dist/core/ingestion/cfg/emit.d.ts +68 -3
  15. package/dist/core/ingestion/cfg/emit.js +173 -13
  16. package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
  17. package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
  18. package/dist/core/ingestion/cfg/reaching-defs.d.ts +14 -0
  19. package/dist/core/ingestion/cfg/reaching-defs.js +28 -0
  20. package/dist/core/ingestion/cfg/types.d.ts +43 -0
  21. package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +3 -1
  22. package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +44 -1
  23. package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +47 -1
  24. package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +3 -1
  25. package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +116 -6
  26. package/dist/core/ingestion/cfg/visitors/dart-harvest.js +423 -52
  27. package/dist/core/ingestion/cfg/visitors/go-harvest.js +12 -20
  28. package/dist/core/ingestion/cfg/visitors/java-harvest.js +12 -2
  29. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +89 -6
  30. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +220 -6
  31. package/dist/core/ingestion/cfg/visitors/php-harvest.js +12 -2
  32. package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +40 -3
  33. package/dist/core/ingestion/cfg/visitors/python-harvest.js +153 -15
  34. package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +83 -4
  35. package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +180 -12
  36. package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +164 -5
  37. package/dist/core/ingestion/cfg/visitors/rust-harvest.js +332 -6
  38. package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +92 -6
  39. package/dist/core/ingestion/cfg/visitors/swift-harvest.js +189 -15
  40. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +6 -1
  41. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +37 -13
  42. package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
  43. package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
  44. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  45. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  46. package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
  47. package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
  48. package/dist/core/ingestion/pipeline.d.ts +5 -0
  49. package/dist/core/ingestion/pipeline.js +2 -1
  50. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
  51. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
  52. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
  53. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
  54. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
  55. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
  56. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +6 -0
  57. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +7 -0
  58. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +5 -0
  59. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +25 -16
  60. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +8 -0
  61. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
  62. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
  63. package/dist/core/ingestion/scope-resolution/pipeline/run.js +65 -8
  64. package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
  65. package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
  66. package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
  67. package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
  68. package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
  69. package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
  70. package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
  71. package/dist/core/ingestion/taint/call-summary-model.js +25 -0
  72. package/dist/core/ingestion/taint/emit.d.ts +2 -1
  73. package/dist/core/ingestion/taint/emit.js +7 -3
  74. package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +25 -1
  75. package/dist/core/ingestion/taint/summary-harvest-driver.js +64 -3
  76. package/dist/core/lbug/csv-generator.d.ts +7 -2
  77. package/dist/core/lbug/csv-generator.js +11 -3
  78. package/dist/core/lbug/lbug-adapter.d.ts +14 -0
  79. package/dist/core/lbug/lbug-adapter.js +60 -7
  80. package/dist/core/lbug/schema.d.ts +2 -2
  81. package/dist/core/lbug/schema.js +3 -0
  82. package/dist/core/run-analyze.js +22 -1
  83. package/dist/mcp/local/local-backend.d.ts +35 -0
  84. package/dist/mcp/local/local-backend.js +419 -33
  85. package/dist/mcp/local/pdg-impact.d.ts +384 -0
  86. package/dist/mcp/local/pdg-impact.js +1972 -0
  87. package/dist/mcp/tools.d.ts +1 -0
  88. package/dist/mcp/tools.js +22 -2
  89. package/dist/storage/parse-cache.js +20 -8
  90. package/dist/storage/repo-manager.d.ts +25 -2
  91. package/dist/storage/repo-manager.js +13 -2
  92. package/package.json +1 -1
@@ -46,6 +46,9 @@ export type NodeProperties = {
46
46
  errorKeys?: string[];
47
47
  middleware?: string[];
48
48
  text?: string;
49
+ /** BasicBlock: space-joined leaf callee names invoked in the block — the
50
+ * statement-precise inter-procedural reach substrate for impact mode. */
51
+ callees?: string;
49
52
  [key: string]: unknown;
50
53
  };
51
54
  export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'METHOD_OVERRIDES' | 'METHOD_IMPLEMENTS' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS' | 'HANDLES_ROUTE' | 'FETCHES' | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' | 'QUERIES'
@@ -93,7 +96,20 @@ export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'METHOD_OVERR
93
96
  * removing it later is a breaking schema change — and it is deliberately
94
97
  * excluded from `VALID_RELATION_TYPES` so it never enters impact-style
95
98
  * symbol-space traversal (same posture as the taint substrate edges). */
96
- | 'POST_DOMINATE';
99
+ | 'POST_DOMINATE'
100
+ /** Per-callee dependence SUMMARY edge (PDG FU-C): a self-loop on a
101
+ * Function/Method/Constructor node carrying that callee's RETURN-VALUE
102
+ * ASCENT — which formal-parameter indices flow to the function's return
103
+ * value, encoded as a versioned bitset in the relation's existing `reason`
104
+ * column (the same single-channel pattern `CFG`/`REACHING_DEF`/`CDG` use,
105
+ * since the lone `CodeRelation` table has no dedicated label column). A
106
+ * later consumer phase lets an interprocedural slice ascend a callee's
107
+ * return effect into the caller continuation. Like the taint substrate
108
+ * edges it is an internal PDG-engine edge: deliberately EXCLUDED from
109
+ * `VALID_RELATION_TYPES` and the web schema so it never leaks into
110
+ * callgraph-style impact/relationship surfaces. Emitted only under `--pdg`;
111
+ * a default analyze emits zero. */
112
+ | 'CALL_SUMMARY';
97
113
  export interface GraphNode {
98
114
  id: string;
99
115
  label: NodeLabel;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/graph/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/graph/types.ts"],"names":[],"mappings":"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"}
@@ -120,7 +120,9 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
120
120
 
121
121
  ## Always Do
122
122
 
123
- - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
123
+ - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.${hasPdg
124
+ ? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF and inter-procedural symbols in \`interproceduralByDepth\`/\`byDepth\`; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer).`
125
+ : ''}
124
126
  - **MUST run \`detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
125
127
  - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
126
128
  - When exploring unfamiliar code, use \`query({search_query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
@@ -50,5 +50,13 @@ export declare function formatListReposResult(result: {
50
50
  repositories: RepoListing[];
51
51
  pagination?: ListReposPagination;
52
52
  }): string;
53
+ export declare function getNextStepHint(toolName: string, result?: any): string;
53
54
  export declare function evalServerCommand(options?: EvalServerOptions): Promise<void>;
55
+ /**
56
+ * Tools the eval-server exposes over HTTP — the read-only query surface the
57
+ * banner advertises. `LocalBackend.callTool` ALSO dispatches write-side / heavier
58
+ * tools (rename, shape_check, tool_map, …); the allowlist keeps a stray
59
+ * `POST /tool/<name>` from reaching those through this Docker/eval-harness server.
60
+ */
61
+ export declare const EVAL_SERVER_TOOLS: ReadonlySet<string>;
54
62
  export declare const MAX_BODY_SIZE: number;
@@ -29,6 +29,7 @@
29
29
  * POST /shutdown — Graceful shutdown.
30
30
  */
31
31
  import http from 'http';
32
+ import crypto from 'node:crypto';
32
33
  import { isIPv4, isIPv6 } from 'node:net';
33
34
  import { writeSync } from 'node:fs';
34
35
  import { LocalBackend, } from '../mcp/local/local-backend.js';
@@ -146,6 +147,14 @@ export function formatContextResult(result) {
146
147
  }
147
148
  return lines.join('\n').trim();
148
149
  }
150
+ function formatTruncationSuffix(result) {
151
+ const label = Array.isArray(result.truncatedByReasons)
152
+ ? result.truncatedByReasons.join(', ')
153
+ : typeof result.truncatedBy === 'string'
154
+ ? result.truncatedBy
155
+ : '';
156
+ return label ? ` (by ${label})` : '';
157
+ }
149
158
  export function formatImpactResult(result) {
150
159
  if (result.error) {
151
160
  const suggestion = result.suggestion ? `\nSuggestion: ${result.suggestion}` : '';
@@ -160,6 +169,25 @@ export function formatImpactResult(result) {
160
169
  // mirroring formatContextResult, so the real impact under whichever symbol the
161
170
  // caller meant is visible on the text surface, not just in the JSON.
162
171
  if (result.status === 'ambiguous') {
172
+ if (result.mode === 'pdg') {
173
+ const shown = result.candidates?.length ?? 0;
174
+ const totalCandidates = result.totalCandidates ?? shown;
175
+ const countPhrase = totalCandidates > shown
176
+ ? `${totalCandidates} symbols (showing ${shown})`
177
+ : `${totalCandidates} symbols`;
178
+ const lines = [
179
+ `${target?.name || '?'}: AMBIGUOUS — ${countPhrase} share this name. ` +
180
+ `PDG impact was not computed until the target is disambiguated. ` +
181
+ `Use --uid, file_path, or kind for one authoritative PDG result.`,
182
+ ];
183
+ if (result.message)
184
+ lines.push(String(result.message));
185
+ for (const c of result.candidates || []) {
186
+ const score = typeof c.score === 'number' ? ` score ${c.score}` : '';
187
+ lines.push(` ${c.kind} ${c.name} → ${c.filePath}:${c.line || '?'}${score} (uid: ${c.uid})`);
188
+ }
189
+ return lines.join('\n');
190
+ }
163
191
  // #2129 review F11 — report the FULL match count (`totalCandidates`), not the
164
192
  // truncated `candidates[]` length; note when the candidate list is capped.
165
193
  const shown = result.candidates?.length ?? 0;
@@ -180,6 +208,176 @@ export function formatImpactResult(result) {
180
208
  }
181
209
  return lines.join('\n');
182
210
  }
211
+ // ─── PDG mode (mode:'pdg') ────────────────────────────────────────────
212
+ // KTD8 presentation half. PDG results are intra-procedural Program
213
+ // Dependence Graph blast radii: the single collapsed `byDepth[1]` bucket
214
+ // has NO call-hop depth meaning (block-hops ≠ call-hops), so we must NOT
215
+ // reuse the callgraph "depth N / WILL BREAK (direct)" framing, the
216
+ // callgraph DI/dynamic-dispatch lower-bound copy, or the confident
217
+ // "isolated" zero. A degraded / no-body PDG result is INCONCLUSIVE, not
218
+ // safe-to-refactor — it gets the explicit caveat + remediation, never an
219
+ // empty blast radius. Detect on `mode:'pdg'` (every PDG return path —
220
+ // findings, degradation, no-body, no-dependence — carries it). Ambiguous
221
+ // PDG results carry `status:'ambiguous'` and are handled above; they never
222
+ // reach here.
223
+ if (result.mode === 'pdg') {
224
+ const name = target?.name || '?';
225
+ const appendPdgInterproceduralSymbols = (lines) => {
226
+ const byDepth = result.interproceduralByDepth || result.pdgInterprocedural?.byDepth || result.byDepth || {};
227
+ const byDepthCounts = result.interproceduralByDepthCounts ||
228
+ result.pdgInterprocedural?.byDepthCounts ||
229
+ result.byDepthCounts ||
230
+ {};
231
+ const depthKeys = Array.from(new Set([...Object.keys(byDepthCounts), ...Object.keys(byDepth)]))
232
+ .map((d) => Number(d))
233
+ .filter((d) => Number.isFinite(d))
234
+ .sort((a, b) => a - b);
235
+ const hasReach = depthKeys.some((depth) => {
236
+ const items = byDepth[depth] || byDepth[String(depth)] || [];
237
+ const count = byDepthCounts[depth] ?? byDepthCounts[String(depth)] ?? items.length;
238
+ return count > 0;
239
+ });
240
+ if (!hasReach)
241
+ return false;
242
+ const totalSymbols = result.pdgInterprocedural?.impactedCount ??
243
+ (typeof result.impactedCount === 'number' ? result.impactedCount : 0);
244
+ lines.push('');
245
+ lines.push(`Inter-procedural symbol reach (${totalSymbols}):`);
246
+ for (const depth of depthKeys) {
247
+ const items = byDepth[depth] || byDepth[String(depth)] || [];
248
+ const count = byDepthCounts[depth] ?? byDepthCounts[String(depth)] ?? items.length;
249
+ if (count <= 0)
250
+ continue;
251
+ lines.push(` d=${depth} (${count})`);
252
+ const shown = Math.min(items.length, 12);
253
+ for (const item of items.slice(0, shown)) {
254
+ const flags = [];
255
+ if (item.unresolved)
256
+ flags.push('unresolved');
257
+ if (item.ambiguous)
258
+ flags.push('ambiguous');
259
+ const flagStr = flags.length ? ` [${flags.join(', ')}]` : '';
260
+ lines.push(` ${item.type || ''} ${item.name} → ${item.filePath}${flagStr}`);
261
+ }
262
+ if (count > shown)
263
+ lines.push(` ... and ${count - shown} more`);
264
+ }
265
+ return true;
266
+ };
267
+ // (1) Degradation — the PDG layer (or a sub-layer) is absent/unreadable.
268
+ // `pdgLayer` is the non-'ready' state from `pdgLayerStatus`. Print the
269
+ // honest remediation, NOT a zero/empty blast radius.
270
+ if (result.pdgLayer) {
271
+ const subLayer = result.missingSubLayer
272
+ ? ` (missing sub-layer: ${result.missingSubLayer})`
273
+ : '';
274
+ return (`${name}: PDG impact unavailable — the index has no usable PDG layer ` +
275
+ `[${result.pdgLayer}]${subLayer}. This is NOT "no impact". ` +
276
+ `Re-index with \`gitnexus analyze --pdg\` to build the control/data ` +
277
+ `dependence layer, or use \`--mode callgraph\` for the call-graph blast radius.` +
278
+ (result.note ? `\n${result.note}` : ''));
279
+ }
280
+ // (2) No-body symbol (KTD6) — interface / type alias / abstract / ambient
281
+ // member / one-line declaration with no CFG. Show the caveat, never
282
+ // "isolated / no dependencies".
283
+ if (result.epistemic === 'no-pdg-body') {
284
+ const noBodyLines = [
285
+ `${name}: local PDG slice not applicable to this symbol — it has no PDG body ` +
286
+ `(no control/data dependence edges; e.g. an interface, type alias, ` +
287
+ `abstract/ambient member, or a one-line declaration). This is NOT a ` +
288
+ `confident "no impact".`,
289
+ ];
290
+ appendPdgInterproceduralSymbols(noBodyLines);
291
+ if (result.note)
292
+ noBodyLines.push(result.note);
293
+ return noBodyLines.join('\n');
294
+ }
295
+ // (2b) STATEMENT-ANCHORED SLICE (mode:'pdg' + line). When `criterionLine` is
296
+ // present the result is a statement slice: the seeded line plus the list of
297
+ // dependent statements (`affectedStatements: {line,filePath,text}[]`). Render
298
+ // those statements directly — this IS the useful output of statement mode —
299
+ // rather than the symbol-projection bucket below. Empty cases:
300
+ // - `pdg-no-block-at-line`: the line is blank / a comment / outside the
301
+ // body (no statement block) — print the steering note.
302
+ // - empty `affectedStatements` with `pdg-intra-procedural`: the line has no
303
+ // dependents in this direction — print the steering note.
304
+ // Each non-empty case also surfaces truncation honestly.
305
+ if (typeof result.criterionLine === 'number') {
306
+ const slice = Array.isArray(result.affectedStatements)
307
+ ? result.affectedStatements
308
+ : [];
309
+ const count = typeof result.affectedStatementCount === 'number'
310
+ ? result.affectedStatementCount
311
+ : slice.length;
312
+ // File anchor for the heading — the seeded statement's file (every slice
313
+ // statement shares the function's file). Fall back to the target's file.
314
+ const anchorFile = slice[0]?.filePath || target?.filePath || name;
315
+ if (count === 0 || slice.length === 0) {
316
+ // No statement block at the line, or no dependents in this direction.
317
+ // Print the honest note (pdg-no-block-at-line or the no-dependence note)
318
+ // verbatim — never an empty "isolated" headline.
319
+ const emptySliceLines = [
320
+ `No statements ${direction}-dependent on ${anchorFile}:${result.criterionLine}.`,
321
+ ];
322
+ if (result.truncated) {
323
+ const by = formatTruncationSuffix(result);
324
+ emptySliceLines.push(`⚠️ Truncated${by} — the dependence slice was bounded; deeper PDG-dependent statements may exist.`);
325
+ }
326
+ appendPdgInterproceduralSymbols(emptySliceLines);
327
+ if (result.note)
328
+ emptySliceLines.push(result.note);
329
+ return emptySliceLines.join('\n');
330
+ }
331
+ const slLines = [];
332
+ slLines.push(`Statements ${direction}-dependent on ${anchorFile}:${result.criterionLine} (${count}):`);
333
+ for (const s of slice) {
334
+ const text = typeof s.text === 'string' ? s.text : '';
335
+ slLines.push(` L${s.line}: ${text}`);
336
+ }
337
+ // Truncation honesty — the slice may be a lower bound (depth or per-step
338
+ // LIMIT bound). Surface it the same way the symbol render does.
339
+ if (result.truncated) {
340
+ const by = formatTruncationSuffix(result);
341
+ slLines.push(`⚠️ Truncated${by} — the dependence slice was bounded; deeper PDG-dependent statements may exist.`);
342
+ }
343
+ appendPdgInterproceduralSymbols(slLines);
344
+ if (result.note) {
345
+ slLines.push('');
346
+ slLines.push(`ℹ️ ${result.note}`);
347
+ }
348
+ return slLines.join('\n').trim();
349
+ }
350
+ const pdgLines = [];
351
+ if (!appendPdgInterproceduralSymbols(pdgLines)) {
352
+ pdgLines.push(`${name} (${direction}): no inter-procedural symbols reached. ` +
353
+ `The local PDG statement slice may still report affectedStatements when seeded with line:<N>.`);
354
+ }
355
+ // The assembled note carries the local-PDG framing plus the unified
356
+ // inter-procedural symbol-reach contract; surface it verbatim so the CLI
357
+ // reader sees the same honesty the JSON consumer does.
358
+ if (result.note) {
359
+ pdgLines.push('');
360
+ pdgLines.push(`ℹ️ ${result.note}`);
361
+ }
362
+ else {
363
+ pdgLines.push('');
364
+ pdgLines.push('ℹ️ Program Dependence Graph result — statement reach is reported in affectedStatements and inter-procedural symbol reach in interproceduralByDepth/byDepth.');
365
+ }
366
+ // Honest incompleteness signals (block-attribution + truncation).
367
+ if (result.ambiguousProjectionCount > 0) {
368
+ pdgLines.push(`⚠️ ${result.ambiguousProjectionCount} block(s) could not be attributed to a ` +
369
+ `unique owning symbol (same-line functions) — all colliding symbols are shown.`);
370
+ }
371
+ if (result.unresolvedBlockCount > 0) {
372
+ pdgLines.push(`⚠️ ${result.unresolvedBlockCount} dependence block(s) map to no owning ` +
373
+ `Function/Method/Constructor (top-level statement / closure) — surfaced under their file.`);
374
+ }
375
+ if (result.truncated) {
376
+ const by = formatTruncationSuffix(result);
377
+ pdgLines.push(`⚠️ Truncated${by} — the dependence traversal was bounded; deeper PDG impacts may exist.`);
378
+ }
379
+ return pdgLines.join('\n').trim();
380
+ }
183
381
  if (total === 0) {
184
382
  // #1858 — "isolated" is a confident claim. If an interface / indirection
185
383
  // boundary is on the path, the true count is a lower bound, not zero;
@@ -317,13 +515,20 @@ function formatToolResult(toolName, result) {
317
515
  // ─── Next-Step Hints ──────────────────────────────────────────────────
318
516
  // Guide the agent to the logical next tool call.
319
517
  // Critical for tool chaining: query → context → impact → fix.
320
- function getNextStepHint(toolName) {
518
+ export function getNextStepHint(toolName, result) {
321
519
  switch (toolName) {
322
520
  case 'query':
323
521
  return '\n---\nNext: Pick a symbol above and run gitnexus-context "<name>" to see all its callers, callees, and execution flows.';
324
522
  case 'context':
325
523
  return '\n---\nNext: To check what breaks if you change this, run gitnexus-impact "<name>" upstream';
326
524
  case 'impact':
525
+ if (result?.error ||
526
+ result?.status === 'ambiguous' ||
527
+ result?.mode === 'pdg' ||
528
+ result?.pdgLayer ||
529
+ typeof result?.criterionLine === 'number') {
530
+ return '';
531
+ }
327
532
  return '\n---\nNext: Review d=1 items first (WILL BREAK). Read the source with cat to understand the code, then make your fix.';
328
533
  case 'cypher':
329
534
  return '\n---\nNext: To explore a result symbol in depth, run gitnexus-context "<name>"';
@@ -376,6 +581,13 @@ export async function evalServerCommand(options) {
376
581
  process.exit(0);
377
582
  }, idleTimeoutSec * 1000);
378
583
  }
584
+ // Startup-generated shutdown token: a `POST /shutdown` must present it in the
585
+ // X-Shutdown-Token header. The local agent that launches the server reads it
586
+ // from the GITNEXUS_EVAL_SERVER_SHUTDOWN_TOKEN line on fd 1 (next to the READY
587
+ // signal); a client on another VM under `--host 0.0.0.0` cannot guess it, so it
588
+ // can no longer kill the server. (SIGINT/SIGTERM and the idle timeout still
589
+ // shut down locally without a token.)
590
+ const shutdownToken = crypto.randomBytes(24).toString('hex');
379
591
  const server = http.createServer(async (req, res) => {
380
592
  resetIdleTimer();
381
593
  try {
@@ -388,6 +600,12 @@ export async function evalServerCommand(options) {
388
600
  }
389
601
  // Shutdown
390
602
  if (req.method === 'POST' && req.url === '/shutdown') {
603
+ if (req.headers['x-shutdown-token'] !== shutdownToken) {
604
+ res.setHeader('Content-Type', 'application/json');
605
+ res.writeHead(403);
606
+ res.end(JSON.stringify({ error: 'forbidden: missing or invalid X-Shutdown-Token' }));
607
+ return;
608
+ }
391
609
  res.setHeader('Content-Type', 'application/json');
392
610
  res.writeHead(200);
393
611
  res.end(JSON.stringify({ status: 'shutting_down' }));
@@ -402,6 +620,12 @@ export async function evalServerCommand(options) {
402
620
  const toolMatch = req.url?.match(/^\/tool\/(\w+)$/);
403
621
  if (req.method === 'POST' && toolMatch) {
404
622
  const toolName = toolMatch[1];
623
+ if (!EVAL_SERVER_TOOLS.has(toolName)) {
624
+ res.setHeader('Content-Type', 'text/plain');
625
+ res.writeHead(400);
626
+ res.end(`Error: unsupported tool '${toolName}'. Supported: ${[...EVAL_SERVER_TOOLS].sort().join(', ')}`);
627
+ return;
628
+ }
405
629
  const body = await readBody(req);
406
630
  let args = {};
407
631
  if (body.trim()) {
@@ -418,7 +642,7 @@ export async function evalServerCommand(options) {
418
642
  // Call tool, format result as text, append next-step hint
419
643
  const result = await backend.callTool(toolName, args);
420
644
  const formatted = formatToolResult(toolName, result);
421
- const hint = getNextStepHint(toolName);
645
+ const hint = getNextStepHint(toolName, result);
422
646
  res.setHeader('Content-Type', 'text/plain');
423
647
  res.writeHead(200);
424
648
  res.end(formatted + hint);
@@ -518,6 +742,8 @@ export async function evalServerCommand(options) {
518
742
  try {
519
743
  // Use fd 1 directly — LadybugDB captures process.stdout (#324)
520
744
  writeSync(1, `GITNEXUS_EVAL_SERVER_READY:${displayHost}:${boundPort}\n`);
745
+ // The launching agent reads this to authorize POST /shutdown.
746
+ writeSync(1, `GITNEXUS_EVAL_SERVER_SHUTDOWN_TOKEN:${shutdownToken}\n`);
521
747
  }
522
748
  catch {
523
749
  // stdout may not be available (e.g., broken pipe)
@@ -533,6 +759,20 @@ export async function evalServerCommand(options) {
533
759
  process.on('SIGINT', shutdown);
534
760
  process.on('SIGTERM', shutdown);
535
761
  }
762
+ /**
763
+ * Tools the eval-server exposes over HTTP — the read-only query surface the
764
+ * banner advertises. `LocalBackend.callTool` ALSO dispatches write-side / heavier
765
+ * tools (rename, shape_check, tool_map, …); the allowlist keeps a stray
766
+ * `POST /tool/<name>` from reaching those through this Docker/eval-harness server.
767
+ */
768
+ export const EVAL_SERVER_TOOLS = new Set([
769
+ 'query',
770
+ 'context',
771
+ 'impact',
772
+ 'cypher',
773
+ 'detect_changes',
774
+ 'list_repos',
775
+ ]);
536
776
  export const MAX_BODY_SIZE = 1024 * 1024; // 1MB
537
777
  function readBody(req) {
538
778
  return new Promise((resolve, reject) => {
package/dist/cli/index.js CHANGED
@@ -224,6 +224,8 @@ program
224
224
  .command('impact [target]')
225
225
  .description('Blast radius analysis: what breaks if you change a symbol')
226
226
  .option('-d, --direction <dir>', 'upstream (dependants) or downstream (dependencies)', 'upstream')
227
+ .option('--mode <mode>', 'Engine: callgraph (default) or pdg (opt-in, intra-procedural; needs analyze --pdg)', 'callgraph')
228
+ .option('--line <number>', '1-based source line — PDG-only statement anchor (--mode pdg): slice the dependence from the statement at this line and show what depends on it')
227
229
  .option('-r, --repo <name>', 'Target repository')
228
230
  .option('--branch <name>', 'Scope to a specific branch index (multi-branch repos)')
229
231
  .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
@@ -31,6 +31,8 @@ export declare function contextCommand(name: string, options?: {
31
31
  }): Promise<void>;
32
32
  export declare function impactCommand(target?: string, options?: {
33
33
  direction?: string;
34
+ mode?: string;
35
+ line?: string;
34
36
  repo?: string;
35
37
  branch?: string;
36
38
  uid?: string;
package/dist/cli/tool.js CHANGED
@@ -120,12 +120,23 @@ export async function impactCommand(target, options) {
120
120
  const rawOffset = parseInt(options?.offset ?? '', 10);
121
121
  const parsedLimit = Number.isFinite(rawLimit) ? rawLimit : undefined;
122
122
  const parsedOffset = Number.isFinite(rawOffset) ? rawOffset : undefined;
123
+ // `--line` is a PDG-only statement anchor (1-based source line). Parse it to
124
+ // an integer when provided and thread it ONLY when present, so the backend's
125
+ // line-without-pdg / non-positive-integer validation fires on the real value
126
+ // rather than on a silently-dropped flag. A non-numeric `--line` parses to
127
+ // NaN, which the backend rejects as a non-positive integer (loud, not silent).
128
+ const parsedLine = options?.line !== undefined ? parseInt(options.line, 10) : undefined;
123
129
  const result = await backend.callTool('impact', {
124
130
  target: target || undefined,
125
131
  target_uid: options?.uid,
126
132
  file_path: options?.file,
127
133
  kind: options?.kind,
128
134
  direction: options?.direction || 'upstream',
135
+ // Forward the engine selector; backend validates the enum (callgraph/pdg)
136
+ // and treats the default 'callgraph' identically to an omitted mode.
137
+ mode: options?.mode,
138
+ // PDG-only statement anchor — forwarded only when --line was given.
139
+ ...(parsedLine !== undefined ? { line: parsedLine } : {}),
129
140
  maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
130
141
  includeTests: options?.includeTests ?? false,
131
142
  repo: options?.repo,
@@ -2,7 +2,7 @@
2
2
  * Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
3
3
  * All bridge Cypher for this feature lives in this module.
4
4
  */
5
- import type { GroupImpactResult } from './types.js';
5
+ import type { CrossRepoImpact, GroupImpactResult } from './types.js';
6
6
  import type { GroupToolPort } from './service.js';
7
7
  /** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
8
8
  export declare const MAX_SUPPORTED_CROSS_DEPTH = 1;
@@ -79,6 +79,7 @@ export declare function collectImpactSymbolUids(local: unknown, servicePrefix: s
79
79
  uids: string[];
80
80
  targetFilePath?: string;
81
81
  };
82
+ export declare function mergeRisk(localRisk: string, cross: CrossRepoImpact[]): string;
82
83
  export declare function runGroupImpact(deps: RunGroupImpactDeps, params: Record<string, unknown>): Promise<GroupImpactResult | {
83
84
  error: string;
84
85
  }>;
@@ -251,7 +251,11 @@ function extractProcessNames(impact) {
251
251
  return [];
252
252
  return o.affected_processes.map((p) => String(p.name ?? '')).filter(Boolean);
253
253
  }
254
- function mergeRisk(localRisk, cross) {
254
+ // Exported so the U4 PDG-result interchangeability contract (KTD8) can assert
255
+ // permanently that a PDG `risk:'UNKNOWN'` never coalesces to a confident `LOW`.
256
+ // No behavior change — `'UNKNOWN'` was already handled correctly at the
257
+ // `(localRisk === 'LOW' || localRisk === 'UNKNOWN')` branch below.
258
+ export function mergeRisk(localRisk, cross) {
255
259
  const highConf = cross.some((c) => c.contract.confidence >= 0.85);
256
260
  if (localRisk === 'CRITICAL')
257
261
  return 'CRITICAL';
@@ -1,5 +1,29 @@
1
1
  import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
2
2
  import type { ExtractedContract, RepoHandle } from '../types.js';
3
+ /**
4
+ * Language-agnostic orchestrator for HTTP route (provider + consumer)
5
+ * contract extraction. Two strategies, in order of preference per role:
6
+ *
7
+ * 1. **Graph-assisted (Strategy A)** — if a per-repo LadybugDB executor
8
+ * is available, read `HANDLES_ROUTE` / `FETCHES` Cypher edges that
9
+ * the ingestion pipeline already produced via tree-sitter. This is
10
+ * the preferred path because the graph has richer symbol metadata
11
+ * (real uids, class/method structure, etc.).
12
+ *
13
+ * 2. **Source-scan supplement (Strategy B)** — parse files directly with
14
+ * the per-language plugin registry in `./http-patterns/`. Used to
15
+ * fill gaps when graph extraction only covers part of a polyglot repo
16
+ * (e.g. Java graph routes plus Go source-scan routes). Graph entries
17
+ * remain authoritative for duplicate contract IDs because they carry
18
+ * richer symbol metadata. Each plugin owns its tree-sitter grammar
19
+ * and query sources — this orchestrator imports NO grammars or query
20
+ * strings.
21
+ *
22
+ * Adding a new language for Strategy B is a one-file edit in
23
+ * `http-patterns/index.ts`: register a new `HttpLanguagePlugin` and
24
+ * widen `HTTP_SCAN_GLOB` if needed.
25
+ */
26
+ export declare const HANDLES_ROUTE_QUERY = "\nMATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)\nRETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,\n route.name AS routePath, route.id AS routeId,\n route.method AS routeMethod,\n route.responseKeys AS responseKeys,\n r.reason AS routeSource";
3
27
  /**
4
28
  * Canonicalize a provider-side HTTP path for contract-id generation:
5
29
  * - strip query string
@@ -4,6 +4,7 @@ import Parser from 'tree-sitter';
4
4
  import { createIgnoreFilter } from '../../../config/ignore-service.js';
5
5
  import { readSafe } from './fs-utils.js';
6
6
  import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
7
+ import { logger } from '../../logger.js';
7
8
  import { getPluginForFile, HTTP_SCAN_GLOB, } from './http-patterns/index.js';
8
9
  /**
9
10
  * Language-agnostic orchestrator for HTTP route (provider + consumer)
@@ -29,10 +30,14 @@ import { getPluginForFile, HTTP_SCAN_GLOB, } from './http-patterns/index.js';
29
30
  * widen `HTTP_SCAN_GLOB` if needed.
30
31
  */
31
32
  // ─── Graph-assisted queries ──────────────────────────────────────────
32
- const HANDLES_ROUTE_QUERY = `
33
+ // Exported so integration tests can run the exact production query against a
34
+ // real LadybugDB (guards the Route.method column contract — see
35
+ // route-method-roundtrip.test.ts).
36
+ export const HANDLES_ROUTE_QUERY = `
33
37
  MATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)
34
38
  RETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,
35
39
  route.name AS routePath, route.id AS routeId,
40
+ route.method AS routeMethod,
36
41
  route.responseKeys AS responseKeys,
37
42
  r.reason AS routeSource`;
38
43
  const FETCHES_QUERY = `
@@ -273,14 +278,26 @@ export class HttpRouteExtractor {
273
278
  try {
274
279
  rows = await db(HANDLES_ROUTE_QUERY);
275
280
  }
276
- catch {
281
+ catch (err) {
282
+ // A failure here silently disables the entire graph-assisted HTTP
283
+ // provider path (the source-scan fallback still runs and masks most
284
+ // of the damage), so surface it at debug level to make a total
285
+ // outage observable instead of invisible.
286
+ logger.debug(`[http-route-extractor] HANDLES_ROUTE query failed; graph providers skipped: ${err instanceof Error ? err.message : String(err)}`);
277
287
  return [];
278
288
  }
279
289
  for (const row of rows) {
280
290
  const filePath = String(row.filePath ?? '');
281
291
  const routePath = String(row.routePath ?? '');
282
292
  const routeSource = String(row.routeSource ?? row.routeReason ?? '');
283
- let method = methodFromRouteReason(routeSource);
293
+ // Prefer the HTTP verb persisted on the Route node by the ingestion
294
+ // routes phase (Spring/Laravel framework routes and decorator routes
295
+ // carry it). Fall back to parsing it out of the edge reason for
296
+ // older indexes or filesystem routes that never stored a method.
297
+ const graphMethod = String(row.routeMethod ?? '')
298
+ .trim()
299
+ .toUpperCase();
300
+ let method = (graphMethod || null) ?? methodFromRouteReason(routeSource);
284
301
  // Look up handler name (and backfill method if missing) from the
285
302
  // plugin's scan of the handler file. This replaces the old
286
303
  // regex-based `inferMethodFromFileScan` and `pickJavaHandlerName`
@@ -396,7 +413,8 @@ export class HttpRouteExtractor {
396
413
  try {
397
414
  rows = await db(FETCHES_QUERY);
398
415
  }
399
- catch {
416
+ catch (err) {
417
+ logger.debug(`[http-route-extractor] FETCHES query failed; graph consumers skipped: ${err instanceof Error ? err.message : String(err)}`);
400
418
  return [];
401
419
  }
402
420
  for (const row of rows) {
@@ -57,7 +57,13 @@ const isGraphWide = (label) => label === 'Community' || label === 'Process';
57
57
  * A→C edge. These are always extracted (and the orchestrator delete-alls them
58
58
  * first, like Community/Process) so they rebuild from the fresh graph.
59
59
  */
60
- const isGraphWideRelType = (type) => type === 'TAINT_PATH';
60
+ // `CALL_SUMMARY` (PDG FU-C) is intra-procedural (a callee's RETURN-VALUE ASCENT
61
+ // depends only on its OWN body), but the orchestrator delete-alls it on an
62
+ // incremental `--pdg` writeback to keep the emit path single — so it must be
63
+ // re-included from the FULL fresh graph (which the emit phase recomputes every
64
+ // run) or an unchanged function's summary would be lost. Cheap: one self-loop
65
+ // edge per return-flowing function.
66
+ const isGraphWideRelType = (type) => type === 'TAINT_PATH' || type === 'CALL_SUMMARY';
61
67
  /**
62
68
  * Build a Map<nodeId, filePath> for every File-bound node in the graph.
63
69
  * Graph-wide nodes (Community/Process) have no filePath and are filtered.