mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
AGENTS.md and CLAUDE.md are the agent guides teams commit, and the injected
block carried live symbol/relationship/flow counts. Those counts move with any
code change, so every reindex rewrote a tracked file and produced a spurious
diff that had to be restored by hand before committing real work.
The write is now skipped when the volatile counts are the only delta. Counts are
substituted with placeholders — not deleted — before the comparison, so
--no-stats REMOVING the parenthetical is still a material change that writes
through; only a numbers-only difference is suppressed. Both the verbose path and
the gitnexus:keep path go through the same rule, and a project rename, a template
change, or a base_ref change still rewrites as before. Live counts remain
available from `gitnexus status` and `gitnexus://repo/{name}/context`.
Two smaller churn sources go with it:
- The file was CREATED without a trailing newline while every update path writes
`.trim() + '\n'`, so the analyze right after committing a freshly created
AGENTS.md dirtied it purely to append that newline.
- `--no-stats` left the per-cluster `(N symbols)` counts in the skills table,
which are exactly as volatile as the header parenthetical the flag removes.
The stale-index hook recommended plain `gitnexus analyze` — the variant that
rewrites those tracked docs — so an agent following the nudge verbatim reindexed
with the most invasive flags. `formatAnalyzeCommand` takes `indexOnly` and the
three hook call sites (Claude, plugin copy, Antigravity) pass it; the injected
"Index stale?" line and the MCP context resource's `re_index` hint name the same
`--index-only` form. Full `analyze` stays the documented way to refresh the docs
and skills.
Both resolve-analyze-cmd.cjs copies stay byte-identical.
Claude-Session: https://claude.ai/code/session_019d85r7TrMYjWbTUT3pXccS
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d2c2f68ee
commit
22d3c2ad74
11 changed files with 223 additions and 23 deletions
|
|
@ -543,7 +543,7 @@ function handlePostToolUse(input) {
|
|||
// If HEAD matches last indexed commit, no reindex needed
|
||||
if (currentHead && currentHead === lastCommit) return;
|
||||
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings, indexOnly: true });
|
||||
sendHookResponse(
|
||||
'PostToolUse',
|
||||
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
||||
|
|
|
|||
|
|
@ -276,7 +276,13 @@ function formatBunxCommand(gitnexusArgs) {
|
|||
}
|
||||
|
||||
function formatAnalyzeCommand(options = {}, deps = {}) {
|
||||
const suffix = options.embeddings ? ' --embeddings' : '';
|
||||
// `--index-only` is what a routine "your index is stale" nudge wants: it
|
||||
// reindexes without rewriting AGENTS.md / CLAUDE.md / skills, so an agent
|
||||
// following the nudge on every commit cannot churn the tracked agent guides
|
||||
// (#2907). Callers that actually want the docs refreshed omit it.
|
||||
const suffix = `${options.indexOnly ? ' --index-only' : ''}${
|
||||
options.embeddings ? ' --embeddings' : ''
|
||||
}`;
|
||||
// Keep the stale-index hook budget tight by querying each tool at most once.
|
||||
// The memoized `probe` is a spawn-free PATH scan (resolveOnPath) shared with
|
||||
// resolveInvocationMode, so `gitnexus` is scanned only once and no subprocess
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ function buildStaleIndexHint(gitNexusDir, cwd) {
|
|||
|
||||
if (currentHead === lastCommit) return '';
|
||||
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings, indexOnly: true });
|
||||
return (
|
||||
`[GitNexus] index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
||||
`Run \`${analyzeCmd}\` to refresh the knowledge graph.`
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ function handlePostToolUse(input) {
|
|||
// If HEAD matches last indexed commit, no reindex needed
|
||||
if (currentHead && currentHead === lastCommit) return;
|
||||
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
|
||||
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings, indexOnly: true });
|
||||
sendHookResponse(
|
||||
'PostToolUse',
|
||||
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
||||
|
|
|
|||
|
|
@ -276,7 +276,13 @@ function formatBunxCommand(gitnexusArgs) {
|
|||
}
|
||||
|
||||
function formatAnalyzeCommand(options = {}, deps = {}) {
|
||||
const suffix = options.embeddings ? ' --embeddings' : '';
|
||||
// `--index-only` is what a routine "your index is stale" nudge wants: it
|
||||
// reindexes without rewriting AGENTS.md / CLAUDE.md / skills, so an agent
|
||||
// following the nudge on every commit cannot churn the tracked agent guides
|
||||
// (#2907). Callers that actually want the docs refreshed omit it.
|
||||
const suffix = `${options.indexOnly ? ' --index-only' : ''}${
|
||||
options.embeddings ? ' --embeddings' : ''
|
||||
}`;
|
||||
// Keep the stale-index hook budget tight by querying each tool at most once.
|
||||
// The memoized `probe` is a spawn-free PATH scan (resolveOnPath) shared with
|
||||
// resolveInvocationMode, so `gitnexus` is scanned only once and no subprocess
|
||||
|
|
|
|||
|
|
@ -157,7 +157,10 @@ export function generateGitNexusContent(
|
|||
? generatedSkills
|
||||
.map(
|
||||
(s) =>
|
||||
`| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/${s.name}/SKILL.md\` |`,
|
||||
// The per-cluster count is as volatile as the header parenthetical,
|
||||
// so --no-stats drops it too (#2907) — otherwise the flag that
|
||||
// promises "omit volatile symbol counts" left a churning one behind.
|
||||
`| Work in the ${s.label} area${noStats ? '' : ` (${s.symbolCount} symbols)`} | \`.claude/skills/${s.name}/SKILL.md\` |`,
|
||||
)
|
||||
.join('\n')
|
||||
: '';
|
||||
|
|
@ -200,7 +203,7 @@ ${tableBody}`
|
|||
|
||||
This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows)`}. Use GitNexus graph tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> Index stale? Run \`${runner} analyze\` from the project root — it auto-selects an available runner. ${bootstrapNote}
|
||||
> Index stale? Run \`${runner} analyze --index-only\` from the project root — it auto-selects an available runner. ${bootstrapNote}
|
||||
|
||||
## Always Do
|
||||
|
||||
|
|
@ -267,11 +270,33 @@ async function fileExists(filePath: string): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the block's volatile counts — the header parenthetical and the
|
||||
* per-cluster symbol counts in the skills table — with fixed placeholders, so
|
||||
* two renderings that differ only in those numbers compare equal.
|
||||
*
|
||||
* Placeholders rather than deletions: `--no-stats` REMOVES the parenthetical,
|
||||
* which must still be written through. Deleting instead of substituting would
|
||||
* make a with-counts block and a without-counts block compare equal, and the
|
||||
* flag would silently stop taking effect on an already-injected file.
|
||||
*/
|
||||
function stripVolatileCounts(section: string): string {
|
||||
return section
|
||||
.replace(/ \(\d+ symbols, \d+ relationships, \d+ execution flows\)/g, ' (<counts>)')
|
||||
.replace(/ \(\d+ symbols\)/g, ' (<count>)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update GitNexus section in a file
|
||||
* - If file doesn't exist: create with GitNexus content
|
||||
* - If file exists without GitNexus section: append
|
||||
* - If file exists with GitNexus section: replace that section
|
||||
* - If file exists with GitNexus section: replace that section, UNLESS the only
|
||||
* delta is the volatile counts (#2907). AGENTS.md and CLAUDE.md are the agent
|
||||
* guides teams commit, and the counts move with any code change, so a
|
||||
* count-only rewrite dirties a tracked file on every reindex for no reader
|
||||
* benefit. Live counts stay available from `gitnexus status` and
|
||||
* `gitnexus://repo/{name}/context`; the committed block keeps whichever
|
||||
* numbers it was last materially updated with.
|
||||
*/
|
||||
async function upsertGitNexusSection(
|
||||
filePath: string,
|
||||
|
|
@ -283,7 +308,10 @@ async function upsertGitNexusSection(
|
|||
const exists = await fileExists(filePath);
|
||||
|
||||
if (!exists) {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
// Same `.trim() + '\n'` shape the update paths write. Creating without the
|
||||
// trailing newline made the NEXT analyze dirty a freshly committed file
|
||||
// even at unchanged counts, purely to append it (#2907).
|
||||
await fs.writeFile(filePath, content.trim() + '\n', 'utf-8');
|
||||
return 'created';
|
||||
}
|
||||
|
||||
|
|
@ -344,6 +372,11 @@ async function upsertGitNexusSection(
|
|||
|
||||
if (statsPattern.test(existingSection)) {
|
||||
const updatedSection = existingSection.replace(statsPattern, statsLine);
|
||||
// Count-only delta — leave the committed lean block alone (#2907). A
|
||||
// project rename, or --no-stats dropping the parenthetical, still writes.
|
||||
if (stripVolatileCounts(updatedSection) === stripVolatileCounts(existingSection)) {
|
||||
return 'preserved';
|
||||
}
|
||||
const before = existingContent.substring(0, startIdx);
|
||||
const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
|
||||
await fs.writeFile(filePath, (before + updatedSection + after).trim() + '\n', 'utf-8');
|
||||
|
|
@ -355,7 +388,11 @@ async function upsertGitNexusSection(
|
|||
return 'preserved';
|
||||
}
|
||||
|
||||
// No keep marker — replace existing section with full verbose content
|
||||
// No keep marker — replace existing section with full verbose content,
|
||||
// unless the counts are the only thing that moved (#2907).
|
||||
if (stripVolatileCounts(existingSection) === stripVolatileCounts(content)) {
|
||||
return 'preserved';
|
||||
}
|
||||
const before = existingContent.substring(0, startIdx);
|
||||
const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
|
||||
const newContent = before + content + after;
|
||||
|
|
|
|||
|
|
@ -375,7 +375,10 @@ async function getContextResource(backend: LocalBackend, repoName?: string): Pro
|
|||
lines.push(' - cypher: Raw graph queries');
|
||||
lines.push(' - list_repos: Discover all indexed repositories');
|
||||
lines.push('');
|
||||
lines.push('re_index: Run `npx gitnexus analyze` in terminal if data is stale');
|
||||
lines.push(
|
||||
're_index: Run `npx gitnexus analyze --index-only` in terminal if data is stale ' +
|
||||
'(drop --index-only to also refresh AGENTS.md/CLAUDE.md and skills)',
|
||||
);
|
||||
lines.push('');
|
||||
lines.push('resources_available:');
|
||||
lines.push(' - gitnexus://repos: All indexed repositories');
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ describe('antigravity hook adapter e2e', () => {
|
|||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.additionalContext).toContain('Run `gitnexus analyze`');
|
||||
expect(output!.additionalContext).toContain('Run `gitnexus analyze --index-only`');
|
||||
expect(output!.additionalContext).not.toContain('npx gitnexus');
|
||||
} finally {
|
||||
gn.cleanup();
|
||||
|
|
@ -240,7 +240,9 @@ describe('antigravity hook adapter e2e', () => {
|
|||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.additionalContext).toContain('npx gitnexus@latest analyze --embeddings');
|
||||
expect(output!.additionalContext).toContain(
|
||||
'npx gitnexus@latest analyze --index-only --embeddings',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers gitnexus.json over meta.json when both are present (dual-write steady state)', () => {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ describe.each(HOOKS)('hooks e2e ($name)', ({ name, path: hookPath }) => {
|
|||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.additionalContext).toContain('Run `gitnexus analyze`');
|
||||
expect(output!.additionalContext).toContain('Run `gitnexus analyze --index-only`');
|
||||
expect(output!.additionalContext).not.toContain('npx gitnexus');
|
||||
} finally {
|
||||
gn.cleanup();
|
||||
|
|
@ -173,7 +173,9 @@ describe.each(HOOKS)('hooks e2e ($name)', ({ name, path: hookPath }) => {
|
|||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.additionalContext).toContain('Run `gitnexus analyze --embeddings`');
|
||||
expect(output!.additionalContext).toContain(
|
||||
'Run `gitnexus analyze --index-only --embeddings`',
|
||||
);
|
||||
expect(output!.additionalContext).not.toContain('npx gitnexus');
|
||||
} finally {
|
||||
gn.cleanup();
|
||||
|
|
@ -231,7 +233,9 @@ describe.each(HOOKS)('hooks e2e ($name)', ({ name, path: hookPath }) => {
|
|||
|
||||
const output = parseHookOutput(result.stdout);
|
||||
expect(output).not.toBeNull();
|
||||
expect(output!.additionalContext).toContain('npx gitnexus@latest analyze --embeddings');
|
||||
expect(output!.additionalContext).toContain(
|
||||
'npx gitnexus@latest analyze --index-only --embeddings',
|
||||
);
|
||||
});
|
||||
|
||||
it('treats missing meta.json as stale', () => {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe('generateAIContextFiles', () => {
|
|||
for (const f of ['CLAUDE.md', 'AGENTS.md']) {
|
||||
const content = await fs.readFile(path.join(subDir, f), 'utf-8');
|
||||
// Primary command is the fixed project-local runner, not machine-resolved.
|
||||
expect(content).toContain('`node .gitnexus/run.cjs analyze`');
|
||||
expect(content).toContain('`node .gitnexus/run.cjs analyze --index-only`');
|
||||
expect(content).not.toContain('run `gitnexus analyze`'); // no machine-resolved leak
|
||||
// Bootstrap path (for a not-yet-analyzed checkout) + npm-11 escape hatch.
|
||||
// Every install-free runner is named, so a machine without npm (bun-only)
|
||||
|
|
@ -272,7 +272,7 @@ describe('generateAIContextFiles', () => {
|
|||
|
||||
const content = await fs.readFile(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
||||
|
||||
expect(content).toContain('Index stale? Run `node .gitnexus/run.cjs analyze`');
|
||||
expect(content).toContain('Index stale? Run `node .gitnexus/run.cjs analyze --index-only`');
|
||||
expect(content).toContain('## Always Do');
|
||||
expect(content).toContain('## Never Do');
|
||||
expect(content).toContain('## Resources');
|
||||
|
|
@ -367,7 +367,7 @@ Some project docs here.
|
|||
<!-- gitnexus:keep -->
|
||||
# GitNexus — Code Knowledge Graph
|
||||
|
||||
Indexed as **TestProject** (50 symbols, 100 relationships, 5 execution flows). MCP tools.
|
||||
Indexed as **OldName** (50 symbols, 100 relationships, 5 execution flows). MCP tools.
|
||||
|
||||
| Tool | Use for |
|
||||
|------|---------|
|
||||
|
|
@ -378,7 +378,9 @@ Resources: gitnexus://repo/TestProject/context
|
|||
`;
|
||||
await fs.writeFile(claudeMdPath, customContent, 'utf-8');
|
||||
|
||||
// Run analyze with new stats — should only update the stats line
|
||||
// Run analyze with new stats — should only update the stats line. The seed
|
||||
// carries a stale project NAME because a counts-only delta is now preserved
|
||||
// rather than written (#2907); the rename is what makes this a real update.
|
||||
const stats = { nodes: 999, edges: 1234, processes: 42 };
|
||||
await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats);
|
||||
|
||||
|
|
@ -967,7 +969,7 @@ Project-specific agent guidance.
|
|||
<!-- gitnexus:keep -->
|
||||
# GitNexus context for AGENTS
|
||||
|
||||
Indexed as **AgentsTest** (10 symbols, 20 relationships, 1 execution flows).
|
||||
Indexed as **AgentsOldName** (10 symbols, 20 relationships, 1 execution flows).
|
||||
|
||||
Use 'query' for finding flows, 'context' for symbol details.
|
||||
<!-- gitnexus:end -->
|
||||
|
|
@ -1032,7 +1034,7 @@ Indexed as **Idem** (1 symbols, 2 relationships, 3 execution flows). Custom.
|
|||
'\r\n' +
|
||||
'<!-- gitnexus:start -->\r\n' +
|
||||
'<!-- gitnexus:keep -->\r\n' +
|
||||
'Indexed as **CRLFTest** (5 symbols, 6 relationships, 7 execution flows). Custom CRLF.\r\n' +
|
||||
'Indexed as **CRLFOldName** (5 symbols, 6 relationships, 7 execution flows). Custom CRLF.\r\n' +
|
||||
'<!-- gitnexus:end -->\r\n';
|
||||
await fs.writeFile(claudePath, crlfContent, 'utf-8');
|
||||
|
||||
|
|
@ -1365,3 +1367,133 @@ Indexed as **P**. Custom.
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// AGENTS.md and CLAUDE.md are the agent guides teams commit, so a rewrite whose
|
||||
// only delta is the volatile counts dirties a tracked file on every reindex
|
||||
// (#2907). These assert the write is skipped for a count-only delta and still
|
||||
// happens for every material one.
|
||||
describe('count-only reindex does not churn the committed block (#2907)', () => {
|
||||
let dir: string;
|
||||
let storage: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-2907-'));
|
||||
storage = path.join(dir, '.gitnexus');
|
||||
await fs.mkdir(storage, { recursive: true });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const read = (file: string): Promise<string> => fs.readFile(path.join(dir, file), 'utf-8');
|
||||
|
||||
it('creates the file with a trailing newline so the next analyze has nothing to append', async () => {
|
||||
await generateAIContextFiles(dir, storage, 'P', { nodes: 10, edges: 20, processes: 3 });
|
||||
expect(await read('CLAUDE.md')).toMatch(/<!-- gitnexus:end -->\n$/);
|
||||
expect(await read('AGENTS.md')).toMatch(/<!-- gitnexus:end -->\n$/);
|
||||
});
|
||||
|
||||
it('leaves both files byte-identical when only the counts moved', async () => {
|
||||
const before = { claude: await read('CLAUDE.md'), agents: await read('AGENTS.md') };
|
||||
|
||||
const result = await generateAIContextFiles(dir, storage, 'P', {
|
||||
nodes: 999999,
|
||||
edges: 888888,
|
||||
processes: 777,
|
||||
});
|
||||
|
||||
expect(await read('CLAUDE.md')).toBe(before.claude);
|
||||
expect(await read('AGENTS.md')).toBe(before.agents);
|
||||
expect(result.files).toContain('CLAUDE.md (preserved)');
|
||||
expect(result.files).toContain('AGENTS.md (preserved)');
|
||||
// The counts the block was created with are the ones still on disk.
|
||||
expect(before.claude).toContain('(10 symbols, 20 relationships, 3 execution flows)');
|
||||
});
|
||||
|
||||
it('still rewrites when something other than the counts changed', async () => {
|
||||
const result = await generateAIContextFiles(dir, storage, 'RenamedProject', {
|
||||
nodes: 10,
|
||||
edges: 20,
|
||||
processes: 3,
|
||||
});
|
||||
|
||||
expect(result.files).toContain('CLAUDE.md (updated)');
|
||||
expect(await read('CLAUDE.md')).toContain('**RenamedProject**');
|
||||
});
|
||||
|
||||
it('still applies --no-stats to an already-injected block', async () => {
|
||||
const result = await generateAIContextFiles(
|
||||
dir,
|
||||
storage,
|
||||
'RenamedProject',
|
||||
{ nodes: 10, edges: 20, processes: 3 },
|
||||
undefined,
|
||||
{ noStats: true },
|
||||
);
|
||||
|
||||
expect(result.files).toContain('CLAUDE.md (updated)');
|
||||
const content = await read('CLAUDE.md');
|
||||
expect(content).toContain('indexed by GitNexus as **RenamedProject**.');
|
||||
expect(content).not.toContain('(10 symbols, 20 relationships, 3 execution flows)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('--no-stats drops the per-cluster symbol counts too (#2907)', () => {
|
||||
const stats = { nodes: 10, edges: 20, processes: 3 };
|
||||
const skills = [{ label: 'ingestion', name: 'p-ingestion', symbolCount: 120 }];
|
||||
|
||||
it('omits the count under --no-stats and keeps it otherwise', () => {
|
||||
const lean = generateGitNexusContent('P', stats, { generatedSkills: skills, noStats: true });
|
||||
const full = generateGitNexusContent('P', stats, { generatedSkills: skills });
|
||||
|
||||
expect(lean).toContain(
|
||||
'| Work in the ingestion area | `.claude/skills/p-ingestion/SKILL.md` |',
|
||||
);
|
||||
expect(lean).not.toContain('(120 symbols)');
|
||||
expect(full).toContain(
|
||||
'| Work in the ingestion area (120 symbols) | `.claude/skills/p-ingestion/SKILL.md` |',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keep-marker blocks follow the same count-only rule (#2907)', () => {
|
||||
const seed = (name: string, counts: string): string => `# Guide
|
||||
|
||||
<!-- gitnexus:start -->
|
||||
<!-- gitnexus:keep -->
|
||||
Indexed as **${name}**${counts}. Lean block.
|
||||
<!-- gitnexus:end -->
|
||||
`;
|
||||
|
||||
it('preserves on a count-only delta and updates on a rename', async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-2907-keep-'));
|
||||
const storage = path.join(dir, '.gitnexus');
|
||||
await fs.mkdir(storage, { recursive: true });
|
||||
try {
|
||||
const original = seed('P', ' (10 symbols, 20 relationships, 3 execution flows)');
|
||||
await fs.writeFile(path.join(dir, 'CLAUDE.md'), original, 'utf-8');
|
||||
await fs.writeFile(path.join(dir, 'AGENTS.md'), original, 'utf-8');
|
||||
|
||||
const preserved = await generateAIContextFiles(dir, storage, 'P', {
|
||||
nodes: 55,
|
||||
edges: 66,
|
||||
processes: 7,
|
||||
});
|
||||
expect(preserved.files).toContain('CLAUDE.md (preserved)');
|
||||
expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toBe(original);
|
||||
|
||||
const renamed = await generateAIContextFiles(dir, storage, 'Q', {
|
||||
nodes: 55,
|
||||
edges: 66,
|
||||
processes: 7,
|
||||
});
|
||||
expect(renamed.files).toContain('CLAUDE.md (updated)');
|
||||
expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toContain(
|
||||
'Indexed as **Q** (55 symbols, 66 relationships, 7 execution flows). Lean block.',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const PLUGIN_CJS = path.resolve(
|
|||
|
||||
interface CjsModule {
|
||||
formatAnalyzeCommand: (
|
||||
o?: { embeddings?: boolean },
|
||||
o?: { embeddings?: boolean; indexOnly?: boolean },
|
||||
deps?: { npmMajor?: number | null; pnpmMajor?: number | null; pnpmMinor?: number | null },
|
||||
) => string;
|
||||
formatBunxCommand: (args: string) => string;
|
||||
|
|
@ -115,6 +115,16 @@ describe('resolve-analyze-cmd.cjs (canonical invocation resolver)', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('appends --index-only for the routine stale-index nudge (#2907)', () => {
|
||||
process.env.GITNEXUS_INVOCATION = 'gitnexus';
|
||||
expect(cjs.formatAnalyzeCommand({ indexOnly: true })).toBe('gitnexus analyze --index-only');
|
||||
expect(cjs.formatAnalyzeCommand({ indexOnly: true, embeddings: true })).toBe(
|
||||
'gitnexus analyze --index-only --embeddings',
|
||||
);
|
||||
// Absent/false leaves the doc-refreshing form untouched.
|
||||
expect(cjs.formatAnalyzeCommand({ indexOnly: false })).toBe('gitnexus analyze');
|
||||
});
|
||||
|
||||
it('auto-selects global gitnexus first', () => {
|
||||
expect(cjs.resolveInvocationMode(() => '/usr/local/bin/gitnexus')).toBe('gitnexus');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue