@optave/codegraph 3.12.0 → 3.13.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/README.md +71 -35
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.js +1 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +12 -8
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +93 -38
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +9 -1
- package/dist/domain/graph/builder/cha.d.ts.map +1 -1
- package/dist/domain/graph/builder/cha.js +17 -2
- package/dist/domain/graph/builder/cha.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +8 -0
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +22 -3
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +1 -1
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +88 -318
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +341 -82
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/parser.d.ts +4 -5
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +46 -15
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +10 -2
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +2 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +1 -0
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/cpp.d.ts.map +1 -1
- package/dist/extractors/cpp.js +42 -1
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.d.ts.map +1 -1
- package/dist/extractors/cuda.js +42 -1
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +8 -7
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.js +137 -6
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/structure-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +77 -4
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +395 -21
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +1 -1
- package/src/domain/graph/builder/call-resolver.ts +99 -41
- package/src/domain/graph/builder/cha.ts +18 -1
- package/src/domain/graph/builder/helpers.ts +24 -4
- package/src/domain/graph/builder/incremental.ts +1 -0
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +130 -399
- package/src/domain/graph/builder/stages/detect-changes.ts +1 -1
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +396 -92
- package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
- package/src/domain/parser.ts +45 -14
- package/src/domain/wasm-worker-entry.ts +10 -2
- package/src/domain/wasm-worker-pool.ts +1 -0
- package/src/domain/wasm-worker-protocol.ts +1 -0
- package/src/extractors/cpp.ts +44 -1
- package/src/extractors/cuda.ts +44 -1
- package/src/extractors/helpers.ts +43 -0
- package/src/extractors/java.ts +8 -7
- package/src/extractors/javascript.ts +127 -6
- package/src/features/structure-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +481 -22
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +41 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
clearConfigCache,
|
|
6
|
+
DEFAULTS,
|
|
7
|
+
getDefaultUserConfigPath,
|
|
8
|
+
loadConfig,
|
|
9
|
+
loadConfigWithProvenance,
|
|
10
|
+
resolveUserConfigPath,
|
|
11
|
+
} from '../../infrastructure/config.js';
|
|
12
|
+
import {
|
|
13
|
+
getUserConfigConsent,
|
|
14
|
+
listUserConfigConsent,
|
|
15
|
+
REGISTRY_PATH,
|
|
16
|
+
setUserConfigConsent,
|
|
17
|
+
} from '../../infrastructure/registry.js';
|
|
18
|
+
import { formatTable } from '../../presentation/table.js';
|
|
19
|
+
import type { ConfigSource } from '../../types.js';
|
|
20
|
+
import type { CommandDefinition } from '../types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recursively flatten a nested config object to dot-notation key/value pairs.
|
|
24
|
+
* Arrays and null values are serialised to strings.
|
|
25
|
+
*/
|
|
26
|
+
function flattenConfig(
|
|
27
|
+
obj: Record<string, unknown>,
|
|
28
|
+
prefix = '',
|
|
29
|
+
): Array<{ key: string; value: string }> {
|
|
30
|
+
const out: Array<{ key: string; value: string }> = [];
|
|
31
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
32
|
+
const fullKey = prefix ? `${prefix}.${k}` : k;
|
|
33
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
34
|
+
out.push(...flattenConfig(v as Record<string, unknown>, fullKey));
|
|
35
|
+
} else if (Array.isArray(v)) {
|
|
36
|
+
out.push({ key: fullKey, value: v.length === 0 ? '[]' : JSON.stringify(v) });
|
|
37
|
+
} else {
|
|
38
|
+
out.push({ key: fullKey, value: v === null ? 'null' : String(v) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Expand a top-level provenance map (e.g. { build: 'project' }) to cover every
|
|
46
|
+
* flattened dot-notation key (e.g. 'build.incremental' → 'project').
|
|
47
|
+
*/
|
|
48
|
+
function expandProvenance(
|
|
49
|
+
flatEntries: Array<{ key: string; value: string }>,
|
|
50
|
+
provenance: Record<string, ConfigSource>,
|
|
51
|
+
): Map<string, ConfigSource> {
|
|
52
|
+
const map = new Map<string, ConfigSource>();
|
|
53
|
+
for (const { key } of flatEntries) {
|
|
54
|
+
// Provenance is keyed by top-level section (e.g. 'build', 'llm'), so
|
|
55
|
+
// extract the first segment to find the governing provenance entry.
|
|
56
|
+
const topLevel = key.split('.')[0] ?? key;
|
|
57
|
+
map.set(key, provenance[topLevel] ?? 'default');
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render the effective config as a human-readable Key/Value/Source table.
|
|
64
|
+
* All rows are shown, sorted so non-default overrides appear first, then
|
|
65
|
+
* remaining defaults alphabetically.
|
|
66
|
+
*/
|
|
67
|
+
function renderConfigTable(
|
|
68
|
+
config: Record<string, unknown>,
|
|
69
|
+
provenance: Record<string, ConfigSource>,
|
|
70
|
+
): string {
|
|
71
|
+
const flat = flattenConfig(config);
|
|
72
|
+
const sourceMap = expandProvenance(flat, provenance);
|
|
73
|
+
|
|
74
|
+
// Show all entries — sorting non-defaults first, then alphabetically
|
|
75
|
+
const rows = flat
|
|
76
|
+
.slice()
|
|
77
|
+
.sort((a, b) => {
|
|
78
|
+
const sa = sourceMap.get(a.key) ?? 'default';
|
|
79
|
+
const sb = sourceMap.get(b.key) ?? 'default';
|
|
80
|
+
// Non-defaults first
|
|
81
|
+
if (sa !== 'default' && sb === 'default') return -1;
|
|
82
|
+
if (sa === 'default' && sb !== 'default') return 1;
|
|
83
|
+
return a.key.localeCompare(b.key);
|
|
84
|
+
})
|
|
85
|
+
.map(({ key, value }) => [key, value, sourceMap.get(key) ?? 'default']);
|
|
86
|
+
|
|
87
|
+
const keyWidth = Math.max(3, ...rows.map((r) => r[0]!.length));
|
|
88
|
+
const valWidth = Math.max(5, ...rows.map((r) => r[1]!.length));
|
|
89
|
+
// Source column is always short ('default', 'user', 'project', 'env')
|
|
90
|
+
const srcWidth = 7;
|
|
91
|
+
|
|
92
|
+
return `${formatTable({
|
|
93
|
+
columns: [
|
|
94
|
+
{ header: 'Key', width: keyWidth },
|
|
95
|
+
{ header: 'Value', width: valWidth },
|
|
96
|
+
{ header: 'Source', width: srcWidth },
|
|
97
|
+
],
|
|
98
|
+
rows: rows as string[][],
|
|
99
|
+
indent: 0,
|
|
100
|
+
})}\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a scaffolded global config JSON file.
|
|
105
|
+
* Produces valid JSON with common sections pre-populated at their defaults.
|
|
106
|
+
* Uses DEFAULTS so the values always reflect the current schema.
|
|
107
|
+
*
|
|
108
|
+
* All keys are optional — users can delete sections they don't need.
|
|
109
|
+
*/
|
|
110
|
+
function buildInitTemplate(): string {
|
|
111
|
+
// Build a plain object — no comments in JSON, but keep it self-explanatory.
|
|
112
|
+
// Unknown top-level keys are silently ignored by mergeConfig.
|
|
113
|
+
const template: Record<string, unknown> = {
|
|
114
|
+
// LLM provider for AI features (codegraph explain, context, etc.)
|
|
115
|
+
// Use apiKeyCommand to pull the key from a secret manager at runtime.
|
|
116
|
+
// Scope to specific repos with:
|
|
117
|
+
// { "appliesTo": ["~/projects/*"], "config": { ... } }
|
|
118
|
+
llm: {
|
|
119
|
+
provider: DEFAULTS.llm.provider,
|
|
120
|
+
model: DEFAULTS.llm.model,
|
|
121
|
+
baseUrl: DEFAULTS.llm.baseUrl,
|
|
122
|
+
apiKey: DEFAULTS.llm.apiKey,
|
|
123
|
+
apiKeyCommand: DEFAULTS.llm.apiKeyCommand,
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
query: {
|
|
127
|
+
defaultDepth: DEFAULTS.query.defaultDepth,
|
|
128
|
+
defaultLimit: DEFAULTS.query.defaultLimit,
|
|
129
|
+
excludeTests: DEFAULTS.query.excludeTests,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
build: {
|
|
133
|
+
incremental: DEFAULTS.build.incremental,
|
|
134
|
+
typescriptResolver: DEFAULTS.build.typescriptResolver,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
ci: {
|
|
138
|
+
failOnCycles: DEFAULTS.ci.failOnCycles,
|
|
139
|
+
impactThreshold: DEFAULTS.ci.impactThreshold,
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
search: {
|
|
143
|
+
defaultMinScore: DEFAULTS.search.defaultMinScore,
|
|
144
|
+
topK: DEFAULTS.search.topK,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return `${JSON.stringify(template, null, 2)}\n`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const command: CommandDefinition = {
|
|
152
|
+
name: 'config',
|
|
153
|
+
description: 'Show or manage codegraph configuration (project + user-level global config)',
|
|
154
|
+
options: [
|
|
155
|
+
['-j, --json', 'Output as JSON'],
|
|
156
|
+
['--explain', 'Show per-key provenance (default / user / project / env)'],
|
|
157
|
+
['--enable-global', 'Record consent to apply the global config to this repo'],
|
|
158
|
+
['--disable-global', 'Record consent to skip the global config for this repo'],
|
|
159
|
+
['--list-global', 'List all repos with a recorded consent decision'],
|
|
160
|
+
[
|
|
161
|
+
'--init',
|
|
162
|
+
'Scaffold a global config file at the default XDG location with all sections pre-populated',
|
|
163
|
+
],
|
|
164
|
+
['--edit', 'Open the global config file in $EDITOR (prints the path if $EDITOR is unset)'],
|
|
165
|
+
],
|
|
166
|
+
execute(_args, opts, ctx) {
|
|
167
|
+
const rootDir = path.resolve('.');
|
|
168
|
+
|
|
169
|
+
// ── Init: scaffold global config ───────────────────────────────────
|
|
170
|
+
|
|
171
|
+
if (opts.init) {
|
|
172
|
+
const targetPath = getDefaultUserConfigPath();
|
|
173
|
+
if (fs.existsSync(targetPath)) {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`Global config already exists at ${targetPath}\n` +
|
|
176
|
+
`Run \`codegraph config --edit\` to open it, or delete it and re-run --init.\n`,
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
181
|
+
fs.writeFileSync(targetPath, buildInitTemplate(), 'utf-8');
|
|
182
|
+
process.stdout.write(`Created global config at ${targetPath}\n`);
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
`Next steps:\n` +
|
|
185
|
+
` 1. Edit the file: codegraph config --edit\n` +
|
|
186
|
+
` 2. Enable it for this repo: codegraph config --enable-global\n`,
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Edit: open global config in $EDITOR ────────────────────────────
|
|
192
|
+
|
|
193
|
+
if (opts.edit) {
|
|
194
|
+
// Prefer the existing file; fall back to the default path so the user
|
|
195
|
+
// can create-and-edit in one step even before running --init.
|
|
196
|
+
const filePath = resolveUserConfigPath() ?? getDefaultUserConfigPath();
|
|
197
|
+
|
|
198
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
199
|
+
if (!editor) {
|
|
200
|
+
process.stdout.write(`${filePath}\n`);
|
|
201
|
+
process.stderr.write(
|
|
202
|
+
`$EDITOR is not set. Set it in your shell profile (e.g. export EDITOR=nano)\n` +
|
|
203
|
+
`or open the file manually at the path printed above.\n`,
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Ensure the directory exists so the editor can create the file
|
|
209
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
210
|
+
|
|
211
|
+
const result = spawnSync(editor, [filePath], { stdio: 'inherit' });
|
|
212
|
+
if (result.error) {
|
|
213
|
+
process.stderr.write(`Failed to launch editor "${editor}": ${result.error.message}\n`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
if (result.status !== 0) {
|
|
217
|
+
process.exit(result.status ?? 1);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Consent management ─────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
if (opts.enableGlobal) {
|
|
225
|
+
setUserConfigConsent(rootDir, 'enabled');
|
|
226
|
+
clearConfigCache();
|
|
227
|
+
const globalPath = resolveUserConfigPath();
|
|
228
|
+
if (!globalPath) {
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
`Consent recorded: "enabled" for ${rootDir}\n` +
|
|
231
|
+
`Note: no global config file found. Create one at ~/.config/codegraph/config.json\n`,
|
|
232
|
+
);
|
|
233
|
+
} else {
|
|
234
|
+
process.stderr.write(
|
|
235
|
+
`Consent recorded: "enabled" for ${rootDir}\n` + `Global config: ${globalPath}\n`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (opts.disableGlobal) {
|
|
242
|
+
setUserConfigConsent(rootDir, 'disabled');
|
|
243
|
+
clearConfigCache();
|
|
244
|
+
process.stderr.write(`Consent recorded: "disabled" for ${rootDir}\n`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (opts.listGlobal) {
|
|
249
|
+
const entries = listUserConfigConsent(REGISTRY_PATH);
|
|
250
|
+
if (opts.json) {
|
|
251
|
+
process.stdout.write(`${JSON.stringify(entries, null, 2)}\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (entries.length === 0) {
|
|
255
|
+
process.stdout.write('No repos have a recorded global-config consent decision.\n');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
process.stdout.write('Global config consent decisions:\n\n');
|
|
259
|
+
for (const { path: p, decision } of entries) {
|
|
260
|
+
process.stdout.write(
|
|
261
|
+
` ${decision === 'enabled' ? '✔' : '✘'} ${decision.padEnd(8)} ${p}\n`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Explain mode ───────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
if (opts.explain) {
|
|
270
|
+
const { config, provenance, appliedGlobalPath, consentDecision } = loadConfigWithProvenance(
|
|
271
|
+
rootDir,
|
|
272
|
+
{
|
|
273
|
+
userConfig: ctx.program.opts().userConfig,
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
const globalPath = resolveUserConfigPath();
|
|
277
|
+
const consent = getUserConfigConsent(rootDir);
|
|
278
|
+
|
|
279
|
+
if (opts.json) {
|
|
280
|
+
process.stdout.write(
|
|
281
|
+
`${JSON.stringify(
|
|
282
|
+
{
|
|
283
|
+
config,
|
|
284
|
+
provenance,
|
|
285
|
+
appliedGlobalPath,
|
|
286
|
+
globalFilePath: globalPath,
|
|
287
|
+
consentDecision: consentDecision ?? consent ?? 'undecided',
|
|
288
|
+
},
|
|
289
|
+
null,
|
|
290
|
+
2,
|
|
291
|
+
)}\n`,
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Human-readable explain output
|
|
297
|
+
process.stdout.write('=== Codegraph config provenance ===\n\n');
|
|
298
|
+
|
|
299
|
+
const consentStr = consentDecision ?? consent ?? 'undecided';
|
|
300
|
+
process.stdout.write(`Global config file : ${globalPath ?? '(none found)'}\n`);
|
|
301
|
+
process.stdout.write(`Applied this run : ${appliedGlobalPath ? 'yes' : 'no'}\n`);
|
|
302
|
+
process.stdout.write(`Consent for repo : ${consentStr}\n`);
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
` (change with \`codegraph config --enable-global\` or \`--disable-global\`)\n`,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (!globalPath) {
|
|
308
|
+
process.stdout.write(
|
|
309
|
+
`\nDiscovery hint: create a global config at ~/.config/codegraph/config.json\n` +
|
|
310
|
+
`then run \`codegraph config --enable-global\` in repos where you want it applied.\n`,
|
|
311
|
+
);
|
|
312
|
+
} else if (!appliedGlobalPath) {
|
|
313
|
+
process.stdout.write(
|
|
314
|
+
`\nDiscovery hint: global config exists but is not applied to this repo.\n` +
|
|
315
|
+
`Run \`codegraph config --enable-global\` to enable it here.\n`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
process.stdout.write('\n--- Per-key provenance ---\n\n');
|
|
320
|
+
const provenanceEntries = Object.entries(provenance).sort(([a], [b]) => a.localeCompare(b));
|
|
321
|
+
for (const [key, source] of provenanceEntries) {
|
|
322
|
+
process.stdout.write(` ${source.padEnd(8)} ${key}\n`);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Default: print effective config ────────────────────────────────
|
|
328
|
+
|
|
329
|
+
const globalPath = resolveUserConfigPath();
|
|
330
|
+
const consent = getUserConfigConsent(rootDir);
|
|
331
|
+
|
|
332
|
+
if (opts.json) {
|
|
333
|
+
const config = loadConfig(rootDir, { userConfig: ctx.program.opts().userConfig });
|
|
334
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
335
|
+
} else {
|
|
336
|
+
// Human-readable table: Key | Value | Source
|
|
337
|
+
const { config, provenance } = loadConfigWithProvenance(rootDir, {
|
|
338
|
+
userConfig: ctx.program.opts().userConfig,
|
|
339
|
+
});
|
|
340
|
+
process.stdout.write(
|
|
341
|
+
renderConfigTable(config as unknown as Record<string, unknown>, provenance),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
if (globalPath && !consent) {
|
|
345
|
+
process.stderr.write(
|
|
346
|
+
`\nℹ Global config found at ${globalPath} — not applied to this repo.\n` +
|
|
347
|
+
` Run \`codegraph config --enable-global\` to opt in, or\n` +
|
|
348
|
+
` \`codegraph config --disable-global\` to dismiss this notice.\n`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
};
|
|
@@ -31,7 +31,7 @@ async function runHotspots(opts: CommandOpts, ctx: CliContext): Promise<void> {
|
|
|
31
31
|
offset: opts.offset ? parseInt(opts.offset as string, 10) : undefined,
|
|
32
32
|
noTests: ctx.resolveNoTests(opts),
|
|
33
33
|
});
|
|
34
|
-
if (!ctx.outputResult(data, '
|
|
34
|
+
if (!ctx.outputResult(data, 'items', opts)) {
|
|
35
35
|
console.log(formatHotspots(data));
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { setUserConfigOverride } from '../infrastructure/config.js';
|
|
5
6
|
import { setVerbose } from '../infrastructure/logger.js';
|
|
6
7
|
import { checkForUpdates, printUpdateNotification } from '../infrastructure/update-check.js';
|
|
7
8
|
import { ConfigError } from '../shared/errors.js';
|
|
@@ -25,9 +26,16 @@ program
|
|
|
25
26
|
.version(pkg.version)
|
|
26
27
|
.option('-v, --verbose', 'Enable verbose/debug output')
|
|
27
28
|
.option('--engine <engine>', 'Parser engine: native, wasm, or auto (default: auto)', 'auto')
|
|
29
|
+
.option('--user-config [path]', 'Apply global user config for this run (optional custom path)')
|
|
30
|
+
.option('--no-user-config', 'Skip global user config for this run')
|
|
28
31
|
.hook('preAction', (thisCommand) => {
|
|
29
32
|
const opts = thisCommand.opts();
|
|
30
33
|
if (opts.verbose) setVerbose(true);
|
|
34
|
+
// Wire user-config flags into the config loader before any command runs.
|
|
35
|
+
// Commander sets opts.userConfig = true (bare flag), a string (path), or undefined.
|
|
36
|
+
// opts.userConfig is false when --no-user-config is passed (Commander negation).
|
|
37
|
+
const uc = opts.userConfig as string | boolean | undefined;
|
|
38
|
+
setUserConfigOverride(uc);
|
|
31
39
|
})
|
|
32
40
|
.hook('postAction', async (_thisCommand, actionCommand) => {
|
|
33
41
|
const name = actionCommand.name();
|
|
@@ -67,6 +75,8 @@ const ctx: CliContext = {
|
|
|
67
75
|
function registerCommand(parent: Command, def: CommandDefinition): Command {
|
|
68
76
|
const cmd = parent.command(def.name).description(def.description);
|
|
69
77
|
|
|
78
|
+
if (def.alias) cmd.alias(def.alias);
|
|
79
|
+
|
|
70
80
|
if (def.queryOpts) applyQueryOpts(cmd);
|
|
71
81
|
|
|
72
82
|
for (const opt of def.options || []) {
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { loadConfig } from '../../infrastructure/config.js';
|
|
3
|
+
import type { CodegraphConfig } from '../../types.js';
|
|
3
4
|
import type { CommandOpts } from '../types.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// Deferred so global --user-config / --no-user-config flags are parsed
|
|
7
|
+
// before config is first accessed (Commander parses flags before any command
|
|
8
|
+
// action runs, but module-level code executes at import time).
|
|
9
|
+
let _config: CodegraphConfig | undefined;
|
|
10
|
+
const config: CodegraphConfig = new Proxy({} as CodegraphConfig, {
|
|
11
|
+
get(_t, prop: string) {
|
|
12
|
+
if (_config === undefined) _config = loadConfig(process.cwd());
|
|
13
|
+
return _config[prop as keyof CodegraphConfig];
|
|
14
|
+
},
|
|
15
|
+
}) as CodegraphConfig;
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Attach the common query options shared by most analysis commands.
|
package/src/cli/types.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface CliContext {
|
|
|
25
25
|
export interface CommandDefinition {
|
|
26
26
|
name: string;
|
|
27
27
|
description: string;
|
|
28
|
+
/** Optional Commander.js alias (e.g. 'explain' for the 'audit' command). */
|
|
29
|
+
alias?: string;
|
|
28
30
|
queryOpts?: boolean;
|
|
29
31
|
options?: Array<[string, string, ...unknown[]]>;
|
|
30
32
|
validate?(args: string[], opts: CommandOpts, ctx: CliContext): string | undefined;
|
package/src/db/migrations.ts
CHANGED
|
@@ -8,7 +8,7 @@ interface Migration {
|
|
|
8
8
|
up: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// IMPORTANT: Migration DDL is mirrored in crates/codegraph-core/src/
|
|
11
|
+
// IMPORTANT: Migration DDL is mirrored in crates/codegraph-core/src/db/connection.rs.
|
|
12
12
|
// Any changes here MUST be reflected there (and vice-versa).
|
|
13
13
|
export const MIGRATIONS: Migration[] = [
|
|
14
14
|
{
|
|
@@ -47,6 +47,22 @@ export function isModuleScopedLanguage(relPath: string): boolean {
|
|
|
47
47
|
|
|
48
48
|
// ── Shared resolution functions ──────────────────────────────────────────
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Callable definition kinds — variable/constant bindings are NOT callable
|
|
52
|
+
* in the function-as-enclosing-scope sense (they are local declarations, not
|
|
53
|
+
* function bodies). Top-level variable bindings (e.g. Haskell `main = do …`)
|
|
54
|
+
* are handled separately as a fallback tier.
|
|
55
|
+
*/
|
|
56
|
+
const CALLABLE_KINDS = new Set(['function', 'method']);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Variable-like binding kinds that may act as top-level callers when no
|
|
60
|
+
* enclosing function/method exists (e.g. Haskell top-level `main` is a
|
|
61
|
+
* `bind` node → kind `variable`). Local variable declarations inside a
|
|
62
|
+
* function body must NOT win over the enclosing function.
|
|
63
|
+
*/
|
|
64
|
+
const TOP_LEVEL_BINDING_KINDS = new Set(['variable', 'constant']);
|
|
65
|
+
|
|
50
66
|
export function findCaller(
|
|
51
67
|
lookup: CallNodeLookup,
|
|
52
68
|
call: { line: number },
|
|
@@ -59,26 +75,63 @@ export function findCaller(
|
|
|
59
75
|
relPath: string,
|
|
60
76
|
fileNodeRow: { id: number },
|
|
61
77
|
): { id: number; callerName: string | null } {
|
|
62
|
-
|
|
63
|
-
let
|
|
64
|
-
let
|
|
78
|
+
// Pass 1: find the narrowest enclosing function/method.
|
|
79
|
+
let fnCaller: { id: number } | null = null;
|
|
80
|
+
let fnCallerName: string | null = null;
|
|
81
|
+
let fnCallerSpan = Infinity;
|
|
82
|
+
|
|
83
|
+
// Pass 2: find the widest (outermost) enclosing variable/constant binding.
|
|
84
|
+
// Used as fallback when no function/method encloses the call site
|
|
85
|
+
// (e.g. Haskell `main = do …` is a `bind` node with kind `variable`).
|
|
86
|
+
// We pick the WIDEST span (outermost binding), not the narrowest, so that
|
|
87
|
+
// nested `let` bindings inside `main`'s do-block do not shadow `main`
|
|
88
|
+
// itself as the attributing caller. The outermost enclosing variable is
|
|
89
|
+
// the "function-like" top-level binding.
|
|
90
|
+
let varCaller: { id: number } | null = null;
|
|
91
|
+
let varCallerName: string | null = null;
|
|
92
|
+
let varCallerSpan = -1; // looking for WIDEST span, so start at -1
|
|
93
|
+
|
|
65
94
|
for (const def of definitions) {
|
|
66
95
|
if (def.line <= call.line) {
|
|
67
|
-
const end = def.endLine
|
|
96
|
+
const end = def.endLine ?? Infinity;
|
|
68
97
|
if (call.line <= end) {
|
|
69
|
-
const span = end - def.line;
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
const span = end === Infinity ? Infinity : end - def.line;
|
|
99
|
+
if (CALLABLE_KINDS.has(def.kind)) {
|
|
100
|
+
if (span < fnCallerSpan) {
|
|
101
|
+
const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
|
|
102
|
+
if (row) {
|
|
103
|
+
fnCaller = row;
|
|
104
|
+
fnCallerName = def.name;
|
|
105
|
+
fnCallerSpan = span;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else if (TOP_LEVEL_BINDING_KINDS.has(def.kind)) {
|
|
109
|
+
if (span > varCallerSpan) {
|
|
110
|
+
const row = lookup.nodeId(def.name, def.kind, relPath, def.line);
|
|
111
|
+
if (row) {
|
|
112
|
+
varCaller = row;
|
|
113
|
+
varCallerName = def.name;
|
|
114
|
+
varCallerSpan = span;
|
|
115
|
+
}
|
|
76
116
|
}
|
|
77
117
|
}
|
|
78
118
|
}
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
|
-
|
|
121
|
+
|
|
122
|
+
// Prefer function/method enclosing scope over variable binding.
|
|
123
|
+
// If a function/method encloses the call, use it — local variable
|
|
124
|
+
// declarations inside the function body must not shadow it.
|
|
125
|
+
// Only fall back to a variable/constant binding when the call is at
|
|
126
|
+
// top-level scope (no enclosing function/method found), which handles
|
|
127
|
+
// languages like Haskell where `main` is a top-level `bind` node.
|
|
128
|
+
if (fnCaller) {
|
|
129
|
+
return { ...fnCaller, callerName: fnCallerName };
|
|
130
|
+
}
|
|
131
|
+
if (varCaller) {
|
|
132
|
+
return { ...varCaller, callerName: varCallerName };
|
|
133
|
+
}
|
|
134
|
+
return { ...fileNodeRow, callerName: null };
|
|
82
135
|
}
|
|
83
136
|
|
|
84
137
|
export function resolveByMethodOrGlobal(
|
|
@@ -94,22 +147,25 @@ export function resolveByMethodOrGlobal(
|
|
|
94
147
|
const effectiveReceiver = call.receiver.startsWith('this.')
|
|
95
148
|
? call.receiver.slice('this.'.length)
|
|
96
149
|
: call.receiver;
|
|
97
|
-
// For this.prop receivers,
|
|
98
|
-
// handlePropWriteTypeMap — prevents false edges when multiple
|
|
99
|
-
// property name (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// same-name rest-binding collision across functions in the same file (#1358).
|
|
105
|
-
(callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
|
|
106
|
-
if (!typeEntry && call.receiver.startsWith('this.') && callerName) {
|
|
150
|
+
// For this.prop receivers, prefer the class-scoped key (ClassName.prop) seeded by
|
|
151
|
+
// handlePropWriteTypeMap / handleFieldDefTypeMap — prevents false edges when multiple
|
|
152
|
+
// classes define the same property name (issues #1323, #1458).
|
|
153
|
+
// Class-scoped lookup runs first so bare fallback keys (confidence 0.6) don't shadow
|
|
154
|
+
// the correct per-class entry when callerName is available.
|
|
155
|
+
let typeEntry: unknown;
|
|
156
|
+
if (call.receiver.startsWith('this.') && callerName) {
|
|
107
157
|
const dotIdx = callerName.lastIndexOf('.');
|
|
108
158
|
if (dotIdx > -1) {
|
|
109
159
|
const callerClass = callerName.slice(0, dotIdx);
|
|
110
160
|
typeEntry = typeMap.get(`${callerClass}.${effectiveReceiver}`);
|
|
111
161
|
}
|
|
112
162
|
}
|
|
163
|
+
typeEntry ??=
|
|
164
|
+
typeMap.get(effectiveReceiver) ??
|
|
165
|
+
typeMap.get(call.receiver) ??
|
|
166
|
+
// Phase 8.3f: callee-scoped rest-param key (`callee::restName`) to avoid
|
|
167
|
+
// same-name rest-binding collision across functions in the same file (#1358).
|
|
168
|
+
(callerName ? typeMap.get(`${callerName}::${effectiveReceiver}`) : undefined);
|
|
113
169
|
let typeName = typeEntry
|
|
114
170
|
? typeof typeEntry === 'string'
|
|
115
171
|
? typeEntry
|
|
@@ -299,13 +355,17 @@ export function resolveCallTargets(
|
|
|
299
355
|
* Returns the edge tuple to insert, or null if nothing matched or the edge
|
|
300
356
|
* was already seen. Callers are responsible for the actual DB/array insert.
|
|
301
357
|
*
|
|
302
|
-
* Receiver resolution
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
358
|
+
* Receiver resolution:
|
|
359
|
+
* 1. Look up same-file nodes for `effectiveReceiver` (unfiltered by kind).
|
|
360
|
+
* 2. If any same-file node exists AND `effectiveReceiver` is not in `importedNames`
|
|
361
|
+
* (i.e. it is a locally-defined symbol, not an import artifact), apply
|
|
362
|
+
* RECEIVER_KINDS and return the filtered set — no global fallback.
|
|
363
|
+
* A local `function C(){}` means this file owns `C`; no cross-file class
|
|
364
|
+
* should win over it (issue #1539).
|
|
365
|
+
* 3. If the same-file node IS an import artifact (e.g. destructured require),
|
|
366
|
+
* or no same-file node exists at all, fall back to global candidates filtered
|
|
367
|
+
* by RECEIVER_KINDS. This preserves the pre-#1539 behaviour for cases where
|
|
368
|
+
* an imported name appears as kind='function' in the importer file.
|
|
309
369
|
*/
|
|
310
370
|
export function resolveReceiverEdge(
|
|
311
371
|
lookup: CallNodeLookup,
|
|
@@ -314,6 +374,7 @@ export function resolveReceiverEdge(
|
|
|
314
374
|
relPath: string,
|
|
315
375
|
typeMap: Map<string, unknown>,
|
|
316
376
|
seenCallEdges: Set<string>,
|
|
377
|
+
importedNames: ReadonlyMap<string, string>,
|
|
317
378
|
): { callerId: number; receiverId: number; confidence: number } | null {
|
|
318
379
|
const typeEntry = typeMap.get(call.receiver);
|
|
319
380
|
const typeName = typeEntry
|
|
@@ -326,18 +387,15 @@ export function resolveReceiverEdge(
|
|
|
326
387
|
? ((typeEntry as { confidence?: number }).confidence ?? null)
|
|
327
388
|
: null;
|
|
328
389
|
const effectiveReceiver = typeName || call.receiver;
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const sameFileCandidates =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
sameFileCandidates.length > 0
|
|
339
|
-
? sameFileCandidates
|
|
340
|
-
: lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
390
|
+
// Block global fallback only when the same-file node is a local definition,
|
|
391
|
+
// not when it's an import artifact (e.g. `const { C } = require(…)` seeds a
|
|
392
|
+
// kind='function' node in the importer but the real class lives elsewhere).
|
|
393
|
+
const sameFileAll = lookup.byNameAndFile(effectiveReceiver, relPath);
|
|
394
|
+
const isLocalDefinition = sameFileAll.length > 0 && !importedNames?.has(effectiveReceiver);
|
|
395
|
+
const sameFileCandidates = sameFileAll.filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
396
|
+
const candidates = isLocalDefinition
|
|
397
|
+
? sameFileCandidates
|
|
398
|
+
: lookup.byName(effectiveReceiver).filter((n) => RECEIVER_KINDS.has(n.kind ?? ''));
|
|
341
399
|
if (candidates.length === 0) return null;
|
|
342
400
|
const recvTarget = candidates[0]!;
|
|
343
401
|
const recvKey = `recv|${caller.id}|${recvTarget.id}`;
|
|
@@ -96,6 +96,14 @@ export function buildChaContext(fileSymbols: ReadonlyMap<string, ExtractorOutput
|
|
|
96
96
|
* For `super`, resolution starts from the parent of the caller's class.
|
|
97
97
|
* For `this`/`self`, resolution starts from the caller's own class and walks
|
|
98
98
|
* up the inheritance chain (supporting inherited method lookup).
|
|
99
|
+
*
|
|
100
|
+
* When `callerFile` is provided, same-file method nodes are preferred: if the
|
|
101
|
+
* hierarchy walk finds a qualified method that exists in both the caller's own
|
|
102
|
+
* file AND in unrelated files (e.g. a class named `A` that appears in multiple
|
|
103
|
+
* fixture files), only the same-file nodes are returned. This prevents
|
|
104
|
+
* cross-fixture false edges caused by accidental name collisions across
|
|
105
|
+
* unrelated files in the same project build. When no same-file nodes exist,
|
|
106
|
+
* all found nodes are returned as before.
|
|
99
107
|
*/
|
|
100
108
|
export function resolveThisDispatch(
|
|
101
109
|
methodName: string,
|
|
@@ -103,6 +111,7 @@ export function resolveThisDispatch(
|
|
|
103
111
|
receiver: 'this' | 'self' | 'super',
|
|
104
112
|
chaCtx: ChaContext,
|
|
105
113
|
lookup: CallNodeLookup,
|
|
114
|
+
callerFile?: string | null,
|
|
106
115
|
): ReadonlyArray<{ id: number; file: string }> {
|
|
107
116
|
if (!callerName) return [];
|
|
108
117
|
const dotIdx = callerName.indexOf('.');
|
|
@@ -119,7 +128,15 @@ export function resolveThisDispatch(
|
|
|
119
128
|
visited.add(current);
|
|
120
129
|
const qualified = `${current}.${methodName}`;
|
|
121
130
|
const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
|
|
122
|
-
if (found.length > 0)
|
|
131
|
+
if (found.length > 0) {
|
|
132
|
+
// When the caller's file is known, prefer same-file nodes to avoid
|
|
133
|
+
// emitting cross-file edges to identically-named methods in unrelated
|
|
134
|
+
// files. Only fall back to the full set when no same-file node exists.
|
|
135
|
+
if (callerFile && found.some((n) => n.file === callerFile)) {
|
|
136
|
+
return found.filter((n) => n.file === callerFile);
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
123
140
|
current = chaCtx.parents.get(current);
|
|
124
141
|
}
|
|
125
142
|
return [];
|