# mind-mem v4.0.1 — Multi-Source Audit (cross-model review + arch-mind + agents)

**Date**: 2026-05-12
**Target**: mind-mem v4.0.1 @ `6bbbe51` (current `main`)
**Method**: cross-model review (primary) + arch-mind v0.0.3 sidecar scan + 4 specialist
agents in parallel (security-reviewer, python-reviewer, code-reviewer,
architect). A second-perspective model rerun was deferred after a provider
quota cap.

---

## arch-mind Q16.16 metrics (deterministic, single-source)

| Kernel | Score (Q16.16) | Raw | Status |
|---|---:|---:|---|
| acyclicity_q16          | 655,360,000 | 10,000.00 | ✅ perfect |
| modularity_q16          | 655,360,000 | 10,000.00 | ✅ perfect |
| redundancy_q16          | 644,874,240 |  9,840.00 | ✅ near-perfect |
| mcp_tool_isolation      | 642,252,800 |  9,800.00 | ✅ near-perfect |
| depth_q16               | 458,752,000 |  7,000.00 | ⚠️ reasonable |
| q16_determinism_purity  | 414,885,161 |  6,330.65 | ⚠️ partial (see A-12) |
| equality_q16            | 348,010,060 |  5,310.21 | ⚠️ Gini paradox (A-14) |
| evidence_chain_density  |   4,337,864 |     66.19 | ℹ️ Python-side only (A-13) |
| governance_kernel_coverage |        0 |      0.00 | ℹ️ MIND kernels via FFI/ctypes, not Python imports (A-13) |

Three of those low scores are **expected by construction** and explained
under A-12/A-13/A-14 below — not actionable defects.

---

## Severity legend

🔴 Critical — exploitable / data loss / silent corruption
🟠 High     — broken contract, security weakness, perf cliff
🟡 Medium   — code smell with downstream risk, partial-failure mode
🟢 Low      — quality / hygiene
ℹ️  Info     — observation / context

---

## Top priorities (what to fix first)

| Rank | Finding | Severity | Cost |
|---:|---|---|---|
| 1 | S-1  Non-constant-time token compare in http_transport.py | 🔴 Critical | 1-line fix (use `hmac.compare_digest`) |
| 2 | FP-1 LAST_WRITER_WINS ≡ HIGHER_VERSION (semantic collapse) | 🟠 High | ~20 lines (query `last_seen_at`) |
| 3 | FP-2 / FP-3 federation conflict-log concurrency bugs | 🟠 High | Add `BEGIN IMMEDIATE` + rowid filter |
| 4 | A-8  capture.py bypasses BlockStore protocol (still open) | 🟠 High | Refactor 60 lines |
| 5 | R-1  RRF first-seen metadata wins | 🟠 High | Prefer fresher `Date` in dedup |
| 6 | R-7  `rebuild_index` bypasses provider chain | 🟠 High | Route through `_embed_for_provider` |
| 7 | A-1 / A-11 SPEC.md is frozen at v3.2.0, v4 has no spec | 🟠 High | Cut SPEC v2.0 or split SPEC-v4.md |
| 8 | S-2  No rate-limiting on HTTP transport | 🟠 High | Reuse `SlidingWindowRateLimiter` |

Quick 1-line wins (S-1, FP-4, FP-5, FP-6, A-5) can land tonight; the rest
deserve a planned v4.1.0 cut.

---

## Security findings

### S-1 — Non-constant-time token compare 🔴 Critical
**Where**: `src/mind_mem/http_transport.py:640`
**What**: `_authenticated()` uses `sent == token` (plain Python equality,
short-circuits on first mismatch). The MCP path at `mcp/infra/http_auth.py:117`
correctly uses `hmac.compare_digest` — the two auth paths are inconsistent.
**Why it matters**: A local-network attacker can brute-force the token
character-by-character by timing latency, eventually reaching `POST /clear`
and `DELETE /memories/{id}`.
**Fix**: `import hmac; return bool(sent) and hmac.compare_digest(sent, token)`

