feat(contradictions): advisory scanner for conflicting approved claims#405
feat(contradictions): advisory scanner for conflicting approved claims#405jsdevninja wants to merge 1 commit into
Conversation
walk approved claims grouped by shared entity, flag same-topic pairs that disagree in polarity, and file a pending `contradicts` relation proposal per surviving pair via proposals.propose_relation. read-only and advisory: the scanner never sets ClaimStatus.CONTESTED, never writes a Relation, and never touches Claim.contradicts — those only land once a human runs `vouch approve` (or the pre-existing manual lifecycle.contradict). scoring/grouping lives in the new src/vouch/contradictions.py, not storage.py. `vouch contradict-scan` mirrors `vouch dedup`'s cli-only shape: --threshold, --entity, --limit, and --dry-run/--no-dry-run (default dry-run). a pair already cross-linked via Claim.contradicts, joined by an approved contradicts edge, or with a pending contradicts proposal is skipped so repeat scans don't duplicate proposals. closes vouchdev#314
📝 WalkthroughWalkthroughAdds a new advisory ChangesContradiction Scan Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as cli.py contradict_scan
participant Scanner as contradictions.scan
participant Finder as find_candidates
participant Store as KBStore
participant Proposals as proposals.propose_relation
User->>CLI: vouch contradict-scan --threshold --entity --limit
CLI->>Scanner: scan(store, threshold, entity, dry_run, limit)
Scanner->>Finder: find_candidates(store, threshold, entity)
Finder->>Store: read approved claims, entities, contradicts, relations, proposals
Finder-->>Scanner: sorted Candidate list
alt dry_run is False
Scanner->>Proposals: propose_relation(contradicts, claim_a, claim_b)
Proposals-->>Scanner: proposal_id or ProposalError
end
Scanner-->>CLI: rows with claim ids, entity, score, proposal_id
CLI-->>User: printed candidate summaries
Related issues: Suggested labels: enhancement, cli Suggested reviewers: vouchdev maintainers familiar with 🐰 A scanner hops through claims so wise, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 `@CHANGELOG.md`:
- Line 16: Capitalize the word after the period in the changelog sentence so it
reads as a new sentence; update the text containing `--entity`, `--limit`, and
the following `scoring` phrase to start with an uppercase letter.
🪄 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: 66e8ef38-6b6c-401c-87d9-3049d7e67505
📒 Files selected for processing (4)
CHANGELOG.mdsrc/vouch/cli.pysrc/vouch/contradictions.pytests/test_contradictions.py
|
@plind-junior Would you review please? |
closes #314
summary
src/vouch/contradictions.py: a read-only heuristic scanner that groups approved claims by sharedClaim.entitiesand flags same-topic pairs whose text disagrees in polarity (one asserts, the other negates) as candidate contradictions.vouch contradict-scan(cli-only, matching howvouch dedupships without a dedicatedkb.*method):--threshold,--entity,--limit,--dry-run/--no-dry-run(default dry-run).--dry-run, each surviving pair files exactly one pendingcontradictsrelation proposal viaproposals.propose_relation, visible inkb.list_pending/vouch pending.Claim.contradicts, already joined by an approvedcontradictsrelation edge, or already carrying a pendingcontradictsproposal is skipped, so repeat scans don't pile up duplicates.ClaimStatus.CONTESTED, never writes aRelation, and never touchesClaim.contradicts— all business logic lives incontradictions.py,storage.pyis untouched.design note (worth a second pair of eyes)
approving the resulting relation proposal currently lands a plain
Relation(relation=CONTRADICTS)edge — it does not flip both claims toCONTESTEDor cross-linkClaim.contradicts. that full symmetric transition still only happens via the pre-existing, deliberately-manuallifecycle.contradict(kb.contradict), whose own docstring says it intentionally bypasses the proposal queue and warns against refactoring it for stricter gating. i keptproposals.approve()untouched rather than special-casingrelation == "contradicts"there, since that's shared code for every relation kind and the issue's acceptance checklist only requires the proposal to land as aCONTRADICTSedge on approval, not the status flip. happy to wire that up instead if the intended reading of "the actual write still flows through lifecycle.contradict semantics on approval" was thatkb.approveitself should trigger the full transition.testing
tests/test_contradictions.py(15 tests): candidate scoring/grouping, entity/threshold/limit filters, inactive-claim exclusion, all three duplicate-skip paths (contradicts field / approved edge / pending proposal), dry-run writes nothing, non-dry-run files exactly one proposal per pair and never mutates claim status or writes a relation directly, repeat scans don't duplicate, and a full scan → propose → approve integration test landing aCONTRADICTSedge.vouch contradict-scanend-to-end against a scratch kb (dry-run,--no-dry-run, re-scan dedup,vouch approve) — matches the automated coverage.ruff check src testsclean.mypy src— same 3 pre-existing errors as main (sandbox.pyos.getuid/os.getgidon non-posix, missingjinja2stubs); none in the touched files.pytest tests/ -q --ignore=tests/embeddings— ran locally on windows. two files (test_http_server.py,test_http_server_mcp.py) hang in this sandbox because they bind a real loopback socket, unrelated to this diff, so i excluded them locally; everything else passed except 8 pre-existing windows-only failures (posixos.getuid, unprivileged symlinks,/vs\path assertions) untouched by this change.tests/test_contradictions.pyis green.test plan
vouch contradict-scanlists candidate pairs, writes nothing in--dry-run(default)--dry-run, each surviving pair produces exactly one pendingcontradictsproposal visible inkb.list_pendingCONTESTED, never writes aRelation, never touchesClaim.contradicts--threshold,--entity,--limit,--dry-run/--no-dry-runbehave as documentedsrc/vouch/contradictions.py,storage.pyunchangedkb.*mcp/jsonl method), per the issue's stated allowanceSummary by CodeRabbit
New Features
Bug Fixes
Tests