Skip to content

SUP-2243 Guard vault root duplicate notes - #69

Merged
levineam merged 1 commit into
mainfrom
SUP-2243/vault-root-duplicate-guard
Jun 1, 2026
Merged

SUP-2243 Guard vault root duplicate notes#69
levineam merged 1 commit into
mainfrom
SUP-2243/vault-root-duplicate-guard

Conversation

@levineam

@levineam levineam commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a zero-byte vault-root duplicate guard for canonical secondbrain note paths
  • repair matching root duplicates from both write-to-vault and link-to-journal
  • keep orphan root files untouched unless a populated Notes/ counterpart exists
  • add regression tests for direct guard behavior, canonical writes, and journal backlinks

Landing rationale

This is the portable jarvOS fix. The local clawd copy needs the same active-runtime backport until this upstream change is merged and consumed downstream.

Verification

  • node --test modules/jarvos-secondbrain/tests/vault-root-duplicate-guard.test.js modules/jarvos-secondbrain/tests/link-to-journal.test.js modules/jarvos-secondbrain/tests/keyword-capture-router.test.js
  • npm test
  • node --check changed JS/test files
  • git diff --check

Summary by CodeRabbit

  • Bug Fixes

    • Added automatic detection and cleanup of zero-byte duplicate vault files that shadow canonical notes
    • Repair runs automatically when writing notes or linking them to journals
  • Tests

    • Comprehensive test coverage added for vault duplicate detection and repair scenarios

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a zero-byte vault-root duplicate guard that prevents Obsidian note shadowing. A new module exports the repair function and path/title helpers, integrated into note writing and journal linking to detect and safely delete stale root duplicates. Comprehensive tests validate direct and integrated behavior.

Changes

Vault Root Duplicate Guard

Layer / File(s) Summary
Guard function implementation
modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/lib/vault-root-duplicate-guard.js
New module exports repairZeroByteVaultRootDuplicate() that resolves canonical vs. root file paths, checks if root is zero-byte while canonical Notes file is non-empty, and unlinks stale root duplicates via fs.unlinkSync(). Also exports sanitizeTitle() and vaultRootForNotesDir() helpers. Returns structured status objects indicating whether the duplicate was checked, repaired, and why.
Integration with note writing
modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/write-to-vault.js
Note writer imports repairZeroByteVaultRootDuplicate and calls it immediately after writing the note file with the note title and vault paths. Captured repair status is included in the returned payload as vaultRootDuplicate.
Integration with journal linking
modules/jarvos-secondbrain/bridge/provenance/src/link-to-journal.js
Journal link module imports the guard function and vault directory helpers (getVaultDir, getVaultJournalDir, getVaultNotesDir). Calls repairZeroByteVaultRootDuplicate with note title and vault paths, and extends the return object to include vaultRootDuplicate status alongside existing linked, journalPath, and alreadyPresent fields.
Comprehensive test suite
modules/jarvos-secondbrain/tests/vault-root-duplicate-guard.test.js
Test file creates isolated temporary vault fixtures with Notes and Journal directories, provides makeVault() and withEnv() helpers, and validates: guard deletes zero-byte root duplicates only when matching Notes file is populated; orphan zero-byte files are left intact; writeNoteFile repairs duplicates after writing; linkNoteToJournal repairs duplicates and writes correct backlink markup into journal entry.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant repairZeroByteVaultRootDuplicate
  participant Filesystem as fs (stat/unlink)

  Caller->>repairZeroByteVaultRootDuplicate: call with noteTitle, vault paths
  repairZeroByteVaultRootDuplicate->>Filesystem: stat canonical Notes path
  Filesystem-->>repairZeroByteVaultRootDuplicate: file exists, size
  repairZeroByteVaultRootDuplicate->>Filesystem: stat vault root path
  Filesystem-->>repairZeroByteVaultRootDuplicate: file exists, size
  alt Root is zero-byte AND Notes is non-empty
    repairZeroByteVaultRootDuplicate->>Filesystem: unlink zero-byte root
    Filesystem-->>repairZeroByteVaultRootDuplicate: success
    repairZeroByteVaultRootDuplicate-->>Caller: {repaired: true}
  else Conditions not met
    repairZeroByteVaultRootDuplicate-->>Caller: {repaired: false, reason}
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • levineam/jarvOS#41: Both PRs modify modules/jarvos-secondbrain/bridge/provenance/src/link-to-journal.js — the retrieved PR refactors linkNoteToJournal for canonical backlink insertion, while this PR extends the same flow to call repairZeroByteVaultRootDuplicate and add vaultRootDuplicate to the return payload.
  • levineam/jarvOS#50: Consumes noteWriter.writeNoteFile()'s journal/linking result shape, which this PR modifies by extending linkNoteToJournal() to include vaultRootDuplicate after repairing zero-byte vault-root duplicates.

Poem

