spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
# 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`