### S-2 — No rate-limiting on HTTP transport 🟠 High
**Where**: `src/mind_mem/http_transport.py` (entire file)
**What**: Source inspection confirms zero rate-limiter calls in `do_GET` /
`do_POST` / `do_DELETE`. The `SlidingWindowRateLimiter` in
`mcp/infra/rate_limit.py` is only reachable via the `@mcp_tool_observe`
decorator — unreachable from the HTTP transport.
**Why it matters**: `POST /consolidate` (full dream cycle), `POST /query`
(vector search), `POST /clear` callable in a tight loop by any
authenticated caller.
**Fix**: Instantiate `SlidingWindowRateLimiter` at `serve_http()` and call
`limiter.allow()` at the top of each handler.

### S-3 — Federation 503 leaks internal exception text 🟡 Medium
**Where**: `src/mind_mem/http_transport.py:493, 511, 551, 599`
**What**: All four `_handle_fed_*` functions catch `Exception` and return
`f"federation disabled: {exc}"`. `FileNotFoundError` surfaces absolute
workspace path; `sqlite3.OperationalError` surfaces schema details;
`ImportError` surfaces internal module structure.
**Fix**: Separate `ImportError`/`FeatureDisabledError` from generic; log
unexpected internally; return `"federation unavailable"` without detail.

### S-4 — `GET /status` leaks absolute workspace path 🟡 Medium
**Where**: `src/mind_mem/http_transport.py:198`
**What**: `os.path.abspath(workspace)` in the 200 body — applies even to
unauthenticated-localhost responses.
**Fix**: Return only `os.path.basename(workspace)`, or expose via a
dedicated admin endpoint.

### S-5 — `/federation/vclock/<block_id>` missing path-traversal guard 🟡 Medium
**Where**: `src/mind_mem/http_transport.py:679-683` (and 541-548, 580-597)
**What**: `_handle_delete_memory` (line 373) guards `block_id` against `/`
and `..`; the federation handlers do not. SQL is parameterized so no SQLi,
but `block_id` is echoed back into responses and any future file-keyed
lookup inherits raw user input.
**Fix**: Apply the same `if "/" in block_id or ".." in block_id or len(block_id) > 256` guard.

### S-6 — arch_mind MCP tools pass unsanitized paths to subprocess 🟠 High
**Where**: `src/mind_mem/mcp/tools/arch_mind.py:71-116, 131, 144, 170-172, 190-200`
**What**: `repo`, `fixture`, `before`, `after`, `rules`, `agent_id`,
`commit_sha` passed verbatim into `subprocess.run()` arg list. `shell=False`
prevents OS-shell injection but flag injection to the underlying
`arch-mind` binary is unguarded. These tools sit in `USER_TOOLS`, so no
admin token required to reach them.
**Fix**: Validate path args via `_validate_path` (reuse from `workspace.py`).
Allowlist `mode` to `{"enforce", "report"}`. Enforce
`agent_id`/`commit_sha` against `^[a-zA-Z0-9_-]{1,128}$`.

### S-7 — No CORS/CSRF guard on HTTP transport 🟡 Medium
**Where**: `src/mind_mem/http_transport.py` (no `do_OPTIONS`, no Origin check)
**What**: Custom header (`X-MindMem-Token`) blocks simple CSRF only if
pre-flight `OPTIONS` is rejected. There's no `Origin`-header guard.
**Fix**: `do_OPTIONS → 405`; reject non-`http://127.0.0.1`/`localhost` Origin.

### S-8 — `MIND_MEM_ACL_DISABLED` permanently disables admin ACLs 🟠 High
**Where**: `src/mind_mem/mcp/infra/observability.py:89-96`
**What**: When the env var is truthy, the ACL check is skipped for every
tool — including `delete_memory_item`, `decrypt_file`, `encrypt_file`,
`rollback_proposal`. A one-time warning is emitted then silenced by
`_acl_disabled_warned`; subsequent admin-tool calls through user tokens
are never logged as bypasses.
**Fix**: Remove from production code (test-only flag), and remove the
one-shot warning suppression so every bypassed call is logged.

