memorix 1.1.13 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/cli/index.js +36313 -31189
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -30
- package/dist/index.js +5348 -674
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +3661 -293
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +19 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +5346 -672
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
- package/docs/1.2.0-CODE-STATE.md +61 -0
- package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
- package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
- package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
- package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
- package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
- package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
- package/docs/1.2.0-PRODUCT-STORY.md +234 -0
- package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
- package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
- package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
- package/docs/API_REFERENCE.md +25 -6
- package/docs/CONFIGURATION.md +21 -3
- package/docs/README.md +17 -2
- package/docs/dev-log/progress.txt +120 -40
- package/llms-full.txt +16 -2
- package/llms.txt +9 -4
- package/package.json +3 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/commands/codegraph.ts +112 -9
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/doctor.ts +73 -4
- package/src/cli/commands/knowledge.ts +282 -0
- package/src/cli/commands/serve-http.ts +12 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/App.tsx +1 -1
- package/src/cli/tui/Panels.tsx +8 -8
- package/src/cli/tui/theme.ts +1 -1
- package/src/cli/tui/views/GraphView.tsx +8 -7
- package/src/cli/tui/views/KnowledgeView.tsx +9 -9
- package/src/codegraph/auto-context.ts +171 -9
- package/src/codegraph/code-state.ts +95 -0
- package/src/codegraph/context-pack.ts +82 -1
- package/src/codegraph/external-provider.ts +581 -0
- package/src/codegraph/lite-provider.ts +64 -19
- package/src/codegraph/project-context.ts +9 -1
- package/src/codegraph/store.ts +154 -6
- package/src/codegraph/types.ts +117 -0
- package/src/config/resolved-config.ts +28 -0
- package/src/config/toml-loader.ts +3 -0
- package/src/config/yaml-loader.ts +6 -0
- package/src/dashboard/server.ts +15 -1
- package/src/evaluation/workset-evaluation.ts +120 -0
- package/src/hooks/handler.ts +48 -6
- package/src/knowledge/claim-store.ts +267 -0
- package/src/knowledge/claims.ts +537 -0
- package/src/knowledge/markdown.ts +129 -0
- package/src/knowledge/types.ts +157 -0
- package/src/knowledge/wiki.ts +524 -0
- package/src/knowledge/workflow-store.ts +168 -0
- package/src/knowledge/workflow-types.ts +95 -0
- package/src/knowledge/workflows.ts +743 -0
- package/src/knowledge/workset.ts +515 -0
- package/src/knowledge/workspace-store.ts +220 -0
- package/src/knowledge/workspace-types.ts +106 -0
- package/src/knowledge/workspace.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/runtime/control-plane-maintenance.ts +5 -0
- package/src/runtime/isolated-maintenance.ts +5 -0
- package/src/runtime/lifecycle-status.ts +102 -0
- package/src/runtime/lifecycle.ts +107 -0
- package/src/runtime/maintenance-jobs.ts +5 -0
- package/src/runtime/project-maintenance.ts +190 -0
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +354 -14
- package/src/store/file-lock.ts +24 -4
- package/src/store/sqlite-db.ts +307 -0
- package/src/wiki/generator.ts +4 -2
- package/src/wiki/knowledge-graph.ts +7 -4
- package/src/wiki/types.ts +16 -4
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
5
|
+
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
6
|
+
import { normalizeCodePath } from './ids.js';
|
|
7
|
+
import type {
|
|
8
|
+
CodeGraphExternalMode,
|
|
9
|
+
CodeGraphProviderQuality,
|
|
10
|
+
ExternalCodeGraphHealth,
|
|
11
|
+
ExternalCodeGraphOutline,
|
|
12
|
+
ExternalCodeGraphRelation,
|
|
13
|
+
ExternalCodeGraphSymbol,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
// The bundled Windows CodeGraph CLI takes roughly 600 ms just to start cold.
|
|
17
|
+
// Keep the default useful in real sessions while still bounding a local call.
|
|
18
|
+
export const DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 1_200;
|
|
19
|
+
export const MAX_EXTERNAL_CODEGRAPH_TIMEOUT_MS = 5_000;
|
|
20
|
+
export const MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES = 256 * 1024;
|
|
21
|
+
const MAX_EXTERNAL_NODE_COUNT = 16;
|
|
22
|
+
const MAX_EXTERNAL_EDGE_COUNT = 24;
|
|
23
|
+
const MAX_EXTERNAL_FILE_COUNT = 12;
|
|
24
|
+
const MAX_EXTERNAL_TEXT_LENGTH = 320;
|
|
25
|
+
|
|
26
|
+
const LITE_SUPPORTED_LANGUAGES = [
|
|
27
|
+
'typescript',
|
|
28
|
+
'javascript',
|
|
29
|
+
'python',
|
|
30
|
+
'go',
|
|
31
|
+
'rust',
|
|
32
|
+
'java',
|
|
33
|
+
'csharp',
|
|
34
|
+
'c',
|
|
35
|
+
'cpp',
|
|
36
|
+
'php',
|
|
37
|
+
'ruby',
|
|
38
|
+
'kotlin',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export interface ExternalCodeGraphCommandInput {
|
|
42
|
+
command?: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
cwd: string;
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
maxOutputBytes: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ExternalCodeGraphCommandResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
stdout: string;
|
|
52
|
+
stderr?: string;
|
|
53
|
+
exitCode?: number;
|
|
54
|
+
timedOut?: boolean;
|
|
55
|
+
outputLimited?: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Injectable boundary keeps provider tests independent from a global install. */
|
|
60
|
+
export interface ExternalCodeGraphRunner {
|
|
61
|
+
run(input: ExternalCodeGraphCommandInput): Promise<ExternalCodeGraphCommandResult>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface InspectExternalCodeGraphInput {
|
|
65
|
+
projectRoot: string;
|
|
66
|
+
mode?: CodeGraphExternalMode;
|
|
67
|
+
command?: string;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
runner?: ExternalCodeGraphRunner;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface InspectExternalCodeGraphResult {
|
|
73
|
+
quality: CodeGraphProviderQuality;
|
|
74
|
+
health: ExternalCodeGraphHealth;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ExternalCodeGraphContextInput extends InspectExternalCodeGraphInput {
|
|
78
|
+
task: string;
|
|
79
|
+
exclude?: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ExternalCodeGraphContextResult extends InspectExternalCodeGraphResult {
|
|
83
|
+
outline?: ExternalCodeGraphOutline;
|
|
84
|
+
/** Only present when a detected external graph could not safely contribute. */
|
|
85
|
+
caution?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface RawExternalStatus {
|
|
89
|
+
initialized: boolean;
|
|
90
|
+
projectPath: string;
|
|
91
|
+
fileCount: number;
|
|
92
|
+
nodeCount: number;
|
|
93
|
+
edgeCount: number;
|
|
94
|
+
languages: string[];
|
|
95
|
+
pendingChanges: {
|
|
96
|
+
added: number;
|
|
97
|
+
modified: number;
|
|
98
|
+
removed: number;
|
|
99
|
+
};
|
|
100
|
+
worktreeMismatch: string | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface RawExternalNode {
|
|
104
|
+
id: string;
|
|
105
|
+
kind: string;
|
|
106
|
+
name: string;
|
|
107
|
+
qualifiedName?: string;
|
|
108
|
+
filePath: string;
|
|
109
|
+
language?: string;
|
|
110
|
+
startLine?: number;
|
|
111
|
+
endLine?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface ResolvedLaunch {
|
|
115
|
+
executable: string;
|
|
116
|
+
prefixArgs: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function boundedTimeout(value: number | undefined): number {
|
|
120
|
+
if (!Number.isFinite(value)) return DEFAULT_EXTERNAL_CODEGRAPH_TIMEOUT_MS;
|
|
121
|
+
return Math.max(100, Math.min(Math.floor(value!), MAX_EXTERNAL_CODEGRAPH_TIMEOUT_MS));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
125
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function nonNegativeInteger(value: unknown): number | undefined {
|
|
129
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function safeText(value: unknown, maxLength = MAX_EXTERNAL_TEXT_LENGTH): string | undefined {
|
|
133
|
+
if (typeof value !== 'string') return undefined;
|
|
134
|
+
const normalized = sanitizeCredentials(value).replace(/[\u0000-\u001f]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
135
|
+
if (!normalized || normalized.length > maxLength) return undefined;
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sameProjectRoot(left: string, right: string): boolean {
|
|
140
|
+
const normalizedLeft = resolve(left).replace(/\\/g, '/');
|
|
141
|
+
const normalizedRight = resolve(right).replace(/\\/g, '/');
|
|
142
|
+
return process.platform === 'win32'
|
|
143
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
144
|
+
: normalizedLeft === normalizedRight;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveCommandCandidates(command: string | undefined): string[] {
|
|
148
|
+
const requested = command?.trim() || 'codegraph';
|
|
149
|
+
if (isAbsolute(requested) || requested.includes('/') || requested.includes('\\')) {
|
|
150
|
+
return [resolve(requested)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const names = extname(requested)
|
|
154
|
+
? [requested]
|
|
155
|
+
: process.platform === 'win32'
|
|
156
|
+
? [requested + '.exe', requested + '.cmd', requested + '.js', requested]
|
|
157
|
+
: [requested];
|
|
158
|
+
const directories = [
|
|
159
|
+
...(process.platform === 'win32' && process.env.LOCALAPPDATA
|
|
160
|
+
? [join(process.env.LOCALAPPDATA, 'codegraph', 'current', 'bin')]
|
|
161
|
+
: []),
|
|
162
|
+
...(process.env.PATH ?? '').split(delimiter).filter(Boolean),
|
|
163
|
+
];
|
|
164
|
+
return [...new Set(directories.flatMap(directory => names.map(name => join(directory, name))))];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveLaunch(command: string | undefined): ResolvedLaunch | undefined {
|
|
168
|
+
for (const candidate of resolveCommandCandidates(command)) {
|
|
169
|
+
if (!existsSync(candidate)) continue;
|
|
170
|
+
const extension = extname(candidate).toLowerCase();
|
|
171
|
+
if (extension === '.cmd') {
|
|
172
|
+
// The official Windows bundle is a small cmd shim. Resolve its Node
|
|
173
|
+
// runtime and JS entrypoint directly so task text never enters a shell.
|
|
174
|
+
const bundleRoot = resolve(dirname(candidate), '..');
|
|
175
|
+
const node = join(bundleRoot, 'node.exe');
|
|
176
|
+
const entry = join(bundleRoot, 'lib', 'dist', 'bin', 'codegraph.js');
|
|
177
|
+
if (existsSync(node) && existsSync(entry)) {
|
|
178
|
+
return { executable: node, prefixArgs: ['--liftoff-only', entry] };
|
|
179
|
+
}
|
|
180
|
+
// Common npm shims are cmd wrappers around a JavaScript entrypoint. Read
|
|
181
|
+
// the entrypoint path and invoke Node directly rather than interpolating
|
|
182
|
+
// task text into cmd.exe. This deliberately supports only a real local
|
|
183
|
+
// JS file, never arbitrary cmd syntax.
|
|
184
|
+
try {
|
|
185
|
+
const shim = readFileSync(candidate, 'utf8');
|
|
186
|
+
const match = shim.match(/["']([^"']*%(?:~)?dp0%?[^"']*\.m?js)["']/i);
|
|
187
|
+
if (match?.[1]) {
|
|
188
|
+
const base = dirname(candidate).endsWith(sep) ? dirname(candidate) : dirname(candidate) + sep;
|
|
189
|
+
const entry = resolve(match[1].replace(/%(?:~)?dp0%?/gi, base));
|
|
190
|
+
if (existsSync(entry)) return { executable: process.execPath, prefixArgs: [entry] };
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// A broken shim is reported as unavailable below, never executed.
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (extension === '.js') {
|
|
198
|
+
return { executable: process.execPath, prefixArgs: [candidate] };
|
|
199
|
+
}
|
|
200
|
+
return { executable: candidate, prefixArgs: [] };
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const defaultRunner: ExternalCodeGraphRunner = {
|
|
206
|
+
async run(input): Promise<ExternalCodeGraphCommandResult> {
|
|
207
|
+
const launch = resolveLaunch(input.command);
|
|
208
|
+
if (!launch) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
stdout: '',
|
|
212
|
+
error: 'CodeGraph executable was not found or is not safely runnable.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return new Promise((resolveResult) => {
|
|
217
|
+
let stdout = Buffer.alloc(0);
|
|
218
|
+
let stderr = Buffer.alloc(0);
|
|
219
|
+
let settled = false;
|
|
220
|
+
let timedOut = false;
|
|
221
|
+
let outputLimited = false;
|
|
222
|
+
let spawnError: string | undefined;
|
|
223
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
224
|
+
const finish = (result: ExternalCodeGraphCommandResult) => {
|
|
225
|
+
if (settled) return;
|
|
226
|
+
settled = true;
|
|
227
|
+
if (timeout) clearTimeout(timeout);
|
|
228
|
+
resolveResult(result);
|
|
229
|
+
};
|
|
230
|
+
let child: ReturnType<typeof spawn>;
|
|
231
|
+
try {
|
|
232
|
+
child = spawn(launch.executable, [...launch.prefixArgs, ...input.args], {
|
|
233
|
+
cwd: input.cwd,
|
|
234
|
+
shell: false,
|
|
235
|
+
windowsHide: true,
|
|
236
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
finish({
|
|
240
|
+
ok: false,
|
|
241
|
+
stdout: '',
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const stop = () => {
|
|
248
|
+
try {
|
|
249
|
+
child.kill();
|
|
250
|
+
} catch {
|
|
251
|
+
// The close/error event will settle the result.
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
timeout = setTimeout(() => {
|
|
255
|
+
timedOut = true;
|
|
256
|
+
stop();
|
|
257
|
+
}, input.timeoutMs);
|
|
258
|
+
|
|
259
|
+
const collect = (target: 'stdout' | 'stderr', chunk: Buffer | string) => {
|
|
260
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
261
|
+
if (stdout.length + stderr.length + buffer.length > input.maxOutputBytes) {
|
|
262
|
+
outputLimited = true;
|
|
263
|
+
stop();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (target === 'stdout') stdout = Buffer.concat([stdout, buffer]);
|
|
267
|
+
else stderr = Buffer.concat([stderr, buffer]);
|
|
268
|
+
};
|
|
269
|
+
child.stdout?.on('data', (chunk) => collect('stdout', chunk));
|
|
270
|
+
child.stderr?.on('data', (chunk) => collect('stderr', chunk));
|
|
271
|
+
child.on('error', (error) => {
|
|
272
|
+
spawnError = error instanceof Error ? error.message : String(error);
|
|
273
|
+
finish({ ok: false, stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'), error: spawnError });
|
|
274
|
+
});
|
|
275
|
+
child.on('close', (exitCode) => {
|
|
276
|
+
finish({
|
|
277
|
+
ok: exitCode === 0 && !timedOut && !outputLimited && !spawnError,
|
|
278
|
+
stdout: stdout.toString('utf8'),
|
|
279
|
+
stderr: stderr.toString('utf8'),
|
|
280
|
+
...(typeof exitCode === 'number' ? { exitCode } : {}),
|
|
281
|
+
...(timedOut ? { timedOut } : {}),
|
|
282
|
+
...(outputLimited ? { outputLimited } : {}),
|
|
283
|
+
...(spawnError ? { error: spawnError } : {}),
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
function providerQuality(input: {
|
|
291
|
+
mode: CodeGraphExternalMode;
|
|
292
|
+
health: ExternalCodeGraphHealth;
|
|
293
|
+
selected?: 'lite' | 'external';
|
|
294
|
+
}): CodeGraphProviderQuality {
|
|
295
|
+
const selected = input.selected ?? 'lite';
|
|
296
|
+
return {
|
|
297
|
+
selected,
|
|
298
|
+
selectedQuality: selected === 'external' ? 'semantic' : 'heuristic',
|
|
299
|
+
mode: input.mode,
|
|
300
|
+
lite: {
|
|
301
|
+
quality: 'heuristic',
|
|
302
|
+
capabilities: {
|
|
303
|
+
declarations: true,
|
|
304
|
+
importHints: true,
|
|
305
|
+
resolvedRelations: false,
|
|
306
|
+
exactLocations: false,
|
|
307
|
+
},
|
|
308
|
+
supportedLanguages: LITE_SUPPORTED_LANGUAGES,
|
|
309
|
+
},
|
|
310
|
+
external: input.health,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseJson(stdout: string): unknown | undefined {
|
|
315
|
+
if (Buffer.byteLength(stdout, 'utf8') > MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES) return undefined;
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(stdout);
|
|
318
|
+
} catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseStatus(value: unknown, projectRoot: string): RawExternalStatus | undefined {
|
|
324
|
+
if (!isRecord(value)) return undefined;
|
|
325
|
+
const initialized = value.initialized;
|
|
326
|
+
const projectPath = safeText(value.projectPath, 4_096);
|
|
327
|
+
const fileCount = nonNegativeInteger(value.fileCount);
|
|
328
|
+
const nodeCount = nonNegativeInteger(value.nodeCount);
|
|
329
|
+
const edgeCount = nonNegativeInteger(value.edgeCount);
|
|
330
|
+
const pending = value.pendingChanges;
|
|
331
|
+
const worktreeMismatch = value.worktreeMismatch;
|
|
332
|
+
if (typeof initialized !== 'boolean' || !projectPath || !sameProjectRoot(projectPath, projectRoot)) return undefined;
|
|
333
|
+
// CodeGraph intentionally emits a minimal payload before its first index.
|
|
334
|
+
// That is a supported optional-provider state, not malformed evidence.
|
|
335
|
+
if (!initialized) {
|
|
336
|
+
return {
|
|
337
|
+
initialized: false,
|
|
338
|
+
projectPath,
|
|
339
|
+
fileCount: 0,
|
|
340
|
+
nodeCount: 0,
|
|
341
|
+
edgeCount: 0,
|
|
342
|
+
languages: [],
|
|
343
|
+
pendingChanges: { added: 0, modified: 0, removed: 0 },
|
|
344
|
+
worktreeMismatch: null,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (fileCount === undefined || nodeCount === undefined || edgeCount === undefined) return undefined;
|
|
348
|
+
if (!isRecord(pending)) return undefined;
|
|
349
|
+
const added = nonNegativeInteger(pending.added);
|
|
350
|
+
const modified = nonNegativeInteger(pending.modified);
|
|
351
|
+
const removed = nonNegativeInteger(pending.removed);
|
|
352
|
+
if (added === undefined || modified === undefined || removed === undefined) return undefined;
|
|
353
|
+
if (worktreeMismatch !== null && typeof worktreeMismatch !== 'string') return undefined;
|
|
354
|
+
const languages = Array.isArray(value.languages)
|
|
355
|
+
? value.languages.map(item => safeText(item, 80)).filter((item): item is string => Boolean(item)).slice(0, 32)
|
|
356
|
+
: [];
|
|
357
|
+
return {
|
|
358
|
+
initialized,
|
|
359
|
+
projectPath,
|
|
360
|
+
fileCount,
|
|
361
|
+
nodeCount,
|
|
362
|
+
edgeCount,
|
|
363
|
+
languages,
|
|
364
|
+
pendingChanges: { added, modified, removed },
|
|
365
|
+
worktreeMismatch,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function healthFromStatus(status: RawExternalStatus): ExternalCodeGraphHealth {
|
|
370
|
+
if (!status.initialized) return { state: 'not-initialized', reason: 'The local CodeGraph index is not initialized.' };
|
|
371
|
+
const pending = status.pendingChanges.added + status.pendingChanges.modified + status.pendingChanges.removed;
|
|
372
|
+
if (pending > 0 || status.worktreeMismatch) {
|
|
373
|
+
return {
|
|
374
|
+
state: 'stale',
|
|
375
|
+
reason: 'The local CodeGraph index has pending changes or a worktree mismatch.',
|
|
376
|
+
indexedFiles: status.fileCount,
|
|
377
|
+
indexedNodes: status.nodeCount,
|
|
378
|
+
indexedEdges: status.edgeCount,
|
|
379
|
+
languages: status.languages,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
state: 'ready',
|
|
384
|
+
indexedFiles: status.fileCount,
|
|
385
|
+
indexedNodes: status.nodeCount,
|
|
386
|
+
indexedEdges: status.edgeCount,
|
|
387
|
+
languages: status.languages,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function unavailableHealth(result: ExternalCodeGraphCommandResult): ExternalCodeGraphHealth {
|
|
392
|
+
if (result.timedOut) return { state: 'timed-out', reason: 'A local CodeGraph command timed out.' };
|
|
393
|
+
if (result.outputLimited) return { state: 'invalid', reason: 'A local CodeGraph response exceeded the safety limit.' };
|
|
394
|
+
return { state: 'unavailable', reason: 'A local CodeGraph command did not complete.' };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function runSafely(
|
|
398
|
+
runner: ExternalCodeGraphRunner,
|
|
399
|
+
input: ExternalCodeGraphCommandInput,
|
|
400
|
+
): Promise<ExternalCodeGraphCommandResult> {
|
|
401
|
+
try {
|
|
402
|
+
return await runner.run(input);
|
|
403
|
+
} catch {
|
|
404
|
+
return { ok: false, stdout: '', error: 'CodeGraph command runner failed.' };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function inspectExternalCodeGraph(input: InspectExternalCodeGraphInput): Promise<InspectExternalCodeGraphResult> {
|
|
409
|
+
const mode = input.mode ?? 'auto';
|
|
410
|
+
if (mode === 'off') {
|
|
411
|
+
const health: ExternalCodeGraphHealth = { state: 'disabled', reason: 'External CodeGraph is disabled by configuration.' };
|
|
412
|
+
return { health, quality: providerQuality({ mode, health }) };
|
|
413
|
+
}
|
|
414
|
+
if (!existsSync(join(input.projectRoot, '.codegraph'))) {
|
|
415
|
+
const health: ExternalCodeGraphHealth = { state: 'not-detected', reason: 'No local .codegraph index is present for this project.' };
|
|
416
|
+
return { health, quality: providerQuality({ mode, health }) };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const result = await runSafely(input.runner ?? defaultRunner, {
|
|
420
|
+
command: input.command,
|
|
421
|
+
args: ['status', input.projectRoot, '--json'],
|
|
422
|
+
cwd: input.projectRoot,
|
|
423
|
+
timeoutMs: boundedTimeout(input.timeoutMs),
|
|
424
|
+
maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
|
|
425
|
+
});
|
|
426
|
+
if (!result.ok) {
|
|
427
|
+
const health = unavailableHealth(result);
|
|
428
|
+
return { health, quality: providerQuality({ mode, health }) };
|
|
429
|
+
}
|
|
430
|
+
const status = parseStatus(parseJson(result.stdout), input.projectRoot);
|
|
431
|
+
if (!status) {
|
|
432
|
+
const health: ExternalCodeGraphHealth = { state: 'invalid', reason: 'The local CodeGraph status payload was invalid for this project.' };
|
|
433
|
+
return { health, quality: providerQuality({ mode, health }) };
|
|
434
|
+
}
|
|
435
|
+
const health = healthFromStatus(status);
|
|
436
|
+
return { health, quality: providerQuality({ mode, health }) };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function safeRelativePath(projectRoot: string, value: unknown, exclude?: string[]): string | undefined {
|
|
440
|
+
const path = safeText(value, 1_024);
|
|
441
|
+
if (!path || isAbsolute(path)) return undefined;
|
|
442
|
+
const absolute = resolve(projectRoot, path);
|
|
443
|
+
const fromRoot = relative(projectRoot, absolute);
|
|
444
|
+
if (!fromRoot || fromRoot === '..' || fromRoot.startsWith('..\\') || fromRoot.startsWith('../') || isAbsolute(fromRoot)) return undefined;
|
|
445
|
+
const normalized = normalizeCodePath(fromRoot);
|
|
446
|
+
if (isCodeGraphExcludedPath(normalized, exclude)) return undefined;
|
|
447
|
+
return normalized;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseNode(value: unknown, projectRoot: string, exclude?: string[]): RawExternalNode | undefined {
|
|
451
|
+
if (!isRecord(value)) return undefined;
|
|
452
|
+
const id = safeText(value.id, 180);
|
|
453
|
+
const kind = safeText(value.kind, 80);
|
|
454
|
+
const name = safeText(value.name, 180);
|
|
455
|
+
const filePath = safeRelativePath(projectRoot, value.filePath, exclude);
|
|
456
|
+
if (!id || !kind || !name || !filePath) return undefined;
|
|
457
|
+
const startLine = value.startLine === undefined ? undefined : nonNegativeInteger(value.startLine);
|
|
458
|
+
const endLine = value.endLine === undefined ? undefined : nonNegativeInteger(value.endLine);
|
|
459
|
+
if ((value.startLine !== undefined && (!startLine || startLine < 1)) || (value.endLine !== undefined && (!endLine || endLine < 1))) return undefined;
|
|
460
|
+
const qualifiedName = value.qualifiedName === undefined ? undefined : safeText(value.qualifiedName, 220);
|
|
461
|
+
const language = value.language === undefined ? undefined : safeText(value.language, 80);
|
|
462
|
+
return {
|
|
463
|
+
id,
|
|
464
|
+
kind,
|
|
465
|
+
name,
|
|
466
|
+
filePath,
|
|
467
|
+
...(qualifiedName ? { qualifiedName } : {}),
|
|
468
|
+
...(language ? { language } : {}),
|
|
469
|
+
...(startLine ? { startLine } : {}),
|
|
470
|
+
...(endLine ? { endLine } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function externalSymbol(raw: RawExternalNode): ExternalCodeGraphSymbol {
|
|
475
|
+
return {
|
|
476
|
+
id: raw.id,
|
|
477
|
+
name: raw.name,
|
|
478
|
+
...(raw.qualifiedName ? { qualifiedName: raw.qualifiedName } : {}),
|
|
479
|
+
kind: raw.kind,
|
|
480
|
+
path: raw.filePath,
|
|
481
|
+
...(raw.startLine ? { startLine: raw.startLine } : {}),
|
|
482
|
+
...(raw.endLine ? { endLine: raw.endLine } : {}),
|
|
483
|
+
...(raw.language ? { language: raw.language } : {}),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function parseOutline(value: unknown, projectRoot: string, exclude?: string[]): ExternalCodeGraphOutline | undefined {
|
|
488
|
+
if (!isRecord(value) || !Array.isArray(value.entryPoints) || !Array.isArray(value.nodes)
|
|
489
|
+
|| !Array.isArray(value.edges) || !Array.isArray(value.relatedFiles)) return undefined;
|
|
490
|
+
if ('codeBlocks' in value && (!Array.isArray(value.codeBlocks) || value.codeBlocks.length > 0)) return undefined;
|
|
491
|
+
if (value.entryPoints.length > MAX_EXTERNAL_NODE_COUNT || value.nodes.length > MAX_EXTERNAL_NODE_COUNT
|
|
492
|
+
|| value.edges.length > MAX_EXTERNAL_EDGE_COUNT || value.relatedFiles.length > MAX_EXTERNAL_FILE_COUNT) return undefined;
|
|
493
|
+
|
|
494
|
+
const nodes = [...value.entryPoints, ...value.nodes]
|
|
495
|
+
.map(item => parseNode(item, projectRoot, exclude));
|
|
496
|
+
if (nodes.some(node => !node)) return undefined;
|
|
497
|
+
const byId = new Map<string, ExternalCodeGraphSymbol>();
|
|
498
|
+
for (const node of nodes as RawExternalNode[]) {
|
|
499
|
+
byId.set(node.id, externalSymbol(node));
|
|
500
|
+
}
|
|
501
|
+
const entryPoints = value.entryPoints
|
|
502
|
+
.map(item => parseNode(item, projectRoot, exclude))
|
|
503
|
+
.filter((node): node is RawExternalNode => Boolean(node))
|
|
504
|
+
.map(externalSymbol)
|
|
505
|
+
.filter((node, index, items) => items.findIndex(item => item.id === node.id) === index)
|
|
506
|
+
.slice(0, 5);
|
|
507
|
+
|
|
508
|
+
const relations: ExternalCodeGraphRelation[] = [];
|
|
509
|
+
for (const rawEdge of value.edges) {
|
|
510
|
+
if (!isRecord(rawEdge)) return undefined;
|
|
511
|
+
const source = safeText(rawEdge.source, 180);
|
|
512
|
+
const target = safeText(rawEdge.target, 180);
|
|
513
|
+
const kind = safeText(rawEdge.kind, 80);
|
|
514
|
+
const line = rawEdge.line === undefined ? undefined : nonNegativeInteger(rawEdge.line);
|
|
515
|
+
if (!source || !target || !kind || (rawEdge.line !== undefined && (!line || line < 1))) return undefined;
|
|
516
|
+
const from = byId.get(source);
|
|
517
|
+
const to = byId.get(target);
|
|
518
|
+
if (!from || !to) continue;
|
|
519
|
+
relations.push({ from, to, kind, ...(line ? { line } : {}) });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const relatedFiles = value.relatedFiles.map(item => safeRelativePath(projectRoot, item, exclude));
|
|
523
|
+
if (relatedFiles.some(file => !file)) return undefined;
|
|
524
|
+
if (!isRecord(value.stats)) return undefined;
|
|
525
|
+
const nodeCount = nonNegativeInteger(value.stats.nodeCount);
|
|
526
|
+
const edgeCount = nonNegativeInteger(value.stats.edgeCount);
|
|
527
|
+
const fileCount = nonNegativeInteger(value.stats.fileCount);
|
|
528
|
+
if (nodeCount === undefined || edgeCount === undefined || fileCount === undefined) return undefined;
|
|
529
|
+
return {
|
|
530
|
+
provider: 'external',
|
|
531
|
+
entryPoints,
|
|
532
|
+
relations: relations.slice(0, 6),
|
|
533
|
+
relatedFiles: [...new Set(relatedFiles as string[])].slice(0, 5),
|
|
534
|
+
stats: { nodes: nodeCount, edges: edgeCount, files: fileCount },
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function fallbackCaution(health: ExternalCodeGraphHealth): string | undefined {
|
|
539
|
+
if (health.state === 'disabled' || health.state === 'not-detected'
|
|
540
|
+
|| health.state === 'not-initialized' || health.state === 'ready') return undefined;
|
|
541
|
+
return 'External semantic CodeGraph is ' + health.state + '; using Lite structural evidence for this brief.';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function getExternalCodeGraphContext(input: ExternalCodeGraphContextInput): Promise<ExternalCodeGraphContextResult> {
|
|
545
|
+
const inspected = await inspectExternalCodeGraph(input);
|
|
546
|
+
if (inspected.health.state !== 'ready') {
|
|
547
|
+
return { ...inspected, caution: fallbackCaution(inspected.health) };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const mode = input.mode ?? 'auto';
|
|
551
|
+
const result = await runSafely(input.runner ?? defaultRunner, {
|
|
552
|
+
command: input.command,
|
|
553
|
+
args: ['context', '--path', input.projectRoot, '--format', 'json', '--max-nodes', '8', '--no-code', input.task],
|
|
554
|
+
cwd: input.projectRoot,
|
|
555
|
+
timeoutMs: boundedTimeout(input.timeoutMs),
|
|
556
|
+
maxOutputBytes: MAX_EXTERNAL_CODEGRAPH_OUTPUT_BYTES,
|
|
557
|
+
});
|
|
558
|
+
if (!result.ok) {
|
|
559
|
+
const health = unavailableHealth(result);
|
|
560
|
+
return {
|
|
561
|
+
health,
|
|
562
|
+
quality: providerQuality({ mode, health }),
|
|
563
|
+
caution: fallbackCaution(health),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const outline = parseOutline(parseJson(result.stdout), input.projectRoot, input.exclude);
|
|
567
|
+
if (!outline) {
|
|
568
|
+
const health: ExternalCodeGraphHealth = { state: 'invalid', reason: 'The local CodeGraph context payload failed validation.' };
|
|
569
|
+
return {
|
|
570
|
+
health,
|
|
571
|
+
quality: providerQuality({ mode, health }),
|
|
572
|
+
caution: fallbackCaution(health),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const contributes = outline.entryPoints.length > 0 || outline.relations.length > 0 || outline.relatedFiles.length > 0;
|
|
576
|
+
return {
|
|
577
|
+
...inspected,
|
|
578
|
+
...(contributes ? { outline } : {}),
|
|
579
|
+
quality: providerQuality({ mode, health: inspected.health, ...(contributes ? { selected: 'external' as const } : {}) }),
|
|
580
|
+
};
|
|
581
|
+
}
|