lynkr 8.0.0 → 9.0.1
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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +196 -322
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +64 -13
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +23 -0
- package/src/config/index.js +77 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +48 -8
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +13 -12
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +4 -0
- package/src/tools/lazy-loader.js +18 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -198
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -584
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/docs/toon-integration-spec.md +0 -130
- package/documentation/README.md +0 -101
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -679
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -734
- package/documentation/docker.md +0 -874
- package/documentation/embeddings.md +0 -762
- package/documentation/faq.md +0 -713
- package/documentation/features.md +0 -403
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -758
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -636
- package/documentation/providers.md +0 -1009
- package/documentation/routing.md +0 -476
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -325
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -969
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/te +0 -11622
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -213
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -287
- package/test/azure-openai-routing.test.js +0 -175
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -457
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -269
- package/test/hybrid-routing-performance.test.js +0 -428
- package/test/llamacpp-integration.test.js +0 -882
- package/test/lmstudio-integration.test.js +0 -347
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -683
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -225
- package/test/toon-compression.test.js +0 -131
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- package/test-subagents.sh +0 -117
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphify Integration — Knowledge Graph for Code Intelligence
|
|
3
|
+
*
|
|
4
|
+
* Communicates with Graphify's CLI to provide blast radius analysis,
|
|
5
|
+
* god node detection, community cohesion, surprise scoring, and
|
|
6
|
+
* structural complexity signals for intelligent routing decisions.
|
|
7
|
+
*
|
|
8
|
+
* Workspace resolution order (per-request):
|
|
9
|
+
* 1. Explicit workspace passed by caller (e.g. from X-Lynkr-Workspace header)
|
|
10
|
+
* 2. Auto-detected from absolute file paths in the conversation messages
|
|
11
|
+
* 3. CODE_GRAPH_WORKSPACE env var
|
|
12
|
+
* 4. process.cwd() (last resort)
|
|
13
|
+
*
|
|
14
|
+
* Graphify: https://github.com/safishamsi/graphify
|
|
15
|
+
*
|
|
16
|
+
* @module tools/code-graph
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const { execFile } = require("child_process");
|
|
21
|
+
const config = require("../config");
|
|
22
|
+
const logger = require("../logger");
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// CACHE
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, { data: any, ts: number }>} */
|
|
29
|
+
const resultCache = new Map();
|
|
30
|
+
const CACHE_TTL_MS = 30_000; // 30 seconds
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve a cached value or null if expired / missing.
|
|
34
|
+
* @param {string} key
|
|
35
|
+
* @returns {any|null}
|
|
36
|
+
*/
|
|
37
|
+
function cacheGet(key) {
|
|
38
|
+
const entry = resultCache.get(key);
|
|
39
|
+
if (!entry) return null;
|
|
40
|
+
if (Date.now() - entry.ts > CACHE_TTL_MS) {
|
|
41
|
+
resultCache.delete(key);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return entry.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Store a value in the cache.
|
|
49
|
+
* @param {string} key
|
|
50
|
+
* @param {any} data
|
|
51
|
+
*/
|
|
52
|
+
function cacheSet(key, data) {
|
|
53
|
+
resultCache.set(key, { data, ts: Date.now() });
|
|
54
|
+
|
|
55
|
+
// Prevent unbounded growth — evict oldest entries beyond 200
|
|
56
|
+
if (resultCache.size > 200) {
|
|
57
|
+
const oldest = resultCache.keys().next().value;
|
|
58
|
+
resultCache.delete(oldest);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// FAILURE SUPPRESSION
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
/** Timestamp of the last logged warning (0 = never) */
|
|
67
|
+
let lastWarningTs = 0;
|
|
68
|
+
const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Log a warning at most once per cooldown period.
|
|
72
|
+
* @param {string} msg
|
|
73
|
+
* @param {Object} [meta]
|
|
74
|
+
*/
|
|
75
|
+
function warnOnce(msg, meta = {}) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (now - lastWarningTs < WARNING_COOLDOWN_MS) return;
|
|
78
|
+
lastWarningTs = now;
|
|
79
|
+
logger.warn(meta, `[graphify] ${msg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// WORKSPACE DETECTION
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect the workspace root from a list of absolute file paths by finding
|
|
88
|
+
* their longest common directory prefix.
|
|
89
|
+
*
|
|
90
|
+
* Example:
|
|
91
|
+
* ["/Users/bob/app/src/a.js", "/Users/bob/app/src/b.js", "/Users/bob/app/test/c.js"]
|
|
92
|
+
* → "/Users/bob/app"
|
|
93
|
+
*
|
|
94
|
+
* Returns null if no absolute paths are provided or they share no common root.
|
|
95
|
+
*
|
|
96
|
+
* @param {string[]} filePaths
|
|
97
|
+
* @returns {string|null}
|
|
98
|
+
*/
|
|
99
|
+
function detectWorkspaceFromPaths(filePaths) {
|
|
100
|
+
// Only consider absolute paths
|
|
101
|
+
const absolute = filePaths.filter((p) => path.isAbsolute(p));
|
|
102
|
+
if (absolute.length === 0) return null;
|
|
103
|
+
|
|
104
|
+
// Split each path into segments
|
|
105
|
+
const segmented = absolute.map((p) => p.split(path.sep).filter(Boolean));
|
|
106
|
+
|
|
107
|
+
// Find common prefix segments
|
|
108
|
+
const first = segmented[0];
|
|
109
|
+
let commonLength = first.length;
|
|
110
|
+
|
|
111
|
+
for (let i = 1; i < segmented.length; i++) {
|
|
112
|
+
const other = segmented[i];
|
|
113
|
+
let j = 0;
|
|
114
|
+
while (j < commonLength && j < other.length && first[j] === other[j]) {
|
|
115
|
+
j++;
|
|
116
|
+
}
|
|
117
|
+
commonLength = j;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (commonLength === 0) return null;
|
|
121
|
+
|
|
122
|
+
// Reconstruct the common path — must be a directory, not a file
|
|
123
|
+
let common = path.sep + first.slice(0, commonLength).join(path.sep);
|
|
124
|
+
|
|
125
|
+
// If the common path looks like a file (has extension), go up one level
|
|
126
|
+
if (path.extname(common)) {
|
|
127
|
+
common = path.dirname(common);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Don't return root or home-level paths — too broad to be useful
|
|
131
|
+
const depth = common.split(path.sep).filter(Boolean).length;
|
|
132
|
+
if (depth < 2) return null;
|
|
133
|
+
|
|
134
|
+
return common;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// CONFIGURATION HELPERS
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return resolved code-graph configuration from config module.
|
|
143
|
+
* @returns {{ enabled: boolean, command: string, defaultWorkspace: string, timeout: number }}
|
|
144
|
+
*/
|
|
145
|
+
function getConfig() {
|
|
146
|
+
const cfg = config.codeGraph || {};
|
|
147
|
+
return {
|
|
148
|
+
enabled: cfg.enabled === true,
|
|
149
|
+
command: cfg.command || "graphify",
|
|
150
|
+
defaultWorkspace: cfg.workspace || process.cwd(),
|
|
151
|
+
timeout: cfg.timeout || 5000,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the workspace for a given request.
|
|
157
|
+
*
|
|
158
|
+
* Priority:
|
|
159
|
+
* 1. Explicit workspace (from header or caller)
|
|
160
|
+
* 2. Auto-detected from file paths
|
|
161
|
+
* 3. CODE_GRAPH_WORKSPACE env var
|
|
162
|
+
* 4. process.cwd()
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} [options]
|
|
165
|
+
* @param {string} [options.workspace] - Explicit workspace from caller/header
|
|
166
|
+
* @param {string[]} [options.filePaths] - File paths from the conversation
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
function resolveWorkspace(options = {}) {
|
|
170
|
+
// 1. Explicit workspace
|
|
171
|
+
if (options.workspace && typeof options.workspace === "string") {
|
|
172
|
+
return options.workspace;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Auto-detect from file paths
|
|
176
|
+
if (Array.isArray(options.filePaths) && options.filePaths.length > 0) {
|
|
177
|
+
const detected = detectWorkspaceFromPaths(options.filePaths);
|
|
178
|
+
if (detected) {
|
|
179
|
+
logger.debug({ workspace: detected }, "[graphify] auto-detected workspace from file paths");
|
|
180
|
+
return detected;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3/4. Static config or cwd
|
|
185
|
+
return getConfig().defaultWorkspace;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// COMMAND EXECUTION
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a Graphify CLI command and parse JSON output.
|
|
194
|
+
*
|
|
195
|
+
* Graphify CLI: `graphify query --workspace <path> <query>`
|
|
196
|
+
* or: `graphify --workspace <path>` (builds graph + reports)
|
|
197
|
+
*
|
|
198
|
+
* @param {string} subcommand — e.g. "query", "benchmark", or null for build
|
|
199
|
+
* @param {string[]} [args] — additional CLI arguments
|
|
200
|
+
* @param {string} [workspace] — resolved workspace path
|
|
201
|
+
* @returns {Promise<Object|null>} Parsed JSON or null on failure
|
|
202
|
+
*/
|
|
203
|
+
function execGraph(subcommand, args = [], workspace = null) {
|
|
204
|
+
const cfg = getConfig();
|
|
205
|
+
if (!cfg.enabled) return Promise.resolve(null);
|
|
206
|
+
|
|
207
|
+
const ws = workspace || cfg.defaultWorkspace;
|
|
208
|
+
const parts = cfg.command.split(/\s+/);
|
|
209
|
+
const bin = parts[0];
|
|
210
|
+
const baseArgs = parts.slice(1);
|
|
211
|
+
|
|
212
|
+
const fullArgs = [
|
|
213
|
+
...baseArgs,
|
|
214
|
+
...(subcommand ? [subcommand] : []),
|
|
215
|
+
"--workspace",
|
|
216
|
+
ws,
|
|
217
|
+
...args,
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
execFile(
|
|
222
|
+
bin,
|
|
223
|
+
fullArgs,
|
|
224
|
+
{ timeout: cfg.timeout, maxBuffer: 1024 * 1024 },
|
|
225
|
+
(err, stdout, stderr) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
warnOnce(`command failed: ${subcommand || 'build'}`, {
|
|
228
|
+
err: err.message,
|
|
229
|
+
stderr: (stderr || "").slice(0, 200),
|
|
230
|
+
});
|
|
231
|
+
return resolve(null);
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const data = JSON.parse(stdout);
|
|
235
|
+
return resolve(data);
|
|
236
|
+
} catch (parseErr) {
|
|
237
|
+
// Graphify may output non-JSON for build commands — try reading report
|
|
238
|
+
warnOnce(`failed to parse JSON for: ${subcommand || 'build'}`, {
|
|
239
|
+
err: parseErr.message,
|
|
240
|
+
});
|
|
241
|
+
return resolve(null);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// AVAILABILITY CHECK
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
/** Cached availability result per workspace */
|
|
253
|
+
const availabilityCache = new Map(); // workspace → { value, ts }
|
|
254
|
+
const AVAILABILITY_TTL_MS = 60_000; // 1 minute
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check whether Graphify is configured and responsive.
|
|
258
|
+
* Result is cached per workspace for 60 seconds.
|
|
259
|
+
*
|
|
260
|
+
* @param {Object} [options]
|
|
261
|
+
* @param {string} [options.workspace] - Explicit workspace
|
|
262
|
+
* @param {string[]} [options.filePaths] - File paths for auto-detection
|
|
263
|
+
* @returns {Promise<boolean>}
|
|
264
|
+
*/
|
|
265
|
+
async function isAvailable(options = {}) {
|
|
266
|
+
const cfg = getConfig();
|
|
267
|
+
if (!cfg.enabled) return false;
|
|
268
|
+
|
|
269
|
+
const ws = resolveWorkspace(options);
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const cached = availabilityCache.get(ws);
|
|
272
|
+
if (cached && cached.value !== null && now - cached.ts < AVAILABILITY_TTL_MS) {
|
|
273
|
+
return cached.value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = await execGraph("query", ["graph_stats"], ws);
|
|
277
|
+
const available = result !== null;
|
|
278
|
+
availabilityCache.set(ws, { value: available, ts: now });
|
|
279
|
+
|
|
280
|
+
if (available) {
|
|
281
|
+
logger.debug({ workspace: ws }, "[graphify] available");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return available;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// PUBLIC API
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @typedef {Object} CodeGraphOptions
|
|
293
|
+
* @property {string} [workspace] - Explicit workspace path (e.g. from X-Lynkr-Workspace header)
|
|
294
|
+
* @property {string[]} [filePaths] - File paths from conversation (used for auto-detection)
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get blast radius for a set of file paths.
|
|
299
|
+
*
|
|
300
|
+
* Uses Graphify's `query get_neighbors` on each file to find affected nodes,
|
|
301
|
+
* then aggregates into blast radius metrics.
|
|
302
|
+
*
|
|
303
|
+
* @param {string[]} filePaths — list of file paths to analyze
|
|
304
|
+
* @param {CodeGraphOptions} [options]
|
|
305
|
+
* @returns {Promise<{ affected_files: number, affected_functions: number, affected_tests: number, dependency_depth: number, risk_score: number }|null>}
|
|
306
|
+
*/
|
|
307
|
+
async function getBlastRadius(filePaths, options = {}) {
|
|
308
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) return null;
|
|
309
|
+
|
|
310
|
+
const ws = resolveWorkspace({ ...options, filePaths });
|
|
311
|
+
const cacheKey = `blast:${ws}:${filePaths.sort().join(",")}`;
|
|
312
|
+
const cached = cacheGet(cacheKey);
|
|
313
|
+
if (cached) return cached;
|
|
314
|
+
|
|
315
|
+
// Query neighbors for each file to estimate blast radius
|
|
316
|
+
const result = await execGraph(
|
|
317
|
+
"query",
|
|
318
|
+
["get_neighbors", "--files", ...filePaths, "--depth", "2", "--json"],
|
|
319
|
+
ws
|
|
320
|
+
);
|
|
321
|
+
if (!result) return null;
|
|
322
|
+
|
|
323
|
+
// Normalize Graphify output into our standard blast radius format
|
|
324
|
+
const nodes = result.nodes || result.neighbors || [];
|
|
325
|
+
const affectedFiles = new Set();
|
|
326
|
+
const affectedFunctions = [];
|
|
327
|
+
const affectedTests = [];
|
|
328
|
+
let maxDepth = 0;
|
|
329
|
+
|
|
330
|
+
for (const node of nodes) {
|
|
331
|
+
const src = node.source_file || node.source || "";
|
|
332
|
+
if (src) affectedFiles.add(src);
|
|
333
|
+
const label = (node.label || node.id || "").toLowerCase();
|
|
334
|
+
if (label.includes("test") || src.includes("test")) {
|
|
335
|
+
affectedTests.push(node);
|
|
336
|
+
} else {
|
|
337
|
+
affectedFunctions.push(node);
|
|
338
|
+
}
|
|
339
|
+
if (node.depth && node.depth > maxDepth) maxDepth = node.depth;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Risk score: based on affected count and depth
|
|
343
|
+
const riskScore = Math.min(100,
|
|
344
|
+
affectedFiles.size * 3 +
|
|
345
|
+
affectedFunctions.length * 2 +
|
|
346
|
+
maxDepth * 5
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const normalized = {
|
|
350
|
+
affected_files: affectedFiles.size,
|
|
351
|
+
affected_functions: affectedFunctions.length,
|
|
352
|
+
affected_tests: affectedTests.length,
|
|
353
|
+
dependency_depth: maxDepth,
|
|
354
|
+
risk_score: riskScore,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
cacheSet(cacheKey, normalized);
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get relevant file paths that should be included as context.
|
|
363
|
+
*
|
|
364
|
+
* Uses Graphify's BFS-based query to find related nodes.
|
|
365
|
+
*
|
|
366
|
+
* @param {string[]} filePaths — seed file paths
|
|
367
|
+
* @param {number} [maxFiles=20] — maximum files to return
|
|
368
|
+
* @param {CodeGraphOptions} [options]
|
|
369
|
+
* @returns {Promise<string[]|null>}
|
|
370
|
+
*/
|
|
371
|
+
async function getRelevantContext(filePaths, maxFiles = 20, options = {}) {
|
|
372
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) return null;
|
|
373
|
+
|
|
374
|
+
const ws = resolveWorkspace({ ...options, filePaths });
|
|
375
|
+
const cacheKey = `ctx:${ws}:${filePaths.sort().join(",")}:${maxFiles}`;
|
|
376
|
+
const cached = cacheGet(cacheKey);
|
|
377
|
+
if (cached) return cached;
|
|
378
|
+
|
|
379
|
+
// Use query_graph with BFS to find related files
|
|
380
|
+
const searchTerms = filePaths.map(f => path.basename(f, path.extname(f))).join(" ");
|
|
381
|
+
const result = await execGraph(
|
|
382
|
+
"query",
|
|
383
|
+
["query_graph", searchTerms, "--max-tokens", String(maxFiles * 100), "--json"],
|
|
384
|
+
ws
|
|
385
|
+
);
|
|
386
|
+
if (!result) return null;
|
|
387
|
+
|
|
388
|
+
// Extract unique source files from result nodes
|
|
389
|
+
const nodes = result.nodes || result.results || [];
|
|
390
|
+
const fileSet = new Set();
|
|
391
|
+
for (const node of nodes) {
|
|
392
|
+
const src = node.source_file || node.source || "";
|
|
393
|
+
if (src) fileSet.add(src);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const files = [...fileSet].slice(0, maxFiles);
|
|
397
|
+
if (files.length === 0) return null;
|
|
398
|
+
|
|
399
|
+
cacheSet(cacheKey, files);
|
|
400
|
+
return files;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get complexity signals for routing decisions.
|
|
405
|
+
*
|
|
406
|
+
* Queries Graphify for god nodes, community cohesion, and structural signals
|
|
407
|
+
* that indicate how complex a code change is.
|
|
408
|
+
*
|
|
409
|
+
* @param {string[]} filePaths — list of file paths to analyze
|
|
410
|
+
* @param {CodeGraphOptions} [options]
|
|
411
|
+
* @returns {Promise<{ blast_radius: number, dependency_depth: number, test_coverage_pct: number, is_infrastructure: boolean, god_node_touched: boolean, community_count: number, cohesion: number }|null>}
|
|
412
|
+
*/
|
|
413
|
+
async function getComplexitySignals(filePaths, options = {}) {
|
|
414
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) return null;
|
|
415
|
+
|
|
416
|
+
const ws = resolveWorkspace({ ...options, filePaths });
|
|
417
|
+
const cacheKey = `complexity:${ws}:${filePaths.sort().join(",")}`;
|
|
418
|
+
const cached = cacheGet(cacheKey);
|
|
419
|
+
if (cached) return cached;
|
|
420
|
+
|
|
421
|
+
// Run parallel queries: neighbors (blast radius) + god_nodes + graph_stats
|
|
422
|
+
const [neighborsResult, godNodesResult, statsResult] = await Promise.all([
|
|
423
|
+
execGraph("query", ["get_neighbors", "--files", ...filePaths, "--depth", "2", "--json"], ws),
|
|
424
|
+
execGraph("query", ["god_nodes", "--json"], ws),
|
|
425
|
+
execGraph("query", ["graph_stats", "--json"], ws),
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
// If all queries failed (tool not available), return null
|
|
429
|
+
if (!neighborsResult && !godNodesResult && !statsResult) return null;
|
|
430
|
+
|
|
431
|
+
// Compute blast radius from neighbors
|
|
432
|
+
let blastRadius = 0;
|
|
433
|
+
let depthMax = 0;
|
|
434
|
+
const affectedFiles = new Set();
|
|
435
|
+
if (neighborsResult) {
|
|
436
|
+
const nodes = neighborsResult.nodes || neighborsResult.neighbors || [];
|
|
437
|
+
for (const node of nodes) {
|
|
438
|
+
if (node.source_file) affectedFiles.add(node.source_file);
|
|
439
|
+
if (node.depth && node.depth > depthMax) depthMax = node.depth;
|
|
440
|
+
}
|
|
441
|
+
blastRadius = affectedFiles.size;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if any touched file contains a god node
|
|
445
|
+
let godNodeTouched = false;
|
|
446
|
+
if (godNodesResult) {
|
|
447
|
+
const godNodes = godNodesResult.god_nodes || godNodesResult.nodes || godNodesResult || [];
|
|
448
|
+
const godFiles = new Set(
|
|
449
|
+
(Array.isArray(godNodes) ? godNodes : []).map(n => n.source_file || n.source || "")
|
|
450
|
+
);
|
|
451
|
+
godNodeTouched = filePaths.some(fp => {
|
|
452
|
+
const base = path.basename(fp);
|
|
453
|
+
for (const gf of godFiles) {
|
|
454
|
+
if (gf.includes(base) || base.includes(path.basename(gf))) return true;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Extract community/cohesion from stats
|
|
461
|
+
let communityCount = 0;
|
|
462
|
+
let cohesion = 1;
|
|
463
|
+
if (statsResult) {
|
|
464
|
+
communityCount = statsResult.communities || statsResult.community_count || 0;
|
|
465
|
+
cohesion = statsResult.avg_cohesion ?? statsResult.cohesion ?? 1;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Detect infrastructure files
|
|
469
|
+
const infraPatterns = [
|
|
470
|
+
/docker/i, /compose/i, /makefile/i, /webpack/i, /babel/i, /eslint/i,
|
|
471
|
+
/tsconfig/i, /package\.json/i, /\.github/i, /ci/i, /cd/i, /deploy/i,
|
|
472
|
+
/terraform/i, /ansible/i, /k8s/i, /kubernetes/i, /helm/i,
|
|
473
|
+
];
|
|
474
|
+
const isInfrastructure = filePaths.some(fp =>
|
|
475
|
+
infraPatterns.some(pattern => pattern.test(fp))
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Estimate test coverage from graph — ratio of test files to affected files
|
|
479
|
+
const testFiles = [...affectedFiles].filter(f => /test|spec|__test/i.test(f));
|
|
480
|
+
const testCoveragePct = affectedFiles.size > 0
|
|
481
|
+
? Math.round((testFiles.length / affectedFiles.size) * 100)
|
|
482
|
+
: 100; // Assume covered if we can't tell
|
|
483
|
+
|
|
484
|
+
const normalized = {
|
|
485
|
+
blast_radius: blastRadius,
|
|
486
|
+
dependency_depth: depthMax,
|
|
487
|
+
test_coverage_pct: testCoveragePct,
|
|
488
|
+
is_infrastructure: isInfrastructure,
|
|
489
|
+
god_node_touched: godNodeTouched,
|
|
490
|
+
community_count: communityCount,
|
|
491
|
+
cohesion,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
cacheSet(cacheKey, normalized);
|
|
495
|
+
return normalized;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get overall graph statistics.
|
|
500
|
+
*
|
|
501
|
+
* @param {CodeGraphOptions} [options]
|
|
502
|
+
* @returns {Promise<{ total_files: number, total_functions: number, total_edges: number, languages: string[], communities: number, god_nodes: string[] }|null>}
|
|
503
|
+
*/
|
|
504
|
+
async function getGraphStats(options = {}) {
|
|
505
|
+
const ws = resolveWorkspace(options);
|
|
506
|
+
const cacheKey = `stats:${ws}`;
|
|
507
|
+
const cached = cacheGet(cacheKey);
|
|
508
|
+
if (cached) return cached;
|
|
509
|
+
|
|
510
|
+
const result = await execGraph("query", ["graph_stats", "--json"], ws);
|
|
511
|
+
if (!result) return null;
|
|
512
|
+
|
|
513
|
+
const normalized = {
|
|
514
|
+
total_files: result.total_files ?? result.files ?? 0,
|
|
515
|
+
total_functions: result.total_functions ?? result.nodes ?? 0,
|
|
516
|
+
total_edges: result.total_edges ?? result.edges ?? 0,
|
|
517
|
+
languages: Array.isArray(result.languages) ? result.languages : [],
|
|
518
|
+
communities: result.communities ?? result.community_count ?? 0,
|
|
519
|
+
god_nodes: Array.isArray(result.god_nodes) ? result.god_nodes.map(n => n.label || n.id || n) : [],
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
cacheSet(cacheKey, normalized);
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// EXPORTS
|
|
528
|
+
// ============================================================================
|
|
529
|
+
|
|
530
|
+
module.exports = {
|
|
531
|
+
isAvailable,
|
|
532
|
+
getBlastRadius,
|
|
533
|
+
getRelevantContext,
|
|
534
|
+
getComplexitySignals,
|
|
535
|
+
getGraphStats,
|
|
536
|
+
resolveWorkspace,
|
|
537
|
+
detectWorkspaceFromPaths,
|
|
538
|
+
};
|