### S-9 — `--token` CLI flag exposes secret in `/proc/cmdline` 🟡 Medium
**Where**: `src/mind_mem/mcp/server.py:209-214`
**What**: `main()` warns about cmdline exposure but then continues. Token
visible in `/proc/<pid>/cmdline` for server lifetime — readable by any
same-user process and captured by audit frameworks.
**Fix**: Remove the flag; require `MIND_MEM_TOKEN` env var only.

### S-10 — TOCTOU symlink race in `_safe_vault_path` / `_validate_path` 🟠 High
**Where**: `src/mind_mem/mcp/tools/encryption.py:37-38`,
`src/mind_mem/mcp/infra/workspace.py:80-81`
**What**: `os.path.realpath()` resolves symlinks at validation time, but
the symlink can be replaced between validation and the subsequent
`open()`. An attacker controlling a writable directory in the workspace
can race the validate→open window and have `encrypt_file` open an
arbitrary out-of-workspace target (e.g., SSH keys).
**Fix**: Reject symlinks outright at validation time; use
`os.open(path, os.O_WRONLY | os.O_NOFOLLOW)` for writes.

### S-11 — `deleted_blocks.jsonl` recovery log written without file lock 🟢 Low
**Where**: `src/mind_mem/mcp/tools/memory_ops.py:270-278`
**What**: `delete_memory_item` holds a `FileLock` on the source `.md` file
but appends to `memory/deleted_blocks.jsonl` with no lock. The
`ThreadingMixIn` server dispatches concurrently — two deletions can
interleave their jsonl writes, corrupting the recovery log.
**Fix**: Add a second `FileLock` on `deleted_blocks.jsonl`, or `flock` the fd.

### Confirmed defenses
- ✅ SQL parameterized everywhere in `v4/federation.py`,
  `federation_client.py`, `http_transport.py` — no f-string / `%` / `.format()`
  in any `conn.execute()` call.
- ✅ Body cap `MAX_BODY_BYTES = 1 MiB` enforced in `_read_body()` before
  base64 decode; effective post-decode size ≤ ~768 KB (S-3 caveat aside).
- ✅ `_safe_log()` strips CR/LF/NUL from user-controlled log values.
- ✅ MCP path uses `hmac.compare_digest`.

---

## Federation v-clock + TAG_v1 preimage findings

### FP-1 — LAST_WRITER_WINS ≡ HIGHER_VERSION 🟠 High
**Where**: `src/mind_mem/v4/federation.py:312-315`
**What**: Both enum arms fall into the same branch and pick
`report.left_agent` (line 266 sorts agents by *version* descending, so
"left" is always the highest-version agent). The module docstring (line 14)
says LAST_WRITER_WINS = "Highest agent version wins" — making it
identical to HIGHER_VERSION. The `resolve_conflict` docstring (line 292)
contradicts: "Pick whichever side wrote most recently (by version vector
tip)" — a wall-clock semantic. Neither strategy reads `last_seen_at` even
though the schema stores it (line 126).
**Fix**: For LAST_WRITER_WINS, query `block_tier_vclock.last_seen_at` and
pick the row with the more recent timestamp (break ties with version).
Or collapse the two enum values into one if the distinction is dropped.

### FP-2 — `_log_conflict` check-then-act TOCTOU 🟠 High
**Where**: `src/mind_mem/v4/federation.py:354-381`
**What**: The duplicate-check `SELECT` and the `INSERT` are in a plain
context — no `BEGIN IMMEDIATE`. No `UNIQUE` constraint on
`(block_id, left_agent, right_agent, left_version, right_version)`. Two
threads calling `detect_conflict` simultaneously will both evaluate
`existing is None` and both insert.
**Fix**: Add `conn.execute("BEGIN IMMEDIATE")` before the SELECT, OR add a
partial `UNIQUE` index filtered on `resolution IS NULL` and use
`INSERT OR IGNORE`.

### FP-3 — `resolve_conflict` UPDATE over-broad 🟠 High
**Where**: `src/mind_mem/v4/federation.py:327-330`
```sql
UPDATE tier_conflict_log SET resolution=?, resolved_to=?, resolved_at=?
WHERE block_id=? AND resolution IS NULL
```
**What**: No filter on `left_agent`, `right_agent`, `left_version`,
`right_version`. If two open rows exist for the same `block_id` (different
version pairs), both close with the same strategy and winner — silently
falsifying the stale row's audit metadata.
**Fix**: `_log_conflict` returns the inserted `rowid`; `resolve_conflict`
pins UPDATE to `WHERE rowid = ?`.

