Skip to content

fix: add existence guards to all put_* methods (#12) - #26

Merged
plind-junior merged 3 commits into
vouchdev:mainfrom
alpurkan17:fix/artifact-id-collision-12
May 19, 2026
Merged

fix: add existence guards to all put_* methods (#12)#26
plind-junior merged 3 commits into
vouchdev:mainfrom
alpurkan17:fix/artifact-id-collision-12

Conversation

@alpurkan17

@alpurkan17 alpurkan17 commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

put_claim, put_page, put_entity, and put_relation wrote files unconditionally — no existence check. When two artifacts shared the same slug, the second approval silently overwrote the first. The original artifact was lost with no error or warning.

Fix

Add an existence check before write in all four put_* methods, raising ValueError on collision so callers must explicitly use update_* or choose a different slug.

Related Issues

Closes #12

Summary by CodeRabbit

  • Bug Fixes
    • Creation of new records (claims, pages, entities, relations, evidence, sessions, proposals) now fails instead of overwriting when an identifier/slug already exists, preventing accidental data loss.
    • Error messages now instruct users to use update operations for existing claims or choose a different slug/ID for other record types when a conflict occurs.

Review Change Stack

vouchdev#12)

put_claim, put_page, put_entity, and put_relation wrote files
unconditionally. When two artifacts shared the same slug, the
second approval silently overwrote the first — data loss with
no error or warning.

Add an existence check before write in all four put_* methods,
raising ValueError on collision.

Closes vouchdev#12
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

KBStore create methods now use exclusive file creation; if a target claim, page, entity, relation, evidence, session, or proposal file already exists, the methods raise ValueError instead of overwriting the existing file.

Changes

Duplicate ID Prevention

Layer / File(s) Summary
Existence checks in put_ methods*
src/vouch/storage.py
put_claim, put_page, put_entity, put_relation, put_evidence, put_session, and put_proposal now write files using exclusive creation (open(..., "x")) and convert FileExistsError into ValueError indicating the artifact already exists and advising use of update methods or choosing a different slug/id.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I’m a rabbit in the data glen,
I nudge each file and guard each pen.
No secret swap, no quiet shove —
Each slug stays true; I watch with love. 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning While the PR addresses the core issue #12 for put_claim/put_page/put_entity/put_relation, reviewer feedback identified that put_evidence, put_session, and put_proposal remain vulnerable but the PR title only mentions the four methods. Ensure put_evidence, put_session, and put_proposal also use exclusive creation (open "x") to prevent silent overwrites, matching the pattern applied to the four main methods mentioned in issue #12.
Out of Scope Changes check ⚠️ Warning The PR includes changes to put_evidence, put_session, and put_proposal that extend beyond the original issue #12 scope, which only specified put_claim, put_page, put_entity, and put_relation. Clarify whether expanding the existence guard pattern to additional put_* methods is intentional scope expansion or should be documented/justified in the PR description.
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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding existence guards across all put_* methods to prevent silent overwrites. It is clear, specific, and reflects the core objective of the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 2

🤖 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 `@src/vouch/storage.py`:
- Around line 276-279: The ValueError messages that reference "use update" are
misleading because there are no update_* APIs; locate the checks that call
self._page_path(page.id).exists() (and the analogous checks for entities and
relations in the same module, e.g., in create_page, create_entity,
create_relation) and change their exception text to only advise choosing a
different slug (for example: "page {page.id} already exists — choose a different
slug") so callers aren’t directed to nonexistent update APIs; update all three
occurrences noted in the review.
- Around line 237-240: The current check-then-write using
self._claim_path(...).exists() is race-prone; replace the existence check +
subsequent write with an atomic exclusive create using path.open("x") (or
Path.open with mode "x") and write the claim content, catching FileExistsError
and mapping it to the same ValueError("claim {claim.id} already exists — use
update_claim()"). Apply this change wherever self._claim_path(...) existence is
checked before writing (the shown block and the similar blocks around the other
occurrences referenced) so creation uses exclusive-create semantics and avoids
TOCTOU races.
🪄 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 Plus

Run ID: 7a03535d-791f-429f-b6a4-48fdff16a01d

📥 Commits

Reviewing files that changed from the base of the PR and between 89172aa and a97d1f4.

📒 Files selected for processing (1)
  • src/vouch/storage.py

Comment thread src/vouch/storage.py Outdated
Comment thread src/vouch/storage.py Outdated
…hods

Use path.open('x') instead of exists()+write_text to avoid TOCTOU races.

Also fix error messages referencing nonexistent update_* APIs for page/entity/relation.

Closes vouchdev#12
@plind-junior

Copy link
Copy Markdown
Member

Nice use of open("x") for exclusive create — it's inherently race-safe, which is better than a separate exists() check. Clean approach.

A few things I noticed:

  1. Other put_* methods have the same bug
    put_evidence (L342), put_session (L361), and put_proposal (L379) still call .write_text() directly, so they'll silently overwrite too. Was that intentional? If it's out of scope for this PR, maybe worth a follow-up issue so it doesn't get forgotten.

  2. Unrelated comment got removed
    The comment around L237-238 explaining that evidence entries can be Source IDs or Evidence IDs was actually pretty helpful — that validation logic isn't obvious at a glance. Looks like it got dropped by accident. Worth putting back.

  3. Error messages aren't quite consistent
    put_claim tells the caller to "use update_claim()", which makes sense since that method exists. But the other three say "choose a different slug" — there's no update_page, update_entity, or update_relation to point to. Not a big deal, but it might confuse someone who sees the put_claim message and then wonders why the others don't offer an update path too.

  4. Small thing: em dash in error strings
    The — could look weird in terminals or logs that aren't UTF-8. Swapping to -- would be the safe bet.

Overall
This is a solid fix for the problem described in #12. The main thing I'd want addressed before merging is the incomplete coverage — leaving three other methods with the same vulnerability feels like it could bite someone later.

- Replace bare write_text() with exclusive open("x") in all three methods
  to prevent silent overwrites, matching the pattern from put_claim et al.
- Restore explanatory comment in put_claim about Source/Evidence ID duality.
- Normalize em dashes to ASCII "--" in error strings for terminal safety.

Addresses reviewer feedback on PR vouchdev#26.

@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.

🧹 Nitpick comments (1)
src/vouch/storage.py (1)

246-252: ⚡ Quick win

Specify encoding explicitly and consider cleaner exception chaining.

All exclusive-create blocks open files in text mode without specifying encoding. This uses the platform-dependent default encoding, which may not be UTF-8 on all systems. YAML files should be written as UTF-8 for portability.

Additionally, re-raising ValueError without from None includes the FileExistsError traceback, which may be confusing since the FileExistsError is an implementation detail.

♻️ Suggested improvements

For put_claim (lines 246-252):

     try:
-        with self._claim_path(claim.id).open("x") as f:
+        with self._claim_path(claim.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(claim.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"claim {claim.id} already exists -- use update_claim()"
-        )
+        ) from None

For put_page (lines 282-288):

     try:
-        with self._page_path(page.id).open("x") as f:
+        with self._page_path(page.id).open("x", encoding="utf-8") as f:
             f.write(_serialize_page(page))
     except FileExistsError:
         raise ValueError(
             f"page {page.id} already exists -- choose a different slug"
-        )
+        ) from None

For put_entity (lines 306-312):

     try:
-        with self._entity_path(entity.id).open("x") as f:
+        with self._entity_path(entity.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(entity.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"entity {entity.id} already exists -- choose a different slug"
-        )
+        ) from None

For put_relation (lines 331-337):

     try:
-        with self._relation_path(rel.id).open("x") as f:
+        with self._relation_path(rel.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(rel.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"relation {rel.id} already exists -- choose a different slug"
-        )
+        ) from None

For put_evidence (lines 364-370):

     try:
-        with self._evidence_path(ev.id).open("x") as f:
+        with self._evidence_path(ev.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(ev.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"evidence {ev.id} already exists -- choose a different slug"
-        )
+        ) from None

For put_session (lines 389-395):

     try:
-        with self._session_path(sess.id).open("x") as f:
+        with self._session_path(sess.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(sess.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"session {sess.id} already exists -- choose a different id"
-        )
+        ) from None

For put_proposal (lines 414-420):

     try:
-        with self._proposal_path(proposal.id).open("x") as f:
+        with self._proposal_path(proposal.id).open("x", encoding="utf-8") as f:
             f.write(_yaml_dump(proposal.model_dump(mode="json")))
     except FileExistsError:
         raise ValueError(
             f"proposal {proposal.id} already exists -- choose a different id"
-        )
+        ) from None

Also applies to: 282-288, 306-312, 331-337, 364-370, 389-395, 414-420

🤖 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 `@src/vouch/storage.py` around lines 246 - 252, The file-open calls in the
exclusive-create blocks (e.g., put_claim using self._claim_path(...).open("x"))
should explicitly set encoding="utf-8" to ensure YAML is written portably, and
when converting the FileExistsError into a ValueError, re-raise with "from None"
to suppress the underlying traceback; apply the same two changes to the other
similar functions: put_page, put_entity, put_relation, put_evidence,
put_session, and put_proposal so each open(..., "x", encoding="utf-8") and each
raise ValueError(... ) uses "from None".
🤖 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.

Nitpick comments:
In `@src/vouch/storage.py`:
- Around line 246-252: The file-open calls in the exclusive-create blocks (e.g.,
put_claim using self._claim_path(...).open("x")) should explicitly set
encoding="utf-8" to ensure YAML is written portably, and when converting the
FileExistsError into a ValueError, re-raise with "from None" to suppress the
underlying traceback; apply the same two changes to the other similar functions:
put_page, put_entity, put_relation, put_evidence, put_session, and put_proposal
so each open(..., "x", encoding="utf-8") and each raise ValueError(... ) uses
"from None".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2433627f-6744-4e88-a54c-9db367f34f0f

📥 Commits

Reviewing files that changed from the base of the PR and between 35b2af1 and fd71a9c.

📒 Files selected for processing (1)
  • src/vouch/storage.py

@alpurkan17 alpurkan17 changed the title fix: add existence guard in put_claim/put_page/put_entity/put_relation (#12) fix: add existence guards to all put_* methods (#12) May 19, 2026
@plind-junior

Copy link
Copy Markdown
Member

LGTM!

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.

Artifact ID collision causes silent data loss on approval

2 participants