diff --git a/crates/hippocampus/src/encoder.rs b/crates/hippocampus/src/encoder.rs index e3e98ef..bd40e99 100644 --- a/crates/hippocampus/src/encoder.rs +++ b/crates/hippocampus/src/encoder.rs @@ -23,23 +23,13 @@ const TARGET_ACTIVE_BITS: usize = 80; const HASH_ROUNDS: usize = 3; /// Metadata associated with a trace for encoding. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Metadata { pub tags: Vec, pub domain: Option, pub source: Option, } -impl Default for Metadata { - fn default() -> Self { - Self { - tags: Vec::new(), - domain: None, - source: None, - } - } -} - /// Encode a message + metadata into a 1-bit SDR bitvector. /// /// Uses multi-round hashing of n-grams to produce a sparse diff --git a/crates/hippocampus/src/graph.rs b/crates/hippocampus/src/graph.rs index 3826276..d425989 100644 --- a/crates/hippocampus/src/graph.rs +++ b/crates/hippocampus/src/graph.rs @@ -54,25 +54,6 @@ pub fn get_neighbors(conn: &Connection, source_id: &str) -> Result Result> { - let mut stmt = conn.prepare( - "SELECT source_id, target_id, weight, edge_type, created_at - FROM graph_edges WHERE source_id = ?1 OR target_id = ?1 - ORDER BY weight DESC", - )?; - let rows = stmt.query_map(params![node_id], |row| { - Ok(GraphEdge { - source_id: row.get(0)?, - target_id: row.get(1)?, - weight: row.get(2)?, - edge_type: row.get(3)?, - created_at: row.get(4)?, - }) - })?; - rows.collect() -} - /// Count total edges in the graph. pub fn edge_count(conn: &Connection) -> Result { conn.query_row("SELECT COUNT(*) FROM graph_edges", [], |r| { diff --git a/crates/hippocampus/src/reflex.rs b/crates/hippocampus/src/reflex.rs index 301489b..23ad0ce 100644 --- a/crates/hippocampus/src/reflex.rs +++ b/crates/hippocampus/src/reflex.rs @@ -103,66 +103,6 @@ pub fn lookup_reflex(conn: &Connection, hash: &str) -> Result> { } } -/// Increment hit count on reflex use. -pub fn increment_hit_count(conn: &Connection, hash: &str) -> Result<()> { - conn.execute( - "UPDATE reflexes SET hit_count = hit_count + 1 WHERE pattern_hash = ?1", - params![hash], - )?; - Ok(()) -} - -/// Record a successful reflex execution. -pub fn record_success(conn: &Connection, hash: &str) -> Result<()> { - conn.execute( - "UPDATE reflexes SET success_count = success_count + 1 WHERE pattern_hash = ?1", - params![hash], - )?; - Ok(()) -} - -/// Rule 32: Single failure = instant de-compilation. -/// compiled=False, success_count=0. Route to Premotor for re-planning. -pub fn decompile_reflex(conn: &Connection, hash: &str) -> Result<()> { - conn.execute( - "UPDATE reflexes SET compiled = 0, success_count = 0 WHERE pattern_hash = ?1", - params![hash], - )?; - Ok(()) -} - -/// List all compiled reflexes. -pub fn list_reflexes(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - "SELECT pattern_hash, response_json, merkle_root, verification_state, - is_permanent, hit_count, compiled, success_count - FROM reflexes ORDER BY hit_count DESC", - )?; - let rows = stmt.query_map([], |row| { - let response_str: String = row.get(1)?; - Ok(Reflex { - pattern_hash: row.get(0)?, - response: serde_json::from_str(&response_str).unwrap_or(serde_json::Value::Null), - merkle_root: row.get(2)?, - verification_state: row.get(3)?, - is_permanent: row.get::<_, i32>(4)? != 0, - hit_count: row.get::<_, u32>(5)?, - compiled: row.get::<_, i32>(6)? != 0, - success_count: row.get::<_, u32>(7)?, - }) - })?; - rows.collect() -} - -/// Invalidate a reflex by hash. -pub fn invalidate_reflex(conn: &Connection, hash: &str) -> Result { - let changed = conn.execute( - "DELETE FROM reflexes WHERE pattern_hash = ?1", - params![hash], - )?; - Ok(changed > 0) -} - #[cfg(test)] mod tests { use super::*; @@ -239,27 +179,4 @@ mod tests { assert!(found.is_none()); } - #[test] - fn test_decompile_reflex_on_failure() { - let conn = open_memory_db().unwrap(); - let mut reflex = make_verified_reflex("fail1"); - reflex.success_count = 5; - store_reflex(&conn, &reflex).unwrap(); - - // Rule 32: Single failure = instant de-compilation - decompile_reflex(&conn, "fail1").unwrap(); - - // Should not be found via lookup (compiled=0) - let found = lookup_reflex(&conn, "fail1").unwrap(); - assert!(found.is_none(), "Decompiled reflex must not be returned"); - } - - #[test] - fn test_invalidate_reflex() { - let conn = open_memory_db().unwrap(); - store_reflex(&conn, &make_verified_reflex("inv1")).unwrap(); - let deleted = invalidate_reflex(&conn, "inv1").unwrap(); - assert!(deleted); - assert!(lookup_reflex(&conn, "inv1").unwrap().is_none()); - } } diff --git a/crates/hippocampus/src/store.rs b/crates/hippocampus/src/store.rs index b71b641..8c94472 100644 --- a/crates/hippocampus/src/store.rs +++ b/crates/hippocampus/src/store.rs @@ -35,14 +35,6 @@ pub struct ApoptosisReport { pub db_size_after: u64, } -/// Consolidation report. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConsolidationReport { - pub traces_merged: usize, - pub graph_nodes: usize, - pub graph_edges: usize, -} - /// Initialize the database schema. pub fn init_db(conn: &Connection) -> Result<()> { conn.execute_batch( diff --git a/docs/PONYTAIL_REVIEW_2026-06-20.md b/docs/PONYTAIL_REVIEW_2026-06-20.md new file mode 100644 index 0000000..c4ff2ce --- /dev/null +++ b/docs/PONYTAIL_REVIEW_2026-06-20.md @@ -0,0 +1,186 @@ +# Ponytail Over-Engineering Review — 2026-06-20 + +> **STATUS — Tier 1 APPLIED 2026-06-20.** 35 files changed, **+54 / −740 (net −686 lines)**, +> 3 files deleted (`modulation/utility_mode.py`, `modulation/burst_verifier.py`, +> `daemon/connection_pool.py`). Coupled tests updated. Also pulled in the coupled Tier-2 +> item **I9** (`globally_enabled` field) since it must be removed together with I8 — its +> short-circuit was a provable no-op (flag was never toggled to False). Tier 2/4/5 NOT applied. +> +> Verification: `cargo test -p hippocampus` → 41 passed / 0 failed (43 baseline − 2 orphaned +> reflex tests). `py_compile` clean on all 31 edited Python files. Import sweep of all edited +> production modules → OK. Repo-wide grep → zero dangling references to any deleted symbol. +> Behavior-preserving refactors (`gate()` LOCKED/INHIBIT, encoder `_embedding_to_sdr`, +> `recover_temp`) reviewed by diff and confirmed equivalent. +> ⚠️ The Python **pytest suite could not be run** in this environment (pytest not installed; +> `harlo` not pip-installed). Run `pytest tests/` in your configured env as the final gate. + +Scope: **over-engineering and unnecessary complexity only.** Not correctness, +security, or performance (route those to a normal review). Findings list what to +cut; this report does **not** apply fixes. + +## How this was produced + +1. Six parallel review agents swept the codebase (`python/harlo/**`, `crates/hippocampus/**`) + and produced **55 raw findings** (~965 lines proposed for removal). +2. **55 read-only skeptic agents** then each tried to *refute* one finding — + full-repo caller search including PyO3 `#[pyfunction]` exposure, MCP tool + registration, Click commands, `__all__` re-exports, `getattr`/string dispatch, + plus dependency-availability and the ponytail self-check exemption. +3. Verdicts: **CONFIRMED** (verified safe), **REFUTED** (real caller / contract / + wrong replacement — keep it), **UNCERTAIN** (park). + +**Result: the verification pass invalidated ~46% of the raw proposals.** Most +deaths were claims that collided with public-API `__all__` contracts, deliberate +performance optimizations, wrong line ranges, or wiring debt misread as redundancy. + +| Bucket | Count | Lines | +|---|---|---| +| Tier 1 — CONFIRMED, cut now | 34 | ~469 | +| Tier 2 — CONFIRMED, apply with a condition | 3 | ~44 | +| Tier 3 — REFUTED, keep (with reason) | 16 | 0 | +| Tier 4 — judgment call | 1 | ~28 | +| Tier 5 — UNCERTAIN, park | 1 | ~85 if dead | +| **Total verified-removable (Tier 1+2)** | **37** | **~513** | + +--- + +## Tier 1 — CONFIRMED. Safe to cut now. + +Verified zero real callers (or proven-equivalent shrink). Format: +`location: tag action. → lines` + +### Dead code — delete + +``` +modulation/utility_mode.py:1-13 delete: Phase-6 stub; real enter/exit live in inquiry/timing.py. → 13 +modulation/burst_verifier.py:1-30 delete: Phase-6 stub; verify_burst/reject_burst zero callers. → 30 +modulation/detector.py:447-484 (+L19) delete: legacy detect_pattern + _PATTERNS; prod uses detect_all. → 39 +modulation/detector.py:434-444 delete: clear_patterns, test-only. → 11 +modulation/profile.py:132-158 delete: save_profile, zero callers; config is hand-edited YAML. → 27 +brainstem/__init__.py:51 delete: "compose" in __all__ — never defined; import errors. → 1 +brainstem/escalation.py:8 delete: unused `from datetime import datetime, timezone`. → 1 +elenchus/depth.py:61-63 delete: get_max_depth; cap hard-coded inline in protocol.py. → 3 +inquiry/engine.py:378-387 delete: get_stats, zero callers anywhere. → 10 +inquiry/crystallization.py:87-92 delete: get_decay_rate; value stored but never retrieved. → 6 +inquiry/apoptosis.py:54-65 delete: remaining_hours; should_delete() is the used path. → 12 +inquiry/dmn_window.py:100-114 delete: to_teardown_context; SessionManager builds its own ctx. → 15 +inquiry/consent.py:70-76 delete: disable_all/enable_all, zero callers (see I9 — apply both). → 7 +composition/merkle.py:103-112 delete: verify_proof, zero callers; proof path never integrated. → 10 +composition/stage.py:81-86 delete: MerkleStage.get_proof wrapper, zero external callers. → 6 +motor/consent.py:90-92 delete: is_locked one-liner; inline `== ConsentLevel.LOCKED`. → 2 +daemon/connection_pool.py:1-71 delete: whole module; only tests/test_tactical imports it. → 71 +injection/__init__.py:155-162 delete: get_by_session + its test; zero production callers. → 8 +encoder/semantic_encoder.py:170-216 delete: __main__ demo; test_semantic.py genuinely covers it. → 47 +crates/.../store.rs:38-44 delete: ConsolidationReport struct, never constructed. → 7 +crates/.../graph.rs:57-74 delete: get_all_neighbors (OR-edge variant), zero callers. → 18 +crates/.../reflex.rs:106-122 delete: increment_hit_count + record_success, not PyO3-exposed. → 17 +crates/.../reflex.rs:124-164 delete: decompile/list/invalidate_reflex, not PyO3-exposed. → 41 +``` + +> Note on `encoder/semantic_encoder.py:170-216`: the ponytail self-check exemption +> was checked — deletion is allowed *only because* `tests/test_encoder/test_semantic.py` +> reproduces every assertion (determinism, sparsity, paraphrase Hamming distances). +> It is redundant, not the only check. + +### Shrink — same logic, fewer lines + +``` +brainstem/epistemological_bypass.py:44 shrink: `in _INQUIRY_SOURCES or == "inquiry"` → just `in _INQUIRY_SOURCES` ("inquiry" is a member). → 1 +brainstem/amygdala.py:97-136 shrink: drop duplicate "response"/"outcome" + unread matched_keywords (keep matched_safety/consent). → 6 +inquiry/rupture_repair.py:70-74 shrink: is_topic_blocked byte-identical to should_offer_stop; repoint engine.py:137, delete method. → 5 +motor/basal_ganglia.py:143-152 shrink: side_effects branch can never fire (config key never set, side_effects never populated) → `return True, None`. → 9 +motor/basal_ganglia.py:266-272 (+L23) shrink: GateDecision.ESCALATE — executor treats it identically to INHIBIT; drop enum + branch. → 8 +motor/basal_ganglia.py:252-264 shrink: gate() recomputes effective_consent_level already done in _check_consent; collapse LOCKED check. → 10 +daemon/dmn_teardown.py:132-146 shrink: single-element candidates list + for-loop → one path + try/unlink. → 3 +encoder/semantic_encoder.py:85-145 shrink: encode_batch re-inlines encode's projection→top-k→pack; extract _embedding_to_sdr (onnx_encoder already has it). → 17 +crates/.../encoder.rs:33-41 shrink: hand-rolled `impl Default for Metadata` → `#[derive(Default)]` (all fields Default). → 8 +composition/layer.py:9 shrink: drop unused `field` from dataclasses import. → 0 +composition/resolver.py:9 shrink: drop unused `field` from dataclasses import. → 0 +``` + +--- + +## Tier 2 — CONFIRMED, but apply with the noted condition. + +``` +composition/merkle.py:83-101 MerkleTree.get_proof — the lone caller is stage.py's get_proof wrapper, itself dead (Tier 1). + DEAD ONCE you delete composition/stage.py:81-86. Delete the cluster (merkle.get_proof + + stage.get_proof + verify_proof) together. → ~19 +inquiry/consent.py:33,51-52 globally_enabled field + is_allowed short-circuit. Dead, BUT only orphaned correctly if you + ALSO delete disable_all/enable_all (Tier 1 I8) which write it. Apply I8 + I9 as one edit. → 3 +crates/.../encoder.rs R5 line range was WRONG: 144-170 includes sdr_to_bytes (161-163), which IS production + (query.rs store_new_trace → py_store_trace). Delete ONLY hamming_distance (146-158) and + bytes_to_sdr (166-170); KEEP sdr_to_bytes. → ~22 +``` + +--- + +## Tier 3 — REFUTED. Do NOT cut. (Recorded so they aren't re-flagged.) + +Each of these *looked* like dead code or bloat and is not. Reasons are the value here. + +``` +modulation/profile.py:47-95 KEEP _parse_yaml_simple. PyYAML is NOT in pyproject — yaml.safe_load would crash offline. + Real issue is dependency hygiene, not over-engineering. (And the fallback mis-parses lists — + a correctness bug for the normal-review pass, out of scope here.) +modulation/state.py + state_store.py KEEP both. Not redundant: state.py encodes ADR-0001's 5-min RED freshness; state_store.py + uses 30-min. Real issue is WIRING (apply_to_session_state never called in the motor path), not duplication. +modulation/gain.py:30-57 apply_modulation has no prod caller, BUT proposed replacement compute_injection_gain is a + different function (dict→dict vs scalar). See Tier 4 — judgment. +modulation/detector.py:40-51 KEEP hand-written to_dict. dataclasses.asdict would leak the large `messages` field; the + selective serialization is intentional and asserted by test + 4 MCP/HTTP callers. +biometric_prior/server.py:33-49 KEEP BiometricStore Protocol + InMemoryStore. Both used as type annotations + a real HTTP + contract-test fixture; deliberate phased architecture. +brainstem/escalation.py:17 KEEP composition_to_layers. Public API in __all__, exercised by a round-trip property test. +elenchus/protocol.py:30-59 KEEP _describe_flaw. Generates targeted flaw strings fed into the LLM revision prompt; not + interchangeable with detect_spec_gaming (that path returns SPEC_GAMED, not FIXABLE). +elenchus/states.py:64-80 KEEP hand-rolled to_dict. asdict leaves the enum unserializable; JSON-safe output is the contract. +elenchus_v8/__init__.py:159-209 KEEP limit=None path. Documented public API ("None = unlimited") with explicit test. +inquiry/engine.py:289-314 KEEP synthesize. Public API via __all__ + ARCHITECTURE.md (backward-compat shim). +inquiry/engine.py:316-324 KEEP prepare_traces_for_dmn. Public API via __all__. +inquiry/__init__.py:17-18,26-27 KEEP the aliases. They ARE the public API surface for the above. +inquiry/rupture_repair.py:35,55 KEEP _topic_counts dict. O(1) cache for hot-path gating; recompute would be O(n). +motor/scope.py:87 KEEP inline `import json`. Style nit (0 lines), not over-engineering; matches a repo pattern. +migrate_v7.py KEEP whole module. Retained v6→v7 migration path per harness/path_c/03_HANDOFF.md + CHANGELOG. +crates/.../reflex.rs:24-37 KEEP UnverifiedReflexError. Live path enforcing Rule 12, PyO3-surfaced, tested both sides. +crates/.../encoder.rs:113-142 KEEP enforce_sparsity early return. Deliberate perf optimization (skips alloc + bitvector scan). +``` + +--- + +## Tier 4 — Judgment call. + +``` +modulation/gain.py:30-57 apply_modulation is genuinely test-only (one caller: test_apply_modulation_preserves_clean). + Deletable IF you accept dropping that test, which asserts the Rule 10 anchor-gain=1.0 safety + property. Harmless if left. Your call: ~28 lines + its test, or keep as a documented invariant. +``` + +--- + +## Tier 5 — UNCERTAIN. Park, revisit. + +``` +daemon/socket_activation.py:161-245 Second "source-label" socket API (acquire_listen_socket + 3 helpers, ~85 lines). + Test-only today, but in __all__ and the module docstring says agents/harness.py + should adopt it in a follow-up. If that follow-up is cancelled → CONFIRMED dead (~85). + If not, it's staged future code. Decide the follow-up first. +``` + +--- + +## Recommended apply order + +1. **Tier 1 deletes** (lowest risk, ~360 lines) — pure dead code, no behavior change. +2. **Tier 1 shrinks** (~67 lines) — verify the one motor refactor (basal_ganglia 252-264 uses a + `"LOCKED" in reason` string match) against `cargo test`/`pytest` motor + compliance suites. +3. **Tier 2 clusters** (~44 lines) — apply each as one atomic edit (merkle/stage/verify_proof together; + I8+I9 together; R5 with the corrected range). +4. Decide **Tier 4** and **Tier 5** explicitly; leave the Tier 3 reasons in place so a future sweep + doesn't re-propose them. + +**net: ~513 lines verified-removable (Tier 1 + Tier 2); up to ~626 if Tier 4 + Tier 5 resolve toward deletion.** + +Two non-scope items surfaced during verification, flagged for a *normal* review (NOT acted on here): +`modulation/profile.py` YAML fallback mis-parses lists (correctness), and the dual modulation-state +stores are a wiring gap (`apply_to_session_state` never called on the motor path). diff --git a/python/harlo/brainstem/__init__.py b/python/harlo/brainstem/__init__.py index 711a150..4b5691d 100644 --- a/python/harlo/brainstem/__init__.py +++ b/python/harlo/brainstem/__init__.py @@ -48,7 +48,6 @@ "elenchus_to_verification", "check_intent_preserved", "compile_to_reflex", - "compose", "composition_to_layers", "compute_surprise", "compute_trace_merkle", diff --git a/python/harlo/brainstem/amygdala.py b/python/harlo/brainstem/amygdala.py index b846b48..5f0e1a3 100644 --- a/python/harlo/brainstem/amygdala.py +++ b/python/harlo/brainstem/amygdala.py @@ -120,12 +120,6 @@ def create_amygdala_reflex(resolution: dict) -> dict: "reflex_hash": reflex_hash, "type": "amygdala_permanent", "triggers": triggers, - "matched_keywords": { - "safety": sorted(matched_safety), - "consent": sorted(matched_consent), - }, - "response": resolution.get("outcome", {}), - "outcome": resolution.get("outcome", {}), "merkle_root": resolution.get("merkle_root", ""), "verification_state": "amygdala_bypass", "is_permanent": True, diff --git a/python/harlo/brainstem/epistemological_bypass.py b/python/harlo/brainstem/epistemological_bypass.py index fa96f65..28a6007 100644 --- a/python/harlo/brainstem/epistemological_bypass.py +++ b/python/harlo/brainstem/epistemological_bypass.py @@ -41,7 +41,7 @@ def should_bypass_elenchus( return True if tag_set & _SELF_REPORTED_TAGS: - if consumer_lower in _INQUIRY_SOURCES or consumer_lower == "inquiry": + if consumer_lower in _INQUIRY_SOURCES: return True if consumer_lower in ("composition", "compose"): return False diff --git a/python/harlo/brainstem/escalation.py b/python/harlo/brainstem/escalation.py index 434bb29..4570979 100644 --- a/python/harlo/brainstem/escalation.py +++ b/python/harlo/brainstem/escalation.py @@ -5,7 +5,6 @@ from __future__ import annotations -from datetime import datetime, timezone from typing import Optional from ..elenchus.protocol import run_gvr diff --git a/python/harlo/composition/layer.py b/python/harlo/composition/layer.py index c1bfd03..ac18de9 100644 --- a/python/harlo/composition/layer.py +++ b/python/harlo/composition/layer.py @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import IntEnum diff --git a/python/harlo/composition/merkle.py b/python/harlo/composition/merkle.py index 2717f75..5810475 100644 --- a/python/harlo/composition/merkle.py +++ b/python/harlo/composition/merkle.py @@ -100,17 +100,6 @@ def get_proof(self, index: int) -> list[tuple[str, str]]: pos //= 2 return proof - @staticmethod - def verify_proof(leaf_hash: str, proof: list[tuple[str, str]], root: str) -> bool: - """Verify a Merkle proof against a known root hash.""" - current = leaf_hash - for sibling_hash, side in proof: - if side == "R": - current = _combine(current, sibling_hash) - else: - current = _combine(sibling_hash, current) - return current == root - @property def leaf_count(self) -> int: return self._leaf_count diff --git a/python/harlo/composition/resolver.py b/python/harlo/composition/resolver.py index e6497e2..8375a47 100644 --- a/python/harlo/composition/resolver.py +++ b/python/harlo/composition/resolver.py @@ -6,7 +6,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional from .layer import ArcType, Layer diff --git a/python/harlo/composition/stage.py b/python/harlo/composition/stage.py index 0faabc7..2370f23 100644 --- a/python/harlo/composition/stage.py +++ b/python/harlo/composition/stage.py @@ -78,13 +78,6 @@ def get_layers(self) -> list[Layer]: """Return all layers in insertion order.""" return list(self._layers) - def get_proof(self, index: int): - """Return a Merkle proof for the layer at *index*.""" - if self._tree is None: - self._rebuild_tree() - assert self._tree is not None - return self._tree.get_proof(index) - def update_layer(self, index: int, layer: Layer) -> str: """Replace a layer at *index* with O(log n) Merkle update.""" if index < 0 or index >= len(self._layers): diff --git a/python/harlo/daemon/connection_pool.py b/python/harlo/daemon/connection_pool.py deleted file mode 100644 index c14da9f..0000000 --- a/python/harlo/daemon/connection_pool.py +++ /dev/null @@ -1,71 +0,0 @@ -"""SQLite connection pooling for the Python layer. - -Provides a thread-local singleton connection per database path, -avoiding the overhead of opening/closing connections per call. -Connections are cached per-thread (sqlite3 requirement). -""" - -from __future__ import annotations - -import sqlite3 -import threading -from typing import Optional - - -_thread_local = threading.local() - - -def get_connection(db_path: str) -> sqlite3.Connection: - """Get a cached SQLite connection for the given database path. - - Returns the same connection for repeated calls with the same path - within the same thread. Creates a new connection on first call. - - Args: - db_path: Path to the SQLite database file. - - Returns: - A sqlite3.Connection instance. - """ - cache_key = f"_conn_{db_path}" - - conn = getattr(_thread_local, cache_key, None) - if conn is not None: - try: - conn.execute("SELECT 1") - return conn - except sqlite3.ProgrammingError: - # Connection was closed; create a new one - pass - - conn = sqlite3.connect(db_path) - setattr(_thread_local, cache_key, conn) - return conn - - -def close_connection(db_path: str) -> None: - """Close and remove the cached connection for a database path. - - Args: - db_path: Path to the SQLite database file. - """ - cache_key = f"_conn_{db_path}" - conn = getattr(_thread_local, cache_key, None) - if conn is not None: - try: - conn.close() - except sqlite3.ProgrammingError: - pass - delattr(_thread_local, cache_key) - - -def close_all() -> None: - """Close all cached connections in the current thread.""" - for attr in list(vars(_thread_local)): - if attr.startswith("_conn_"): - conn = getattr(_thread_local, attr) - try: - conn.close() - except (sqlite3.ProgrammingError, AttributeError): - pass - delattr(_thread_local, attr) diff --git a/python/harlo/daemon/dmn_teardown.py b/python/harlo/daemon/dmn_teardown.py index 78fc94e..93dd46b 100644 --- a/python/harlo/daemon/dmn_teardown.py +++ b/python/harlo/daemon/dmn_teardown.py @@ -131,18 +131,15 @@ def _dump_to_temp(self, data): def recover_temp(self) -> Optional[dict]: """Recover partial results from temp file on boot.""" - candidates = [ - TEMP_DIR / "twin_dmn_partial.json", - ] - for path in candidates: - if path.exists(): - try: - with open(path) as f: - data = json.load(f) - path.unlink() # Delete after recovery - return data - except (json.JSONDecodeError, OSError): - path.unlink(missing_ok=True) + path = TEMP_DIR / "twin_dmn_partial.json" + if path.exists(): + try: + with open(path) as f: + data = json.load(f) + path.unlink() # Delete after recovery + return data + except (json.JSONDecodeError, OSError): + path.unlink(missing_ok=True) return None diff --git a/python/harlo/elenchus/depth.py b/python/harlo/elenchus/depth.py index ffb6e20..5d7bcb7 100644 --- a/python/harlo/elenchus/depth.py +++ b/python/harlo/elenchus/depth.py @@ -56,8 +56,3 @@ def get_depth(domain: str) -> int: """ config = _load_config() return config.get(domain.lower(), _DEFAULT_DEPTH) - - -def get_max_depth() -> int: - """Get the maximum allowed verification depth.""" - return 3 diff --git a/python/harlo/encoder/semantic_encoder.py b/python/harlo/encoder/semantic_encoder.py index a5d374b..c00e71d 100644 --- a/python/harlo/encoder/semantic_encoder.py +++ b/python/harlo/encoder/semantic_encoder.py @@ -63,29 +63,22 @@ def _create_projection_matrix(self) -> np.ndarray: matrix *= 0.1 # Scale for numerical stability return matrix - def encode(self, text: str) -> bytes: - """Encode text to a 2048-bit SDR as bytes. + def _embedding_to_sdr(self, embedding: np.ndarray) -> bytes: + """Convert one 384-dim float embedding to a 256-byte SDR blob. + + Shared projection → abs/top-k → bit-pack pipeline so encode() and + encode_batch() produce byte-identical output. Args: - text: Input text to encode. + embedding: 384-dim float vector (L2-normalized). Returns: 256 bytes (2048 bits) representing the sparse distributed representation. - - Raises: - ValueError: If text is empty. """ - text = text.strip() - if not text: - raise ValueError("Text cannot be empty") - - # Step 1: Get 384-dim embedding from BGE - embedding = self.model.encode(text, normalize_embeddings=True) - - # Step 2: Project through LSH matrix + # Project through LSH matrix projections = self.projection_matrix @ embedding # (2048,) - # Step 3: Select top-k bits by absolute magnitude where projection > 0 + # Select top-k bits by absolute magnitude where projection > 0 abs_projections = np.abs(projections) sorted_indices = np.argsort(abs_projections)[::-1] # Descending @@ -96,7 +89,7 @@ def encode(self, text: str) -> bytes: if projections[idx] > 0: active_bits.append(int(idx)) - # Step 4: Pack into bytes (LSB first, matching bitvec) + # Pack into bytes (LSB first, matching bitvec) sdr_bytes = bytearray(SDR_WIDTH // 8) # 256 bytes for bit_idx in active_bits: byte_idx = bit_idx // 8 @@ -105,6 +98,25 @@ def encode(self, text: str) -> bytes: return bytes(sdr_bytes) + def encode(self, text: str) -> bytes: + """Encode text to a 2048-bit SDR as bytes. + + Args: + text: Input text to encode. + + Returns: + 256 bytes (2048 bits) representing the sparse distributed representation. + + Raises: + ValueError: If text is empty. + """ + text = text.strip() + if not text: + raise ValueError("Text cannot be empty") + + embedding = self.model.encode(text, normalize_embeddings=True) + return self._embedding_to_sdr(embedding) + def encode_batch(self, texts: list[str]) -> list[bytes]: """Encode multiple texts at once (faster than one-by-one). @@ -121,28 +133,7 @@ def encode_batch(self, texts: list[str]) -> list[bytes]: # Batch encode embeddings embeddings = self.model.encode(texts, normalize_embeddings=True) - results = [] - for embedding in embeddings: - projections = self.projection_matrix @ embedding - abs_projections = np.abs(projections) - sorted_indices = np.argsort(abs_projections)[::-1] - - active_bits = [] - for idx in sorted_indices: - if len(active_bits) >= TARGET_ACTIVE_BITS: - break - if projections[idx] > 0: - active_bits.append(int(idx)) - - sdr_bytes = bytearray(SDR_WIDTH // 8) - for bit_idx in active_bits: - byte_idx = bit_idx // 8 - bit_offset = bit_idx % 8 - sdr_bytes[byte_idx] |= (1 << bit_offset) - - results.append(bytes(sdr_bytes)) - - return results + return [self._embedding_to_sdr(embedding) for embedding in embeddings] def hamming_distance(a: bytes, b: bytes) -> int: @@ -165,52 +156,3 @@ def sdr_sparsity(sdr: bytes) -> float: """Calculate the sparsity (fraction of active bits) of an SDR.""" active = sum(bin(byte).count('1') for byte in sdr) return active / (len(sdr) * 8) - - -# ─── Quick self-test ───────────────────────────────────────────────────────── - -if __name__ == "__main__": - print("Loading BGE model...") - enc = SemanticEncoder() - print("Model loaded.\n") - - # Test 1: Basic encoding - sdr = enc.encode("The cat sat on the mat") - print(f"SDR size: {len(sdr)} bytes ({len(sdr) * 8} bits)") - print(f"Sparsity: {sdr_sparsity(sdr):.3f} ({sum(bin(b).count('1') for b in sdr)} active bits)") - - # Test 2: Determinism - sdr2 = enc.encode("The cat sat on the mat") - assert sdr == sdr2, "FAIL: Encoding not deterministic!" - print("Determinism: PASS") - - # Test 3: Semantic similarity (THE key test) - pairs = [ - ("The cat sat on the mat", "A feline rested on the rug"), - ("It's raining outside", "Rain is falling outdoors"), - ("The car is fast", "The vehicle has high speed"), - ] - - print("\nSemantic similarity tests:") - for a_text, b_text in pairs: - a = enc.encode(a_text) - b = enc.encode(b_text) - dist = hamming_distance(a, b) - print(f" '{a_text}' vs '{b_text}'") - print(f" Hamming distance: {dist} / {SDR_WIDTH}") - - # Test 4: Dissimilarity - unrelated = [ - ("The cat sat on the mat", "Quantum physics is complex"), - ("I love pizza", "Space exploration is expensive"), - ] - - print("\nDissimilarity tests:") - for a_text, b_text in unrelated: - a = enc.encode(a_text) - b = enc.encode(b_text) - dist = hamming_distance(a, b) - print(f" '{a_text}' vs '{b_text}'") - print(f" Hamming distance: {dist} / {SDR_WIDTH}") - - print("\nDone.") diff --git a/python/harlo/injection/__init__.py b/python/harlo/injection/__init__.py index acee0e1..ca534bb 100644 --- a/python/harlo/injection/__init__.py +++ b/python/harlo/injection/__init__.py @@ -152,15 +152,6 @@ def get_by_profile(self, profile: str, limit: int = 50) -> list[InjectionTrace]: ) return [self._row_to_trace(row) for row in cursor.fetchall()] - def get_by_session(self, session_id: str) -> list[InjectionTrace]: - """Get all injection traces for a session.""" - cursor = self._conn.execute( - "SELECT trace_id, profile, s_nm, alpha, exchange_count, transition, session_id, timestamp " - "FROM injection_traces WHERE session_id = ? ORDER BY timestamp ASC", - (session_id,), - ) - return [self._row_to_trace(row) for row in cursor.fetchall()] - def get_activation_count(self, profile: str, since_timestamp: float = 0.0) -> int: """Count activations of a specific profile since a timestamp.""" row = self._conn.execute( diff --git a/python/harlo/inquiry/apoptosis.py b/python/harlo/inquiry/apoptosis.py index 07a3b3b..21ad68c 100644 --- a/python/harlo/inquiry/apoptosis.py +++ b/python/harlo/inquiry/apoptosis.py @@ -51,19 +51,6 @@ def should_delete(self, now: float | None = None) -> bool: """S5: Below 20% vitality = delete.""" return self.vitality(now) < VITALITY_THRESHOLD - def remaining_hours(self, now: float | None = None) -> float: - """Hours until vitality drops below threshold.""" - # Solve: e^(-3t/ttl) = 0.20 - # t = -ttl * ln(0.20) / 3 - if now is None: - now = time.time() - elapsed = max(0.0, now - self.created_at) - ttl = self.ttl_seconds - # Time at which vitality = threshold - t_threshold = -ttl * math.log(VITALITY_THRESHOLD) / DECAY_K - remaining_s = t_threshold - elapsed - return max(0.0, remaining_s / 3600.0) - def sweep_expired(vitalities: list[InquiryVitality], now: float | None = None) -> tuple[list[str], list[InquiryVitality]]: """Sweep and return (expired_ids, surviving_vitalities).""" diff --git a/python/harlo/inquiry/consent.py b/python/harlo/inquiry/consent.py index 380c243..66e5b41 100644 --- a/python/harlo/inquiry/consent.py +++ b/python/harlo/inquiry/consent.py @@ -30,7 +30,6 @@ class ConsentManager: but can be re-enabled here. """ records: dict[str, ConsentRecord] = field(default_factory=dict) - globally_enabled: bool = True def set_consent( self, @@ -48,8 +47,6 @@ def set_consent( def is_allowed(self, key: str) -> bool: """Check if a key is allowed. Default: True.""" - if not self.globally_enabled: - return False record = self.records.get(key) if record is None: return True # Opt-in default @@ -67,14 +64,6 @@ def unblock_topic(self, topic_key: str, reason: str = "user_request") -> Consent """Unblock a previously blocked topic.""" return self.set_consent(topic_key, allowed=True, reason=reason) - def disable_all(self, reason: str = "user_request") -> None: - """Globally disable all inquiries.""" - self.globally_enabled = False - - def enable_all(self, reason: str = "user_request") -> None: - """Re-enable inquiries globally.""" - self.globally_enabled = True - def get_blocked_keys(self) -> list[str]: """Return all explicitly blocked keys.""" return [k for k, r in self.records.items() if not r.allowed] diff --git a/python/harlo/inquiry/crystallization.py b/python/harlo/inquiry/crystallization.py index 47a1a70..01b661b 100644 --- a/python/harlo/inquiry/crystallization.py +++ b/python/harlo/inquiry/crystallization.py @@ -84,12 +84,5 @@ def _evict_lowest(self) -> None: worst_idx = min(range(len(self.traces)), key=lambda i: self.traces[i].preservation_score) self.traces.pop(worst_idx) - def get_decay_rate(self, trace_id: str, default_rate: float) -> float: - """Get effective decay rate for a trace (crystallized or default).""" - for t in self.traces: - if t.trace_id == trace_id: - return t.crystallized_decay_rate - return default_rate - def count(self) -> int: return len(self.traces) diff --git a/python/harlo/inquiry/dmn_window.py b/python/harlo/inquiry/dmn_window.py index b68b0c5..9eec823 100644 --- a/python/harlo/inquiry/dmn_window.py +++ b/python/harlo/inquiry/dmn_window.py @@ -97,22 +97,6 @@ def synthesize(self, abort_check=None) -> list[SynthesisCandidate]: return candidates - def to_teardown_context(self) -> dict: - """Package state for DMNTeardown background thread.""" - return { - "session_start": self.session_start, - "observation_count": len(self.observations), - "observations": [ - { - "content": o.content, - "category": o.category.value, - "weight": o.weight, - "timestamp": o.timestamp, - } - for o in self.observations - ], - } - def clear(self) -> None: """Reset for a new session.""" self.observations.clear() diff --git a/python/harlo/inquiry/engine.py b/python/harlo/inquiry/engine.py index 594c013..49a686e 100644 --- a/python/harlo/inquiry/engine.py +++ b/python/harlo/inquiry/engine.py @@ -21,7 +21,6 @@ import hashlib import time from dataclasses import dataclass, field -from typing import Any from .types import InquiryType, TTL_HOURS from .apophenia_guard import EvidenceBundle, evaluate as guard_evaluate @@ -134,7 +133,7 @@ def _process_candidate(self, candidate: SynthesisCandidate) -> Inquiry | None: return None # S3: Rupture check — blocked topics - if self.rupture_ledger.is_topic_blocked(topic_key): + if self.rupture_ledger.should_offer_stop(topic_key): return None # S1: Apophenia guard @@ -374,14 +373,3 @@ def _format_question(candidate: SynthesisCandidate) -> str: f"I've noticed something about your {candidate.inquiry_type.value} " f"— {candidate.hypothesis}. What are your thoughts?" ) - - def get_stats(self) -> dict[str, Any]: - """Return engine statistics for diagnostics.""" - return { - "active_inquiries": len(self._active), - "crystallized_traces": self.crystal_store.count(), - "pending_observations": len(self.dmn_window.observations), - "rejected_topics": self.rupture_ledger.get_all_rejected_topics(), - "blocked_keys": self.consent.get_blocked_keys(), - "utility_mode": self.timing.utility_mode, - } diff --git a/python/harlo/inquiry/rupture_repair.py b/python/harlo/inquiry/rupture_repair.py index 8531835..d037fc6 100644 --- a/python/harlo/inquiry/rupture_repair.py +++ b/python/harlo/inquiry/rupture_repair.py @@ -67,12 +67,6 @@ def topic_weight(self, topic_key: str) -> float: """Total rejection weight for a topic.""" return self.rejection_count(topic_key) * REJECTION_WEIGHT - def is_topic_blocked(self, topic_key: str) -> bool: - """Check if a topic has been explicitly blocked after offer-to-stop.""" - # After 3 rejections and the stop offer, the topic is suppressed - # unless explicitly re-enabled via consent module - return self.rejection_count(topic_key) >= REJECTION_LIMIT - def get_all_rejected_topics(self) -> list[str]: """Return topic keys that have reached the rejection limit.""" return [ diff --git a/python/harlo/modulation/burst_verifier.py b/python/harlo/modulation/burst_verifier.py deleted file mode 100644 index 6bcdf90..0000000 --- a/python/harlo/modulation/burst_verifier.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Burst verifier stub. Placeholder for Phase 6.""" - -from __future__ import annotations - -from typing import Any, Dict - - -def verify_burst(burst: Dict[str, Any]) -> bool: - """Verify a burst payload. Phase 6 implementation. - - Args: - burst: Burst data dict to verify. - - Returns: - True if burst is valid (stub: always True). - """ - return True - - -def reject_burst(burst: Dict[str, Any], reason: str) -> Dict[str, Any]: - """Reject a burst with reason. Phase 6 implementation. - - Args: - burst: The burst that failed verification. - reason: Why it was rejected. - - Returns: - Rejection record. - """ - return {"rejected": True, "reason": reason, "burst_id": burst.get("id")} diff --git a/python/harlo/modulation/detector.py b/python/harlo/modulation/detector.py index 049e9d9..05ffbdc 100644 --- a/python/harlo/modulation/detector.py +++ b/python/harlo/modulation/detector.py @@ -13,10 +13,6 @@ import sqlite3 import time from dataclasses import dataclass, field -from typing import List, Optional - -# ── Legacy pattern labels (kept for backward compat) ──────────────── -_PATTERNS = {"adhd", "analytical", "creative", "depleted", "default"} # ── Clustering constants ──────────────────────────────────────────── DEFAULT_SIMILARITY_THRESHOLD = 100 # Hamming distance < this = "similar" @@ -430,55 +426,3 @@ def get_stored_patterns(self) -> list[DetectedPattern]: ) for r in rows ] - - def clear_patterns(self) -> int: - """Delete all stored patterns. Returns count deleted.""" - conn = self._connect() - try: - _ensure_patterns_table(conn) - count = conn.execute("SELECT COUNT(*) FROM patterns").fetchone()[0] - conn.execute("DELETE FROM patterns") - conn.commit() - return count - finally: - conn.close() - - -# ── Legacy API (backward compatible) ──────────────────────────────── - -def detect_pattern(messages: List[dict]) -> str: - """Detect conversational pattern from message history. - - Legacy heuristic interface. For full pattern detection, use - PatternDetector.detect_all() instead. - - Args: - messages: List of message dicts with 'content' key. - - Returns: - One of: "adhd", "analytical", "creative", "depleted", "default". - """ - if not messages: - return "default" - - def _msg_len(m): - """Compute message length.""" - if isinstance(m, dict): - return len(str(m.get("content", ""))) - return len(str(m)) - - total_len = sum(_msg_len(m) for m in messages) - count = len(messages) - - if count == 0: - return "default" - - avg_len = total_len / count - - if avg_len < 20 and count > 5: - return "adhd" - - if avg_len > 200: - return "analytical" - - return "default" diff --git a/python/harlo/modulation/profile.py b/python/harlo/modulation/profile.py index dc1d8c7..e58ad57 100644 --- a/python/harlo/modulation/profile.py +++ b/python/harlo/modulation/profile.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from dataclasses import dataclass, field, asdict +from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List @@ -127,32 +127,3 @@ def load_profile(path: str | Path | None = None) -> Profile: motor_session_consent=int(motor.get("session_consent", 0)), motor_scope=motor.get("scope", {}), ) - - -def save_profile(profile: Profile, path: str | Path) -> None: - """Save a Profile to YAML config file.""" - p = Path(path) - data = { - "modulation": { - "s_nm": profile.s_nm, - "association_radius": profile.association_radius, - "escalation_threshold": profile.escalation_threshold, - "decay_lambda": profile.decay_lambda, - "tangent_tolerance": profile.tangent_tolerance, - "verbosity": profile.verbosity, - }, - "anchors": profile.anchors, - "inquiry": { - "depth": profile.inquiry_depth, - "consent_level": profile.inquiry_consent_level, - "boundaries": profile.inquiry_boundaries, - }, - "motor": { - "session_consent": profile.motor_session_consent, - "scope": profile.motor_scope, - }, - } - if _HAS_YAML: - p.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8") - else: - p.write_text(json.dumps(data, indent=2), encoding="utf-8") diff --git a/python/harlo/modulation/utility_mode.py b/python/harlo/modulation/utility_mode.py deleted file mode 100644 index 315d5a2..0000000 --- a/python/harlo/modulation/utility_mode.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Utility mode stub. Placeholder for Phase 6.""" - -from __future__ import annotations - - -def enter_utility_mode() -> dict: - """Enter utility mode. Phase 6 implementation.""" - return {"mode": "utility", "active": False, "phase": 6} - - -def exit_utility_mode() -> dict: - """Exit utility mode. Phase 6 implementation.""" - return {"mode": "default", "active": False, "phase": 6} diff --git a/python/harlo/motor/basal_ganglia.py b/python/harlo/motor/basal_ganglia.py index 186a19f..26be247 100644 --- a/python/harlo/motor/basal_ganglia.py +++ b/python/harlo/motor/basal_ganglia.py @@ -16,11 +16,16 @@ from .consent import ConsentLevel, ConsentState, effective_consent_level from .scope import Scope, validate_scope +# Single source of truth for the Rule 25 LOCKED failure reason. gate() keys the +# LOCKED decision off an exact match against this constant (not a substring), so +# a missing-consent reason that happens to mention "LOCKED" in its action_id +# cannot be misclassified as LOCKED. +_LOCKED_CONSENT_REASON = "LOCKED consent — gate NEVER opens (Rule 25)" + class GateDecision(Enum): DISINHIBIT = "disinhibit" # All checks pass — action may proceed INHIBIT = "inhibit" # One or more checks failed - ESCALATE = "escalate" # Needs higher consent LOCKED = "locked" # Level 3 — NEVER opens @@ -100,7 +105,7 @@ def _check_consent( # Rule 25: LOCKED never opens if level == ConsentLevel.LOCKED: - return False, "LOCKED consent — gate NEVER opens (Rule 25)" + return False, _LOCKED_CONSENT_REASON action_id = f"{action.action_type}:{action.target}" if not consent_state.has_consent(level, action_id): @@ -140,16 +145,7 @@ def _check_reversibility(action: PlannedAction, session_state: dict) -> tuple[bo return False, "Irreversible LOCKED action — permanently blocked" # Warn but pass — consent check already escalated the level - if not action.side_effects: - return True, None - - # Side effects on irreversible action — extra scrutiny - max_side_effects = session_state.get("max_irreversible_side_effects", 3) - if len(action.side_effects) > max_side_effects: - return False, ( - f"Irreversible action has {len(action.side_effects)} side effects " - f"(max {max_side_effects})" - ) + return True, None return True, None @@ -188,7 +184,7 @@ def gate( DEFAULT: INHIBIT (Rule 23). ALL five checks must pass for DISINHIBIT. - ANY failure results in INHIBIT, ESCALATE, or LOCKED. + ANY failure results in INHIBIT or LOCKED. The five checks: 1. Anchor alignment @@ -249,29 +245,16 @@ def gate( if all_passed: return GateResult(decision=GateDecision.DISINHIBIT, checks=checks) - # Rule 25: If consent was LOCKED, return LOCKED - level = ConsentLevel(action.consent_level) - effective = effective_consent_level( - level, - is_depleted=session_state.get("is_depleted", False), - is_irreversible=not action.reversible, - ) - if effective == ConsentLevel.LOCKED or level == ConsentLevel.LOCKED: + # Rule 25: If the consent check failed because consent was LOCKED, + # return LOCKED (gate NEVER opens). Exact match — never a substring. + if not consent_ok and consent_reason == _LOCKED_CONSENT_REASON: return GateResult( decision=GateDecision.LOCKED, checks=checks, - failure_reason=first_failure or "LOCKED — gate NEVER opens", - ) - - # If consent check failed but others passed, suggest escalation - if not consent_ok and all(v for k, v in checks.items() if k != "consent"): - return GateResult( - decision=GateDecision.ESCALATE, - checks=checks, - failure_reason=first_failure, + failure_reason=consent_reason, ) - # Default: INHIBIT + # Default: INHIBIT on the first failing check. return GateResult( decision=GateDecision.INHIBIT, checks=checks, diff --git a/python/harlo/motor/consent.py b/python/harlo/motor/consent.py index f4a585b..902e009 100644 --- a/python/harlo/motor/consent.py +++ b/python/harlo/motor/consent.py @@ -87,11 +87,6 @@ def effective_consent_level( return level -def is_locked(level: ConsentLevel) -> bool: - """Rule 25: check if consent level is LOCKED (never opens).""" - return level == ConsentLevel.LOCKED - - class ConsentState: """Track per-session consent grants.""" diff --git a/tests/test_injection/test_injection.py b/tests/test_injection/test_injection.py index a5b0382..1b132b0 100644 --- a/tests/test_injection/test_injection.py +++ b/tests/test_injection/test_injection.py @@ -129,22 +129,6 @@ def test_get_by_profile(self, inj_store): assert len(classical) == 2 assert all(t.profile == "classical" for t in classical) - def test_get_by_session(self, inj_store): - """get_by_session returns traces for a specific session.""" - inj_store.store(profile="classical", s_nm=0.02, alpha=0.9, - exchange_count=1, transition="activated", - session_id="sess-A") - inj_store.store(profile="classical", s_nm=0.02, alpha=0.5, - exchange_count=10, transition="deactivated", - session_id="sess-A") - inj_store.store(profile="microdose", s_nm=0.005, alpha=0.3, - exchange_count=1, transition="activated", - session_id="sess-B") - - sess_a = inj_store.get_by_session("sess-A") - assert len(sess_a) == 2 - assert all(t.session_id == "sess-A" for t in sess_a) - def test_count(self, inj_store): """count returns total injection traces.""" assert inj_store.count() == 0 diff --git a/tests/test_integration/test_compliance.py b/tests/test_integration/test_compliance.py index bdbef5f..7efd8c3 100644 --- a/tests/test_integration/test_compliance.py +++ b/tests/test_integration/test_compliance.py @@ -362,9 +362,9 @@ class TestRule25_Level3Locked: """Rule 25: Level 3 (LOCKED) gate NEVER opens.""" def test_locked_never_opens(self): - from harlo.motor.consent import ConsentLevel, ConsentState, is_locked + from harlo.motor.consent import ConsentLevel, ConsentState - assert is_locked(ConsentLevel.LOCKED) + assert ConsentLevel.LOCKED == ConsentLevel.LOCKED state = ConsentState() state.grant_session() diff --git a/tests/test_modulation/test_detector.py b/tests/test_modulation/test_detector.py index 8ccad20..1196da6 100644 --- a/tests/test_modulation/test_detector.py +++ b/tests/test_modulation/test_detector.py @@ -8,7 +8,6 @@ - Pattern persistence in SQLite - Persistence across detector instances - Inquiry engine integration via router -- Legacy detect_pattern backward compat - Edge cases """ @@ -23,7 +22,6 @@ from harlo.modulation.detector import ( DetectedPattern, PatternDetector, - detect_pattern, _hamming_distance, ) @@ -327,20 +325,6 @@ def test_patterns_survive_new_instance(self, db_path): stored = d2.get_stored_patterns() assert len(stored) >= 1 - def test_clear_patterns(self, db_path): - """clear_patterns() should remove all stored patterns.""" - sdr = _make_sdr(list(range(0, 80))) - for i in range(4): - _store_trace_with_sdr(db_path, f"t{i}", f"msg {i}", sdr) - - detector = PatternDetector(db_path) - detector.detect_all() - assert len(detector.get_stored_patterns()) >= 1 - - deleted = detector.clear_patterns() - assert deleted >= 1 - assert len(detector.get_stored_patterns()) == 0 - # ───────────────────────────────────────────────────────────────────── # DetectedPattern Serialization @@ -391,33 +375,6 @@ def test_detect_command_exists(self): assert "count" in result["result"] -# ───────────────────────────────────────────────────────────────────── -# Legacy API -# ───────────────────────────────────────────────────────────────────── - -class TestLegacyAPI: - """Test backward-compatible detect_pattern function.""" - - def test_empty_messages_returns_default(self): - """Empty messages should return 'default'.""" - assert detect_pattern([]) == "default" - - def test_adhd_pattern(self): - """Many short messages should return 'adhd'.""" - messages = [{"content": "hi"} for _ in range(10)] - assert detect_pattern(messages) == "adhd" - - def test_analytical_pattern(self): - """Long messages should return 'analytical'.""" - messages = [{"content": "x" * 250} for _ in range(3)] - assert detect_pattern(messages) == "analytical" - - def test_default_pattern(self): - """Normal-length messages should return 'default'.""" - messages = [{"content": "A normal message of medium length"} for _ in range(3)] - assert detect_pattern(messages) == "default" - - # ───────────────────────────────────────────────────────────────────── # Compliance # ───────────────────────────────────────────────────────────────────── diff --git a/tests/test_modulation/test_profile.py b/tests/test_modulation/test_profile.py index 1fb7194..ae6e1cc 100644 --- a/tests/test_modulation/test_profile.py +++ b/tests/test_modulation/test_profile.py @@ -165,22 +165,6 @@ def test_validate_rejects_non_json(self): validate_llm_output("not json at all") -class TestDetector: - """Pattern detection tests.""" - - def test_detect_default(self): - from harlo.modulation.detector import detect_pattern - - result = detect_pattern([]) - assert result in ("adhd", "analytical", "creative", "depleted", "default") - - def test_detect_returns_string(self): - from harlo.modulation.detector import detect_pattern - - result = detect_pattern(["hello", "world"]) - assert isinstance(result, str) - - class TestCompliance: """Phase 2 compliance checks.""" diff --git a/tests/test_motor/test_motor.py b/tests/test_motor/test_motor.py index 3917f6f..f82e98d 100644 --- a/tests/test_motor/test_motor.py +++ b/tests/test_motor/test_motor.py @@ -49,7 +49,7 @@ def test_default_is_inhibit(self): side_effects=[], ) result = gate(action, {}) - assert result.decision in (GateDecision.INHIBIT, GateDecision.ESCALATE, GateDecision.LOCKED) + assert result.decision == GateDecision.INHIBIT def test_level_3_always_locked(self): """Rule 25: Level 3 = LOCKED regardless.""" diff --git a/tests/test_tactical/test_tactical.py b/tests/test_tactical/test_tactical.py index 4d85d60..10cce63 100644 --- a/tests/test_tactical/test_tactical.py +++ b/tests/test_tactical/test_tactical.py @@ -10,7 +10,6 @@ import json import os -import sqlite3 import tempfile import pytest @@ -265,89 +264,6 @@ def test_default_handler_still_works(self): assert result["action_type"] == "unknown_type" -# ───────────────────────────────────────────────────────────────────── -# Connection Pool -# ───────────────────────────────────────────────────────────────────── - -class TestConnectionPool: - """Test SQLite connection pooling.""" - - def test_get_connection_returns_connection(self): - """get_connection should return a sqlite3 Connection.""" - from harlo.daemon.connection_pool import get_connection, close_connection - - db = tempfile.mktemp(suffix=".db") - try: - conn = get_connection(db) - assert isinstance(conn, sqlite3.Connection) - conn.execute("SELECT 1") - close_connection(db) - finally: - if os.path.exists(db): - os.unlink(db) - - def test_same_connection_returned(self): - """Repeated calls should return the same connection.""" - from harlo.daemon.connection_pool import get_connection, close_connection - - db = tempfile.mktemp(suffix=".db") - try: - conn1 = get_connection(db) - conn2 = get_connection(db) - assert conn1 is conn2 - close_connection(db) - finally: - if os.path.exists(db): - os.unlink(db) - - def test_different_paths_different_connections(self): - """Different db paths should get different connections.""" - from harlo.daemon.connection_pool import get_connection, close_all - - db1 = tempfile.mktemp(suffix=".db") - db2 = tempfile.mktemp(suffix=".db") - try: - conn1 = get_connection(db1) - conn2 = get_connection(db2) - assert conn1 is not conn2 - close_all() - finally: - for f in [db1, db2]: - if os.path.exists(f): - os.unlink(f) - - def test_close_connection(self): - """close_connection should close and remove cached connection.""" - from harlo.daemon.connection_pool import get_connection, close_connection - - db = tempfile.mktemp(suffix=".db") - try: - conn1 = get_connection(db) - close_connection(db) - # Next call should get a NEW connection - conn2 = get_connection(db) - assert conn1 is not conn2 - close_connection(db) - finally: - if os.path.exists(db): - os.unlink(db) - - def test_close_all(self): - """close_all should close all connections.""" - from harlo.daemon.connection_pool import get_connection, close_all - - db1 = tempfile.mktemp(suffix=".db") - db2 = tempfile.mktemp(suffix=".db") - try: - get_connection(db1) - get_connection(db2) - close_all() # Should not raise - finally: - for f in [db1, db2]: - if os.path.exists(f): - os.unlink(f) - - # ───────────────────────────────────────────────────────────────────── # Compliance # ───────────────────────────────────────────────────────────────────── @@ -355,20 +271,6 @@ def test_close_all(self): class TestCompliance: """Verify no rules violated.""" - def test_no_sleep_in_connection_pool(self): - """Rule 1: No sleep() in connection pool.""" - import inspect - from harlo.daemon import connection_pool - source = inspect.getsource(connection_pool) - assert "sleep(" not in source - - def test_no_while_true_in_connection_pool(self): - """Rule 1: No while True in connection pool.""" - import inspect - from harlo.daemon import connection_pool - source = inspect.getsource(connection_pool) - assert "while True" not in source - def test_no_sleep_in_executor(self): """Rule 1: No sleep() in executor.""" import inspect