### FP-4 — THREE_WAY_MERGE with `merger=None` returns None silently 🟡 Medium
**Where**: `src/mind_mem/v4/federation.py:316-318`
**What**: Three distinct outcomes all return None: (1) no conflict found,
(2) strategy=THREE_WAY_MERGE but merger=None (caller error), (3)
unrecognized strategy. Public docstring documents only (1).
**Fix**: Raise `ValueError("THREE_WAY_MERGE requires a merger callable")`.

### FP-5 — `preimage()` None semantics: doc/type-alias/impl all disagree 🟡 Medium
**Where**: `src/mind_mem/preimage.py:24-26`, `:55-56`, `:43`
**What**: Module docstring claims `None → empty bytes`. `_render` raises
`ValueError`. Type alias `Value = Union[..., None]` still includes None,
so mypy accepts a None call that always raises at runtime.
**Fix**: Drop None from the `Value` union. Rewrite docstring lines 24-26
to state None is rejected; require `""` or sentinel.

### FP-6 — `+Inf` and any `float >= 32768.0` produce identical preimages 🟡 Medium
**Where**: `src/mind_mem/q1616.py:46-48`; `src/mind_mem/preimage.py:69-73`
**What**: `to_q16_16` saturates `+Inf` to `_MAX = 0x7FFFFFFF`. Any finite
float that overflows Q16.16 also saturates to `_MAX`. Empirically:
`hex_q16_16(float('inf')) == hex_q16_16(32768.0) == '7fffffff'` — they
collide in preimage. Preimage rejects NaN but lets `+Inf` through.
**Fix**: Add `math.isinf(value)` guard in `_render` alongside the NaN
check: `raise ValueError("preimage(): ±Inf is not hashable")`.

### FP-7 — Three+ agent divergence: only top-2 logged 🟡 Medium
**Where**: `src/mind_mem/v4/federation.py:265-267`
**What**: `detect_conflict` picks `sorted_agents[0]` + `sorted_agents[1]`
only. With agents A:10, B:7, C:3, only A vs B is logged; C is invisible
indefinitely. After A vs B resolves, the same top-2 pair re-appears and C
is still ignored.
**Fix**: Iterate `sorted_agents[1:]` and log one row per lagging agent.

### FP-8 — `_log_conflict` + `resolve_conflict` missing `BEGIN IMMEDIATE` 🟡 Medium
**Where**: `src/mind_mem/v4/federation.py:354, 326`
**What**: `record_agent_write` uses `BEGIN IMMEDIATE` (correct). The
log/update writes don't. Two concurrent `resolve_conflict` calls can both
succeed; the second silently overwrites the first's audit metadata.
**Fix**: Add `conn.execute("BEGIN IMMEDIATE")` in both paths.

### FP-9 — Legacy v1 audit-chain `|` separator: pipe in target path 🟢 Low
**Where**: `src/mind_mem/audit_chain.py:157`
**What**: Linux paths can contain `|`; a target like `"docs/a|b.md"`
shifts field boundaries and causes verify() to flag a false tamper. The
v3 downgrade-attack mitigation at line 378 limits the blast radius.
**Fix**: Emit `LEGACY_SEPARATOR_AMBIGUITY` distinct from generic tamper so
operators can distinguish encoding artifact from real tampering.

### Confirmed preimage correctness
- ✅ `bool` vs `int` distinctness verified (`True → b"t"`, `1 → b"1"`).
- ✅ NaN rejection correct (`value != value` is the right IEEE 754 test).
- ✅ Q16.16 negative-hex encoding correct (`q & 0xFFFFFFFF` mask works on
  all CPython targets).
- ✅ Cross-arch float stability defensible in the stated `[0.0, 1.0]` domain.
- ✅ Downgrade-attack mitigation correctly blocks retroactive v3→v1 hash
  downgrade after a v3 entry is seen.

---

## Retrieval + RRF fusion findings

