diff --git a/TASK_BACKLOG.md b/TASK_BACKLOG.md new file mode 100644 index 0000000..e0e8466 --- /dev/null +++ b/TASK_BACKLOG.md @@ -0,0 +1,130 @@ +# memU Task Backlog (local fallback) + +Updated: 2026-03-05 +Reason: production `/tasks` endpoint returned 404; captured here for later API sync. + +## P0 + +### 1) Temporal graph decay engine +- **Task:** Implement temporal graph decay for memU memories (event-time + access-time half-life scoring) with tunable lane profiles and audit trail. +- **Owner:** lenny +- **Lane:** memory-core +- **Why now:** stale memories are likely diluting retrieval quality as corpus grows. +- **Acceptance criteria:** + - configurable half-life policy by memory lane + - retrieval score includes temporal decay factor + - stale-hit rate metric in dashboard + +### 2) Neuro-symbolic memory router +- **Task:** Prototype neuro-symbolic memory router: vector ANN candidate retrieval + symbolic constraint/filter pass + explainability traces. +- **Owner:** lenny +- **Lane:** retrieval +- **Why now:** improves correctness and trust while preserving semantic recall. +- **Acceptance criteria:** + - rule-filter stage after vector retrieval + - explanation trace for final memories + - A/B report with precision@5 and contradiction-rate deltas + +## P1 + +### 3) Local-first CRDT sync layer +- **Task:** Add local-first CRDT sync layer for offline agents (append-only op log + merge/replay + conflict metrics dashboard). +- **Owner:** lenny +- **Lane:** sync +- **Why now:** enables resilient offline-first operation with deterministic convergence. +- **Acceptance criteria:** + - peer merge semantics documented and tested + - replay endpoint + conflict counter metrics + - deterministic convergence test suite + +### 4) Vector compression + reranking pipeline +- **Task:** Introduce vector compression pipeline (int8/binary quantization + rerank on full-float vectors) and benchmark recall/latency/cost tradeoffs. +- **Owner:** lenny +- **Lane:** vector-db +- **Why now:** reduce infra cost and improve query latency at rising memory scale. +- **Acceptance criteria:** + - benchmark harness for recall@k / p95 latency / RAM usage + - quantized index profile toggles + - production recommendation doc with guardrails + +### 5) Hybrid retrieval fusion baseline (BM25 + ANN + RRF) +- **Task:** Add a first-class hybrid retrieval path combining keyword BM25 and vector ANN, merged with Reciprocal Rank Fusion. +- **Owner:** lenny +- **Lane:** retrieval +- **Why now:** deep-research evidence shows better recall on exact entities/terms while preserving semantic retrieval. +- **Acceptance criteria:** + - dual retrieval execution path in API + - configurable fusion weights / RRF mode + - benchmark showing precision@5 and miss-rate improvements on hard queries + +### 6) Tiered memory architecture (L0/L1/L2) +- **Task:** Implement memory tiers: L0 working set (hot RAM), L1 episodic vectors, L2 symbolic graph facts with policy gating. +- **Owner:** lenny +- **Lane:** memory-core +- **Why now:** separates fast-context operations from durable long-term reasoning and improves debuggability. +- **Acceptance criteria:** + - explicit routing policy for each tier + - migration plan for existing memories + - observability dashboard showing tier hit ratios and latency + +### 7) Retrieval-triggered reinforcement decay +- **Task:** Upgrade temporal decay to a reinforcement model where successful retrieval/access boosts edge/node vitality while inactive memory decays. +- **Owner:** lenny +- **Lane:** memory-core +- **Why now:** prevents over-pruning high-value memories and suppresses stale noise automatically. +- **Acceptance criteria:** + - decay + reinforcement formula documented and configurable + - audit log for score changes + - offline replay test proving stability across 30+ day simulations + +### 8) Local-first CRDT sync MVP (SQLite WAL + op-log + peer merge) +- **Task:** Build a local-first sync MVP for offline-capable agents with CRDT merge semantics and opportunistic peer synchronization. +- **Owner:** lenny +- **Lane:** sync +- **Why now:** supports intermittent connectivity and reduces central API dependency for short outages. +- **Acceptance criteria:** + - append-only operation log persisted locally + - merge/replay API and conflict telemetry + - deterministic convergence test across 3 simulated peers + +## P2 + +### 9) Adaptive vector index maintenance (IVF/HNSW drift repair) +- **Task:** Implement adaptive index maintenance that monitors partition recall/latency drift and triggers targeted re-clustering only for degraded regions. +- **Owner:** lenny +- **Lane:** vector-db +- **Why now:** avoids full index rebuilds while preserving retrieval quality under continuous ingest. +- **Acceptance criteria:** + - drift detector with configurable thresholds + - targeted reindex jobs + audit logs + - throughput/latency benchmark vs full rebuild baseline + +### 10) Provenance-weighted contradiction guardrail +- **Task:** Add provenance/confidence metadata to memories and enforce a symbolic contradiction veto during final retrieval ranking. +- **Owner:** lenny +- **Lane:** retrieval +- **Why now:** improves trustworthiness by reducing high-similarity but low-trust conflicting recalls. +- **Acceptance criteria:** + - confidence score stored per memory/edge + - contradiction checker in retrieval pipeline + - measurable contradiction-rate reduction on regression set + +### 11) Two-stage context compression (masking → structured summary) +- **Task:** Introduce staged context compression where old context is first masked, then summarized only when token pressure persists. +- **Owner:** lenny +- **Lane:** memory-core +- **Why now:** minimizes lost-in-the-middle failures while controlling token spend. +- **Acceptance criteria:** + - policy engine for compression stage transitions + - regression suite for context retention quality + - token-cost and completion-quality dashboard deltas + +### 12) Event-driven multi-device memory coherence +- **Task:** Implement delta-push sync notifications with CRDT merge acknowledgements to tighten cross-device memory convergence. +- **Owner:** lenny +- **Lane:** sync +- **Why now:** reduces stale state windows when multiple devices/agents update shared memory concurrently. +- **Acceptance criteria:** + - changefeed/delta event channel + - merge ack + retry tracking + - deterministic convergence tests under concurrent write storms diff --git a/memu/api.py b/memu/api.py index 0126f46..b63a929 100644 --- a/memu/api.py +++ b/memu/api.py @@ -78,7 +78,13 @@ @asynccontextmanager async def lifespan(app: FastAPI): global pool, _fastembed_model, _nats_cluster, _nats_publisher - pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10) + async def _init_hnsw_search(conn): + """Set HNSW ef_search for quality filtered retrieval (migration 013).""" + await conn.execute("SET hnsw.ef_search = 100") + + pool = await asyncpg.create_pool( + DATABASE_URL, min_size=2, max_size=10, init=_init_hnsw_search + ) # Run DB migrations try: diff --git a/memu/migrations/013_hnsw_filtered_retrieval.sql b/memu/migrations/013_hnsw_filtered_retrieval.sql new file mode 100644 index 0000000..1f9e8db --- /dev/null +++ b/memu/migrations/013_hnsw_filtered_retrieval.sql @@ -0,0 +1,41 @@ +-- Migration 013: Upgrade IVFFlat → HNSW index for filtered ANN retrieval +-- Motivation: HNSW supports efficient pre-filtering by agent_id/memory_type +-- which eliminates the post-filter accuracy loss we get with IVFFlat. +-- pgvector >= 0.7.0 required (HNSW support), 0.8+ recommended for filtered search. + +-- Step 1: Drop the old IVFFlat index +DROP INDEX IF EXISTS idx_memories_embedding; + +-- Step 2: Create HNSW index on embedding column +-- m=16, ef_construction=200 are good defaults for our ~3000 memory corpus. +-- For larger corpora (>100k), increase ef_construction to 256-512. +CREATE INDEX idx_memories_embedding_hnsw + ON memories USING hnsw (embedding vector_cosine_ops) + WITH (m = 16, ef_construction = 200); + +-- Step 3: Create composite indexes for common filtered queries +-- These let pgvector push agent_id/memory_type filters INTO the ANN scan +-- instead of post-filtering (which discards valid nearest neighbors). + +-- Agent-scoped search (most common query pattern for multi-agent) +CREATE INDEX IF NOT EXISTS idx_memories_agent_created + ON memories (agent_id, created_at DESC); + +-- Type-scoped search (e.g., "find all lessons") +CREATE INDEX IF NOT EXISTS idx_memories_type_created + ON memories (memory_type, created_at DESC); + +-- Confidence-filtered search +CREATE INDEX IF NOT EXISTS idx_memories_confidence + ON memories (confidence DESC) + WHERE confidence >= 0.5; + +-- Step 4: Set HNSW search parameters for quality +-- ef (search-time beam width) trades speed for recall accuracy. +-- Default 40 is too low for filtered search; 100 gives good recall without +-- significant latency increase at our corpus size. +-- This is a session-level setting; we'll set it in the application connection pool. +-- ALTER SYSTEM SET hnsw.ef_search = 100; -- uncomment if you want cluster-wide + +COMMENT ON INDEX idx_memories_embedding_hnsw IS + 'HNSW index for filtered ANN retrieval. Replaces IVFFlat (migration 013). m=16, ef_construction=200.'; diff --git a/reports/2026-03-04-fumemory-evolution-proposal.md b/reports/2026-03-04-fumemory-evolution-proposal.md new file mode 100644 index 0000000..0a3d245 --- /dev/null +++ b/reports/2026-03-04-fumemory-evolution-proposal.md @@ -0,0 +1,77 @@ +# FuMemory Evolution Proposal — 2026-03-04 + +## Run Evidence +- `routine_memu_evolution.sh`: ✅ MD sync ran, health check passed (`ok=true`, `db=schema=vector=ok`, `total_memories=2700`) +- Deep research: ✅ Perplexity `sonar-deep-research` completed (`29 in / 9450 out tokens`) +- `/tasks` API write: ❌ unavailable in prod for this route (`POST /api/v1/memu/tasks` -> 405, `POST /tasks` -> 404) +- Fallback applied: ✅ backlog updated in `memu-oss/TASK_BACKLOG.md` + +## Most Promising Architecture Moves + +### 1) Hybrid Neuro-Symbolic Memory Tiers (highest impact) +**Concept:** split memory into three tiers: +- **L0 (Working):** short-window, high-churn context in RAM +- **L1 (Episodic):** quantized vector store for semantic retrieval +- **L2 (Semantic/Rules):** graph facts + symbolic constraints + policy checks + +**Why this wins:** +- Keeps latency low for active reasoning +- Preserves long-horizon recall +- Adds explainability and contradiction resistance through symbolic checks + +### 2) Reinforcement Temporal Decay (not just time decay) +**Concept:** memory strength decays with inactivity but gets reinforced on successful retrieval and task outcomes. + +**Why this wins:** +- Prevents useful memories from being pruned +- Naturally suppresses stale/unhelpful memory +- Creates quality drift control without manual curation + +### 3) Local-First CRDT Sync +**Concept:** agent writes first to local SQLite WAL + op-log, then merges with peers/backend using CRDT semantics when connectivity returns. + +**Why this wins:** +- Offline resilience +- Lower dependency on centralized uptime +- Deterministic convergence in distributed multi-agent setups + +### 4) Vector DB Efficiency Stack +**Concept:** int8 quantization + HNSW tuning + hybrid BM25 fusion + optional GPU path. + +**Why this wins:** +- Major cost reduction (RAM + compute) +- Better p95 latency at scale +- Better exact-term recall with hybrid retrieval + +## 30-Day Implementation Plan + +### Phase 1 (Week 1): Baseline + Safety Rails +1. Add hybrid retrieval endpoint (ANN + BM25 + RRF). +2. Instrument precision@5, contradiction-rate, stale-hit-rate, p95 latency. +3. Ship benchmark harness with fixed query corpus. + +### Phase 2 (Week 2): Tiered Routing +1. Introduce L0/L1/L2 memory router. +2. Route writes by memory type (observation/fact/decision/policy). +3. Add trace output: `why_this_memory` for every retrieval response. + +### Phase 3 (Week 3): Reinforcement Decay +1. Add decay + reinforcement scoring formula. +2. Record score deltas in audit table. +3. Run 30-day replay simulation and tune half-life profiles by lane. + +### Phase 4 (Week 4): Local-First Sync MVP +1. Implement local op-log + WAL. +2. Build CRDT merge/replay endpoint and conflict metrics. +3. Run 3-peer deterministic convergence tests under partition/reconnect scenarios. + +## Concrete Success Targets +- Precision@5: **+12%** on hard retrieval set +- Contradiction rate: **-40%** +- p95 retrieval latency: **<120ms** at 10k+ memory scale +- Memory infra cost/query: **-35%** +- Offline task continuity: **>=95%** for local-first agents during simulated API outages + +## Notes +- This proposal was grounded in the deep-research run focused on neuro-symbolic storage, temporal graph decay, local-first P2P sync, and vector optimization. +- Backlog items reflecting this proposal were added to local fallback backlog because production task API is still inconsistent. diff --git a/reports/2026-03-05-fumemory-evolution-proposal.md b/reports/2026-03-05-fumemory-evolution-proposal.md new file mode 100644 index 0000000..9b6f5af --- /dev/null +++ b/reports/2026-03-05-fumemory-evolution-proposal.md @@ -0,0 +1,98 @@ +# FuMemory Evolution Proposal — 2026-03-05 + +## Run Evidence +- `routine_memu_evolution.sh`: ✅ MD sync + health check passed (`ok=true`, `db/scheme/vector=ok`, `total_memories=2733`). +- Perplexity deep research: ✅ completed via `sonar-deep-research` (`29 in / 9399 out tokens`). +- memU task API writes: ❌ unavailable in production routing (`POST /api/v1/memu/tasks` -> 405, `POST /tasks` -> 404). +- Fallback applied: ✅ backlog updated in `memu-oss/TASK_BACKLOG.md` with new P2 items (#9-#12). + +## Creative Architecture Proposal: “Living Memory Mesh” + +### Core Idea +Evolve memU from a single vector recall layer into a **living memory mesh**: +1. **Neuro layer** for fuzzy semantic recall (ANN vectors). +2. **Symbolic layer** for truth constraints, provenance, and contradiction checks. +3. **Temporal vitality layer** to promote memories that repeatedly prove useful. +4. **Local-first sync layer** to survive outages and converge deterministically. + +This gives better recall quality, less stale noise, and stronger reliability under real multi-agent load. + +## Most Promising Upgrades (ranked) + +### 1) Provenance-weighted neuro-symbolic retrieval (highest leverage) +**What changes** +- Store confidence/provenance with each memory (source, timestamp, validation state). +- Keep ANN candidate generation, then run symbolic contradiction and policy filters. +- Final rank = semantic similarity × confidence × recency/vitality. + +**Why now** +- Current failure mode is not “can’t find memory,” it’s “finds conflicting or weak memory.” +- This directly attacks contradiction-rate without killing semantic recall breadth. + +### 2) Adaptive index maintenance (IVF/HNSW drift repair) +**What changes** +- Monitor shard-level latency + recall proxies. +- Re-cluster only degraded partitions instead of full rebuilds. +- Keep ingest online while tuning indexes. + +**Why now** +- Corpus is growing; full rebuild strategy won’t scale operationally. +- Incremental repair improves stability and cost. + +### 3) Two-stage context compression (masking → summary) +**What changes** +- Stage 1: mask low-value old context. +- Stage 2: summarize only when token pressure remains high. +- Regression tests for “lost-in-the-middle” behavior. + +**Why now** +- Cheap compression-first strategy cuts token burn while preserving quality. +- Prevents summary-overcompression from degrading answer accuracy. + +### 4) Event-driven multi-device coherence (delta push + merge ack) +**What changes** +- Publish memory deltas as events; peers apply CRDT merge. +- Track merge acknowledgements and retries. +- Add convergence tests under concurrent updates. + +**Why now** +- Required for reliable local-first operation across agents/devices. +- Reduces stale windows after updates. + +## Implementation Steps (specific) + +### Phase A (Week 1): Retrieval integrity foundation +1. Add `confidence`, `provenance`, and `validation_state` fields to memory metadata. +2. Implement contradiction checker in post-ANN pipeline. +3. Add retrieval trace output (`candidate -> filtered -> final`) for QA. + +### Phase B (Week 2): Adaptive vector ops +1. Add index health telemetry by partition (p95, miss proxy, queue depth). +2. Build targeted reindex worker for degraded partitions. +3. Add guardrails to prevent over-aggressive re-clustering. + +### Phase C (Week 3): Context compression policy engine +1. Implement masking-first context reducer. +2. Implement structured summarizer fallback triggered by token pressure. +3. Add benchmark suite comparing quality/cost across policies. + +### Phase D (Week 4): Coherence and sync hardening +1. Add delta event stream for memory mutations. +2. Add merge ack + retry queue + dead-letter handling. +3. Run 3-peer partition/reconnect convergence tests and publish report. + +## Target Metrics +- Contradiction rate: **-40%** +- p95 retrieval latency at 10k+ memory scale: **<120ms** +- Token cost for long-context tasks: **-25%** +- Incremental index maintenance overhead: **<20%** of full rebuild equivalent +- Cross-peer convergence success under partition test: **>=99%** + +## Backlog Sync Result +Attempted production task ingestion failed due routing mismatch. Added the most promising upgrades to local fallback backlog: +- #9 Adaptive vector index maintenance +- #10 Provenance-weighted contradiction guardrail +- #11 Two-stage context compression +- #12 Event-driven multi-device memory coherence + +These are now ready for implementation sequencing once `/tasks` API route is fixed in prod. diff --git a/tests/test_temporal_decay_regression.py b/tests/test_temporal_decay_regression.py new file mode 100644 index 0000000..6694c63 --- /dev/null +++ b/tests/test_temporal_decay_regression.py @@ -0,0 +1,208 @@ +"""Regression tests for temporal decay scoring and filtered retrieval. + +QA verification gate: these tests must pass before any decay/retrieval +changes are merged. They validate: +1. Temporal decay scoring ranks recent memories higher +2. Access count boosts work with diminishing returns +3. Agent-scoped filtering returns only matching memories +4. Deduplication threshold catches near-duplicates +5. Edge cases: zero-age, very old, high-access memories +""" + +import math +from datetime import datetime, timedelta, timezone + +import pytest + +from memu.decay import compute_final_score, should_deduplicate + + +# --- Fixtures --- + +NOW = datetime.now(timezone.utc) + + +def _make_ts(days_ago: int) -> datetime: + """Create a UTC timestamp N days in the past.""" + return NOW - timedelta(days=days_ago) + + +# --- Temporal Decay Scoring --- + + +class TestComputeFinalScore: + """Verify that compute_final_score correctly blends similarity, recency, and access.""" + + def test_recent_beats_old_at_equal_similarity(self): + """A memory from today should score higher than one from 90 days ago, + given identical similarity and access counts.""" + recent = compute_final_score( + similarity=0.85, created_at=_make_ts(0), access_count=1 + ) + old = compute_final_score( + similarity=0.85, created_at=_make_ts(90), access_count=1 + ) + assert recent > old, f"Recent ({recent:.4f}) should beat old ({old:.4f})" + + def test_very_old_memory_does_not_reach_zero(self): + """Even a 365-day-old memory should have a score > 0 (min_score floor).""" + score = compute_final_score( + similarity=0.9, created_at=_make_ts(365), access_count=0 + ) + assert score > 0, "Very old memory score should be > 0" + assert score >= 0.1 * 0.1, "Score should be bounded by min_score interactions" + + def test_brand_new_memory_max_decay(self): + """A brand-new memory (0 days old) should get decay_factor ≈ 1.0.""" + score = compute_final_score( + similarity=1.0, created_at=NOW, access_count=0 + ) + # With access_count=0: access_boost = 1 + log(1) = 1.0 + # decay_factor = 1.0, recency = 1.0 + # blended = 0.7 * 1.0 + 0.3 * 1.0 = 1.0 + # final = 1.0 * 1.0 * 1.0 = 1.0 + assert score == pytest.approx(1.0, abs=0.01) + + def test_access_count_boost_diminishing_returns(self): + """More accesses should boost score, but with diminishing returns. + + We use uniform step sizes to properly test that log(n+1) produces + diminishing marginal gains. + """ + ts = _make_ts(7) + # Uniform steps: 0, 10, 20, 30, 40 + access_levels = [0, 10, 20, 30, 40] + scores = [] + for access in access_levels: + s = compute_final_score( + similarity=0.8, created_at=ts, access_count=access + ) + scores.append(s) + + # Each subsequent score should be higher + for i in range(1, len(scores)): + assert scores[i] > scores[i - 1], ( + f"access={access_levels[i]} should score higher than access={access_levels[i-1]}" + ) + + # Marginal gain should decrease with uniform step increases + gains = [scores[i] - scores[i - 1] for i in range(1, len(scores))] + for i in range(1, len(gains)): + assert gains[i] < gains[i - 1], ( + f"Gain from step {i+1} ({gains[i]:.4f}) should be less than step {i} ({gains[i-1]:.4f})" + ) + + def test_similarity_zero_still_gets_recency_component(self): + """Even with 0 similarity, temporal_weight gives a recency-based score.""" + score = compute_final_score( + similarity=0.0, created_at=NOW, access_count=0, temporal_weight=0.3 + ) + # blended = 0.7 * 0.0 + 0.3 * 1.0 = 0.3 + assert score > 0, "Zero similarity should still have recency component" + assert score == pytest.approx(0.3, abs=0.05) + + def test_higher_temporal_weight_favors_recency(self): + """Increasing temporal_weight should make recency matter more than similarity.""" + old_high_sim = compute_final_score( + similarity=0.95, created_at=_make_ts(60), access_count=1, temporal_weight=0.8 + ) + new_low_sim = compute_final_score( + similarity=0.5, created_at=_make_ts(1), access_count=1, temporal_weight=0.8 + ) + # With high temporal weight, recency should dominate + assert new_low_sim > old_high_sim, ( + "High temporal_weight should make recent low-sim beat old high-sim" + ) + + def test_decay_rate_controls_speed(self): + """Higher decay_rate makes old memories score lower.""" + slow_decay = compute_final_score( + similarity=0.8, created_at=_make_ts(30), access_count=1, decay_rate=0.005 + ) + fast_decay = compute_final_score( + similarity=0.8, created_at=_make_ts(30), access_count=1, decay_rate=0.05 + ) + assert slow_decay > fast_decay, "Slow decay should preserve score better" + + def test_deterministic_output(self): + """Same inputs should produce same output (no random component). + + Use a fixed timestamp to avoid sub-second drift between calls. + """ + ts = datetime(2026, 1, 1, tzinfo=timezone.utc) + a = compute_final_score(similarity=0.75, created_at=ts, access_count=3) + b = compute_final_score(similarity=0.75, created_at=ts, access_count=3) + assert a == pytest.approx(b, abs=1e-9), "Scores should be deterministic" + + +# --- Deduplication --- + + +class TestDeduplication: + """Verify deduplication threshold logic.""" + + def test_exact_duplicate_caught(self): + assert should_deduplicate(1.0) is True + + def test_near_duplicate_caught(self): + assert should_deduplicate(0.96) is True + + def test_similar_but_distinct_passes(self): + assert should_deduplicate(0.90) is False + + def test_threshold_boundary(self): + assert should_deduplicate(0.95) is True + assert should_deduplicate(0.9499) is False + + def test_custom_threshold(self): + assert should_deduplicate(0.85, threshold=0.80) is True + assert should_deduplicate(0.75, threshold=0.80) is False + + +# --- Ranking Order Verification --- + + +class TestRankingOrder: + """End-to-end ranking order checks that simulate real retrieval scenarios.""" + + def test_ranking_recent_relevant_first(self): + """Simulate 5 memories and verify correct ranking order.""" + memories = [ + {"sim": 0.90, "age_days": 1, "access": 5, "label": "recent-relevant"}, + {"sim": 0.92, "age_days": 30, "access": 2, "label": "older-slightly-more-relevant"}, + {"sim": 0.70, "age_days": 0, "access": 0, "label": "brand-new-low-sim"}, + {"sim": 0.95, "age_days": 120, "access": 10, "label": "very-old-high-sim-accessed"}, + {"sim": 0.60, "age_days": 200, "access": 0, "label": "ancient-low-sim"}, + ] + + scored = [] + for m in memories: + score = compute_final_score( + similarity=m["sim"], + created_at=_make_ts(m["age_days"]), + access_count=m["access"], + ) + scored.append((m["label"], score)) + + scored.sort(key=lambda x: x[1], reverse=True) + labels_ranked = [s[0] for s in scored] + + # The recent-relevant should be top-2 and ancient-low-sim should be last + assert labels_ranked[-1] == "ancient-low-sim", ( + f"Ancient low-sim should be last, got: {labels_ranked}" + ) + assert "recent-relevant" in labels_ranked[:2], ( + f"Recent relevant should be top-2, got: {labels_ranked}" + ) + + def test_all_scores_positive(self): + """No memory should ever score <= 0 regardless of inputs.""" + edge_cases = [ + (0.0, _make_ts(1000), 0), + (0.01, _make_ts(365), 0), + (1.0, _make_ts(0), 0), + (0.5, _make_ts(30), 1000), + ] + for sim, ts, access in edge_cases: + score = compute_final_score(similarity=sim, created_at=ts, access_count=access) + assert score > 0, f"Score should be positive for sim={sim}, age={ts}, access={access}"