gitnexus 1.6.6-rc.59 → 1.6.6-rc.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli/i18n/en.d.ts +1 -1
  2. package/dist/cli/i18n/en.js +1 -1
  3. package/dist/cli/i18n/resources.d.ts +1 -1
  4. package/dist/cli/i18n/zh-CN.js +1 -1
  5. package/dist/cli/index.js +1 -1
  6. package/dist/cli/wiki.js +57 -19
  7. package/dist/core/ingestion/languages/rust/arity.d.ts +2 -0
  8. package/dist/core/ingestion/languages/rust/arity.js +13 -0
  9. package/dist/core/ingestion/languages/rust/cache-stats.d.ts +7 -0
  10. package/dist/core/ingestion/languages/rust/cache-stats.js +15 -0
  11. package/dist/core/ingestion/languages/rust/captures.d.ts +2 -0
  12. package/dist/core/ingestion/languages/rust/captures.js +179 -0
  13. package/dist/core/ingestion/languages/rust/import-decomposer.d.ts +8 -0
  14. package/dist/core/ingestion/languages/rust/import-decomposer.js +164 -0
  15. package/dist/core/ingestion/languages/rust/import-target.d.ts +14 -0
  16. package/dist/core/ingestion/languages/rust/import-target.js +105 -0
  17. package/dist/core/ingestion/languages/rust/index.d.ts +12 -0
  18. package/dist/core/ingestion/languages/rust/index.js +12 -0
  19. package/dist/core/ingestion/languages/rust/interpret.d.ts +4 -0
  20. package/dist/core/ingestion/languages/rust/interpret.js +178 -0
  21. package/dist/core/ingestion/languages/rust/merge-bindings.d.ts +2 -0
  22. package/dist/core/ingestion/languages/rust/merge-bindings.js +18 -0
  23. package/dist/core/ingestion/languages/rust/method-owners.d.ts +19 -0
  24. package/dist/core/ingestion/languages/rust/method-owners.js +63 -0
  25. package/dist/core/ingestion/languages/rust/query.d.ts +3 -0
  26. package/dist/core/ingestion/languages/rust/query.js +145 -0
  27. package/dist/core/ingestion/languages/rust/range-binding.d.ts +18 -0
  28. package/dist/core/ingestion/languages/rust/range-binding.js +570 -0
  29. package/dist/core/ingestion/languages/rust/receiver-binding.d.ts +20 -0
  30. package/dist/core/ingestion/languages/rust/receiver-binding.js +134 -0
  31. package/dist/core/ingestion/languages/rust/scope-resolver.d.ts +2 -0
  32. package/dist/core/ingestion/languages/rust/scope-resolver.js +62 -0
  33. package/dist/core/ingestion/languages/rust/simple-hooks.d.ts +8 -0
  34. package/dist/core/ingestion/languages/rust/simple-hooks.js +27 -0
  35. package/dist/core/ingestion/languages/rust.js +9 -0
  36. package/dist/core/ingestion/registry-primary-flag.js +1 -0
  37. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  38. package/dist/core/wiki/generator.d.ts +1 -1
  39. package/dist/core/wiki/generator.js +12 -1
  40. package/dist/core/wiki/llm-client.d.ts +2 -2
  41. package/dist/core/wiki/llm-client.js +13 -5
  42. package/dist/core/wiki/local-cli-client.d.ts +17 -0
  43. package/dist/core/wiki/local-cli-client.js +270 -0
  44. package/dist/storage/repo-manager.d.ts +3 -1
  45. package/package.json +1 -1
  46. package/scripts/cross-platform-tests.ts +1 -0
@@ -155,7 +155,7 @@ export declare const en: {
155
155
  readonly 'help.option.clean.all': "Clean all indexed repos";
156
156
  readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
157
157
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
158
- readonly 'help.option.wiki.provider': "LLM provider: openai or cursor (default: openai)";
158
+ readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)";
159
159
  readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
160
160
  readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
161
161
  readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
@@ -155,7 +155,7 @@ export const en = {
155
155
  'help.option.clean.all': 'Clean all indexed repos',
156
156
  'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
157
157
  'help.option.wiki.force': 'Force full regeneration even if up to date',
158
- 'help.option.wiki.provider': 'LLM provider: openai or cursor (default: openai)',
158
+ 'help.option.wiki.provider': 'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
159
159
  'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
160
160
  'help.option.wiki.baseUrl': 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
161
161
  'help.option.wiki.apiKey': 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)',