### R-1 — RRF first-seen metadata wins (silent staleness) 🟠 High
**Where**: `src/mind_mem/hybrid_recall.py:99-100`
**What**: `block_data[bid]` is set only on first encounter. If BM25
(list 0) returns a stale-metadata snapshot and vector (list 1) returns
the same block with fresher metadata, BM25's version is permanently kept.
RRF score is correct; every field comes from whichever list is first.
**Fix**: Prefer the higher-`Date` metadata in dedup, or take a union with
the fresher field winning.

### R-2 — Weight normalization undocumented; `rrf_score` inflates with large weights 🟡 Medium
**Where**: `src/mind_mem/hybrid_recall.py:95`, docstring 72-86
**What**: Weights are relative multipliers, not probability masses. Setting
`vector_weight=10.0` inflates all `rrf_score` values ~5x. These scores
flow to `knee_cutoff` (`_recall_core.py:163-204`) as absolute thresholds.
**Fix**: Either normalize (`w /= sum(weights)`), or document explicitly
and add a validation warning for extreme values.

### R-3 — Partial-empty `ranked_lists` silent degradation 🟡 Medium
**Where**: `src/mind_mem/hybrid_recall.py:88-110`, metric at `:411`
**What**: When BM25 succeeds + vector fails *at runtime*, `ranked_lists =
[bm25_results, []]`. RRF math is mathematically correct (empty list
contributes zero), but score values are roughly half. The
`hybrid_searches_fused` counter still increments — single-leg degradation
is invisible.
**Fix**: After both futures complete, emit
`hybrid_single_list_degenerate` metric.

### R-4 — `_get_block_id` `file:line` fallback silent dedup collision 🟡 Medium
**Where**: `src/mind_mem/hybrid_recall.py:113-124`
**What**: Two distinct chunks of the same source can share `file:line`
and collapse in the first-seen dedup with no log.
**Fix**: `_log.warning("rrf_fallback_id_used", ...)` when fallback is taken.

### R-5 — Multi-query expansion runs serially, not in parallel 🟡 Medium
**Where**: `src/mind_mem/hybrid_recall.py:503-515`
**What**: `for q in queries` calls `self.search()` serially. Each call
spawns its own 2-thread pool internally, but the N expansion variants wait
for each other. With N=4 at ~50ms each, total ~200ms instead of ~50ms.
`prefetch_context` correctly uses `as_completed` for the analogous pattern.
**Fix**: Replace serial loop with `ThreadPoolExecutor(max_workers=len(queries))`.

### R-6 — `detect_query_type` called 3× per request 🟢 Low
**Where**: `src/mind_mem/hybrid_recall.py:257, 312, 760`
**What**: Same regex classification on the same query, three times. Not
memoized.
**Fix**: Compute once at top of `search()`, or `functools.lru_cache`.

### R-7 — `rebuild_index` bypasses provider chain + writes wrong format 🟠 High
**Where**: `src/mind_mem/recall_vector.py:1371, 1377-1388`
**What**: Calls `backend.model.encode(...)` directly — bypasses the
Ollama → llama_cpp → fastembed → sentence-transformers fallback chain AND
the circuit-breaker. Writes embeddings in the legacy flat `index.json`
format which the sqlite-vec search path never consults. A workspace
configured with `provider=sqlite_vec` calling `rebuild_index` silently
gets no usable index.
**Fix**: Replace with `backend._embed_for_provider(texts)`; route to the
correct provider-specific index format based on `self.provider`.

### R-8 — sqlite-vec `active_only` 3× overfetch not adaptive 🟢 Low
**Where**: `src/mind_mem/recall_vector.py:727`
**What**: `fetch_limit = limit * 3 if active_only else limit`. In sparse-
active workspaces (<33% active), filtered result count < limit. Vector
leg silently contributes fewer candidates to RRF than intended.
**Fix**: If `len(results) < limit // 2`, log warning + retry with doubled
`fetch_limit`, or expose an `overfetch_factor` config key.

