From 2ff3cae808a073a804a254533df9a02c375e63fa Mon Sep 17 00:00:00 2001 From: Bradley Gauthier <2234748+bradleygauthier@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:22:23 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20v0.10.0=20=E2=80=94=20search=20intellig?= =?UTF-8?q?ence,=20self-healing,=20streaming,=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search: deduplication, pagination offset, explain mode (scoring breakdown). Integrity: semantic near-duplicate detection, contradiction detection. Streaming: VaultEventStream for real-time event consumption. Telemetry: VaultTelemetry with operation counters and latency. Per-resource health: vault.health(resource_id). Import/export: vault.export_vault(), vault.import_vault(). Removed [atlas] extra (no implementation). 448 tests. Lint clean. Build verified. --- CHANGELOG.md | 13 ++++ pyproject.toml | 7 +- src/qp_vault/__init__.py | 2 +- src/qp_vault/adversarial.py | 4 +- src/qp_vault/core/search_engine.py | 2 +- src/qp_vault/enums.py | 16 ++--- src/qp_vault/integrity/detector.py | 108 ++++++++++++++++++++++++++++ src/qp_vault/models.py | 6 +- src/qp_vault/streaming.py | 89 +++++++++++++++++++++++ src/qp_vault/telemetry.py | 109 +++++++++++++++++++++++++++++ src/qp_vault/vault.py | 94 +++++++++++++++++++++++-- tests/test_cis.py | 6 +- tests/test_cis_edge_cases.py | 6 +- tests/test_cis_phase2.py | 4 +- tests/test_search_engine_cis.py | 2 +- 15 files changed, 435 insertions(+), 33 deletions(-) create mode 100644 src/qp_vault/streaming.py create mode 100644 src/qp_vault/telemetry.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dfe94b..e163972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.0] - 2026-04-06 + +### Added +- **Search intelligence**: deduplication (one result per resource), pagination offset, explain mode (scoring breakdown) +- **Knowledge self-healing**: semantic near-duplicate detection, contradiction detection (trust/lifecycle conflicts) +- **Real-time event streaming**: VaultEventStream for subscribing to vault mutations +- **Telemetry**: VaultTelemetry with operation counters, latency, error rates +- **Per-resource health**: vault.health(resource_id) for individual quality assessment +- **Import/export**: vault.export_vault(path) and vault.import_vault(path) for portable vaults + +### Removed +- `[atlas]` extra (no implementation; removed to avoid confusion) + ## [0.9.0] - 2026-04-06 ### Added diff --git a/pyproject.toml b/pyproject.toml index 40dca11..e34e6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "qp-vault" -version = "0.9.0" +version = "0.10.0" description = "Governed knowledge store for autonomous organizations. Trust tiers, cryptographic audit trails, content-addressed storage, air-gap native." readme = "README.md" license = "Apache-2.0" @@ -74,9 +74,6 @@ integrity = [ fastapi = [ "fastapi>=0.135", ] -atlas = [ - "gitpython>=3.1", -] cli = [ "typer>=0.21", "rich>=14.3", @@ -90,7 +87,7 @@ dev = [ "ruff>=0.9", ] all = [ - "qp-vault[sqlite,postgres,docling,capsule,encryption,integrity,fastapi,atlas,cli]", + "qp-vault[sqlite,postgres,docling,local,openai,capsule,encryption,integrity,fastapi,cli]", ] [project.scripts] diff --git a/src/qp_vault/__init__.py b/src/qp_vault/__init__.py index bdcc0de..a60a086 100644 --- a/src/qp_vault/__init__.py +++ b/src/qp_vault/__init__.py @@ -26,7 +26,7 @@ Docs: https://github.com/quantumpipes/vault """ -__version__ = "0.9.0" +__version__ = "0.10.0" __author__ = "Quantum Pipes Technologies, LLC" __license__ = "Apache-2.0" diff --git a/src/qp_vault/adversarial.py b/src/qp_vault/adversarial.py index b813f01..5b6903d 100644 --- a/src/qp_vault/adversarial.py +++ b/src/qp_vault/adversarial.py @@ -8,8 +8,8 @@ model where effective RAG weight = trust_tier_weight * adversarial_multiplier. Status transitions: - UNVERIFIED -> VERIFIED (all CIS stages passed) - UNVERIFIED -> SUSPICIOUS (one or more CIS stages flagged) + UNVERIFIED -> VERIFIED (all Membrane stages passed) + UNVERIFIED -> SUSPICIOUS (one or more Membrane stages flagged) SUSPICIOUS -> VERIFIED (human reviewer cleared after investigation) VERIFIED -> SUSPICIOUS (re-assessment flagged new concerns) """ diff --git a/src/qp_vault/core/search_engine.py b/src/qp_vault/core/search_engine.py index d1bdec6..28fc96a 100644 --- a/src/qp_vault/core/search_engine.py +++ b/src/qp_vault/core/search_engine.py @@ -178,7 +178,7 @@ def apply_trust_weighting( tier = result.trust_tier.value if hasattr(result.trust_tier, "value") else str(result.trust_tier) tw = compute_trust_weight(tier, config) - # CIS 2D trust: multiply by adversarial verification status + # Membrane 2D trust: multiply by adversarial verification status adv_status = getattr(result, "adversarial_status", None) adv_str = adv_status.value if hasattr(adv_status, "value") else str(adv_status or "unverified") adv_mult = compute_adversarial_multiplier(adv_str) diff --git a/src/qp_vault/enums.py b/src/qp_vault/enums.py index 2a8a5fb..141826e 100644 --- a/src/qp_vault/enums.py +++ b/src/qp_vault/enums.py @@ -59,7 +59,7 @@ class ResourceStatus(StrEnum): """Registered, not yet processed.""" QUARANTINED = "quarantined" - """CIS screening in progress. Excluded from search and RAG retrieval.""" + """Membrane screening in progress. Excluded from search and RAG retrieval.""" PROCESSING = "processing" """Being chunked and embedded.""" @@ -75,24 +75,24 @@ class ResourceStatus(StrEnum): class AdversarialStatus(StrEnum): - """CIS adversarial verification status (second dimension of trust). + """Membrane adversarial verification status (second dimension of trust). Orthogonal to TrustTier (organizational confidence). Effective RAG weight = trust_tier_weight * adversarial_multiplier. """ UNVERIFIED = "unverified" - """Not yet screened by CIS. Default for legacy content. Multiplier: 0.7x.""" + """Not yet screened by Membrane. Default for legacy content. Multiplier: 0.7x.""" VERIFIED = "verified" - """Passed all CIS stages. Multiplier: 1.0x.""" + """Passed all Membrane stages. Multiplier: 1.0x.""" SUSPICIOUS = "suspicious" - """Flagged by one or more CIS stages. Multiplier: 0.3x.""" + """Flagged by one or more Membrane stages. Multiplier: 0.3x.""" class CISStage(StrEnum): - """Content Immune System pipeline stages.""" + """Membrane pipeline stages.""" INGEST = "ingest" """Stage 1: Provenance recording, format validation.""" @@ -120,9 +120,9 @@ class CISStage(StrEnum): class CISResult(StrEnum): - """Result of a single CIS stage evaluation.""" + """Result of a single Membrane stage evaluation.""" - PASS = "pass" # nosec B105 — CIS stage result, not a password + PASS = "pass" # nosec B105 — Membrane stage result, not a password """Content cleared this stage.""" FLAG = "flag" diff --git a/src/qp_vault/integrity/detector.py b/src/qp_vault/integrity/detector.py index c3f8918..770666e 100644 --- a/src/qp_vault/integrity/detector.py +++ b/src/qp_vault/integrity/detector.py @@ -14,6 +14,7 @@ import math from collections import Counter from datetime import UTC, datetime +from typing import Any from qp_vault.models import HealthScore, Resource @@ -106,6 +107,113 @@ def find_orphans( return orphans +def find_near_duplicates( + resources: list[Resource], + chunks_by_resource: dict[str, list[Any]] | None = None, + *, + similarity_threshold: float = 0.85, +) -> list[tuple[Resource, Resource, float]]: + """Find semantically similar resources using chunk embedding comparison. + + Compares resources by their first chunk's embedding. Returns pairs + above the similarity threshold. + + Args: + resources: Resources to compare. + chunks_by_resource: Dict mapping resource_id to list of Chunk objects. + similarity_threshold: Minimum cosine similarity to flag as near-duplicate. + + Returns: + List of (resource_a, resource_b, similarity_score) tuples. + """ + if not chunks_by_resource: + return [] + + # Get first-chunk embeddings per resource + embeddings: dict[str, list[float]] = {} + for r in resources: + chunks = chunks_by_resource.get(r.id, []) + if chunks and hasattr(chunks[0], "embedding") and chunks[0].embedding: + embeddings[r.id] = chunks[0].embedding + + # Pairwise comparison + resource_map = {r.id: r for r in resources} + pairs: list[tuple[Resource, Resource, float]] = [] + ids = list(embeddings.keys()) + + for i in range(len(ids)): + for j in range(i + 1, len(ids)): + a, b = embeddings[ids[i]], embeddings[ids[j]] + if len(a) != len(b) or not a: + continue + dot = sum(x * y for x, y in zip(a, b, strict=False)) + norm_a = sum(x * x for x in a) ** 0.5 + norm_b = sum(x * x for x in b) ** 0.5 + if norm_a == 0 or norm_b == 0: + continue + sim = dot / (norm_a * norm_b) + if sim >= similarity_threshold: + pairs.append((resource_map[ids[i]], resource_map[ids[j]], sim)) + + return sorted(pairs, key=lambda x: x[2], reverse=True) + + +def detect_contradictions( + resources: list[Resource], + chunks_by_resource: dict[str, list[Any]] | None = None, +) -> list[dict[str, Any]]: + """Detect potential contradictions between resources. + + Looks for resources with similar topics (high embedding similarity) + but different trust tiers or opposing lifecycle states, which may + indicate conflicting information. + + This is a heuristic approach. For full NLI-based contradiction + detection, an LLM provider is required (future enhancement). + + Args: + resources: Resources to analyze. + chunks_by_resource: Dict mapping resource_id to chunk lists. + + Returns: + List of contradiction records with resource pairs and reasons. + """ + contradictions: list[dict[str, Any]] = [] + + # Find resources that are semantically similar but have different trust tiers + near_dupes = find_near_duplicates( + resources, chunks_by_resource, similarity_threshold=0.75 + ) + + for r_a, r_b, similarity in near_dupes: + tier_a = r_a.trust_tier.value if hasattr(r_a.trust_tier, "value") else str(r_a.trust_tier) + tier_b = r_b.trust_tier.value if hasattr(r_b.trust_tier, "value") else str(r_b.trust_tier) + + # Flag if semantically similar but different trust tiers + if tier_a != tier_b: + contradictions.append({ + "type": "trust_conflict", + "resource_a": {"id": r_a.id, "name": r_a.name, "trust_tier": tier_a}, + "resource_b": {"id": r_b.id, "name": r_b.name, "trust_tier": tier_b}, + "similarity": similarity, + "reason": f"Similar content ({similarity:.0%}) with different trust tiers ({tier_a} vs {tier_b})", + }) + + # Flag if one is active and one is superseded (potential stale reference) + lc_a = r_a.lifecycle.value if hasattr(r_a.lifecycle, "value") else str(r_a.lifecycle) + lc_b = r_b.lifecycle.value if hasattr(r_b.lifecycle, "value") else str(r_b.lifecycle) + if lc_a == "active" and lc_b == "superseded" or lc_a == "superseded" and lc_b == "active": + contradictions.append({ + "type": "lifecycle_conflict", + "resource_a": {"id": r_a.id, "name": r_a.name, "lifecycle": lc_a}, + "resource_b": {"id": r_b.id, "name": r_b.name, "lifecycle": lc_b}, + "similarity": similarity, + "reason": f"Similar content ({similarity:.0%}) but conflicting lifecycle ({lc_a} vs {lc_b})", + }) + + return contradictions + + def compute_health_score( resources: list[Resource], ) -> HealthScore: diff --git a/src/qp_vault/models.py b/src/qp_vault/models.py index 1c8f0af..fe4431c 100644 --- a/src/qp_vault/models.py +++ b/src/qp_vault/models.py @@ -191,7 +191,7 @@ class HealthScore(BaseModel): resource_count: int = 0 -# --- Content Immune System models --- +# --- Membrane models --- class ContentProvenance(BaseModel): @@ -214,7 +214,7 @@ class ContentProvenance(BaseModel): class CISStageRecord(BaseModel): - """Result of a single CIS pipeline stage evaluation. + """Result of a single Membrane pipeline stage evaluation. Every stage (INGEST through REMEMBER) creates one record per document. Records are immutable once created. @@ -233,7 +233,7 @@ class CISStageRecord(BaseModel): class CISPipelineStatus(BaseModel): - """Aggregate status of the CIS pipeline for a single document.""" + """Aggregate status of the Membrane pipeline for a single document.""" resource_id: str = "" stages: list[CISStageRecord] = Field(default_factory=list) diff --git a/src/qp_vault/streaming.py b/src/qp_vault/streaming.py new file mode 100644 index 0000000..69f335e --- /dev/null +++ b/src/qp_vault/streaming.py @@ -0,0 +1,89 @@ +# Copyright 2026 Quantum Pipes Technologies, LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Real-time event streaming for vault mutations. + +Allows agents and consumers to subscribe to vault events +and react in real-time to knowledge changes. +""" + +from __future__ import annotations + +import asyncio +from collections import deque +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from qp_vault.models import VaultEvent + + +class VaultEventStream: + """Real-time event stream for vault mutations. + + Consumers subscribe and receive VaultEvents as they occur. + Supports multiple concurrent subscribers. + + Usage: + stream = VaultEventStream() + vault = AsyncVault("./knowledge", auditor=stream) + + # Subscribe in an async context + async for event in stream.subscribe(): + print(f"{event.event_type}: {event.resource_name}") + """ + + def __init__(self, *, buffer_size: int = 1000) -> None: + self._subscribers: list[asyncio.Queue[VaultEvent]] = [] + self._history: deque[VaultEvent] = deque(maxlen=buffer_size) + + async def record(self, event: VaultEvent) -> str: + """Record an event and broadcast to all subscribers. + + Implements AuditProvider protocol so it can be used as auditor. + """ + import uuid + event_id = str(uuid.uuid4()) + self._history.append(event) + + # Broadcast to all subscribers (drop if slow) + import contextlib + for queue in self._subscribers: + with contextlib.suppress(asyncio.QueueFull): + queue.put_nowait(event) + + return event_id + + async def subscribe(self, *, replay: bool = False) -> Any: + """Subscribe to the event stream. + + Args: + replay: If True, replay recent history before live events. + + Yields: + VaultEvent objects as they occur. + """ + queue: asyncio.Queue[VaultEvent] = asyncio.Queue(maxsize=100) + self._subscribers.append(queue) + + try: + # Replay history if requested + if replay: + for event in self._history: + yield event + + # Stream live events + while True: + event = await queue.get() + yield event + finally: + self._subscribers.remove(queue) + + @property + def history(self) -> list[VaultEvent]: + """Get recent event history.""" + return list(self._history) + + @property + def subscriber_count(self) -> int: + """Number of active subscribers.""" + return len(self._subscribers) diff --git a/src/qp_vault/telemetry.py b/src/qp_vault/telemetry.py new file mode 100644 index 0000000..f5ce460 --- /dev/null +++ b/src/qp_vault/telemetry.py @@ -0,0 +1,109 @@ +# Copyright 2026 Quantum Pipes Technologies, LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Basic telemetry for vault operations. + +Tracks operation counts, latencies, and error rates. +Designed for autonomous AI systems that need to monitor +their own knowledge infrastructure. +""" + +from __future__ import annotations + +import time +from collections import defaultdict +from dataclasses import dataclass +from datetime import UTC, datetime + + +@dataclass +class OperationMetrics: + """Metrics for a single operation type.""" + + count: int = 0 + errors: int = 0 + total_duration_ms: float = 0 + last_duration_ms: float = 0 + last_timestamp: str = "" + + @property + def avg_duration_ms(self) -> float: + return self.total_duration_ms / self.count if self.count > 0 else 0 + + +class VaultTelemetry: + """Lightweight telemetry collector for vault operations. + + Usage: + telemetry = VaultTelemetry() + + with telemetry.track("search"): + results = vault.search("query") + + print(telemetry.summary()) + """ + + def __init__(self) -> None: + self._metrics: dict[str, OperationMetrics] = defaultdict(OperationMetrics) + self._started_at = datetime.now(tz=UTC).isoformat() + + def track(self, operation: str) -> _TrackerContext: + """Context manager to track an operation's duration. + + Args: + operation: Name of the operation (e.g., "search", "add", "verify"). + """ + return _TrackerContext(self, operation) + + def record(self, operation: str, duration_ms: float, *, error: bool = False) -> None: + """Manually record an operation metric.""" + m = self._metrics[operation] + m.count += 1 + m.total_duration_ms += duration_ms + m.last_duration_ms = duration_ms + m.last_timestamp = datetime.now(tz=UTC).isoformat() + if error: + m.errors += 1 + + def get(self, operation: str) -> OperationMetrics: + """Get metrics for a specific operation.""" + return self._metrics[operation] + + def summary(self) -> dict[str, dict[str, float | int | str]]: + """Get a summary of all operation metrics.""" + result: dict[str, dict[str, float | int | str]] = {} + for op, m in self._metrics.items(): + result[op] = { + "count": m.count, + "errors": m.errors, + "avg_ms": round(m.avg_duration_ms, 2), + "last_ms": round(m.last_duration_ms, 2), + } + result["_meta"] = {"started_at": self._started_at} + return result + + def reset(self) -> None: + """Reset all metrics.""" + self._metrics.clear() + self._started_at = datetime.now(tz=UTC).isoformat() + + +class _TrackerContext: + """Context manager for tracking operation duration.""" + + def __init__(self, telemetry: VaultTelemetry, operation: str) -> None: + self._telemetry = telemetry + self._operation = operation + self._start: float = 0 + + def __enter__(self) -> _TrackerContext: + self._start = time.monotonic() + return self + + def __exit__(self, exc_type: type | None, *_: object) -> None: + duration_ms = (time.monotonic() - self._start) * 1000 + self._telemetry.record( + self._operation, + duration_ms, + error=exc_type is not None, + ) diff --git a/src/qp_vault/vault.py b/src/qp_vault/vault.py index e01664b..df70d93 100644 --- a/src/qp_vault/vault.py +++ b/src/qp_vault/vault.py @@ -561,11 +561,14 @@ async def search( *, tenant_id: str | None = None, top_k: int = 10, + offset: int = 0, threshold: float = 0.0, trust_min: TrustTier | str | None = None, layer: MemoryLayer | str | None = None, collection: str | None = None, as_of: date | None = None, + deduplicate: bool = True, + explain: bool = False, _layer_boost: float = 1.0, ) -> list[SearchResult]: """Trust-weighted hybrid search. @@ -617,7 +620,32 @@ async def search( # Apply threshold after trust weighting filtered = [r for r in weighted if r.relevance >= threshold] - return filtered[:top_k] + # Deduplicate by resource_id (keep best chunk per resource) + if deduplicate: + seen: dict[str, SearchResult] = {} + for r in filtered: + if r.resource_id not in seen or r.relevance > seen[r.resource_id].relevance: + seen[r.resource_id] = r + filtered = sorted(seen.values(), key=lambda x: x.relevance, reverse=True) + + # Apply pagination + paginated = filtered[offset : offset + top_k] + + # Add explain metadata if requested + if explain: + for r in paginated: + r.metadata = { # type: ignore[attr-defined] + "explain": { + "vector_similarity": r.vector_similarity, + "text_rank": r.text_rank, + "trust_weight": r.trust_weight, + "freshness": r.freshness, + "layer_boost": _layer_boost, + "composite_relevance": r.relevance, + } + } + + return paginated # --- Verification --- @@ -774,16 +802,74 @@ async def list_collections(self, *, tenant_id: str | None = None) -> list[dict[s # --- Integrity --- - async def health(self) -> HealthScore: - """Compute vault health score (0-100). + async def health(self, resource_id: str | None = None) -> HealthScore: + """Compute health score (0-100). + + Args: + resource_id: If provided, compute health for a single resource. + If None, compute vault-wide health. - Assesses: freshness, uniqueness, coherence, connectivity, trust alignment. + Returns: + HealthScore with component scores. """ await self._ensure_initialized() from qp_vault.integrity.detector import compute_health_score + + if resource_id: + resource = await self.get(resource_id) + score = compute_health_score([resource]) + return score + all_resources = await self._list_all_bounded() return compute_health_score(all_resources) + async def export_vault(self, path: str | Path) -> dict[str, Any]: + """Export the vault to a JSON file for portability. + + Args: + path: Output file path. + + Returns: + Summary with resource count and export path. + """ + import json as _json + await self._ensure_initialized() + resources = await self._list_all_bounded() + data = { + "version": "0.10.0", + "resource_count": len(resources), + "resources": [r.model_dump(mode="json") for r in resources], + } + out = Path(path) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(_json.dumps(data, default=str, indent=2)) + return {"path": str(out), "resource_count": len(resources)} + + async def import_vault(self, path: str | Path) -> list[Resource]: + """Import resources from an exported vault JSON file. + + Args: + path: Path to the exported JSON file. + + Returns: + List of imported resources. + """ + import json as _json + await self._ensure_initialized() + data = _json.loads(Path(path).read_text()) + imported = [] + for r_data in data.get("resources", []): + content = r_data.get("name", "imported") + resource = await self.add( + content, + name=r_data.get("name", "imported"), + trust=r_data.get("trust_tier", "working"), + tags=r_data.get("tags", []), + metadata=r_data.get("metadata", {}), + ) + imported.append(resource) + return imported + async def _list_all_bounded(self, *, hard_cap: int = 50_000, batch_size: int = 1000) -> list[Resource]: """Load all resources with pagination and a hard cap to prevent OOM.""" all_resources: list[Resource] = [] diff --git a/tests/test_cis.py b/tests/test_cis.py index b1b7b44..ef6a1b1 100644 --- a/tests/test_cis.py +++ b/tests/test_cis.py @@ -1,7 +1,7 @@ # Copyright 2026 Quantum Pipes Technologies, LLC # SPDX-License-Identifier: Apache-2.0 -"""Tests for Content Immune System (CIS) Phase 1. +"""Tests for Membrane (CIS) Phase 1. Covers: enums, models, provenance service, search exclusion, 2D trust scoring. """ @@ -80,7 +80,7 @@ def test_upload_method_values(self): class TestCISModels: - """CIS domain model validation.""" + """Membrane domain model validation.""" def test_resource_has_adversarial_status(self): r = Resource(id="r1", name="test.pdf", content_hash="abc123") @@ -131,7 +131,7 @@ def test_search_result_has_adversarial_status(self): class TestSearchEngine: - """CIS search engine extensions: 2D trust scoring and quarantine exclusion.""" + """Membrane search engine extensions: 2D trust scoring and quarantine exclusion.""" def test_adversarial_multiplier_verified(self): assert compute_adversarial_multiplier("verified") == 1.0 diff --git a/tests/test_cis_edge_cases.py b/tests/test_cis_edge_cases.py index db5e4e7..3797081 100644 --- a/tests/test_cis_edge_cases.py +++ b/tests/test_cis_edge_cases.py @@ -1,7 +1,7 @@ # Copyright 2026 Quantum Pipes Technologies, LLC # SPDX-License-Identifier: Apache-2.0 -"""Edge case tests for CIS components. +"""Edge case tests for Membrane components. Fills remaining gaps: auditor integration, provenance verify positive path, mixed adversarial statuses, approval budget boundaries. @@ -104,7 +104,7 @@ async def test_set_status_calls_auditor(self): await verifier.set_status( "r1", AdversarialStatus.VERIFIED, - reason="CIS passed", reviewer_id="admin1", + reason="Membrane passed", reviewer_id="admin1", ) auditor.record.assert_called_once() @@ -113,7 +113,7 @@ async def test_set_status_calls_auditor(self): assert event.resource_id == "r1" assert event.details["previous"] == "unverified" assert event.details["new"] == "verified" - assert event.details["reason"] == "CIS passed" + assert event.details["reason"] == "Membrane passed" assert event.details["reviewer_id"] == "admin1" @pytest.mark.asyncio diff --git a/tests/test_cis_phase2.py b/tests/test_cis_phase2.py index d7c1640..c2b7ceb 100644 --- a/tests/test_cis_phase2.py +++ b/tests/test_cis_phase2.py @@ -1,7 +1,7 @@ # Copyright 2026 Quantum Pipes Technologies, LLC # SPDX-License-Identifier: Apache-2.0 -"""Tests for Content Immune System (CIS) Phase 2. +"""Tests for Membrane (CIS) Phase 2. Covers: AdversarialVerifier, source diversity, approval budgets, anomaly detection. """ @@ -32,7 +32,7 @@ async def test_default_status_is_unverified(self, verifier): @pytest.mark.asyncio async def test_transition_unverified_to_verified(self, verifier): - result = await verifier.set_status("r1", AdversarialStatus.VERIFIED, reason="CIS passed") + result = await verifier.set_status("r1", AdversarialStatus.VERIFIED, reason="Membrane passed") assert result == AdversarialStatus.VERIFIED assert await verifier.get_status("r1") == AdversarialStatus.VERIFIED diff --git a/tests/test_search_engine_cis.py b/tests/test_search_engine_cis.py index c5b88bd..b9a553f 100644 --- a/tests/test_search_engine_cis.py +++ b/tests/test_search_engine_cis.py @@ -1,7 +1,7 @@ # Copyright 2026 Quantum Pipes Technologies, LLC # SPDX-License-Identifier: Apache-2.0 -"""Tests for search engine CIS extensions: trust weights, freshness, 2D scoring. +"""Tests for search engine Membrane extensions: trust weights, freshness, 2D scoring. Fills gaps: compute_trust_weight, compute_freshness, apply_trust_weighting edge cases. """