@@ -156,7 +156,7 @@ export declare const cliResources: {
156
156
  readonly 'help.option.clean.all': "Clean all indexed repos";
157
157
  readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
158
158
  readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
159
- readonly 'help.option.wiki.provider': "LLM provider: openai or cursor (default: openai)";
159
+ readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)";
160
160
  readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
161
161
  readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
162
162
  readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
@@ -155,7 +155,7 @@ export const zhCN = {
155
155
  'help.option.clean.all': '清理所有已索引仓库',
156
156
  'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
157
157
  'help.option.wiki.force': '即使已是最新也强制完整重新生成',
158
- 'help.option.wiki.provider': 'LLM 提供商:openai 或 cursor(默认:openai)',
158
+ 'help.option.wiki.provider': 'LLM 提供商:openai、openrouter、azure、custom、cursor、claudecodex(默认:openai)',
159
159
  'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
160
160
  'help.option.wiki.baseUrl': 'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
161
161
  'help.option.wiki.apiKey': 'LLM API key 或 Azure api-key(保存到 ~/.gitnexus/config.json)',
package/dist/cli/index.js CHANGED
@@ -94,7 +94,7 @@ program
94
94
  .command('wiki [path]')
95
95
  .description('Generate repository wiki from knowledge graph')
96
96
  .option('-f, --force', 'Force full regeneration even if up to date')
97
- .option('--provider <provider>', 'LLM provider: openai or cursor (default: openai)')
97
+ .option('--provider <provider>', 'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)')
98
98
  .option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
99
99
  .option('--base-url <url>', 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1')
100
100
  .option('--api-key <key>', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)')
package/dist/cli/wiki.js CHANGED
@@ -13,6 +13,7 @@ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig, } from '../sto
13
13
  import { WikiGenerator } from '../core/wiki/generator.js';
14
14
  import { resolveLLMConfig } from '../core/wiki/llm-client.js';
15
15
  import { detectCursorCLI } from '../core/wiki/cursor-client.js';
16
+ import { detectLocalCLI } from '../core/wiki/local-cli-client.js';
16
17
  import { logger } from '../core/logger.js';
17
18
  function parsePositiveIntegerOption(value, flag, multiplier = 1) {
18
19
  if (value === undefined)
@@ -27,6 +28,16 @@ function parsePositiveIntegerOption(value, flag, multiplier = 1) {
27
28
  }
28
29
  return parsed;
29
30
  }
31
+ function isLocalProvider(provider) {
32
+ return provider === 'cursor' || provider === 'claude' || provider === 'codex';
33
+ }
34
+ function localModelConfigKey(provider) {
35
+ if (provider === 'cursor')
36
+ return 'cursorModel';
37
+ if (provider === 'claude')
38
+ return 'claudeModel';
39
+ return 'codexModel';
40
+ }
30
41
  /**
31
42
  * Prompt the user for input via stdin.
32
43
  */
@@ -163,10 +174,11 @@ const wikiCommandImpl = async (inputPath, options) => {
163
174
  updates.apiVersion = options.apiVersion;
164
175
  if (options.reasoningModel !== undefined)
165
176
  updates.isReasoningModel = options.reasoningModel;
166
- // Save model to appropriate field based on provider
177
+ // Save model to appropriate field based on provider.
167
178
  if (options.model) {
168
- if (options.provider === 'cursor') {
169
- updates.cursorModel = options.model;
179
+ const targetProvider = options.provider ?? existing.provider;
180
+ if (isLocalProvider(targetProvider)) {
181
+ updates[localModelConfigKey(targetProvider)] = options.model;
170
182
  }
171
183
  else {
172
184
  updates.model = options.model;
@@ -176,7 +188,7 @@ const wikiCommandImpl = async (inputPath, options) => {
176
188
  console.log(' Config saved to ~/.gitnexus/config.json\n');
177
189
  }
178
190
  const savedConfig = await loadCLIConfig();
179
- const hasSavedConfig = !!(savedConfig.provider === 'cursor' ||
191
+ const hasSavedConfig = !!(isLocalProvider(savedConfig.provider) ||
180
192
  (savedConfig.apiKey && savedConfig.baseUrl));
181
193
  const hasCLIOverrides = !!(options?.apiKey ||
182
194
  options?.model ||
@@ -197,10 +209,10 @@ const wikiCommandImpl = async (inputPath, options) => {
197
209
  if (!hasSavedConfig && !hasCLIOverrides) {
198
210
  if (!process.stdin.isTTY) {
199
211
  // Non-interactive mode — need either API key or Cursor CLI
200
- if (!llmConfig.apiKey && llmConfig.provider !== 'cursor') {
212
+ if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
201
213
  console.log(' Error: No LLM API key found.');
202
214
  console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
203
- console.log(' or pass --api-key <key>, or use --provider cursor.\n');
215
+ console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex.\n');
204
216
  process.exitCode = 1;
205
217
  return;
206
218
  }
@@ -208,37 +220,62 @@ const wikiCommandImpl = async (inputPath, options) => {
208
220
  }
209
221
  else {
210
222
  console.log(" No LLM configured. Let's set it up.\n");
211
- console.log(' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, or Cursor CLI.\n');
212
- // Check if Cursor CLI is available
223
+ console.log(' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, or Codex CLI.\n');
224
+ // Check if local agent CLIs are available.
213
225
  const hasCursor = detectCursorCLI();
226
+ const hasClaude = detectLocalCLI('claude');
227
+ const hasCodex = detectLocalCLI('codex');
228
+ const localChoices = [];
214
229
  // Provider selection
215
230
  console.log(' [1] OpenAI (api.openai.com)');
216
231
  console.log(' [2] OpenRouter (openrouter.ai)');
217
232
  console.log(' [3] Azure OpenAI');
218
233
  console.log(' [4] Custom endpoint');
234
+ let nextChoice = 5;
219
235
  if (hasCursor) {
220
- console.log(' [5] Cursor CLI (local, uses your Cursor subscription)');
236
+ const choice = String(nextChoice++);
237
+ localChoices.push({
238
+ choice,
239
+ provider: 'cursor',
240
+ });
241
+ console.log(` [${choice}] Cursor CLI (local, uses your Cursor subscription)`);
242
+ }
243
+ if (hasClaude) {
244
+ const choice = String(nextChoice++);
245
+ localChoices.push({
246
+ choice,
247
+ provider: 'claude',
248
+ });
249
+ console.log(` [${choice}] Claude CLI (local, uses your Claude Code login)`);
250
+ }
251
+ if (hasCodex) {
252
+ const choice = String(nextChoice++);
253
+ localChoices.push({
254
+ choice,
255
+ provider: 'codex',
256
+ });
257
+ console.log(` [${choice}] Codex CLI (local, uses your Codex login)`);
221
258
  }
222
259
  console.log('');
223
- const maxChoice = hasCursor ? '5' : '4';
260
+ const maxChoice = String(nextChoice - 1);
224
261
  const choice = await prompt(` Select provider (1/${maxChoice}): `);
225
262
  let baseUrl;
226
263
  let defaultModel;
227
264
  let provider = 'openai';
228
265
  let key = '';
229
- if (choice === '5' && hasCursor) {
230
- // Cursor CLI selected - model defaults to 'auto' (Cursor's default)
231
- provider = 'cursor';
266
+ const selectedLocal = localChoices.find((item) => item.choice === choice);
267
+ if (selectedLocal) {
268
+ // Local CLI selected - model defaults to the CLI's configured default.
269
+ provider = selectedLocal.provider;
232
270
  baseUrl = '';
233
- const modelInput = await prompt(' Model (leave empty for auto): ');
271
+ const modelInput = await prompt(' Model (leave empty for CLI default): ');
234
272
  const model = modelInput || '';
235
- // Save config for Cursor
236
- const cursorConfig = { provider: 'cursor' };
273
+ const localConfig = { ...savedConfig, provider };
237
274
  if (model)
238
- cursorConfig.cursorModel = model;
239
- await saveCLIConfig(cursorConfig);
275
+ localConfig[localModelConfigKey(provider)] = model;
276
+ await saveCLIConfig(localConfig);
240
277
  console.log(' Config saved to ~/.gitnexus/config.json\n');
241
- llmConfig = { ...llmConfig, provider: 'cursor', model, apiKey: '', baseUrl: '' };
278
+ llmConfig = { ...llmConfig, provider, model, apiKey: '', baseUrl: '' };
242
279
  }
243
280
  else if (choice === '3') {
244
281
  // Azure OpenAI guided setup — minimal prompts
@@ -281,6 +318,7 @@ const wikiCommandImpl = async (inputPath, options) => {
281
318
  // Always use v1 API format — no need for api-version
282
319
  const azureBaseUrl = `${endpoint}/openai/v1`;
283
320
  await saveCLIConfig({
321
+ ...savedConfig,
284
322
  apiKey: azureKey,
285
323
  baseUrl: azureBaseUrl,
286
324
  model: deploymentName,
@@ -0,0 +1,2 @@
1
+ import type { Callsite, SymbolDefinition } from '../../../../_shared/index.js';
2
+ export declare function rustArityCompatibility(def: SymbolDefinition, callsite: Callsite): 'compatible' | 'unknown' | 'incompatible';
@@ -0,0 +1,13 @@
1
+ export function rustArityCompatibility(def, callsite) {
2
+ const max = def.parameterCount;
3
+ const min = def.requiredParameterCount;
4
+ if (max === undefined && min === undefined)
5
+ return 'unknown';
6
+ if (!Number.isFinite(callsite.arity) || callsite.arity < 0)
7
+ return 'unknown';
8
+ if (min !== undefined && callsite.arity < min)
9
+ return 'incompatible';
10
+ if (max !== undefined && callsite.arity > max)
11
+ return 'incompatible';
12
+ return 'compatible';
13
+ }
@@ -0,0 +1,7 @@
1
+ export declare function recordRustCacheHit(): void;
2
+ export declare function recordRustCacheMiss(): void;
3
+ export declare function getRustCaptureCacheStats(): {
4
+ readonly hits: number;
5
+ readonly misses: number;
6
+ };
7
+ export declare function resetRustCaptureCacheStats(): void;
@@ -0,0 +1,15 @@
1
+ let hits = 0;
2
+ let misses = 0;
3
+ export function recordRustCacheHit() {
4
+ hits++;
5
+ }
6
+ export function recordRustCacheMiss() {
7
+ misses++;
8
+ }
9
+ export function getRustCaptureCacheStats() {
10
+ return { hits, misses };
11
+ }
12
+ export function resetRustCaptureCacheStats() {
13
+ hits = 0;
14
+ misses = 0;
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ export declare function emitRustScopeCaptures(sourceText: string, _filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
@@ -0,0 +1,179 @@
1
+ import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
2
+ import { getRustParser, getRustScopeQuery } from './query.js';
3
+ import { recordRustCacheHit, recordRustCacheMiss } from './cache-stats.js';
4
+ import { splitRustUseDeclaration } from './import-decomposer.js';
5
+ import { synthesizeRustReceiverBinding } from './receiver-binding.js';
6
+ import { getTreeSitterBufferSize } from '../../constants.js';
7
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
8
+ export function emitRustScopeCaptures(sourceText, _filePath, cachedTree) {
9
+ let tree = cachedTree;
10
+ if (tree === undefined) {
11
+ tree = parseSourceSafe(getRustParser(), sourceText, undefined, {
12
+ bufferSize: getTreeSitterBufferSize(sourceText),
13
+ });
14
+ recordRustCacheMiss();
15
+ }
16
+ else {
17
+ recordRustCacheHit();
18
+ }
19
+ const rawMatches = getRustScopeQuery().matches(tree.rootNode);
20
+ const out = [];
21
+ for (const m of rawMatches) {
22
+ const grouped = {};
23
+ for (const c of m.captures) {
24
+ const tag = '@' + c.name;
25
+ if (tag.startsWith('@_'))
26
+ continue;
27
+ grouped[tag] = nodeToCapture(tag, c.node);
28
+ }
29
+ if (Object.keys(grouped).length === 0)
30
+ continue;
31
+ // Decompose use declarations into individual import captures
32
+ if (grouped['@import.statement'] !== undefined) {
33
+ const anchor = grouped['@import.statement'];
34
+ const useNode = findNodeAtRange(tree.rootNode, anchor.range, 'use_declaration');
35
+ if (useNode !== null) {
36
+ out.push(...splitRustUseDeclaration(useNode));
37
+ continue;
38
+ }
39
+ }
40
+ // Synthesize self receiver bindings for methods inside impl blocks
41
+ let cachedImplLookup;
42
+ if (grouped['@scope.function'] !== undefined) {
43
+ const scopeCap = grouped['@scope.function'];
44
+ const fnNode = findNodeAtRange(tree.rootNode, scopeCap.range, 'function_item');
45
+ if (fnNode !== null) {
46
+ const implNode = findEnclosingImpl(fnNode);
47
+ cachedImplLookup = { fnNode, implNode };
48
+ const receiver = synthesizeRustReceiverBinding(fnNode, implNode);
49
+ if (receiver !== null)
50
+ out.push(receiver);
51
+ }
52
+ }
53
+ // Attach declaration arity for functions/methods
54
+ const declAnchor = grouped['@declaration.function'];
55
+ if (declAnchor !== undefined) {
56
+ const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_item');
57
+ if (fnNode !== null) {
58
+ const implNode = cachedImplLookup?.fnNode === fnNode
59
+ ? cachedImplLookup.implNode
60
+ : findEnclosingImpl(fnNode);
61
+ const traitNode = implNode === null ? findEnclosingTrait(fnNode) : null;
62
+ // Reclassify as method if inside an impl block or trait definition
63
+ if (implNode !== null || traitNode !== null) {
64
+ const nameCap = grouped['@declaration.name'];
65
+ delete grouped['@declaration.function'];
66
+ grouped['@declaration.method'] = syntheticCapture('@declaration.method', fnNode, fnNode.text);
67
+ if (nameCap !== undefined) {
68
+ grouped['@declaration.name'] = nameCap;
69
+ }
70
+ }
71
+ const arity = computeRustDeclarationArity(fnNode);
72
+ if (arity.parameterCount !== undefined) {
73
+ grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
74
+ }
75
+ if (arity.requiredParameterCount !== undefined) {
76
+ grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
77
+ }
78
+ }
79
+ }
80
+ // Hoist return-type bindings from impl block functions to module level.
81
+ // The auto-hoist in the scope-extractor places a type binding whose
82
+ // anchor matches its innermost scope on the parent scope. By using the
83
+ // impl_item node as the anchor (which matches the impl's Class scope),
84
+ // the binding lands on the Module scope — making it visible to the
85
+ // compound receiver's hoistTypeBindingsToModule walk.
86
+ if (grouped['@type-binding.return'] !== undefined &&
87
+ grouped['@type-binding.name'] !== undefined) {
88
+ const tbReturnAnchor = grouped['@type-binding.return'];
89
+ const fnNode = findNodeAtRange(tree.rootNode, tbReturnAnchor.range, 'function_item');
90
+ if (fnNode !== null) {
91
+ const implNode = findEnclosingImpl(fnNode);
92
+ if (implNode !== null) {
93
+ out.push({
94
+ '@type-binding.name': syntheticCapture('@type-binding.name', implNode, grouped['@type-binding.name'].text),
95
+ '@type-binding.type': syntheticCapture('@type-binding.type', implNode, grouped['@type-binding.type'].text),
96
+ '@type-binding.return': syntheticCapture('@type-binding.return', implNode, tbReturnAnchor.text),
97
+ });
98
+ }
99
+ }
100
+ }
101
+ // Attach call arity for call expressions
102
+ const callAnchor = grouped['@reference.call.free'] ??
103
+ grouped['@reference.call.member'] ??
104
+ grouped['@reference.call.constructor'];
105
+ if (callAnchor !== undefined) {
106
+ const callNode = findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
107
+ findNodeAtRange(tree.rootNode, callAnchor.range, 'struct_expression');
108
+ if (callNode !== null) {
109
+ const arity = computeRustCallArity(callNode);
110
+ grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(arity));
111
+ }
112
+ }
113
+ out.push(grouped);
114
+ }
115
+ return out;
116
+ }
117
+ function findEnclosingImpl(node) {
118
+ let current = node.parent;
119
+ while (current !== null) {
120
+ if (current.type === 'impl_item')
121
+ return current;
122
+ if (current.type === 'source_file' || current.type === 'mod_item')
123
+ return null;
124
+ current = current.parent;
125
+ }
126
+ return null;
127
+ }
128
+ function findEnclosingTrait(node) {
129
+ let current = node.parent;
130
+ while (current !== null) {
131
+ if (current.type === 'trait_item')
132
+ return current;
133
+ if (current.type === 'source_file' || current.type === 'mod_item')
134
+ return null;
135
+ current = current.parent;
136
+ }
137
+ return null;
138
+ }
139
+ function computeRustDeclarationArity(fnNode) {
140
+ const params = fnNode.childForFieldName('parameters');
141
+ if (params === null)
142
+ return {};
143
+ let count = 0;
144
+ for (let i = 0; i < params.namedChildCount; i++) {
145
+ const child = params.namedChild(i);
146
+ if (child === null)
147
+ continue;
148
+ if (child.type === 'self_parameter')
149
+ continue;
150
+ if (child.type === 'parameter')
151
+ count++;
152
+ }
153
+ // Rust has no default parameters or overloading
154
+ return { parameterCount: count, requiredParameterCount: count };
155
+ }
156
+ function computeRustCallArity(callNode) {
157
+ if (callNode.type === 'struct_expression') {
158
+ const body = callNode.childForFieldName('body');
159
+ if (body === null)
160
+ return 0;
161
+ let count = 0;
162
+ for (let i = 0; i < body.namedChildCount; i++) {
163
+ const t = body.namedChild(i)?.type;
164
+ if (t === 'field_initializer' || t === 'shorthand_field_initializer')
165
+ count++;
166
+ }
167
+ return count;
168
+ }
169
+ const args = callNode.childForFieldName('arguments');
170
+ if (args === null)
171
+ return 0;
172
+ let count = 0;
173
+ for (let i = 0; i < args.namedChildCount; i++) {
174
+ const child = args.namedChild(i);
175
+ if (child !== null)
176
+ count++;
177
+ }
178
+ return count;
179
+ }
@@ -0,0 +1,8 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
3
+ /**
4
+ * Decompose a Rust `use_declaration` into individual import captures.
5
+ * Handles simple paths, grouped imports ({A, B}), wildcards (*),
6
+ * renames (as), and `pub use` re-exports.
7
+ */
8
+ export declare function splitRustUseDeclaration(node: SyntaxNode): CaptureMatch[];
@@ -0,0 +1,164 @@
1
+ import { syntheticCapture } from '../../utils/ast-helpers.js';
2
+ /**
3
+ * Decompose a Rust `use_declaration` into individual import captures.
4
+ * Handles simple paths, grouped imports ({A, B}), wildcards (*),
5
+ * renames (as), and `pub use` re-exports.
6
+ */
7
+ export function splitRustUseDeclaration(node) {
8
+ if (node.type !== 'use_declaration')
9
+ return [];
10
+ const isReexport = hasVisibilityModifier(node);
11
+ const argument = getUseArgument(node);
12
+ if (argument === null)
13
+ return [];
14
+ return decomposeUseArgument(argument, '', isReexport, node);
15
+ }
16
+ function hasVisibilityModifier(node) {
17
+ for (let i = 0; i < node.childCount; i++) {
18
+ if (node.child(i)?.type === 'visibility_modifier')
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ function getUseArgument(node) {
24
+ for (let i = 0; i < node.childCount; i++) {
25
+ const child = node.child(i);
26
+ if (child === null)
27
+ continue;
28
+ if (child.type === 'scoped_identifier' ||
29
+ child.type === 'scoped_use_list' ||
30
+ child.type === 'use_wildcard' ||
31
+ child.type === 'use_as_clause' ||
32
+ child.type === 'identifier' ||
33
+ child.type === 'use_list') {
34
+ return child;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function decomposeUseArgument(node, prefixPath, isReexport, anchor) {
40
+ switch (node.type) {
41
+ case 'scoped_identifier': {
42
+ const path = buildScopedPath(node);
43
+ const segments = path.split('::');
44
+ const name = segments[segments.length - 1];
45
+ return [
46
+ makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, path), name, undefined),
47
+ ];
48
+ }
49
+ case 'scoped_use_list': {
50
+ const pathNode = node.childForFieldName('path');
51
+ const listNode = node.childForFieldName('list');
52
+ const pathStr = pathNode ? buildNodePath(pathNode) : '';
53
+ const fullPrefix = joinPaths(prefixPath, pathStr);
54
+ if (listNode === null)
55
+ return [];
56
+ return decomposeUseList(listNode, fullPrefix, isReexport, anchor);
57
+ }
58
+ case 'use_list': {
59
+ return decomposeUseList(node, prefixPath, isReexport, anchor);
60
+ }
61
+ case 'use_wildcard': {
62
+ const wcPath = buildWildcardPath(node);
63
+ return [makeImportCapture(anchor, 'wildcard', joinPaths(prefixPath, wcPath), '*', undefined)];
64
+ }
65
+ case 'use_as_clause': {
66
+ const pathChild = node.childForFieldName('path');
67
+ const aliasChild = node.childForFieldName('alias');
68
+ if (pathChild === null || aliasChild === null)
69
+ return [];
70
+ const originalName = pathChild.type === 'scoped_identifier' ? buildScopedPath(pathChild) : pathChild.text;
71
+ const aliasName = aliasChild.text;
72
+ const segments = originalName.split('::');
73
+ const importedName = segments[segments.length - 1];
74
+ return [
75
+ makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, originalName), importedName, aliasName),
76
+ ];
77
+ }
78
+ case 'identifier': {
79
+ return [
80
+ makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, node.text), node.text, undefined),
81
+ ];
82
+ }
83
+ default:
84
+ return [];
85
+ }
86
+ }
87
+ function decomposeUseList(listNode, prefix, isReexport, anchor) {
88
+ const out = [];
89
+ for (let i = 0; i < listNode.namedChildCount; i++) {
90
+ const child = listNode.namedChild(i);
91
+ if (child === null)
92
+ continue;
93
+ if (child.type === 'self') {
94
+ // `use crate::models::{self}` — imports the module itself
95
+ const segments = prefix.split('::').filter(Boolean);
96
+ const name = segments[segments.length - 1] ?? 'self';
97
+ out.push(makeImportCapture(anchor, 'namespace', prefix, name, undefined));
98
+ }
99
+ else {
100
+ out.push(...decomposeUseArgument(child, prefix, isReexport, anchor));
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ function buildScopedPath(node) {
106
+ if (node.type === 'scoped_identifier') {
107
+ const parts = [];
108
+ collectScopedParts(node, parts);
109
+ return parts.join('::');
110
+ }
111
+ return node.text;
112
+ }
113
+ function collectScopedParts(node, parts) {
114
+ if (node.type === 'scoped_identifier') {
115
+ const pathNode = node.childForFieldName('path');
116
+ const nameNode = node.childForFieldName('name');
117
+ if (pathNode)
118
+ collectScopedParts(pathNode, parts);
119
+ if (nameNode)
120
+ parts.push(nameNode.text);
121
+ }
122
+ else {
123
+ parts.push(node.text);
124
+ }
125
+ }
126
+ function buildNodePath(node) {
127
+ if (node.type === 'scoped_identifier') {
128
+ return buildScopedPath(node);
129
+ }
130
+ return node.text;
131
+ }
132
+ function buildWildcardPath(node) {
133
+ for (let i = 0; i < node.childCount; i++) {
134
+ const child = node.child(i);
135
+ if (child === null)
136
+ continue;
137
+ if (child.type === 'scoped_identifier')
138
+ return buildScopedPath(child);
139
+ if (child.type === 'identifier')
140
+ return child.text;
141
+ }
142
+ return '';
143
+ }
144
+ function joinPaths(prefix, suffix) {
145
+ if (!prefix)
146
+ return suffix;
147
+ if (!suffix)
148
+ return prefix;
149
+ return `${prefix}::${suffix}`;
150
+ }
151
+ function makeImportCapture(anchor, kind, source, name, alias) {
152
+ return {
153
+ '@import.statement': syntheticCapture('@import.statement', anchor, anchor.text),
154
+ '@import.kind': syntheticCapture('@import.kind', anchor, kind),
155
+ '@import.source': syntheticCapture('@import.source', anchor, source),
156
+ '@import.name': syntheticCapture('@import.name', anchor, alias ?? name),
157
+ ...(alias !== undefined
158
+ ? {
159
+ '@import.alias': syntheticCapture('@import.alias', anchor, alias),
160
+ '@import.original-name': syntheticCapture('@import.original-name', anchor, name),
161
+ }
162
+ : {}),
163
+ };
164
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve a Rust `use` import path to a repo-relative file path.
3
+ *
4
+ * Rust module resolution rules:
5
+ * - `crate::foo::bar` → `src/foo/bar.rs` or `src/foo/bar/mod.rs`
6
+ * - `super::foo` → parent directory's `foo.rs` or `foo/mod.rs`
7
+ * - `self::foo` → same directory's `foo.rs` or `foo/mod.rs`
8
+ * - External crate imports (no `crate::`/`super::`/`self::`) → null
9
+ */
10
+ export declare function resolveRustImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, _resolutionConfig?: unknown): string | readonly string[] | null;
11
+ export interface RustResolveContext {
12
+ readonly fromFile: string;
13
+ readonly allFilePaths: ReadonlySet<string>;
14
+ }