### R-9 — Pinecone missing key → silent BM25-only degradation 🟡 Medium
**Where**: `src/mind_mem/recall_vector.py:1221-1223`,
`src/mind_mem/hybrid_recall.py:881-883`
**What**: `_search_pinecone` raises `ValueError("PINECONE_API_KEY
required")`. Caught by bare `except Exception` in `_vector_search`, returns
`[]`. Hybrid continues BM25-only with HTTP 200, `hybrid_searches_fused`
still increments.
**Fix**: At `VectorBackend.__init__`, when `provider == "pinecone"`, check
for the key immediately and log a WARNING. In `_vector_search`, re-raise
or emit CRITICAL when the error message contains `"API_KEY"`.

### R-10 — `_maybe_temporal_decay` mutates fused dicts in-place 🟢 Low
**Where**: `src/mind_mem/hybrid_recall.py:591-596`
**What**: Loop mutates `r["score"]` directly. The dicts are `.copy()`-ed
from `block_data` in `rrf_fuse` (line 105), but `fused[:limit]` and `fused`
both alias the same objects post-mutation. Inconsistent with the
project-wide immutability convention.
**Fix**: Rebuild list: `[{**r, "score": ..., "_temporal_decay": ...} for r in results]`.

### R-11 — `prefetch_context` category-boost is dead code with multi-signal 🟢 Low
**Where**: `src/mind_mem/_recall_core.py:1326-1338`
**What**: Category hits appended after parallel signal phase, then sliced
off by `results[:limit]`. With 4 signals × `limit=5`, the list reaches 20
before category append, then truncates to 5.
**Fix**: Reserve category slots before appending.

### Confirmed correctness
- ✅ RRF formula correct (`enumerate` 0-based + `rank_0 + 1` = standard
  1-based RRF).
- ✅ Empty `ranked_lists` guard correct.
- ✅ Rerank short-circuits when disabled.
- ✅ No `time.sleep()` in any `async def`; FastAPI routes are sync and
  run in threadpool — correct.

---

## Architecture findings (SPEC + drift)

### A-1 — SPEC.md frozen at v3.2.0; v3.3 through v4.0.1 surfaces undocumented 🟠 High
**Where**: `SPEC.md` (whole file)
**What**: Latest spec reference is "v3.2.0 §2.2 refinement". SPEC documents
zero of: Q16.16 semantics, TAG_v1 NUL-separated preimage rule, audit-chain
v2, MIC/MAP wire format, hash-of-code invalidation, typed lineage edges,
`validate_block` quality gate, lineage→staleness BFS, federation v-clock
schema, cognitive kernel registry, `block_kind_tags` table,
`block_recall_tier` CAS contract, eviction enum, PQ codec, circuit
breaker, backpressure, observability primitives, federation HTTP wire.
~30 release notes worth of architecture undocumented in the file that
says "All implementations MUST conform to this specification."
**Fix**: Cut SPEC v2.0 with §§9-14 covering Q16.16, TAG_v1, federation,
cognitive kernel, v4 feature flags, wire transport. Or split SPEC-v4.md.

### A-2 — SPEC self-version inconsistent 🟢 Low
**Where**: `SPEC.md:1` ("v1.0") vs `SPEC.md:382` ("Current version: 1.5.1")
**What**: Header and footer disagree about the spec's own version.
**Fix**: Pick one. Add a pre-commit hook that fails on mismatch.

### A-3 — MCP tool count drift: 81 vs 84 vs 83 actual 🟡 Medium
**Where**: `README.md:23` (badge "84"), `:683` ("81 tools"), `:803`
("81 = 58 + 7 + 16"), `CHANGELOG.md` v3.11.0 ("81 → 84"), `CLAUDE.md`
("84"), live registration count: **84 `mcp.tool()` calls** across 22
modules with **1 deliberate shadow** (`public.py:422` re-binds `recall` over
the dispatcher), so unique surface = **83**.
**Fix**: Add `scripts/count_mcp_tools.py` that imports
`mind_mem.mcp.server.mcp` and asserts live registry length matches a
constant. Source-of-truth the README from that constant.

### A-4 — README ships pre-decomposition module tree 🟡 Medium
**Where**: `README.md:683, 759-785`
**What**: Tree lists `mcp_server.py` as if still the 4604-line monolith;
omits `mcp/` package entirely; omits `v4/`; omits ~200 modules.
**Fix**: Regenerate from `git ls-files src/mind_mem/` (max depth 3) or
delete the tree section.

