gitnexus 1.6.10-rc.77 → 1.6.10-rc.78

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.
@@ -3549,28 +3549,13 @@ export class LocalBackend {
3549
3549
  if (oldName === new_name) {
3550
3550
  return { error: 'New name is the same as the current name.' };
3551
3551
  }
3552
- // Step 2: Collect edits from graph (high confidence)
3553
- const changes = new Map();
3554
- const addEdit = (filePath, line, oldText, newText, confidence) => {
3555
- if (!changes.has(filePath)) {
3556
- changes.set(filePath, { file_path: filePath, edits: [] });
3557
- }
3558
- changes.get(filePath).edits.push({ line, old_text: oldText, new_text: newText, confidence });
3559
- };
3560
- // The definition itself
3561
- if (sym.filePath && sym.startLine) {
3562
- try {
3563
- const content = await fs.readFile(assertSafePath(sym.filePath), 'utf-8');
3564
- const lines = content.split('\n');
3565
- const lineIdx = sym.startLine - 1;
3566
- if (lineIdx >= 0 && lineIdx < lines.length && lines[lineIdx].includes(oldName)) {
3567
- const defRegex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
3568
- addEdit(sym.filePath, sym.startLine, lines[lineIdx].trim(), lines[lineIdx].replace(defRegex, new_name).trim(), 'graph');
3569
- }
3570
- }
3571
- catch (e) {
3572
- logQueryError('rename:read-definition', e);
3573
- }
3552
+ const escapedOldName = oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3553
+ // Classify each file to rewrite by how it was discovered. Definition and
3554
+ // graph-ref files carry graph confidence; files found only by text search
3555
+ // carry text_search confidence. A graph-classified file is never downgraded.
3556
+ const fileConfidence = new Map();
3557
+ if (sym.filePath) {
3558
+ fileConfidence.set(sym.filePath, 'graph');
3574
3559
  }
3575
3560
  // All incoming refs from graph (callers, importers, etc.)
3576
3561
  const allIncoming = [
@@ -3579,31 +3564,12 @@ export class LocalBackend {
3579
3564
  ...(lookupResult.incoming.extends || []),
3580
3565
  ...(lookupResult.incoming.implements || []),
3581
3566
  ];
3582
- let graphEdits = changes.size > 0 ? 1 : 0; // count definition edit
3583
3567
  for (const ref of allIncoming) {
3584
- if (!ref.filePath)
3585
- continue;
3586
- try {
3587
- const content = await fs.readFile(assertSafePath(ref.filePath), 'utf-8');
3588
- const lines = content.split('\n');
3589
- for (let i = 0; i < lines.length; i++) {
3590
- if (lines[i].includes(oldName)) {
3591
- addEdit(ref.filePath, i + 1, lines[i].trim(), lines[i]
3592
- .replace(new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'), new_name)
3593
- .trim(), 'graph');
3594
- graphEdits++;
3595
- break; // one edit per file from graph refs
3596
- }
3597
- }
3598
- }
3599
- catch (e) {
3600
- logQueryError('rename:read-ref', e);
3568
+ if (ref.filePath) {
3569
+ fileConfidence.set(ref.filePath, 'graph');
3601
3570
  }
3602
3571
  }
3603
- // Step 3: Text search for refs the graph might have missed
3604
- let astSearchEdits = 0;
3605
- const graphFiles = new Set([sym.filePath, ...allIncoming.map((r) => r.filePath)].filter(Boolean));
3606
- // Simple text search across the repo for the old name (in files not already covered by graph)
3572
+ // Text search for files the graph might have missed entirely.
3607
3573
  try {
3608
3574
  const { execFileSync } = await import('child_process');
3609
3575
  const rgArgs = [
@@ -3629,61 +3595,97 @@ export class LocalBackend {
3629
3595
  .filter((f) => f.length > 0);
3630
3596
  for (const file of files) {
3631
3597
  const normalizedFile = file.replace(/\\/g, '/').replace(/^\.\//, '');
3632
- if (graphFiles.has(normalizedFile))
3633
- continue; // already covered by graph
3634
- try {
3635
- const content = await fs.readFile(assertSafePath(normalizedFile), 'utf-8');
3636
- const lines = content.split('\n');
3637
- const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
3638
- for (let i = 0; i < lines.length; i++) {
3639
- regex.lastIndex = 0;
3640
- if (regex.test(lines[i])) {
3641
- regex.lastIndex = 0;
3642
- addEdit(normalizedFile, i + 1, lines[i].trim(), lines[i].replace(regex, new_name).trim(), 'text_search');
3643
- astSearchEdits++;
3644
- }
3645
- }
3646
- }
3647
- catch (e) {
3648
- logQueryError('rename:text-search-read', e);
3598
+ // Never downgrade a graph-classified file to text_search.
3599
+ if (!fileConfidence.has(normalizedFile)) {
3600
+ fileConfidence.set(normalizedFile, 'text_search');
3649
3601
  }
3650
3602
  }
3651
3603
  }
3652
3604
  catch (e) {
3653
3605
  logQueryError('rename:ripgrep', e);
3654
3606
  }
3655
- // Step 4: Apply or preview
3656
- const allChanges = Array.from(changes.values());
3657
- const totalEdits = allChanges.reduce((sum, c) => sum + c.edits.length, 0);
3607
+ // Enumerate every `\boldName\b` line in each file to rewrite, so the previewed
3608
+ // file set is exactly the set the apply loop below rewrites. A file with no
3609
+ // matching line is dropped (apply would write nothing to it). `wordTest`
3610
+ // (non-global) probes each line; `wordReplace` (global) rewrites it and is
3611
+ // reused by the apply loop — compiled once each rather than once per line,
3612
+ // and one escaping formula serves both passes.
3613
+ const wordTest = new RegExp(`\\b${escapedOldName}\\b`);
3614
+ const wordReplace = new RegExp(`\\b${escapedOldName}\\b`, 'g');
3615
+ const changes = new Map();
3616
+ for (const [filePath, confidence] of fileConfidence) {
3617
+ try {
3618
+ const content = await fs.readFile(assertSafePath(filePath), 'utf-8');
3619
+ const lines = content.split('\n');
3620
+ const edits = [];
3621
+ for (let i = 0; i < lines.length; i++) {
3622
+ if (!wordTest.test(lines[i])) {
3623
+ continue;
3624
+ }
3625
+ edits.push({
3626
+ line: i + 1,
3627
+ old_text: lines[i].trim(),
3628
+ new_text: lines[i].replace(wordReplace, new_name).trim(),
3629
+ confidence,
3630
+ });
3631
+ }
3632
+ if (edits.length > 0) {
3633
+ changes.set(filePath, { file_path: filePath, edits });
3634
+ }
3635
+ }
3636
+ catch (e) {
3637
+ logQueryError('rename:enumerate', e);
3638
+ }
3639
+ }
3640
+ // Step 4: Apply or preview.
3658
3641
  const failedFiles = [];
3659
3642
  if (!dry_run) {
3660
- // Apply edits to files
3661
- for (const change of allChanges) {
3643
+ for (const change of changes.values()) {
3662
3644
  try {
3663
3645
  const fullPath = assertSafePath(change.file_path);
3664
- let content = await fs.readFile(fullPath, 'utf-8');
3665
- const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
3666
- content = content.replace(regex, new_name);
3667
- await fs.writeFile(fullPath, content, 'utf-8');
3646
+ const content = await fs.readFile(fullPath, 'utf-8');
3647
+ await fs.writeFile(fullPath, content.replace(wordReplace, new_name), 'utf-8');
3668
3648
  }
3669
3649
  catch (e) {
3670
- // A swallowed write failure must not be reported as a full success
3671
- // (#2283): record the file so the result can degrade to 'partial'
3672
- // with the unwritten files listed, rather than masquerading as done.
3650
+ // A swallowed write failure must not be reported as success (#2283):
3651
+ // record the file so the result degrades to 'partial'.
3673
3652
  logQueryError('rename:apply-edit', e);
3674
3653
  failedFiles.push(change.file_path);
3675
3654
  }
3676
3655
  }
3656
+ // A file whose write threw did not land, so drop its edits from the
3657
+ // reported result — total_edits/changes must describe what actually
3658
+ // reached disk, not what was attempted (#2605: the report matches reality
3659
+ // even on partial failure). failed_files still names every dropped file.
3660
+ for (const f of failedFiles) {
3661
+ changes.delete(f);
3662
+ }
3663
+ }
3664
+ // Counts derive from the reported set (dry-run: every enumerated file;
3665
+ // apply: only files that landed), so the graph/text_search split always
3666
+ // sums to total_edits and never overstates a partial apply.
3667
+ const reported = Array.from(changes.values());
3668
+ let graphEdits = 0;
3669
+ let astSearchEdits = 0;
3670
+ for (const change of reported) {
3671
+ for (const edit of change.edits) {
3672
+ if (edit.confidence === 'graph') {
3673
+ graphEdits++;
3674
+ }
3675
+ else {
3676
+ astSearchEdits++;
3677
+ }
3678
+ }
3677
3679
  }
3678
3680
  return {
3679
3681
  status: failedFiles.length > 0 ? 'partial' : 'success',
3680
3682
  old_name: oldName,
3681
3683
  new_name,
3682
- files_affected: allChanges.length,
3683
- total_edits: totalEdits,
3684
+ files_affected: reported.length,
3685
+ total_edits: graphEdits + astSearchEdits,
3684
3686
  graph_edits: graphEdits,
3685
3687
  text_search_edits: astSearchEdits,
3686
- changes: allChanges,
3688
+ changes: reported,
3687
3689
  applied: !dry_run,
3688
3690
  ...(failedFiles.length > 0 && { failed_files: failedFiles }),
3689
3691
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.77",
3
+ "version": "1.6.10-rc.78",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",