# Phase 4 Round 18 Plan — Contradiction Detection
## Summary
Round 17 added stale detection. Round 18 adds **contradiction
detection**: when a lifecycle candidate's content conflicts with an
existing accepted/canonical memory, the system flags the conflict so
the user can resolve it (keep one, archive the other, or merge).
This round implements **Tier 1 heuristic** contradiction detection
only. Tier 2 (LLM-powered semantic comparison via sampling) is
deferred until the sampling reverse-call path has been exercised on
real workflows.
## Design
### What counts as a contradiction
Two memories contradict when they:
1. Share the same `memory_type` (or closely related types)
2. Have significant token overlap in title/summary (same topic)
3. Contain opposing signals (negation, replacement, different values)
Examples:
- A: "用 cargo install 安装" vs B: "不用 cargo install,改用 brew"
- A: "默认用 React" vs B: "决定用 Vue 替代 React"
- A: "测试覆盖率 80%" vs B: "测试覆盖率降到 60%"
### What does NOT count
- Different topics with no token overlap
- Same topic with additive information (not conflicting)
- Different scopes (user vs project) — these can coexist
- Archived memories — already out of the active set
## Architecture
### New module: `src/contradiction.rs`
```rust
pub struct ContradictionHit {
pub existing_record_id: String,
pub existing_title: String,
pub overlap_score: f32, // 0.0–1.0 token overlap
pub signal: ContradictionSignal,
}
pub enum ContradictionSignal {
Negation, // "不", "not", "don't", "never", "停止"
Replacement, // "替代", "改用", "instead of", "replace"
ValueChange, // numeric/enum value differs for same key
}
pub fn detect(
new_summary: &str,
new_memory_type: &str,
existing: &[(String, MemoryRecord)],
) -> Vec<ContradictionHit>
```
### Detection algorithm (Tier 1)
1. Filter `existing` to same `memory_type` + active states
(Accepted/Canonical only).
2. For each existing record, compute token overlap between
`new_summary` and `existing.summary`:
- Tokenize both (reuse `domain::note::tokenize`)
- Jaccard similarity = |intersection| / |union|
- Threshold: overlap >= 0.3 (at least 30% shared tokens)
3. For records passing the overlap threshold, scan for
contradiction signals:
- Negation markers in the new text that negate tokens from
the existing text
- Replacement patterns ("X 替代 Y", "instead of X use Y")
- Value changes (same prefix, different numeric/enum suffix)
4. Emit `ContradictionHit` for each match.
### Integration points
1. **`LifecycleCandidate` gains `contradicts: Vec<String>`** —
record IDs of conflicting existing memories. Empty when no
contradiction detected.
2. **Scorer**: after scoring a lifecycle candidate, run
contradiction detection against the existing wakeup-ready set.
Populate `contradicts` on the candidate. No score penalty in
Round 18 (visibility only, like confidence in Round 16).
3. **Write path**: after `propose_ai` / `record_manual`, run
contradiction detection. If hits found, include them in the
`LifecycleWriteResult` so callers (CLI, MCP, desktop) can
surface warnings.
4. **MCP tool `memory_check_contradictions`**: explicit tool that
checks a given record_id against the existing set and returns
structured contradiction hits.
## DTO Changes
- `domain::lifecycle_candidate::LifecycleCandidate.contradicts: Vec<String>`
- `lifecycle_service::LifecycleWriteResult.contradictions: Vec<ContradictionHit>`
- New MCP tool: `memory_check_contradictions` (input: record_id,
output: list of contradiction hits)
Frontend bindings regenerated via `cargo test --lib export_bindings`.
## Test Plan
1. **Unit (contradiction)**:
- Same-type memories with negation detected
- Same-type memories with replacement detected
- Different-type memories NOT flagged
- Low overlap NOT flagged
- Archived memories NOT checked
2. **Unit (scorer)**:
- Lifecycle candidate with contradiction has non-empty
`contradicts` field
- Lifecycle candidate without contradiction has empty
`contradicts`
3. **Integration (lifecycle_service)**:
- `propose_ai` returns contradictions in write result
- `record_manual` returns contradictions in write result
4. **CLI smoke**:
- Existing tests still pass
5. **Bench**:
- `build_bundle@5000` stays under 200ms
## Out of Scope
- ❌ Tier 2 LLM-powered semantic comparison (needs real sampling)
- ❌ Auto-resolution of contradictions (user must decide)
- ❌ Score penalty for contradicting memories (visibility only)
- ❌ Cross-type contradiction (e.g. decision vs constraint)
- ❌ UI for contradiction resolution workflow
## Completion Status
Last checked: `2026-05-08`