gitnexus 1.6.13-rc.46 → 1.6.13-rc.47

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.
@@ -1605,6 +1605,36 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1605
1605
  // fast path because the previous analyze just wrote them
1606
1606
  // (regression vs PR #1233 behavior).
1607
1607
  const dirty = isWorkingTreeDirty(repoPath);
1608
+ // A clean porcelain status is not enough when the previous index captured
1609
+ // uncommitted content at this same HEAD: assume-unchanged and skip-worktree
1610
+ // paths are deliberately absent from porcelain. Re-hash only the paths
1611
+ // recorded dirty by the previous run. Matching hashes mean the index still
1612
+ // describes disk and may take the fast path; a mismatch (including an
1613
+ // unreadable/deleted file) must fall through to incremental reconciliation.
1614
+ const indexedDirtyPaths = existingMeta.indexCoverage?.dirtyPaths ?? [];
1615
+ let indexedContentChanged = indexedDirtyPaths.length > 0;
1616
+ let reconciledCleanCoverage = false;
1617
+ if (!dirty && existingMeta.fileHashes) {
1618
+ // Porcelain also hides newly edited assume-unchanged/skip-worktree
1619
+ // paths that were absent from the previous receipt. Include the live
1620
+ // hidden-path set in the bounded comparison so those edits cannot take
1621
+ // the fast path and publish stale content for HEAD.
1622
+ const liveDirtyPaths = listWorkingTreeDirtyPaths(repoPath);
1623
+ if (liveDirtyPaths === null) {
1624
+ indexedContentChanged = true;
1625
+ }
1626
+ else {
1627
+ const pathsToCheck = [...new Set([...indexedDirtyPaths, ...liveDirtyPaths])].filter((rel) => existingMeta.fileHashes?.[rel] !== undefined);
1628
+ const currentDirtyHashes = await computeFileHashes(repoPath, pathsToCheck);
1629
+ indexedContentChanged = pathsToCheck.some((rel) => currentDirtyHashes.get(rel) !== existingMeta.fileHashes?.[rel]);
1630
+ // A mode-only dirty snapshot can leave a coverage receipt even though
1631
+ // restoring the mode makes porcelain clean and content hashes equal.
1632
+ // Clear that receipt before taking the fast path; otherwise shared
1633
+ // publication remains blocked forever despite a clean checkout.
1634
+ reconciledCleanCoverage =
1635
+ !indexedContentChanged && indexedDirtyPaths.length > 0 && liveDirtyPaths.length === 0;
1636
+ }
1637
+ }
1608
1638
  // Registration wrinkle around the fast path (#2264). A prior
1609
1639
  // `analyze --name X` that hit a name collision writes meta.json (meta-save
1610
1640
  // runs before registerRepo) then fails before registering, leaving the
@@ -1618,6 +1648,20 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1618
1648
  // possibly half-finalized index. `isRepoRegistered` is only read on the
1619
1649
  // opt-in branch so the common fast path keeps its single-stat cost.
1620
1650
  const healUnregistered = options.allowDuplicateName === true && !(await isRepoRegistered(repoPath));
1651
+ if (reconciledCleanCoverage) {
1652
+ try {
1653
+ existingMeta.indexCoverage = {
1654
+ ...existingMeta.indexCoverage,
1655
+ dirtyPaths: [],
1656
+ };
1657
+ await saveMeta(metaDir, existingMeta);
1658
+ }
1659
+ catch {
1660
+ // If the receipt cannot be persisted, fall through to the normal
1661
+ // reconciliation path instead of returning with stale metadata.
1662
+ indexedContentChanged = true;
1663
+ }
1664
+ }
1621
1665
  // §5.C is deliberately NOT self-healed here. An #2841 FTS-forced rebuild
1622
1666
  // stamps `lastCommit`, so a plain rerun lands on this fast path and the
1623
1667
  // search indexes stay missing until the next content change. The fix for
@@ -1633,7 +1677,7 @@ async function runFullAnalysisInner(repoPath, options, callbacks, writeTarget, c
1633
1677
  // re-analysis whenever an index authored where FTS was unavailable was
1634
1678
  // later read on a host where it loads — which is a legitimate, common
1635
1679
  // state, and the invariant `analyzer-identity-cli.test.ts` pins.
1636
- if (!dirty && !healUnregistered) {
1680
+ if (!dirty && !indexedContentChanged && !healUnregistered) {
1637
1681
  const processDetectionStamp = existingMeta.processDetection ?? toProcessDetectionStamp(processDetectionBudget);
1638
1682
  if (options.registryName) {
1639
1683
  await registerRepo(repoPath, existingMeta, {
@@ -321,9 +321,8 @@ export const publishSharedGraph = async (layout, repoPath, currentCommit, log) =
321
321
  if (!meta)
322
322
  return;
323
323
  const own = path.join(slot, LBUG_DIRECTORY);
324
- // A graph built while files were dirty still holds those edits even after
325
- // they are reverted (the up-to-date path does not re-diff a clean tree), so
326
- // only a graph whose build saw no dirty covered file may become shared.
324
+ // A graph built while files were dirty must not become shared until analyze
325
+ // has reconciled those paths with disk and cleared them from the coverage receipt.
327
326
  const builtClean = (meta.indexCoverage?.dirtyPaths ?? []).length === 0;
328
327
  const shareable = currentCommit !== '' &&
329
328
  meta.lastCommit === currentCommit &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.46",
3
+ "version": "1.6.13-rc.47",
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",