🐰 A zero-byte ghost haunts the vault root,
Shadowing Notes that spring from good fruit,
Our guard sweeps it clean with surgical care—
The duplicate falls, the canonical's there! 📝✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'SUP-2243 Guard vault root duplicate notes' accurately and concisely summarizes the main change: adding a guard against zero-byte vault root duplicate notes across vault operations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SUP-2243/vault-root-duplicate-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0dac56762c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +133 to +137
const vaultRootDuplicate = repairZeroByteVaultRootDuplicate({
noteTitle: safeName,
notesDir,
notesFilePath: filePath,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the configured vault root to the writer guard

When JARVOS_NOTES_DIR or paths.notes points to a folder that is not literally named Notes, this call cannot infer the vault root from notesDir, so repairZeroByteVaultRootDuplicate returns checked: false and leaves the zero-byte root duplicate behind. This shows up especially when journal backlinking is disabled (JARVOS_JOURNAL_BACKLINK=0), because the linker path does pass getVaultDir() and therefore is the only path that would repair custom note directories.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/lib/vault-root-duplicate-guard.js`:
- Around line 56-72: The guard currently lets fileStat and fs.unlinkSync throw
and abort the calling flow; modify the function that uses fileStat and
fs.unlinkSync (vault-root-duplicate-guard.js) to make cleanup best-effort by
wrapping the fileStat(canonicalPath) and fs.unlinkSync(rootPath) calls in
try/catch blocks: if fileStat throws treat it as a non-fatal “not repaired”
result (return checked:true, repaired:false and include the error message in
reason), and when unlinkSync throws catch the error, log or include the error in
the returned reason and return checked:true, repaired:false instead of
rethrowing; keep existing successful-path behavior when no errors occur.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8af8568-8334-43f2-81e8-c3e60b0bcfb8

📥 Commits

Reviewing files that changed from the base of the PR and between 5624e0f and 0dac567.

📒 Files selected for processing (4)
  • modules/jarvos-secondbrain/bridge/provenance/src/link-to-journal.js
  • modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/lib/vault-root-duplicate-guard.js
  • modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/write-to-vault.js
  • modules/jarvos-secondbrain/tests/vault-root-duplicate-guard.test.js

Comment on lines +56 to +72
const rootStat = fileStat(rootPath);
if (!rootStat) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'no root duplicate' };
}

if (rootStat.size !== 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'root duplicate is not zero-byte' };
}

const canonicalStat = fileStat(canonicalPath);
if (!canonicalStat || canonicalStat.size === 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'matching populated Notes file not found' };
}

fs.unlinkSync(rootPath);
return { checked: true, repaired: true, rootPath, notesPath: canonicalPath, reason: 'removed zero-byte vault-root duplicate' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the repair best-effort so cleanup failures don't abort the primary flow.

This guard runs inline on the main write path (write-to-vault.js line 133, after the note is already persisted) and the journal-link path (link-to-journal.js line 57). Both fileStat (which rethrows any non-ENOENT stat error) and fs.unlinkSync can throw on transient/permission conditions (e.g., EPERM/EBUSY/races). If that happens, writeNoteFile/linkNoteToJournal will throw and report the entire operation as failed even though the note was written successfully. Since this is a cleanup guard, consider catching and surfacing the error as a non-fatal status instead of propagating.

🛡️ One option: wrap the unlink so failures degrade to a status
-  fs.unlinkSync(rootPath);
-  return { checked: true, repaired: true, rootPath, notesPath: canonicalPath, reason: 'removed zero-byte vault-root duplicate' };
+  try {
+    fs.unlinkSync(rootPath);
+  } catch (error) {
+    return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: `unlink failed: ${error.code || error.message}` };
+  }
+  return { checked: true, repaired: true, rootPath, notesPath: canonicalPath, reason: 'removed zero-byte vault-root duplicate' };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rootStat = fileStat(rootPath);
if (!rootStat) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'no root duplicate' };
}
if (rootStat.size !== 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'root duplicate is not zero-byte' };
}
const canonicalStat = fileStat(canonicalPath);
if (!canonicalStat || canonicalStat.size === 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'matching populated Notes file not found' };
}
fs.unlinkSync(rootPath);
return { checked: true, repaired: true, rootPath, notesPath: canonicalPath, reason: 'removed zero-byte vault-root duplicate' };
}
const rootStat = fileStat(rootPath);
if (!rootStat) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'no root duplicate' };
}
if (rootStat.size !== 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'root duplicate is not zero-byte' };
}
const canonicalStat = fileStat(canonicalPath);
if (!canonicalStat || canonicalStat.size === 0) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: 'matching populated Notes file not found' };
}
try {
fs.unlinkSync(rootPath);
} catch (error) {
return { checked: true, repaired: false, rootPath, notesPath: canonicalPath, reason: `unlink failed: ${error.code || error.message}` };
}
return { checked: true, repaired: true, rootPath, notesPath: canonicalPath, reason: 'removed zero-byte vault-root duplicate' };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/jarvos-secondbrain/packages/jarvos-secondbrain-notes/src/lib/vault-root-duplicate-guard.js`
around lines 56 - 72, The guard currently lets fileStat and fs.unlinkSync throw
and abort the calling flow; modify the function that uses fileStat and
fs.unlinkSync (vault-root-duplicate-guard.js) to make cleanup best-effort by
wrapping the fileStat(canonicalPath) and fs.unlinkSync(rootPath) calls in
try/catch blocks: if fileStat throws treat it as a non-fatal “not repaired”
result (return checked:true, repaired:false and include the error message in
reason), and when unlinkSync throws catch the error, log or include the error in
the returned reason and return checked:true, repaired:false instead of
rethrowing; keep existing successful-path behavior when no errors occur.

@levineam
levineam merged commit 0d33eb8 into main Jun 1, 2026
7 checks passed
@levineam
levineam deleted the SUP-2243/vault-root-duplicate-guard branch June 1, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant