gitnexus 1.6.10-rc.95 → 1.6.10-rc.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/ai-context.d.ts +7 -0
- package/dist/cli/ai-context.js +34 -3
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/skill-gen.js +50 -0
- package/dist/core/ingestion/languages/rust/range-binding.d.ts +1 -11
- package/dist/core/ingestion/languages/rust/range-binding.js +143 -61
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +2 -0
- package/dist/storage/git.d.ts +7 -4
- package/dist/storage/git.js +9 -4
- package/dist/storage/repo-manager.d.ts +15 -2
- package/dist/storage/repo-manager.js +14 -1
- package/package.json +1 -1
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface GitNexusContentOptions {
|
|
|
63
63
|
hasPdg?: boolean;
|
|
64
64
|
}
|
|
65
65
|
export declare function generateGitNexusContent(projectName: string, stats: RepoStats, opts?: GitNexusContentOptions): string;
|
|
66
|
+
/**
|
|
67
|
+
* Some agents read skills from a repo-local `.agents/skills/` directory and
|
|
68
|
+
* prefer it over the global `~/.agents/skills/` install. When the repo contains
|
|
69
|
+
* an `.agents/` directory, skills written to `.claude/skills/` are mirrored
|
|
70
|
+
* there too so those agents serve the up-to-date copies.
|
|
71
|
+
*/
|
|
72
|
+
export declare function shouldMirrorSkillsToAgents(repoPath: string): Promise<boolean>;
|
|
66
73
|
/**
|
|
67
74
|
* Generate AI context files after indexing
|
|
68
75
|
*/
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -250,14 +250,31 @@ async function upsertGitNexusSection(filePath, content, projectName, stats, noSt
|
|
|
250
250
|
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
251
251
|
return 'appended';
|
|
252
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Some agents read skills from a repo-local `.agents/skills/` directory and
|
|
255
|
+
* prefer it over the global `~/.agents/skills/` install. When the repo contains
|
|
256
|
+
* an `.agents/` directory, skills written to `.claude/skills/` are mirrored
|
|
257
|
+
* there too so those agents serve the up-to-date copies.
|
|
258
|
+
*/
|
|
259
|
+
export async function shouldMirrorSkillsToAgents(repoPath) {
|
|
260
|
+
try {
|
|
261
|
+
const stat = await fs.stat(path.join(repoPath, '.agents'));
|
|
262
|
+
return stat.isDirectory();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
253
268
|
/**
|
|
254
269
|
* Install GitNexus skills as direct children of .claude/skills/
|
|
255
|
-
* Works natively with Claude Code, Cursor, and GitHub Copilot
|
|
270
|
+
* Works natively with Claude Code, Cursor, and GitHub Copilot.
|
|
271
|
+
* Mirrored to .agents/skills/ when .agents/ exists.
|
|
256
272
|
*/
|
|
257
273
|
async function installSkills(repoPath) {
|
|
258
274
|
const skillsDir = path.join(repoPath, '.claude', 'skills');
|
|
259
275
|
const legacySkillsDir = path.join(skillsDir, 'gitnexus');
|
|
260
276
|
const installedSkills = [];
|
|
277
|
+
const agentsMirror = await shouldMirrorSkillsToAgents(repoPath);
|
|
261
278
|
for (const skill of STANDARD_SKILL_CATALOG.filter((entry) => entry.distributions.project && entry.distributions.npm)) {
|
|
262
279
|
const skillDir = path.join(skillsDir, skill.name);
|
|
263
280
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
@@ -285,6 +302,17 @@ Use GitNexus tools to accomplish this task.
|
|
|
285
302
|
`;
|
|
286
303
|
}
|
|
287
304
|
await fs.writeFile(skillPath, skillContent, 'utf-8');
|
|
305
|
+
// Mirror to .agents/skills/ for agents that read repo-local skills
|
|
306
|
+
if (agentsMirror) {
|
|
307
|
+
try {
|
|
308
|
+
const agentsSkillDir = path.join(repoPath, '.agents', 'skills', skill.name);
|
|
309
|
+
await fs.mkdir(agentsSkillDir, { recursive: true });
|
|
310
|
+
await fs.writeFile(path.join(agentsSkillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
logger.warn({ err }, `Warning: Could not mirror skill ${skill.name} to .agents/skills:`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
288
316
|
installedSkills.push(skill.name);
|
|
289
317
|
// Previous releases installed these known standard skills one level too
|
|
290
318
|
// deep. Remove only the child owned by this installer; unknown siblings
|
|
@@ -301,7 +329,7 @@ Use GitNexus tools to accomplish this task.
|
|
|
301
329
|
logger.warn({ err }, `Warning: Could not install skill ${skill.name}:`);
|
|
302
330
|
}
|
|
303
331
|
}
|
|
304
|
-
return installedSkills;
|
|
332
|
+
return { skills: installedSkills, agentsMirror };
|
|
305
333
|
}
|
|
306
334
|
/**
|
|
307
335
|
* Generate AI context files after indexing
|
|
@@ -349,9 +377,12 @@ export async function generateAIContextFiles(repoPath, storagePath, projectName,
|
|
|
349
377
|
}
|
|
350
378
|
// Install standard skills directly under .claude/skills/ (unless --skip-skills)
|
|
351
379
|
if (!options?.skipSkills) {
|
|
352
|
-
const installedSkills = await installSkills(repoPath);
|
|
380
|
+
const { skills: installedSkills, agentsMirror } = await installSkills(repoPath);
|
|
353
381
|
if (installedSkills.length > 0) {
|
|
354
382
|
createdFiles.push(`.claude/skills/gitnexus-*/ (${installedSkills.length} skills)`);
|
|
383
|
+
if (agentsMirror) {
|
|
384
|
+
createdFiles.push(`.agents/skills/gitnexus-*/ (${installedSkills.length} skills mirrored for .agents)`);
|
|
385
|
+
}
|
|
355
386
|
}
|
|
356
387
|
}
|
|
357
388
|
else {
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -153,7 +153,7 @@ export declare const en: {
|
|
|
153
153
|
readonly 'help.option.analyze.skills': "Generate repo-specific skill files from detected communities (no-op when --index-only is also set).";
|
|
154
154
|
readonly 'help.option.analyze.skipAgentsMd': "Skip updating the gitnexus section in AGENTS.md and CLAUDE.md";
|
|
155
155
|
readonly 'help.option.analyze.noStats': "Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md";
|
|
156
|
-
readonly 'help.option.analyze.skipSkills': "Skip installing standard GitNexus skill files directly under .claude/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.";
|
|
156
|
+
readonly 'help.option.analyze.skipSkills': "Skip installing standard GitNexus skill files directly under .claude/skills/ and .agents/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.";
|
|
157
157
|
readonly 'help.option.analyze.indexOnly': "Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)";
|
|
158
158
|
readonly 'help.option.skipGit': "Treat the provided path/cwd as the index root and skip parent git-root discovery";
|
|
159
159
|
readonly 'help.option.analyze.name': "Register this repo under a custom name in ~/.gitnexus/registry.json (disambiguates repos whose paths share a basename, e.g. two different .../app folders)";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -153,7 +153,7 @@ export const en = {
|
|
|
153
153
|
'help.option.analyze.skills': 'Generate repo-specific skill files from detected communities (no-op when --index-only is also set).',
|
|
154
154
|
'help.option.analyze.skipAgentsMd': 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md',
|
|
155
155
|
'help.option.analyze.noStats': 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md',
|
|
156
|
-
'help.option.analyze.skipSkills': 'Skip installing standard GitNexus skill files directly under .claude/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.',
|
|
156
|
+
'help.option.analyze.skipSkills': 'Skip installing standard GitNexus skill files directly under .claude/skills/ and .agents/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.',
|
|
157
157
|
'help.option.analyze.indexOnly': 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)',
|
|
158
158
|
'help.option.skipGit': 'Treat the provided path/cwd as the index root and skip parent git-root discovery',
|
|
159
159
|
'help.option.analyze.name': 'Register this repo under a custom name in ~/.gitnexus/registry.json (disambiguates repos whose paths share a basename, e.g. two different .../app folders)',
|
|
@@ -154,7 +154,7 @@ export declare const cliResources: {
|
|
|
154
154
|
readonly 'help.option.analyze.skills': "Generate repo-specific skill files from detected communities (no-op when --index-only is also set).";
|
|
155
155
|
readonly 'help.option.analyze.skipAgentsMd': "Skip updating the gitnexus section in AGENTS.md and CLAUDE.md";
|
|
156
156
|
readonly 'help.option.analyze.noStats': "Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md";
|
|
157
|
-
readonly 'help.option.analyze.skipSkills': "Skip installing standard GitNexus skill files directly under .claude/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.";
|
|
157
|
+
readonly 'help.option.analyze.skipSkills': "Skip installing standard GitNexus skill files directly under .claude/skills/ and .agents/skills/. Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). Use --index-only to skip all AI-context file injection.";
|
|
158
158
|
readonly 'help.option.analyze.indexOnly': "Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)";
|
|
159
159
|
readonly 'help.option.skipGit': "Treat the provided path/cwd as the index root and skip parent git-root discovery";
|
|
160
160
|
readonly 'help.option.analyze.name': "Register this repo under a custom name in ~/.gitnexus/registry.json (disambiguates repos whose paths share a basename, e.g. two different .../app folders)";
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -153,7 +153,7 @@ export const zhCN = {
|
|
|
153
153
|
'help.option.analyze.skills': '根据检测到的社区生成仓库专属 skill 文件(同时设置 --index-only 时无效)。',
|
|
154
154
|
'help.option.analyze.skipAgentsMd': '跳过更新 AGENTS.md 和 CLAUDE.md 中的 gitnexus 区块',
|
|
155
155
|
'help.option.analyze.noStats': '从 AGENTS.md 和 CLAUDE.md 中省略易变的文件/符号计数',
|
|
156
|
-
'help.option.analyze.skipSkills': '跳过直接安装在 .claude/skills/ 下的标准 GitNexus skill 文件。不抑制 --skills 生成的社区 skill(位于 .claude/skills/gitnexus-area-*)。使用 --index-only 可跳过所有 AI 上下文文件注入。',
|
|
156
|
+
'help.option.analyze.skipSkills': '跳过直接安装在 .claude/skills/ 和 .agents/skills/ 下的标准 GitNexus skill 文件。不抑制 --skills 生成的社区 skill(位于 .claude/skills/gitnexus-area-*)。使用 --index-only 可跳过所有 AI 上下文文件注入。',
|
|
157
157
|
'help.option.analyze.indexOnly': '纯索引模式:跳过所有文件注入(AGENTS.md、CLAUDE.md、skills)',
|
|
158
158
|
'help.option.skipGit': '将提供的路径/cwd 视为索引根目录,并跳过向上查找 git 根目录',
|
|
159
159
|
'help.option.analyze.name': '在 ~/.gitnexus/registry.json 中使用自定义名称注册该仓库(用于区分路径 basename 相同的仓库,例如两个不同的 .../app 目录)',
|
package/dist/cli/index.js
CHANGED
|
@@ -53,7 +53,7 @@ program
|
|
|
53
53
|
'Without this flag, analyze always updates the workspace index, which follows the ' +
|
|
54
54
|
'checked-out working tree. Distinct from --default-branch (cosmetic base_ref).')
|
|
55
55
|
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
|
|
56
|
-
.option('--skip-skills', 'Skip installing standard GitNexus skill files directly under .claude/skills/. ' +
|
|
56
|
+
.option('--skip-skills', 'Skip installing standard GitNexus skill files directly under .claude/skills/ and .agents/skills/. ' +
|
|
57
57
|
'Does not suppress community skills from --skills (those use .claude/skills/gitnexus-area-*). ' +
|
|
58
58
|
'Use --index-only to skip all AI-context file injection.')
|
|
59
59
|
.option('--index-only', 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)')
|
package/dist/cli/skill-gen.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import path from 'path';
|
|
11
|
+
import { shouldMirrorSkillsToAgents } from './ai-context.js';
|
|
11
12
|
const GENERATED_SKILL_PREFIX = 'gitnexus-area-';
|
|
12
13
|
const MAX_SKILL_NAME_LENGTH = 64;
|
|
13
14
|
const MAX_COMMUNITY_NAME_LENGTH = MAX_SKILL_NAME_LENGTH - GENERATED_SKILL_PREFIX.length;
|
|
@@ -25,6 +26,12 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
25
26
|
const { communityResult, processResult, graph } = pipelineResult;
|
|
26
27
|
const outputDir = path.join(repoPath, '.claude', 'skills');
|
|
27
28
|
const legacyOutputDir = path.join(outputDir, 'generated');
|
|
29
|
+
// Some agents prioritize repo-local .agents/skills over the global
|
|
30
|
+
// ~/.agents/skills install (see shouldMirrorSkillsToAgents). When .agents/
|
|
31
|
+
// exists, mirror the generated community skills there too so those agents
|
|
32
|
+
// serve the up-to-date copies.
|
|
33
|
+
const agentsOutputDir = path.join(repoPath, '.agents', 'skills');
|
|
34
|
+
let mirrorToAgents = await shouldMirrorSkillsToAgents(repoPath);
|
|
28
35
|
// Community skills used to live under an undiscoverable `generated/`
|
|
29
36
|
// grouping directory. Clear that GitNexus-owned legacy output and
|
|
30
37
|
// stale direct outputs in the reserved namespace, while preserving every
|
|
@@ -44,6 +51,20 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
44
51
|
catch {
|
|
45
52
|
/* legacy output may not exist */
|
|
46
53
|
}
|
|
54
|
+
// Mirror cleanup: clear only stale GitNexus-generated community skills under
|
|
55
|
+
// .agents/skills/ (reserved gitnexus-area-* namespace), preserving mirrored
|
|
56
|
+
// standard skills and any user-authored skills. Never clear the whole root.
|
|
57
|
+
if (mirrorToAgents) {
|
|
58
|
+
try {
|
|
59
|
+
const entries = await fs.readdir(agentsOutputDir, { withFileTypes: true });
|
|
60
|
+
await Promise.all(entries
|
|
61
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(GENERATED_SKILL_PREFIX))
|
|
62
|
+
.map((entry) => fs.rm(path.join(agentsOutputDir, entry.name), { recursive: true, force: true })));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* mirror root may not exist yet */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
47
68
|
if (!communityResult || !communityResult.memberships.length) {
|
|
48
69
|
console.log('\n Skills: no communities detected, skipping skill generation');
|
|
49
70
|
return { skills: [], outputPath: outputDir };
|
|
@@ -73,6 +94,19 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
73
94
|
// Step 4: Ensure the shared project-skill root exists. Never clear it: it
|
|
74
95
|
// also contains user-authored and standard GitNexus skills.
|
|
75
96
|
await fs.mkdir(outputDir, { recursive: true });
|
|
97
|
+
// The .agents/ mirror is a side flow: keep it a weak dependency. If the
|
|
98
|
+
// mirror root cannot be created (e.g. `.agents/skills` exists as a file),
|
|
99
|
+
// warn and disable mirroring for this run instead of aborting canonical
|
|
100
|
+
// community-skill generation. Canonical writes below stay unaffected.
|
|
101
|
+
if (mirrorToAgents) {
|
|
102
|
+
try {
|
|
103
|
+
await fs.mkdir(agentsOutputDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.log(`Warning: Could not create mirror root ${agentsOutputDir} — .agents/skills mirroring disabled for this run: ${err}`);
|
|
107
|
+
mirrorToAgents = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
76
110
|
// Step 5: Generate skill files
|
|
77
111
|
const skills = [];
|
|
78
112
|
const usedNames = new Set();
|
|
@@ -99,6 +133,19 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
99
133
|
const skillDir = path.join(outputDir, skillName);
|
|
100
134
|
await fs.mkdir(skillDir, { recursive: true });
|
|
101
135
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
136
|
+
// Mirror to .agents/skills/ for agents that read repo-local skills
|
|
137
|
+
// (see mirrorToAgents above). Best-effort: a per-skill mirror failure
|
|
138
|
+
// must not abort canonical community-skill generation.
|
|
139
|
+
if (mirrorToAgents) {
|
|
140
|
+
try {
|
|
141
|
+
const agentsSkillDir = path.join(agentsOutputDir, skillName);
|
|
142
|
+
await fs.mkdir(agentsSkillDir, { recursive: true });
|
|
143
|
+
await fs.writeFile(path.join(agentsSkillDir, 'SKILL.md'), content, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.log(`Warning: Could not mirror skill ${skillName} to .agents/skills: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
102
149
|
const info = {
|
|
103
150
|
name: skillName,
|
|
104
151
|
label: community.label,
|
|
@@ -109,6 +156,9 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
109
156
|
console.log(` \u2713 ${community.label} (${community.symbolCount} symbols, ${files.length} files)`);
|
|
110
157
|
}
|
|
111
158
|
console.log(`\n ${skills.length} skills generated \u2192 .claude/skills/${GENERATED_SKILL_PREFIX}*/`);
|
|
159
|
+
if (mirrorToAgents) {
|
|
160
|
+
console.log(` ${skills.length} skills mirrored \u2192 .agents/skills/${GENERATED_SKILL_PREFIX}*/ (.agents)`);
|
|
161
|
+
}
|
|
112
162
|
return { skills, outputPath: outputDir };
|
|
113
163
|
};
|
|
114
164
|
// ============================================================================
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
2
2
|
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
3
|
-
|
|
4
|
-
* Populate type bindings for patterns and iterators that the tree-sitter
|
|
5
|
-
* query can't handle (they need runtime type lookup, not just syntax).
|
|
6
|
-
*
|
|
7
|
-
* Covers: for-loop element types, if-let/while-let pattern bindings,
|
|
8
|
-
* match arm patterns, and struct destructuring.
|
|
9
|
-
*
|
|
10
|
-
* Runs in Phase 2 (after propagateImportedReturnTypes) so all cross-file
|
|
11
|
-
* type bindings are available for lookup.
|
|
12
|
-
*/
|
|
13
|
-
export declare function populateRustRangeBindings(parsedFiles: readonly ParsedFile[], _indexes: ScopeResolutionIndexes, ctx: {
|
|
3
|
+
export declare function populateRustRangeBindings(parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: {
|
|
14
4
|
readonly fileContents: ReadonlyMap<string, string>;
|
|
15
5
|
readonly treeCache?: {
|
|
16
6
|
get(filePath: string): unknown;
|
|
@@ -2,54 +2,84 @@ import { getRustParser } from './query.js';
|
|
|
2
2
|
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
3
3
|
import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js';
|
|
4
4
|
import { logger } from '../../../logger.js';
|
|
5
|
+
import { lookupBindingsAt } from '../../scope-resolution/scope/walkers.js';
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* match arm patterns, and struct destructuring.
|
|
11
|
-
*
|
|
12
|
-
* Runs in Phase 2 (after propagateImportedReturnTypes) so all cross-file
|
|
13
|
-
* type bindings are available for lookup.
|
|
7
|
+
* Hold parsed trees for reuse across both prepass loops only when the whole
|
|
8
|
+
* Rust source fits this budget. Trees are much larger than their source, so a
|
|
9
|
+
* modest source cap keeps peak held-tree memory bounded; larger repos fall
|
|
10
|
+
* back to re-parsing per loop (unchanged RSS).
|
|
14
11
|
*/
|
|
15
|
-
|
|
12
|
+
const TREE_REUSE_SOURCE_BUDGET_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
/**
|
|
14
|
+
* Parse `filePath`'s source once, honoring the caller's `treeCache` and, when
|
|
15
|
+
* provided, an in-function `store` so the two prepass loops share a single
|
|
16
|
+
* parse instead of re-parsing every file. Returns null when the source is
|
|
17
|
+
* missing or parsing times out.
|
|
18
|
+
*/
|
|
19
|
+
function getOrParseTree(parser, filePath, ctx, store) {
|
|
20
|
+
const cached = (ctx.treeCache?.get(filePath) ?? store?.get(filePath));
|
|
21
|
+
if (cached !== undefined)
|
|
22
|
+
return cached;
|
|
23
|
+
const sourceText = ctx.fileContents.get(filePath);
|
|
24
|
+
if (sourceText === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
let tree;
|
|
27
|
+
try {
|
|
28
|
+
tree = parseSourceSafe(parser, sourceText, undefined, {
|
|
29
|
+
bufferSize: getTreeSitterBufferSize(sourceText),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof ParseTimeoutError) {
|
|
34
|
+
logger.warn({ file: filePath }, 'rust range-binding: parse timed out, skipping file');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
store?.set(filePath, tree);
|
|
40
|
+
return tree;
|
|
41
|
+
}
|
|
42
|
+
export function populateRustRangeBindings(parsedFiles, indexes, ctx) {
|
|
16
43
|
const parser = getRustParser();
|
|
17
44
|
const allReturnTypes = new Map();
|
|
45
|
+
const ambiguousReturnTypes = new Set();
|
|
18
46
|
const allFieldTypes = new Map();
|
|
47
|
+
const ambiguousFieldTypes = new Set();
|
|
48
|
+
// Per-defining-file, un-collapsed, FULL-generic return/field types. When a
|
|
49
|
+
// bare name is ambiguous (#2514) but the call site's `use` import pins a
|
|
50
|
+
// single definition, we resolve that definition's file here and read its
|
|
51
|
+
// untruncated type so a generic `Vec<Repo>` element type survives (#2514
|
|
52
|
+
// follow-up: import-disambiguated duplicates resolve like the compiler).
|
|
53
|
+
const returnTypeByFile = new Map();
|
|
54
|
+
const fieldTypeByFile = new Map();
|
|
55
|
+
// Parse each file once and reuse across both loops when the workspace fits
|
|
56
|
+
// the byte budget; otherwise re-parse per loop to bound RSS (see helper).
|
|
57
|
+
let totalSourceBytes = 0;
|
|
19
58
|
for (const parsed of parsedFiles) {
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
totalSourceBytes += ctx.fileContents.get(parsed.filePath)?.length ?? 0;
|
|
60
|
+
}
|
|
61
|
+
const treeStore = totalSourceBytes <= TREE_REUSE_SOURCE_BUDGET_BYTES ? new Map() : undefined;
|
|
62
|
+
for (const parsed of parsedFiles) {
|
|
63
|
+
const tree = getOrParseTree(parser, parsed.filePath, ctx, treeStore);
|
|
64
|
+
if (tree === null)
|
|
22
65
|
continue;
|
|
23
|
-
const cachedTree = ctx.treeCache?.get(parsed.filePath);
|
|
24
|
-
let tree;
|
|
25
|
-
if (cachedTree !== undefined) {
|
|
26
|
-
tree = cachedTree;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
try {
|
|
30
|
-
tree = parseSourceSafe(parser, sourceText, undefined, {
|
|
31
|
-
bufferSize: getTreeSitterBufferSize(sourceText),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
if (err instanceof ParseTimeoutError) {
|
|
36
|
-
logger.warn({ file: parsed.filePath }, 'rust range-binding: parse timed out, skipping file');
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
66
|
for (const fn of tree.rootNode.descendantsOfType('function_item')) {
|
|
43
67
|
const nameNode = fn.childForFieldName('name');
|
|
44
68
|
const retType = fn.childForFieldName('return_type');
|
|
45
69
|
if (nameNode !== null && retType !== null) {
|
|
46
70
|
const name = nameNode.text;
|
|
71
|
+
// Ambiguity is a latch, not a toggle: once a name has two or more
|
|
72
|
+
// workspace definitions it stays ambiguous for the rest of the
|
|
73
|
+
// prepass, regardless of duplicate count or file order (#2514).
|
|
47
74
|
if (allReturnTypes.has(name)) {
|
|
48
75
|
allReturnTypes.delete(name);
|
|
76
|
+
ambiguousReturnTypes.add(name);
|
|
49
77
|
}
|
|
50
|
-
else {
|
|
78
|
+
else if (!ambiguousReturnTypes.has(name)) {
|
|
51
79
|
allReturnTypes.set(name, retType.text);
|
|
52
80
|
}
|
|
81
|
+
// Full-generic record per defining file for import-disambiguated lookup.
|
|
82
|
+
recordByFile(returnTypeByFile, parsed.filePath, name, retType.text);
|
|
53
83
|
}
|
|
54
84
|
}
|
|
55
85
|
for (const structNode of tree.rootNode.descendantsOfType('struct_item')) {
|
|
@@ -67,12 +97,17 @@ export function populateRustRangeBindings(parsedFiles, _indexes, ctx) {
|
|
|
67
97
|
}
|
|
68
98
|
if (fields.size > 0) {
|
|
69
99
|
const name = nameNode.text;
|
|
100
|
+
// Same ambiguity latch as return types (#2514): a third same-named
|
|
101
|
+
// struct must not restore a resolvable global field map.
|
|
70
102
|
if (allFieldTypes.has(name)) {
|
|
71
103
|
allFieldTypes.delete(name);
|
|
104
|
+
ambiguousFieldTypes.add(name);
|
|
72
105
|
}
|
|
73
|
-
else {
|
|
106
|
+
else if (!ambiguousFieldTypes.has(name)) {
|
|
74
107
|
allFieldTypes.set(name, fields);
|
|
75
108
|
}
|
|
109
|
+
// Full-generic record per defining file for import-disambiguated lookup.
|
|
110
|
+
recordByFile(fieldTypeByFile, parsed.filePath, name, fields);
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
// Publish per-type member bindings for the whole workspace before resolving
|
|
@@ -83,35 +118,16 @@ export function populateRustRangeBindings(parsedFiles, _indexes, ctx) {
|
|
|
83
118
|
processIdentityMethodBindings(parsed);
|
|
84
119
|
}
|
|
85
120
|
for (const parsed of parsedFiles) {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
121
|
+
const tree = getOrParseTree(parser, parsed.filePath, ctx, treeStore);
|
|
122
|
+
if (tree === null)
|
|
88
123
|
continue;
|
|
89
|
-
const cachedTree = ctx.treeCache?.get(parsed.filePath);
|
|
90
|
-
let tree;
|
|
91
|
-
if (cachedTree !== undefined) {
|
|
92
|
-
tree = cachedTree;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
try {
|
|
96
|
-
tree = parseSourceSafe(parser, sourceText, undefined, {
|
|
97
|
-
bufferSize: getTreeSitterBufferSize(sourceText),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
if (err instanceof ParseTimeoutError) {
|
|
102
|
-
logger.warn({ file: parsed.filePath }, 'rust range-binding: parse timed out, skipping file');
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
throw err;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
124
|
const scopeMap = new Map(parsed.scopes.map((s) => [s.id, s]));
|
|
109
125
|
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
|
110
126
|
if (moduleScope === undefined)
|
|
111
127
|
continue;
|
|
112
|
-
processForLoops(tree.rootNode, parsed, scopeMap, moduleScope, allReturnTypes);
|
|
128
|
+
processForLoops(tree.rootNode, parsed, scopeMap, moduleScope, allReturnTypes, indexes, returnTypeByFile);
|
|
113
129
|
processPatternBindings(tree.rootNode, parsed, scopeMap, moduleScope);
|
|
114
|
-
processStructDestructuring(tree.rootNode, parsed, scopeMap, moduleScope, allFieldTypes);
|
|
130
|
+
processStructDestructuring(tree.rootNode, parsed, scopeMap, moduleScope, allFieldTypes, indexes, fieldTypeByFile);
|
|
115
131
|
processPendingAssignments(tree.rootNode, parsed, parsedFiles, scopeMap, moduleScope, allReturnTypes);
|
|
116
132
|
}
|
|
117
133
|
}
|
|
@@ -158,7 +174,60 @@ function normalizeFieldType(text) {
|
|
|
158
174
|
t = t.slice(lastColon + 2);
|
|
159
175
|
return t.trim();
|
|
160
176
|
}
|
|
161
|
-
|
|
177
|
+
/** Get-or-create the inner map for `file` and record `name -> value`. */
|
|
178
|
+
function recordByFile(byFile, file, name, value) {
|
|
179
|
+
let inner = byFile.get(file);
|
|
180
|
+
if (inner === undefined) {
|
|
181
|
+
inner = new Map();
|
|
182
|
+
byFile.set(file, inner);
|
|
183
|
+
}
|
|
184
|
+
inner.set(name, value);
|
|
185
|
+
}
|
|
186
|
+
/** Final segment of a dot-joined qualified name (`a.make` -> `make`), or the
|
|
187
|
+
* bare name when the def carries no qualifier. */
|
|
188
|
+
function simpleName(qualifiedName, bareName) {
|
|
189
|
+
if (qualifiedName === undefined)
|
|
190
|
+
return bareName;
|
|
191
|
+
const dot = qualifiedName.lastIndexOf('.');
|
|
192
|
+
return dot === -1 ? qualifiedName : qualifiedName.slice(dot + 1);
|
|
193
|
+
}
|
|
194
|
+
/** Distinct `(file, name)` definitions, in first-seen order. */
|
|
195
|
+
function uniqueDefs(defs) {
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const d of defs) {
|
|
199
|
+
const key = `${d.file} ${d.name}`;
|
|
200
|
+
if (seen.has(key))
|
|
201
|
+
continue;
|
|
202
|
+
seen.add(key);
|
|
203
|
+
out.push(d);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve `name` at `moduleScope` to the value recorded in `byFile` for the one
|
|
209
|
+
* definition visible here, or null when zero or several are visible (which
|
|
210
|
+
* keeps the #2514 ambiguity latch). Mirrors Rust name resolution: explicit
|
|
211
|
+
* `use`/re-export imports and local defs shadow `use x::*` globs, so a glob is
|
|
212
|
+
* consulted only when no explicit binding names `name`, and even then only when
|
|
213
|
+
* exactly one glob-target file actually defines it.
|
|
214
|
+
*/
|
|
215
|
+
function resolveImportedDef(name, moduleScope, indexes, byFile) {
|
|
216
|
+
const explicit = uniqueDefs(lookupBindingsAt(moduleScope.id, name, indexes)
|
|
217
|
+
.filter((r) => r.origin === 'import' || r.origin === 'reexport' || r.origin === 'local')
|
|
218
|
+
.map((r) => ({ file: r.def.filePath, name: simpleName(r.def.qualifiedName, name) })));
|
|
219
|
+
const defs = explicit.length > 0
|
|
220
|
+
? explicit
|
|
221
|
+
: uniqueDefs((indexes.imports.get(moduleScope.id) ?? [])
|
|
222
|
+
.filter((e) => e.kind === 'wildcard-expanded' &&
|
|
223
|
+
e.targetFile !== null &&
|
|
224
|
+
byFile.get(e.targetFile)?.has(name) === true)
|
|
225
|
+
.map((e) => ({ file: e.targetFile, name })));
|
|
226
|
+
if (defs.length !== 1)
|
|
227
|
+
return null;
|
|
228
|
+
return byFile.get(defs[0].file)?.get(defs[0].name) ?? null;
|
|
229
|
+
}
|
|
230
|
+
function processForLoops(root, parsed, scopeMap, moduleScope, allReturnTypes, indexes, returnTypeByFile) {
|
|
162
231
|
for (const forNode of root.descendantsOfType('for_expression')) {
|
|
163
232
|
const patternNode = forNode.childForFieldName('pattern');
|
|
164
233
|
const valueNode = forNode.childForFieldName('value');
|
|
@@ -167,7 +236,7 @@ function processForLoops(root, parsed, scopeMap, moduleScope, allReturnTypes) {
|
|
|
167
236
|
const varName = extractVarName(patternNode);
|
|
168
237
|
if (varName === null)
|
|
169
238
|
continue;
|
|
170
|
-
const elementType = resolveIterableElementType(valueNode, parsed, scopeMap, moduleScope, allReturnTypes);
|
|
239
|
+
const elementType = resolveIterableElementType(valueNode, parsed, scopeMap, moduleScope, allReturnTypes, indexes, returnTypeByFile);
|
|
171
240
|
if (elementType === null)
|
|
172
241
|
continue;
|
|
173
242
|
const targetScope = findEnclosingFunctionScope(forNode, scopeMap) ?? moduleScope;
|
|
@@ -251,7 +320,7 @@ function processTupleStructPattern(patternNode, contextNode, parsed, scopeMap, m
|
|
|
251
320
|
const targetScope = findEnclosingFunctionScope(contextNode, scopeMap) ?? moduleScope;
|
|
252
321
|
injectTypeBinding(targetScope, varName, resolvedType);
|
|
253
322
|
}
|
|
254
|
-
function processStructDestructuring(root, parsed, scopeMap, moduleScope, allFieldTypes) {
|
|
323
|
+
function processStructDestructuring(root, parsed, scopeMap, moduleScope, allFieldTypes, indexes, fieldTypeByFile) {
|
|
255
324
|
for (const letNode of root.descendantsOfType('let_declaration')) {
|
|
256
325
|
const patternNode = letNode.childForFieldName('pattern');
|
|
257
326
|
if (patternNode === null || patternNode.type !== 'struct_pattern')
|
|
@@ -274,7 +343,13 @@ function processStructDestructuring(root, parsed, scopeMap, moduleScope, allFiel
|
|
|
274
343
|
continue;
|
|
275
344
|
let fieldType = lookupFieldType(typeName, fieldName, parsed, scopeMap, moduleScope);
|
|
276
345
|
if (fieldType === null) {
|
|
277
|
-
fieldType = allFieldTypes
|
|
346
|
+
fieldType = allFieldTypes.get(typeName)?.get(fieldName) ?? null;
|
|
347
|
+
}
|
|
348
|
+
if (fieldType === null) {
|
|
349
|
+
// Import-disambiguated duplicate struct (#2514 follow-up): the global
|
|
350
|
+
// field map is ambiguous, but a `use` import pins one definition.
|
|
351
|
+
const fields = resolveImportedDef(typeName, moduleScope, indexes, fieldTypeByFile);
|
|
352
|
+
fieldType = fields?.get(fieldName) ?? null;
|
|
278
353
|
}
|
|
279
354
|
if (fieldType !== null) {
|
|
280
355
|
injectTypeBinding(targetScope, fieldName, fieldType);
|
|
@@ -367,7 +442,7 @@ function processPendingAssignments(root, parsed, allParsedFiles, scopeMap, modul
|
|
|
367
442
|
}
|
|
368
443
|
}
|
|
369
444
|
}
|
|
370
|
-
function resolveIterableElementType(valueNode, parsed, scopeMap, moduleScope, allReturnTypes) {
|
|
445
|
+
function resolveIterableElementType(valueNode, parsed, scopeMap, moduleScope, allReturnTypes, indexes, returnTypeByFile) {
|
|
371
446
|
let iterableNode = valueNode;
|
|
372
447
|
if (iterableNode.type === 'reference_expression') {
|
|
373
448
|
iterableNode = iterableNode.firstNamedChild ?? iterableNode;
|
|
@@ -390,12 +465,19 @@ function resolveIterableElementType(valueNode, parsed, scopeMap, moduleScope, al
|
|
|
390
465
|
}
|
|
391
466
|
}
|
|
392
467
|
if (func.type === 'identifier') {
|
|
393
|
-
const crossFileReturn = allReturnTypes
|
|
468
|
+
const crossFileReturn = allReturnTypes.get(func.text);
|
|
394
469
|
if (crossFileReturn !== undefined)
|
|
395
470
|
return unwrapGeneric(crossFileReturn);
|
|
396
471
|
const rawReturn = lookupRawFunctionReturnType(func.text, valueNode);
|
|
397
472
|
if (rawReturn !== null)
|
|
398
473
|
return unwrapGeneric(rawReturn);
|
|
474
|
+
// Import-disambiguated duplicate: the bare-name map is ambiguous (#2514)
|
|
475
|
+
// but a `use` import pins one definition. Read its FULL return type
|
|
476
|
+
// here, BEFORE the scope-binding lookup below, because that binding is
|
|
477
|
+
// generic-truncated (`Vec<Repo>` becomes `Vec`), losing the element.
|
|
478
|
+
const importedReturn = resolveImportedDef(func.text, moduleScope, indexes, returnTypeByFile);
|
|
479
|
+
if (importedReturn !== null)
|
|
480
|
+
return unwrapGeneric(importedReturn);
|
|
399
481
|
const returnType = lookupReturnTypeInScopes(func.text, parsed, scopeMap, moduleScope);
|
|
400
482
|
if (returnType !== null)
|
|
401
483
|
return unwrapGeneric(returnType);
|
|
@@ -420,6 +420,7 @@ export function runScopeResolution(input, provider) {
|
|
|
420
420
|
if (provider.propagatesReturnTypesAcrossImports !== false) {
|
|
421
421
|
propagateImportedReturnTypes(parsedFiles, indexes, workspaceIndex);
|
|
422
422
|
}
|
|
423
|
+
const tRangeBindStart = PROF ? process.hrtime.bigint() : 0n;
|
|
423
424
|
if (provider.populateRangeBindings !== undefined) {
|
|
424
425
|
provider.populateRangeBindings(parsedFiles, indexes, {
|
|
425
426
|
fileContents: getFileContents(),
|
|
@@ -908,6 +909,7 @@ export function runScopeResolution(input, provider) {
|
|
|
908
909
|
logger.warn(`[scope-resolution prof] extract=${ns(tStart, tExtract).toFixed(0)}ms` +
|
|
909
910
|
` finalize=${ns(tExtract, tFinalize).toFixed(0)}ms` +
|
|
910
911
|
` propagate=${ns(tFinalize, tPropagate).toFixed(0)}ms` +
|
|
912
|
+
` rangeBind=${ns(tRangeBindStart, tPropagate).toFixed(1)}ms` +
|
|
911
913
|
` resolve=${ns(tPropagate, tResolve).toFixed(0)}ms` +
|
|
912
914
|
` emit=${ns(tResolve, tEnd).toFixed(0)}ms` +
|
|
913
915
|
// pdg ⊆ emit: the M2 reaching-defs share of the emit bucket (#2082 U4).
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* True when the working tree has uncommitted changes that analyze would
|
|
3
3
|
* re-index, even at a matching HEAD. Excludes the paths GitNexus writes during
|
|
4
|
-
* analyze (.gitnexus/, .claude/, .cursor/, AGENTS.md, CLAUDE.md
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* analyze (.gitnexus/, .claude/, .cursor/, AGENTS.md, CLAUDE.md, and the
|
|
5
|
+
* repo-local .agents/ mirror) so its own output never counts as dirty
|
|
6
|
+
* (regression vs PR #1233 behavior). The entire .agents/ tree is excluded,
|
|
7
|
+
* matching the .claude/ treatment, because the skill mirror writes across
|
|
8
|
+
* .agents/skills/ and deeper paths. Conservative on any git failure. Shared
|
|
9
|
+
* so `analyze`'s fast-path gate and `status`'s freshness report agree on what
|
|
10
|
+
* "dirty" means.
|
|
8
11
|
*/
|
|
9
12
|
export declare const isWorkingTreeDirty: (repoPath: string) => boolean;
|
|
10
13
|
export declare const isGitRepo: (repoPath: string) => boolean;
|
package/dist/storage/git.js
CHANGED
|
@@ -7,10 +7,13 @@ const chompGitOutput = (value) => value.toString().replace(/\r?\n$/, '');
|
|
|
7
7
|
/**
|
|
8
8
|
* True when the working tree has uncommitted changes that analyze would
|
|
9
9
|
* re-index, even at a matching HEAD. Excludes the paths GitNexus writes during
|
|
10
|
-
* analyze (.gitnexus/, .claude/, .cursor/, AGENTS.md, CLAUDE.md
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* analyze (.gitnexus/, .claude/, .cursor/, AGENTS.md, CLAUDE.md, and the
|
|
11
|
+
* repo-local .agents/ mirror) so its own output never counts as dirty
|
|
12
|
+
* (regression vs PR #1233 behavior). The entire .agents/ tree is excluded,
|
|
13
|
+
* matching the .claude/ treatment, because the skill mirror writes across
|
|
14
|
+
* .agents/skills/ and deeper paths. Conservative on any git failure. Shared
|
|
15
|
+
* so `analyze`'s fast-path gate and `status`'s freshness report agree on what
|
|
16
|
+
* "dirty" means.
|
|
14
17
|
*/
|
|
15
18
|
export const isWorkingTreeDirty = (repoPath) => {
|
|
16
19
|
try {
|
|
@@ -27,6 +30,8 @@ export const isWorkingTreeDirty = (repoPath) => {
|
|
|
27
30
|
':(exclude).cursor/**',
|
|
28
31
|
':(exclude)AGENTS.md',
|
|
29
32
|
':(exclude)CLAUDE.md',
|
|
33
|
+
':(exclude).agents',
|
|
34
|
+
':(exclude).agents/**',
|
|
30
35
|
], {
|
|
31
36
|
cwd: repoPath,
|
|
32
37
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -418,8 +418,21 @@ export interface RepoMeta {
|
|
|
418
418
|
* incremental write set only covers changed files, so a top-up against a
|
|
419
419
|
* pre-v11 index would keep silently missing these CALLS edges for every
|
|
420
420
|
* unchanged Rust trait file; force a full re-analyze instead.
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
* v12: Rust range-binding stopped restoring ambiguous duplicate type names
|
|
422
|
+
* (#2514): a function/struct name defined three or more times used to
|
|
423
|
+
* re-resolve to the last-scanned file (a presence toggle), so odd duplicate
|
|
424
|
+
* counts emitted a wrong cross-file CALLS edge. Same v7/v11 contract: the
|
|
425
|
+
* incremental write set only covers changed files, so a top-up against a
|
|
426
|
+
* pre-v12 index would keep these spurious CALLS edges on every unchanged Rust
|
|
427
|
+
* file. v12 also changes edges in the other direction: range-binding now
|
|
428
|
+
* RESOLVES import-disambiguated duplicate names (`for item in make()` /
|
|
429
|
+
* `let Struct { f } = ..` where a `use` or `use x::*` import pins one of several
|
|
430
|
+
* same-named definitions) to the imported definition's type. Both the removed
|
|
431
|
+
* spurious edges and these new resolved edges are cross-file, so a pre-v12
|
|
432
|
+
* top-up would leave unchanged Rust files stale either way; force a full
|
|
433
|
+
* re-analyze instead.
|
|
434
|
+
*/
|
|
435
|
+
export declare const INCREMENTAL_SCHEMA_VERSION = 12;
|
|
423
436
|
export interface IndexedRepo {
|
|
424
437
|
repoPath: string;
|
|
425
438
|
storagePath: string;
|
|
@@ -139,8 +139,21 @@ export const cloneDirBelongsToEntry = (cloneDir, entryPath) => registryPathEqual
|
|
|
139
139
|
* incremental write set only covers changed files, so a top-up against a
|
|
140
140
|
* pre-v11 index would keep silently missing these CALLS edges for every
|
|
141
141
|
* unchanged Rust trait file; force a full re-analyze instead.
|
|
142
|
+
* v12: Rust range-binding stopped restoring ambiguous duplicate type names
|
|
143
|
+
* (#2514): a function/struct name defined three or more times used to
|
|
144
|
+
* re-resolve to the last-scanned file (a presence toggle), so odd duplicate
|
|
145
|
+
* counts emitted a wrong cross-file CALLS edge. Same v7/v11 contract: the
|
|
146
|
+
* incremental write set only covers changed files, so a top-up against a
|
|
147
|
+
* pre-v12 index would keep these spurious CALLS edges on every unchanged Rust
|
|
148
|
+
* file. v12 also changes edges in the other direction: range-binding now
|
|
149
|
+
* RESOLVES import-disambiguated duplicate names (`for item in make()` /
|
|
150
|
+
* `let Struct { f } = ..` where a `use` or `use x::*` import pins one of several
|
|
151
|
+
* same-named definitions) to the imported definition's type. Both the removed
|
|
152
|
+
* spurious edges and these new resolved edges are cross-file, so a pre-v12
|
|
153
|
+
* top-up would leave unchanged Rust files stale either way; force a full
|
|
154
|
+
* re-analyze instead.
|
|
142
155
|
*/
|
|
143
|
-
export const INCREMENTAL_SCHEMA_VERSION =
|
|
156
|
+
export const INCREMENTAL_SCHEMA_VERSION = 12;
|
|
144
157
|
const GITNEXUS_DIR = '.gitnexus';
|
|
145
158
|
const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
|
|
146
159
|
export const INDEX_METADATA_FILE = 'gitnexus.json';
|
package/package.json
CHANGED