From 98228f3fb9eb07261e8aa934f4c83ddc3fe082ee Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sat, 9 May 2026 21:58:50 -0700 Subject: [PATCH 1/4] chore(models): BELIEF_SPECULATIVE + EDGE_RESOLVES + CORROBORATION_SOURCE_WONDER_INGEST + Belief.valid_to (#548) Pre-stages the type/edge/corroboration enum entries and the Belief.valid_to soft-delete field that the wonder lifecycle module depends on. RESOLVES gets edge_valence=0.0 (marker, not evidential). --- src/aelfrice/models.py | 25 +++++++++++++++++++++++++ tests/test_corroborations.py | 2 ++ 2 files changed, 27 insertions(+) diff --git a/src/aelfrice/models.py b/src/aelfrice/models.py index deac5de3c..0b9f62e1e 100644 --- a/src/aelfrice/models.py +++ b/src/aelfrice/models.py @@ -14,12 +14,17 @@ BELIEF_CORRECTION: Final[str] = "correction" BELIEF_PREFERENCE: Final[str] = "preference" BELIEF_REQUIREMENT: Final[str] = "requirement" +# v2.1 #548 wonder lifecycle. Speculative beliefs are wonder-generated +# candidates pending promotion. Not user-facing until `aelf confirm` +# promotes them to a real type. C4 retags this → real type on threshold. +BELIEF_SPECULATIVE: Final[str] = "speculative" BELIEF_TYPES: Final[frozenset[str]] = frozenset({ BELIEF_FACTUAL, BELIEF_CORRECTION, BELIEF_PREFERENCE, BELIEF_REQUIREMENT, + BELIEF_SPECULATIVE, }) # --- Edge types --- @@ -32,6 +37,11 @@ EDGE_IMPLEMENTS: Final[str] = "IMPLEMENTS" EDGE_TEMPORAL_NEXT: Final[str] = "TEMPORAL_NEXT" EDGE_TESTS: Final[str] = "TESTS" +# v2.1 #548 wonder lifecycle. RESOLVES marks that a speculative phantom +# resolves (answers or supersedes) an existing belief. A phantom with any +# RESOLVES edge (incoming or outgoing) is excluded from GC — the edge +# signals human-observable intent that the phantom should persist. +EDGE_RESOLVES: Final[str] = "RESOLVES" # Marker edge — semantically distinct from the relational edge types # above. POTENTIALLY_STALE tags a target belief as suspected stale; it @@ -61,6 +71,9 @@ # TESTS (0.55): evidential edge — source is a test belief, target is the # spec/claim under test. Placed just below SUPPORTS (0.60) because a test # asserts coverage of a claim rather than directly arguing for it. +# RESOLVES (0.0): wonder-lifecycle marker. No propagation valence because +# resolution intent (phantom answers an existing belief) doesn't carry +# evidential weight in the Bayesian update chain. EDGE_VALENCE: Final[dict[str, float]] = { EDGE_SUPPORTS: 1.0, EDGE_CITES: 0.5, @@ -71,6 +84,7 @@ EDGE_IMPLEMENTS: 0.65, EDGE_TEMPORAL_NEXT: 0.2, EDGE_TESTS: 0.55, + EDGE_RESOLVES: 0.0, } EDGE_TYPES: Final[frozenset[str]] = frozenset(EDGE_VALENCE.keys()) @@ -177,6 +191,11 @@ def retention_class_for_source(source_kind: str) -> str: # downstream consumers know the row is migration-produced, not a live # re-ingest. CORROBORATION_SOURCE_CONSOLIDATION_MIGRATION: Final[str] = "consolidation_migration" +# v2.1 #548 wonder lifecycle. Records the `wonder_ingest` assertion that +# produced a speculative phantom. The `source_path_hash` field carries +# `"@"` so the provenance is auditable without a +# dedicated audit_log table (Track A3 is deferred). +CORROBORATION_SOURCE_WONDER_INGEST: Final[str] = "wonder_ingest" CORROBORATION_SOURCE_TYPES: Final[frozenset[str]] = frozenset({ CORROBORATION_SOURCE_COMMIT_INGEST, @@ -185,6 +204,7 @@ def retention_class_for_source(source_kind: str) -> str: CORROBORATION_SOURCE_FILESYSTEM_INGEST, CORROBORATION_SOURCE_CLI_REMEMBER, CORROBORATION_SOURCE_CONSOLIDATION_MIGRATION, + CORROBORATION_SOURCE_WONDER_INGEST, }) # v2.0 #205 ingest_log source_kind enum. Wire-format strings; do not @@ -250,6 +270,10 @@ class Belief: is active. `activation_condition` is JSON-encoded TEXT when set. Behavior (when to set, when to wake, predicate evaluator) is a follow-up issue; this commit only locks in the round-trip shape. + + `valid_to` (v2.1 #548) is the soft-delete timestamp for wonder GC. + NULL = active. Non-NULL = GC'd by `wonder_gc`; the belief is excluded + from retrieval once set. Only speculative phantoms are GC-eligible. """ id: str @@ -269,6 +293,7 @@ class Belief: hibernation_score: float | None = None activation_condition: str | None = None retention_class: str = RETENTION_UNKNOWN + valid_to: str | None = None ANCHOR_TEXT_MAX_LEN: Final[int] = 1000 diff --git a/tests/test_corroborations.py b/tests/test_corroborations.py index 67d9104d7..fb66a7303 100644 --- a/tests/test_corroborations.py +++ b/tests/test_corroborations.py @@ -269,6 +269,7 @@ def test_source_type_enum_covers_all_variants() -> None: Updated for #219: filesystem_ingest, cli_remember, and consolidation_migration added to support cross-source dedup. + Updated for #548: wonder_ingest added for speculative phantom audit trail. """ expected = { "commit_ingest", @@ -277,6 +278,7 @@ def test_source_type_enum_covers_all_variants() -> None: "filesystem_ingest", "cli_remember", "consolidation_migration", + "wonder_ingest", } assert CORROBORATION_SOURCE_TYPES == expected From a0ea667aad019d68986a3e274a641cdd6f0db46d Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sat, 9 May 2026 21:58:50 -0700 Subject: [PATCH 2/4] feat(store): valid_to soft-delete migration + wonder GC query method + lifecycle allowlist (#548) ALTER TABLE beliefs ADD COLUMN valid_to TEXT (NULL=active). Partial index idx_beliefs_speculative_gc on (origin, created_at) WHERE valid_to IS NULL for GC scan performance. New MemoryStore.soft_delete_belief() (idempotent via WHERE valid_to IS NULL) and query_wonder_gc_candidates() (NOT EXISTS guards on feedback_history and RESOLVES edges). aelfrice.wonder.lifecycle added to INSERT_BELIEF_ALLOWLIST per the PR #478 gate; allowlist guard test updated. --- src/aelfrice/store.py | 87 ++++++++++++++++++++++++++++++-- tests/test_insert_belief_gate.py | 11 ++-- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/aelfrice/store.py b/src/aelfrice/store.py index ffd90baec..9a61b95e1 100644 --- a/src/aelfrice/store.py +++ b/src/aelfrice/store.py @@ -65,6 +65,7 @@ INSERT_BELIEF_ALLOWLIST: Final[frozenset[str]] = frozenset({ "aelfrice.derivation_worker", "aelfrice.wonder.simulator", + "aelfrice.wonder.lifecycle", "aelfrice.benchmark", "aelfrice.migrate", }) @@ -449,6 +450,10 @@ def _check_insert_belief_authority() -> None: # ADD COLUMN with a CHECK is brittle across SQLite versions. # Python-side RETENTION_CLASSES validates inserts. "ALTER TABLE beliefs ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'unknown'", + # v2.1 #548 wonder lifecycle. Soft-delete timestamp: NULL = active, + # non-NULL = GC'd by `wonder_gc`. Existing rows default to NULL + # (active) which is correct — only speculative phantoms are GC'd. + "ALTER TABLE beliefs ADD COLUMN valid_to TEXT", ) # Indexes that depend on migrated columns. Run after _MIGRATIONS so @@ -456,6 +461,9 @@ def _check_insert_belief_authority() -> None: _POST_MIGRATION_INDEXES: tuple[str, ...] = ( "CREATE INDEX IF NOT EXISTS idx_beliefs_session ON beliefs(session_id)", "CREATE INDEX IF NOT EXISTS idx_beliefs_origin ON beliefs(origin)", + # v2.1 #548: partial index on active speculative beliefs for GC scans. + "CREATE INDEX IF NOT EXISTS idx_beliefs_speculative_gc " + "ON beliefs(origin, created_at) WHERE valid_to IS NULL", ) # One-shot backfill for v1.0/v1.1 stores opening on v1.2+. Each row @@ -507,6 +515,9 @@ def _row_to_belief(row: sqlite3.Row) -> Belief: row["retention_class"] if "retention_class" in keys else RETENTION_UNKNOWN ) + # valid_to column added in v2.1 (#548). Pre-migration rows default + # to None (active). Same fallback pattern as retention_class. + valid_to = row["valid_to"] if "valid_to" in keys else None return Belief( id=row["id"], content=row["content"], @@ -525,6 +536,7 @@ def _row_to_belief(row: sqlite3.Row) -> Belief: hibernation_score=row["hibernation_score"], activation_condition=row["activation_condition"], retention_class=retention_class, + valid_to=valid_to, ) @@ -1334,15 +1346,15 @@ def insert_belief(self, b: Belief) -> None: lock_level, locked_at, demotion_pressure, created_at, last_retrieved_at, session_id, origin, hibernation_score, activation_condition, - retention_class - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + retention_class, valid_to + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( b.id, b.content, b.content_hash, b.alpha, b.beta, b.type, b.lock_level, b.locked_at, b.demotion_pressure, b.created_at, b.last_retrieved_at, b.session_id, b.origin, b.hibernation_score, b.activation_condition, - b.retention_class, + b.retention_class, b.valid_to, ), ) self._conn.execute( @@ -1446,6 +1458,75 @@ def delete_belief(self, belief_id: str) -> None: self._conn.commit() self._fire_invalidation() + def soft_delete_belief(self, belief_id: str, ts: str | None = None) -> None: + """Set `valid_to` on a belief to soft-delete it (v2.1 #548 wonder GC). + + Only moves beliefs from active (valid_to IS NULL) to soft-deleted. + Calling this on an already-GC'd belief is a no-op (idempotent via + the WHERE clause). Does not remove edges or corroboration rows — + the phantom's evidence trail is preserved for audit. + """ + now = ts if ts is not None else datetime.now(timezone.utc).isoformat() + self._conn.execute( + "UPDATE beliefs SET valid_to = ? WHERE id = ? AND valid_to IS NULL", + (now, belief_id), + ) + self._bump_belief_version(belief_id) + self._conn.commit() + self._fire_invalidation() + + def query_wonder_gc_candidates( + self, + *, + cutoff_ts: str, + alpha_default: float = 0.3, + beta_default: float = 1.0, + alpha_epsilon: float = 1e-9, + beta_epsilon: float = 1e-9, + ) -> list[str]: + """Return belief IDs eligible for wonder GC (v2.1 #548). + + Candidates satisfy ALL of: + - type = 'speculative' + - origin = ORIGIN_SPECULATIVE + - valid_to IS NULL (still active) + - created_at < cutoff_ts (older than ttl_days) + - alpha <= alpha_default + epsilon AND beta <= beta_default + epsilon + (priors unchanged from wonder_ingest defaults) + - no feedback_history rows (apply_feedback never called) + - no RESOLVES edges (incoming or outgoing) + + The caller is responsible for computing `cutoff_ts` from `ttl_days`. + Returns a list of belief IDs; order is not guaranteed. + """ + cur = self._conn.execute( + """ + SELECT b.id + FROM beliefs b + WHERE b.type = 'speculative' + AND b.origin = 'speculative' + AND b.valid_to IS NULL + AND b.created_at < ? + AND b.alpha <= ? + AND b.beta <= ? + AND NOT EXISTS ( + SELECT 1 FROM feedback_history fh + WHERE fh.belief_id = b.id + ) + AND NOT EXISTS ( + SELECT 1 FROM edges e + WHERE e.type = 'RESOLVES' + AND (e.src = b.id OR e.dst = b.id) + ) + """, + ( + cutoff_ts, + alpha_default + alpha_epsilon, + beta_default + beta_epsilon, + ), + ) + return [row["id"] for row in cur.fetchall()] + # --- Entity index (v1.3 L2.5 retrieval) ------------------------------ def _write_belief_entities(self, belief_id: str, content: str) -> None: diff --git a/tests/test_insert_belief_gate.py b/tests/test_insert_belief_gate.py index ebfcf23e6..01a737ab7 100644 --- a/tests/test_insert_belief_gate.py +++ b/tests/test_insert_belief_gate.py @@ -185,12 +185,17 @@ def test_gate_on_allowlisted_module_passes( def test_allowlist_contents_are_ratified_set() -> None: - """Hypothesis: the allowlist is exactly the four modules ratified - on PR #478. Adding or removing entries is a design decision that - requires re-ratification. Falsifiable by drift on either side.""" + """Hypothesis: the allowlist is exactly the ratified set of modules. + + PR #478 ratified the original four. #548 added wonder.lifecycle for + speculative phantom ingest. Adding or removing entries is a design + decision that requires re-ratification. Falsifiable by drift on either + side. + """ assert INSERT_BELIEF_ALLOWLIST == frozenset({ "aelfrice.derivation_worker", "aelfrice.wonder.simulator", + "aelfrice.wonder.lifecycle", "aelfrice.benchmark", "aelfrice.migrate", }) From 7934f9cd41ae3cc039151d59b5bc97404b858b00 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sat, 9 May 2026 21:58:50 -0700 Subject: [PATCH 3/4] feat(wonder): wonder_ingest + wonder_gc lifecycle module (#548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New src/aelfrice/wonder/lifecycle.py. wonder_ingest persists Phantom candidates as type='speculative' beliefs (alpha=0.3, beta=1.0, origin=ORIGIN_SPECULATIVE), writes RELATES_TO edges to each constituent, and records a wonder_ingest corroboration row. Idempotent via SHA-256 of sorted constituent_belief_ids as content_hash (not text — phantoms with identical text from different pairs are distinct). wonder_gc soft-deletes stale phantoms via store.soft_delete_belief(). Preserves any phantom with a RESOLVES edge or alpha-update beyond epsilon band. Closes the cli.py:884 TODO against #229. --- src/aelfrice/wonder/lifecycle.py | 201 +++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/aelfrice/wonder/lifecycle.py diff --git a/src/aelfrice/wonder/lifecycle.py b/src/aelfrice/wonder/lifecycle.py new file mode 100644 index 000000000..8596c517d --- /dev/null +++ b/src/aelfrice/wonder/lifecycle.py @@ -0,0 +1,201 @@ +"""Wonder lifecycle: ingest and GC for speculative phantom beliefs (#548). + +Two entry points: + +* ``wonder_ingest`` — persists in-memory ``Phantom`` candidates to the + store as ``type='speculative'`` beliefs with ``origin=ORIGIN_SPECULATIVE``, + Bayesian prior α=0.3 / β=1.0, ``RELATES_TO`` edges to each constituent + belief, and a ``wonder_ingest`` corroboration row for audit. + +* ``wonder_gc`` — soft-deletes stale speculative beliefs by setting + ``valid_to`` on candidates that have received no feedback and whose + priors are still at the ingest defaults. +""" +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING + +from aelfrice.models import ( + BELIEF_SPECULATIVE, + CORROBORATION_SOURCE_WONDER_INGEST, + EDGE_RELATES_TO, + LOCK_NONE, + ORIGIN_SPECULATIVE, + RETENTION_SNAPSHOT, + Belief, + Edge, + Phantom, +) +from aelfrice.ulid import ulid + +if TYPE_CHECKING: + from aelfrice.store import MemoryStore + +# Default Bayesian priors for freshly ingested speculative beliefs. +# Calibrated conservatively: α=0.3 gives a weak positive prior; +# β=1.0 reflects genuine uncertainty. GC uses these as the "unchanged" +# threshold — any α-update above the epsilon band means a feedback event +# has touched the belief, so it survives. +_INGEST_ALPHA: float = 0.3 +_INGEST_BETA: float = 1.0 + + +def _constituent_key(constituent_belief_ids: tuple[str, ...]) -> str: + """SHA-256 of the sorted constituent IDs — the idempotency key. + + Keyed on the sorted tuple rather than on content text so that two + phantoms produced from the same constituent pair (but with different + generated text) are treated as the same candidate. Phantoms from + *different* constituent pairs with *identical* text are distinct — + hence content hash is not the dedup axis here. + """ + raw = "wonder_ingest:" + ":".join(sorted(constituent_belief_ids)) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + +@dataclass(frozen=True) +class WonderIngestResult: + """Summary returned by ``wonder_ingest``.""" + + inserted: int + skipped: int + edges_created: int + + +@dataclass(frozen=True) +class WonderGCResult: + """Summary returned by ``wonder_gc``.""" + + scanned: int + deleted: int + surviving: int + + +def wonder_ingest( + store: "MemoryStore", + phantoms: list[Phantom], + session_id: str | None = None, +) -> WonderIngestResult: + """Persist speculative phantom candidates to the store. + + For each ``Phantom``: + + 1. Derive a deterministic ``content_hash`` from the sorted + ``constituent_belief_ids``; if a belief with that hash already + exists, skip insertion (idempotent on the constituent-pair key). + 2. Insert a ``Belief`` with ``type='speculative'``, + ``origin=ORIGIN_SPECULATIVE``, α=0.3, β=1.0. + 3. Insert ``RELATES_TO`` edges from the new belief to every + constituent. + 4. Record a ``wonder_ingest`` corroboration row; ``source_path_hash`` + encodes ``"@"`` for audit. + + Returns ``WonderIngestResult(inserted, skipped, edges_created)``. + """ + now = datetime.now(timezone.utc).isoformat() + inserted = 0 + skipped = 0 + edges_created = 0 + + for phantom in phantoms: + key = _constituent_key(phantom.constituent_belief_ids) + existing = store.get_belief_by_content_hash(key) + if existing is not None: + skipped += 1 + continue + + belief_id = ulid() + belief = Belief( + id=belief_id, + content=phantom.content, + content_hash=key, + alpha=_INGEST_ALPHA, + beta=_INGEST_BETA, + type=BELIEF_SPECULATIVE, + lock_level=LOCK_NONE, + locked_at=None, + demotion_pressure=0, + created_at=now, + last_retrieved_at=None, + session_id=session_id, + origin=ORIGIN_SPECULATIVE, + retention_class=RETENTION_SNAPSHOT, + ) + store.insert_belief(belief) + + for constituent_id in phantom.constituent_belief_ids: + store.insert_edge(Edge( + src=belief_id, + dst=constituent_id, + type=EDGE_RELATES_TO, + weight=1.0, + )) + edges_created += 1 + + audit_meta = f"{phantom.generator}@{phantom.score:.4f}" + store.record_corroboration( + belief_id, + source_type=CORROBORATION_SOURCE_WONDER_INGEST, + session_id=session_id, + source_path_hash=audit_meta, + ) + + inserted += 1 + + return WonderIngestResult( + inserted=inserted, + skipped=skipped, + edges_created=edges_created, + ) + + +def wonder_gc( + store: "MemoryStore", + ttl_days: int = 14, + dry_run: bool = False, +) -> WonderGCResult: + """Soft-delete stale speculative beliefs that have received no feedback. + + Candidates must satisfy ALL of: + - ``type = 'speculative'`` and ``origin = ORIGIN_SPECULATIVE`` + - ``valid_to IS NULL`` (still active) + - ``created_at`` older than ``ttl_days`` days ago + - α ≤ 0.3 + ε and β ≤ 1.0 + ε (priors unchanged from ingest defaults) + - no ``feedback_history`` rows (``apply_feedback`` never called) + - no ``RESOLVES`` edges (incoming or outgoing) + + If ``dry_run`` is True, reports candidates without mutating the store. + The second run in non-dry-run mode finds zero new candidates + (idempotent because ``soft_delete_belief`` guards on ``valid_to IS NULL``). + + Returns ``WonderGCResult(scanned, deleted, surviving)``. + """ + cutoff = datetime.now(timezone.utc) - timedelta(days=ttl_days) + cutoff_ts = cutoff.isoformat() + + candidate_ids = store.query_wonder_gc_candidates(cutoff_ts=cutoff_ts) + scanned = len(candidate_ids) + + if dry_run: + return WonderGCResult(scanned=scanned, deleted=0, surviving=scanned) + + now = datetime.now(timezone.utc).isoformat() + for belief_id in candidate_ids: + store.soft_delete_belief(belief_id, ts=now) + + return WonderGCResult( + scanned=scanned, + deleted=scanned, + surviving=0, + ) + + +__all__ = [ + "WonderGCResult", + "WonderIngestResult", + "wonder_gc", + "wonder_ingest", +] From e45fde306984133f577de26dd2dee5accd93bf1d Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sat, 9 May 2026 21:58:51 -0700 Subject: [PATCH 4/4] test(wonder): lifecycle ingest/gc/idempotency/preservation cases (#548) 14 tests covering wonder_ingest (belief schema, session_id propagation, RELATES_TO edges, audit corroboration row, idempotency on constituent-pair key, distinct-pair non-dedup) and wonder_gc (dry-run, non-dry-run, fresh-skip, RESOLVES preservation incoming/outgoing, alpha-update preservation, idempotency). --- tests/test_wonder_lifecycle.py | 395 +++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 tests/test_wonder_lifecycle.py diff --git a/tests/test_wonder_lifecycle.py b/tests/test_wonder_lifecycle.py new file mode 100644 index 000000000..69ed3b9a5 --- /dev/null +++ b/tests/test_wonder_lifecycle.py @@ -0,0 +1,395 @@ +"""Tests for wonder_ingest and wonder_gc lifecycle (#548).""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +import pytest + +from aelfrice.models import ( + BELIEF_FACTUAL, + BELIEF_SPECULATIVE, + CORROBORATION_SOURCE_WONDER_INGEST, + EDGE_RELATES_TO, + EDGE_RESOLVES, + LOCK_NONE, + ORIGIN_SPECULATIVE, + RETENTION_FACT, + Belief, + Edge, + Phantom, +) +from aelfrice.store import MemoryStore +from aelfrice.wonder.lifecycle import ( + WonderGCResult, + WonderIngestResult, + wonder_gc, + wonder_ingest, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +_ALPHA_DEFAULT = 0.3 +_BETA_DEFAULT = 1.0 + + +def _constituent(bid: str) -> Belief: + return Belief( + id=bid, + content=f"constituent content for {bid}", + content_hash=f"ch_{bid}", + alpha=1.0, + beta=1.0, + type=BELIEF_FACTUAL, + lock_level=LOCK_NONE, + locked_at=None, + demotion_pressure=0, + created_at="2026-05-01T00:00:00+00:00", + last_retrieved_at=None, + retention_class=RETENTION_FACT, + ) + + +def _phantom( + a_id: str, + b_id: str, + *, + content: str = "speculative content", + score: float = 0.75, + generator: str = "bfs+wonder_consolidation", +) -> Phantom: + return Phantom( + constituent_belief_ids=(a_id, b_id), + generator=generator, + content=content, + score=score, + ) + + +@pytest.fixture +def store() -> MemoryStore: + return MemoryStore(":memory:") + + +@pytest.fixture +def store_with_constituents(store: MemoryStore) -> MemoryStore: + store.insert_belief(_constituent("a")) + store.insert_belief(_constituent("b")) + store.insert_belief(_constituent("c")) + return store + + +# --------------------------------------------------------------------------- +# wonder_ingest: schema correctness +# --------------------------------------------------------------------------- + + +def test_ingest_writes_belief_with_speculative_type( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantom = _phantom("a", "b") + result = wonder_ingest(store, [phantom]) + + assert isinstance(result, WonderIngestResult) + assert result.inserted == 1 + assert result.skipped == 0 + + beliefs = [store.get_belief(bid) for bid in store.list_belief_ids()] + speculative = [b for b in beliefs if b is not None and b.type == BELIEF_SPECULATIVE] + assert len(speculative) == 1 + + s = speculative[0] + assert s.origin == ORIGIN_SPECULATIVE + assert s.alpha == pytest.approx(_ALPHA_DEFAULT) + assert s.beta == pytest.approx(_BETA_DEFAULT) + assert s.content == phantom.content + assert s.lock_level == LOCK_NONE + assert s.valid_to is None + + +def test_ingest_writes_belief_with_session_id( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + result = wonder_ingest(store, [_phantom("a", "b")], session_id="sess-1") + assert result.inserted == 1 + + beliefs = [store.get_belief(bid) for bid in store.list_belief_ids()] + speculative = [b for b in beliefs if b is not None and b.type == BELIEF_SPECULATIVE] + assert len(speculative) == 1 + assert speculative[0].session_id == "sess-1" + + +# --------------------------------------------------------------------------- +# wonder_ingest: RELATES_TO edges +# --------------------------------------------------------------------------- + + +def test_ingest_writes_relates_to_edges_to_all_constituents( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantom = Phantom( + constituent_belief_ids=("a", "b", "c"), + generator="bfs", + content="three-way speculative", + score=0.5, + ) + result = wonder_ingest(store, [phantom]) + assert result.inserted == 1 + assert result.edges_created == 3 + + beliefs = [store.get_belief(bid) for bid in store.list_belief_ids()] + phantom_belief = next( + b for b in beliefs if b is not None and b.type == BELIEF_SPECULATIVE + ) + edges = store.edges_from(phantom_belief.id) + relates = [e for e in edges if e.type == EDGE_RELATES_TO] + assert len(relates) == 3 + assert {e.dst for e in relates} == {"a", "b", "c"} + + +def test_ingest_edge_count_matches_result( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantoms = [_phantom("a", "b"), _phantom("b", "c")] + result = wonder_ingest(store, phantoms) + assert result.inserted == 2 + assert result.edges_created == 4 + + +# --------------------------------------------------------------------------- +# wonder_ingest: audit row +# --------------------------------------------------------------------------- + + +def test_ingest_writes_audit_corroboration_row( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantom = _phantom("a", "b", score=0.8765, generator="bfs+wonder_consolidation") + wonder_ingest(store, [phantom]) + + beliefs = [store.get_belief(bid) for bid in store.list_belief_ids()] + speculative = next( + b for b in beliefs if b is not None and b.type == BELIEF_SPECULATIVE + ) + corroborations = store.list_corroborations(speculative.id) + assert len(corroborations) == 1 + + ingested_at, source_type, _session, source_path_hash = corroborations[0] + assert source_type == CORROBORATION_SOURCE_WONDER_INGEST + assert source_path_hash is not None + assert "bfs+wonder_consolidation" in source_path_hash + assert "0.8765" in source_path_hash + + +# --------------------------------------------------------------------------- +# wonder_ingest: idempotency +# --------------------------------------------------------------------------- + + +def test_ingest_is_idempotent_on_same_constituent_pair( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantom = _phantom("a", "b") + + r1 = wonder_ingest(store, [phantom]) + r2 = wonder_ingest(store, [phantom]) + + assert r1.inserted == 1 + assert r1.skipped == 0 + assert r2.inserted == 0 + assert r2.skipped == 1 + + beliefs = [store.get_belief(bid) for bid in store.list_belief_ids()] + speculative = [b for b in beliefs if b is not None and b.type == BELIEF_SPECULATIVE] + assert len(speculative) == 1 + + +def test_ingest_distinct_constituent_pairs_are_not_deduped( + store_with_constituents: MemoryStore, +) -> None: + store = store_with_constituents + phantom_ab = _phantom("a", "b", content="same text") + phantom_bc = _phantom("b", "c", content="same text") + + r = wonder_ingest(store, [phantom_ab, phantom_bc]) + assert r.inserted == 2 + assert r.skipped == 0 + + +# --------------------------------------------------------------------------- +# wonder_gc: dry_run +# --------------------------------------------------------------------------- + + +def _old_ts(days_ago: int = 15) -> str: + dt = datetime.now(timezone.utc) - timedelta(days=days_ago) + return dt.isoformat() + + +def _insert_old_speculative(store: MemoryStore, bid: str, days_ago: int = 15) -> str: + """Insert a stale speculative belief directly (bypassing lifecycle for date control).""" + b = Belief( + id=bid, + content=f"speculative content {bid}", + content_hash=f"spec_hash_{bid}", + alpha=_ALPHA_DEFAULT, + beta=_BETA_DEFAULT, + type=BELIEF_SPECULATIVE, + lock_level=LOCK_NONE, + locked_at=None, + demotion_pressure=0, + created_at=_old_ts(days_ago), + last_retrieved_at=None, + origin=ORIGIN_SPECULATIVE, + retention_class="snapshot", + ) + store.insert_belief(b) + return bid + + +def test_gc_dry_run_reports_candidates_without_deleting(store: MemoryStore) -> None: + _insert_old_speculative(store, "spec1") + _insert_old_speculative(store, "spec2") + + result = wonder_gc(store, ttl_days=14, dry_run=True) + + assert isinstance(result, WonderGCResult) + assert result.scanned == 2 + assert result.deleted == 0 + assert result.surviving == 2 + + b1 = store.get_belief("spec1") + assert b1 is not None + assert b1.valid_to is None + + +# --------------------------------------------------------------------------- +# wonder_gc: non-dry-run sets valid_to +# --------------------------------------------------------------------------- + + +def test_gc_non_dry_run_sets_valid_to(store: MemoryStore) -> None: + _insert_old_speculative(store, "spec1") + + result = wonder_gc(store, ttl_days=14, dry_run=False) + + assert result.scanned == 1 + assert result.deleted == 1 + assert result.surviving == 0 + + b = store.get_belief("spec1") + assert b is not None + assert b.valid_to is not None + + +def test_gc_skips_fresh_beliefs(store: MemoryStore) -> None: + b = Belief( + id="fresh", + content="fresh speculative", + content_hash="fresh_hash", + alpha=_ALPHA_DEFAULT, + beta=_BETA_DEFAULT, + type=BELIEF_SPECULATIVE, + lock_level=LOCK_NONE, + locked_at=None, + demotion_pressure=0, + created_at=datetime.now(timezone.utc).isoformat(), + last_retrieved_at=None, + origin=ORIGIN_SPECULATIVE, + retention_class="snapshot", + ) + store.insert_belief(b) + + result = wonder_gc(store, ttl_days=14, dry_run=False) + assert result.deleted == 0 + + b2 = store.get_belief("fresh") + assert b2 is not None + assert b2.valid_to is None + + +# --------------------------------------------------------------------------- +# wonder_gc: RESOLVES-edge preservation +# --------------------------------------------------------------------------- + + +def test_gc_preserves_phantom_with_outgoing_resolves_edge(store: MemoryStore) -> None: + _insert_old_speculative(store, "spec1") + store.insert_belief(_constituent("real")) + store.insert_edge(Edge(src="spec1", dst="real", type=EDGE_RESOLVES, weight=1.0)) + + result = wonder_gc(store, ttl_days=14, dry_run=False) + assert result.deleted == 0 + + b = store.get_belief("spec1") + assert b is not None + assert b.valid_to is None + + +def test_gc_preserves_phantom_with_incoming_resolves_edge(store: MemoryStore) -> None: + _insert_old_speculative(store, "spec1") + store.insert_belief(_constituent("real")) + store.insert_edge(Edge(src="real", dst="spec1", type=EDGE_RESOLVES, weight=1.0)) + + result = wonder_gc(store, ttl_days=14, dry_run=False) + assert result.deleted == 0 + + b = store.get_belief("spec1") + assert b is not None + assert b.valid_to is None + + +# --------------------------------------------------------------------------- +# wonder_gc: alpha-update preservation +# --------------------------------------------------------------------------- + + +def test_gc_preserves_phantom_with_alpha_update(store: MemoryStore) -> None: + b = Belief( + id="updated", + content="updated alpha speculative", + content_hash="updated_hash", + alpha=0.5, + beta=_BETA_DEFAULT, + type=BELIEF_SPECULATIVE, + lock_level=LOCK_NONE, + locked_at=None, + demotion_pressure=0, + created_at=_old_ts(20), + last_retrieved_at=None, + origin=ORIGIN_SPECULATIVE, + retention_class="snapshot", + ) + store.insert_belief(b) + + result = wonder_gc(store, ttl_days=14, dry_run=False) + assert result.deleted == 0 + + b2 = store.get_belief("updated") + assert b2 is not None + assert b2.valid_to is None + + +# --------------------------------------------------------------------------- +# wonder_gc: idempotency +# --------------------------------------------------------------------------- + + +def test_gc_is_idempotent(store: MemoryStore) -> None: + _insert_old_speculative(store, "spec1") + _insert_old_speculative(store, "spec2") + + r1 = wonder_gc(store, ttl_days=14, dry_run=False) + assert r1.deleted == 2 + + r2 = wonder_gc(store, ttl_days=14, dry_run=False) + assert r2.scanned == 0 + assert r2.deleted == 0