### A-5 — README points at `kernels/`, actual dir is `mind/` 🟢 Low
**Where**: `README.md:64` (CLAUDE block) vs disk (`mind/*.mind`)
**What**: README claims kernels at `kernels/` — directory doesn't exist;
26 `.mind` files live at `mind/`.
**Fix**: Update README; regenerate count from `ls mind/*.mind | wc -l`.

### A-6 — `build/lib/` artifacts in worktree (partial stale mirror) 🟡 Medium
**Where**: `build/lib/mind_mem/` (~200 stale files; partial — only 4 tool
modules vs 23 in `src/`)
**What**: Properly `.gitignore`'d so won't enter commits, but visible to
every editor index, LSP, ripgrep-without-config, and arch-mind scan. LSP
can resolve to the stale `build/` copy and present phantom older code.
**Fix**: `trash build/ dist/ *.egg-info/` on release. Add `make clean`
target. Verify `.arch-mind/` config excludes `build/`.

### A-7 — Three MCP entry-points still exist (now as shims) 🟢 Low
**Where**: `mcp_server.py` (root, 90L), `src/mcp_server.py` (35L),
`src/mind_mem/mcp_server.py` (252L), `src/mind_mem/mcp_entry.py` (51L)
**What**: All four are documented thin shims now. Implementation lives in
`src/mind_mem/mcp/server.py` (241L). Net: god object closed; layout
clutter remains.
**Fix**: Document each shim's role in `CONTRIBUTING.md`. Drop
`mind_mem/mcp_server.py` in v5.0 (its comment says "stays until v4.0" —
deadline passed).

### A-8 — Storage protocol bypass (capture.py → SIGNALS.md) STILL OPEN 🟠 High
**Where**: `src/mind_mem/capture.py:262-326`
**What**: Direct `open(signals_path, "a") + write()` for every signal.
`gate.admit(action="WRITE", ...)` runs at line 339 — *after* the write is
already durable on disk. The governance gate is post-hoc logging, not a
gate.
**Why it matters**: (1) Postgres backend invisible to capture writes
until reindex. (2) Audit chain order broken (admit after commit).
(3) SPEC §M4 enforced by reviewer discipline, not code.
**Fix**: Route through `BlockStore.append_signal(...)`. Make
`gate.admit(action="WRITE_INTENT")` return ok/deny *before* the write,
then commit.

### A-9 — Validation duality (bash + python) FIXED ℹ️ closed
`validate.sh` is now a 32L forwarder to `validate_py`;
`_task_status_literals.sh` auto-generated from `enums.py`. The 600L
`.pre-forwarder` is past its v4.0 removal deadline — remove in v4.1.

### A-10 — Snapshot exclusion split (`maintenance/append-only` vs `maintenance/tracked`) FIXED ℹ️ closed
v3.2.0 split exclusions correctly. `_is_in_excluded_dir` enforces walk and
rollback orphan detection. Cite as example of well-resolved prior finding.

### A-11 — v4 surface decoupled from v3 but no SPEC 🟠 High
**Where**: `src/mind_mem/v4/*.py` (22 modules)
**What**: v4 isolation is correct (19/22 modules call `require_enabled()`;
the 3 exceptions — `feature_flags.py`, `__init__.py`, `federation_client.py`
— have equivalent enforcement). But SPEC has zero v4 mention. Federation
v-clock schema, `MergeStrategy` enum, HTTP endpoint contracts, the
`block_recall_tier` CAS invariant — all live only in CHANGELOG prose +
inline docstrings. Federation interop with non-mind-mem clients
(CHANGELOG flags ActivityPub + gRPC parity as v4.x deferrals) is
unspecified.
**Fix**: SPEC v2.0 §§11-14 with: (a) the 35 v4 flag names, (b) `block_tier_vclock`
+ `tier_conflict_log` schemas, (c) the 4 federation HTTP endpoints +
their flag-gating + 503-when-disabled contract, (d) `KernelKind` enum,
(e) `block_version` monotonic CAS + `StaleVersionError`, (f) `MergeStrategy`
+ THREE_WAY_MERGE callable contract.

