"""Shared fixtures for engine-level end-to-end tests. These tests exercise the *public* engine surface delivered earlier in this milestone. The contract they rely on (a maintainer can diff this list against the modules quickly): mnema.core.keys.KeyPair.generate() -> KeyPair mnema.core.log.OperationLog(path) -> iterable of ops, each op has `.kind` mnema.derive.loaders.load_calendar(path) -> list[Evidence] mnema.derive.loaders.load_notes(path) -> list[Evidence] mnema.derive.loaders.load_photos(path) -> list[Evidence] mnema.derive.engine.DerivationEngine(log=, keypair=, derivers=None) .ingest(evidence) append signed evidence operations .run() -> DerivationResult(.new_claims, .explanations) .claims() -> list[Claim] currently ACTIVE .explain(claim_id) -> Explanation .refute(claim_id, reason=...) -> report with `.invalidated` .correct(claim_id, value=, reason=...) -> report with `.replacement`, `.invalidated` .graph -> DerivationGraph .inputs(node_id) -> list[str] direct inputs (claims/evidence) .descendants(id) -> set[str] transitive dependents .status(claim_id) -> ClaimStatus mnema.derive.model.Claim: .claim_id .subject .predicate .value .confidence .deriver mnema.derive.model.ClaimStatus: ACTIVE / REFUTED / INVALIDATED mnema.derive.model.Evidence: .evidence_id Sample evidence is loaded from data/samples/*.jsonl (delivered in a previous pass of this milestone). """ from __future__ import annotations from pathlib import Path import pytest from mnema.core.keys import KeyPair from mnema.core.log import OperationLog from mnema.derive.engine import DerivationEngine from mnema.derive.loaders import load_calendar, load_notes, load_photos REPO_ROOT = Path(__file__).resolve().parents[2] SAMPLES_DIR = REPO_ROOT / "data" / "samples" @pytest.fixture() def samples_dir() -> Path: assert SAMPLES_DIR.is_dir(), f"sample data missing at {SAMPLES_DIR}" return SAMPLES_DIR @pytest.fixture() def keypair() -> KeyPair: return KeyPair.generate() @pytest.fixture() def op_log(tmp_path) -> OperationLog: return OperationLog(tmp_path / "ops.jsonl") @pytest.fixture() def sample_evidence(samples_dir): """The full sample corpus: calendar + notes + photo metadata.""" evidence = [] evidence += load_calendar(samples_dir / "calendar.sample.jsonl") evidence += load_notes(samples_dir / "notes.sample.jsonl") evidence += load_photos(samples_dir / "photos.sample.jsonl") assert evidence, "sample corpus must not be empty" return evidence @pytest.fixture() def engine(op_log, keypair) -> DerivationEngine: """An engine with the default deriver set over a fresh log.""" return DerivationEngine(log=op_log, keypair=keypair) @pytest.fixture() def ran(engine, sample_evidence): """Engine after one full ingest + derivation pass. Returns (engine, first DerivationResult, ingested evidence list). """ engine.ingest(sample_evidence) result = engine.run() return engine, result, sample_evidence @pytest.fixture() def provenance_roots(): """Callable that walks `graph.inputs` transitively from a claim and returns the set of terminal (input-less, i.e. evidence) node ids.""" def _roots(engine, claim_id: str) -> set[str]: roots: set[str] = set() seen: set[str] = set() stack = list(engine.graph.inputs(claim_id)) while stack: node = stack.pop() if node in seen: continue seen.add(node) inputs = list(engine.graph.inputs(node)) if inputs: stack.extend(inputs) else: roots.add(node) return roots return _roots