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.
- package/dist/_shared/graph/types.d.ts +17 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/cli/ai-context.js +3 -1
- package/dist/cli/eval-server.d.ts +8 -0
- package/dist/cli/eval-server.js +242 -2
- package/dist/cli/index.js +2 -0
- package/dist/cli/tool.d.ts +2 -0
- package/dist/cli/tool.js +11 -0
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +5 -1
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +22 -4
- package/dist/core/incremental/subgraph-extract.js +7 -1
- package/dist/core/ingestion/cfg/emit.d.ts +68 -3
- package/dist/core/ingestion/cfg/emit.js +173 -13
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +14 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +28 -0
- package/dist/core/ingestion/cfg/types.d.ts +43 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +3 -1
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +44 -1
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +47 -1
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +3 -1
- package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +116 -6
- package/dist/core/ingestion/cfg/visitors/dart-harvest.js +423 -52
- package/dist/core/ingestion/cfg/visitors/go-harvest.js +12 -20
- package/dist/core/ingestion/cfg/visitors/java-harvest.js +12 -2
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +89 -6
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +220 -6
- package/dist/core/ingestion/cfg/visitors/php-harvest.js +12 -2
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +40 -3
- package/dist/core/ingestion/cfg/visitors/python-harvest.js +153 -15
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +83 -4
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +180 -12
- package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +164 -5
- package/dist/core/ingestion/cfg/visitors/rust-harvest.js +332 -6
- package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +92 -6
- package/dist/core/ingestion/cfg/visitors/swift-harvest.js +189 -15
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +6 -1
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +37 -13
- package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
- package/dist/core/ingestion/pipeline.d.ts +5 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +6 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +7 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +5 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +25 -16
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +8 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +65 -8
- package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
- package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
- package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
- package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
- package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
- package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
- package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
- package/dist/core/ingestion/taint/call-summary-model.js +25 -0
- package/dist/core/ingestion/taint/emit.d.ts +2 -1
- package/dist/core/ingestion/taint/emit.js +7 -3
- package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +25 -1
- package/dist/core/ingestion/taint/summary-harvest-driver.js +64 -3
- package/dist/core/lbug/csv-generator.d.ts +7 -2
- package/dist/core/lbug/csv-generator.js +11 -3
- package/dist/core/lbug/lbug-adapter.d.ts +14 -0
- package/dist/core/lbug/lbug-adapter.js +60 -7
- package/dist/core/lbug/schema.d.ts +2 -2
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/run-analyze.js +22 -1
- package/dist/mcp/local/local-backend.d.ts +35 -0
- package/dist/mcp/local/local-backend.js +419 -33
- package/dist/mcp/local/pdg-impact.d.ts +384 -0
- package/dist/mcp/local/pdg-impact.js +1972 -0
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +22 -2
- package/dist/storage/parse-cache.js +20 -8
- package/dist/storage/repo-manager.d.ts +25 -2
- package/dist/storage/repo-manager.js +13 -2
- 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":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,UAAU,GACV,WAAW,GACX,MAAM,GACN,WAAW,GACX,QAAQ,GACR,MAAM,GACN,aAAa,GACb,WAAW,GACX,SAAS,GAET,QAAQ,GACR,OAAO,GACP,SAAS,GACT,OAAO,GACP,WAAW,GACX,OAAO,GACP,MAAM,GACN,WAAW,GACX,OAAO,GACP,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,aAAa,GACb,UAAU,GACV,SAAS,GACT,OAAO,GACP,MAAM,GAGN,YAAY,CAAC;AAEjB,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;IAEjC,WAAW,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAAC;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/graph/types.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -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;
|
package/dist/cli/eval-server.js
CHANGED
|
@@ -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)')
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|