### A-12 — `q16_determinism_purity = 6330/10000` is expected ℹ️ Info
Q16.16 is the audit-preimage quantization boundary (per `q1616.py`
docstring), NOT a whole-codebase invariant. BM25F / RRF / vector ops
legitimately use float. The right metric is "does float leak into a
hashed preimage" — and the 6 modules that DO participate in preimages
(`audit_chain.py`, `preimage.py`, `hash_chain_v2.py`, `merkle_tree.py`,
`field_audit.py`, `evidence_objects.py`) are clean.

### A-13 — `governance_kernel_coverage = 0/10000` is expected ℹ️ Info
Python never `import`s `.mind` files. MIND kernels are either compiled to
`lib/libmindmem.so` and loaded via ctypes (`mind_ffi.py:126`), or read as
INI config (`mind_ffi.py:560-619`). Right metric: `len(glob('lib/*.so')) /
len(glob('mind/*.mind'))`. **Rename `governance_kernel_coverage` →
`python_import_kernel_coverage`** in the arch-mind output to avoid this
false-alarm reading.

### A-14 — `equality_q16 = 5310/10000` is the known Gini paradox ℹ️ Info
Largest file: `mm_cli.py` at 2087L (well under any catastrophic threshold).
The score reflects long-tail distribution: many 100-300L modules vs a
handful of 1000+ L ones. The mcp_server.py decomposition (4604L → 252L
shim + 22 cohesive modules) actually worsened this score while clearly
improving maintainability. **Don't optimize.**

### A-15 — God-object FIXED; new shim cost 🟢 Low
**Where**: `src/mind_mem/mcp_server.py:1-252`
**What**: Re-export shim across `mcp.infra.*`, `mcp.resources`, `mcp.tools.*`.
Honest, well-commented. Cost: every new symbol must be added in 2 places.
**Fix**: Regression test `tests/test_shim_completeness.py` asserting every
`__all__` from each submodule is re-exported by `mind_mem.mcp_server`.

---

## Prior-audit status (one-liner each)

| Prior finding | Current state |
|---|---|
| §1.1 Duplicate MCP entry points | PARTIAL — functionally collapsed to thin shims; layout still 4 files (A-7) |
| §1.2 Bloated 4604-line god object | FIXED — 252L shim + 22 cohesive modules (A-15) |
| §1.3 Validation duality | FIXED — `validate.sh` is now a forwarder; bash engine past removal deadline (A-9) |
| §1.4 Storage protocol bypass (capture.py) | **STILL OPEN** (A-8) |
| §2.1 State-machine DRY (bash vs Python) | FIXED — `_task_status_literals.sh` auto-generated |
| §2.2 Snapshot exclusion atomicity | FIXED — v3.2.0 split tracked/append-only (A-10) |
| §3 Static analysis (mypy/ruff) | FIXED — clean across all 4 reviewed files |

---

## Summary

**Findings**: 46 total
- 🔴 1 Critical (S-1 timing-attack token compare)
- 🟠 12 High (S-2, S-6, S-8, S-10, FP-1, FP-2, FP-3, R-1, R-7, A-1, A-8, A-11)
- 🟡 18 Medium
- 🟢 12 Low
- ℹ️ 3 Informational + 4 prior-audit closures

**Verdict**: Strong codebase (arch-mind acyclicity / modularity / redundancy /
mcp_tool_isolation all ≥ 9800/10000). The decomposition that closed the
4604-line god-object is the headline win. The remaining work is concentrated
in three areas:
1. **Federation correctness** (FP-1/2/3) — three concurrency/semantic
   bugs in code that's shipped on PyPI as v4.0.1.
2. **Spec coverage** (A-1/A-11) — the formal spec hasn't moved in ~6
   minor releases.
3. **One outstanding prior finding** (A-8 capture.py bypass).

**Followup**: rerun the audit with a second-perspective model when the
provider quota resets; cross-check against this doc for cross-model agreement.

---

*STARGA, Inc. · 2026-05-12 · cross-model review + arch-mind v0.0.3 + 4 agent specialists*
