# Codevira v3.1.1 — release notes

**Released:** 2026-05-30
**Supersedes:** 3.1.0 (yanked — released without this notes/CHANGELOG
entry; same code shape, the only difference is documentation
discipline)
**Install:** `pipx install --upgrade codevira` or
`pip install codevira==3.1.1`

---

## TL;DR

v3.1.1 finishes what 3.1.0 started. Five memory subsystems (M1–M9)
shipped in 3.1.0; v3.1.1 adds the hardening sweep on top:

- Every store now scrubs secrets at the write boundary
- The graph viewer learned to **answer questions** (ranked search +
  Q&A + outcome lens + lineage trace)
- The release gauntlet's last permanently-skipped gate (G3) ships
  as a real check
- `codevira sync` now classifies outcomes automatically (the
  viewer's outcome lens lights up on real projects)
- 4 silent bugs (path traversal, type coercion, off-by-one, spatial
  BFS crash) are fixed
- A schema addition lets every new decision carry its
  *alternatives_considered* and *would_re_examine_if* — turning
  `do_not_revert` from a one-way ratchet into a self-documented
  precondition

Full per-line history is in [CHANGELOG.md](../../CHANGELOG.md#311--2026-05-30--hardening-viewer-overhaul-g3-sync-observe-git).

---

## Upgrading from 3.0.x or 3.1.0

```bash
pipx upgrade codevira         # or `pip install --upgrade codevira`
codevira --version            # should report 3.1.1
codevira setup                # re-stamps env.CODEVIRA_IDE into every IDE
codevira sync                 # one-shot: rebuilds indexes + classifies outcomes
```

That's it. The wire format is back-compat:

- **v3.0.x records** (no `origin` field) keep loading; readers treat
  them as `ide="unknown"`.
- **3.1.0 records** that wrote *with* `origin` continue loading.
- **Counter-decision fields** (`alternatives_considered`,
  `would_re_examine_if`) are optional on every legacy decision —
  populate them on new writes when you have the context.

The one user-visible change after `setup`: existing IDE configs gain
`env.CODEVIRA_IDE` so cross-IDE consensus checks can attribute
writes. Without this, every decision shows up as `ide=unknown`.

---

## The new things you'll notice

### `codevira graph` is interactive now

Open the rendered HTML, then:

- **Type in the search box** — top-K matches appear ranked below.
  Token grammar: `tag:auth ide:cursor kind:decision protected:true
  since:2026-01-01 until:2026-04-01`.
- **Ask the graph in plain English**:
  - "what did we decide about auth?"
  - "why did we pick bcrypt?"
  - "what got reverted?"
  - "what's protected?"
- **Switch the Lens dropdown** to "Outcome" — color decisions by
  classified result. Run `codevira sync` first so outcomes are
  populated.
- **Click any decision** — rich detail panel shows
  alternatives_considered, would_re_examine_if, lineage chain,
  outcome badge.
- **Click "trace" on a lineage** — graph dims, the supersession
  chain is highlighted, camera fits to it.

Press `?` in the viewer for the full key+gesture cheatsheet.

### `record_decision` accepts losers + invalidation triggers

```python
record_decision(
    decision="Use bcrypt for password hashing",
    do_not_revert=True,
    alternatives_considered=[
        "argon2id (rejected: heavier on cheap mobile clients)",
        "scrypt (rejected: less well-vetted in our ecosystem)",
    ],
    would_re_examine_if=(
        "if argon2id native bindings ship in the stdlib OR "
        "if we move off mobile clients"
    ),
)
```

Future sessions see WHY the decision won and the rule for revisiting
it — instead of just the winner. The viewer's rich-detail panel
surfaces both fields. Empty values are fine; populate when you have
the context.

### Secrets never land on disk

If you accidentally paste a curl example into a decision, working
memory note, session log, skill procedure, or reflection:

```
record_decision(decision="see api_key=hunter2-deadbeefcafe for /v1/things")
```

The on-disk record reads `see <redacted:api-key> for /v1/things`.
Six pattern families are scrubbed: api-key, Bearer, password, AWS
AKIA, long hex, long base64. Shared scrubber in
`mcp_server/storage/sanitize.py`.

### `codevira sync` quietly does more work

Every `sync` now runs `observe-git` as a best-effort tail step:

```
✓ AGENTS.md regenerated (5,137 bytes)
✓ observe-git (8 kept · 13 modified · 2 reverted · 6 unclassified,
                23 new outcome(s))
```

The outcome counts populate the viewer's outcome lens + the Q&A
"what got reverted" surface. Non-git projects degrade silently.

### G3 real-IDE smoke

`scripts/check_real_ide_smoke.sh` was a stub for half a year. Now
it runs the real check during `make release-gauntlet`:

- Finds every IDE config on the machine
- Verifies codevira is registered in each
- Spawns an MCP stdio server, runs initialize + tools/list
- Asserts tools/list <1s (the Claude Desktop disconnect class)

Run it standalone any time: `scripts/check_real_ide_smoke.sh`.

---

## Bugs fixed (real ones)

1. `working_store.commit_session("../escape")` would write outside
   `.codevira/working_archived/`. Now raises `ValueError`.
2. `skills_store.record(triggers={"tags": "git"})` silently stored
   `["g", "i", "t"]`. Now raises `ValueError` pointing to wrap as
   a list.
3. `skills_store.list_all(limit=0)` returned the first row (append-
   then-check off-by-one). Now returns `[]`.
4. `spatial._bfs_distances` crashed on corrupt/schema-missing
   `graph.db` (only caught connect-time errors). Now catches
   query-time `sqlite3.DatabaseError` and falls back to
   neighborhood-only mode.
5. `promote_skill_to_playbook` silently allowed archived (low-value)
   skills. Now refuses unless `force=True`.
6. `origin.current_origin` passed `"null"` / `"  "` strings through
   verbatim as `agent_model`. Now normalizes to `None`.
7. `inject_global_antigravity` left partial state when target #2
   failed mid-iteration. Now snapshots + rolls back.

---

## Honest process notes

- **3.1.0 yanked**, 3.1.1 ships in its place. 3.1.0 was published
  before this CHANGELOG/README entry existed. Same code; pure
  documentation discipline gap.
- **A bug almost shipped.** Mid-session a tweak to
  `_DEFAULT_MIN_SCORE` (0.10 → 0.25) silently broke the cross-tool
  wedge — single-FTS-match decisions stopped surfacing to other
  IDEs. The `make test-e2e` gate ran (per D000010's procedural
  lock) but didn't include `test_cross_tool_universality.py`. A
  full `pytest tests/` (no ignores) before publish caught it.
  Reverted the threshold; widened the gate; added a unit-level
  wedge-regression test so the same class of regression also fails
  fast.
- **CLAUDE.md's "MUST" downgrade.** The "before-you-finish" contract
  in CLAUDE.md said `MUST call write_session_log`. The engine
  didn't enforce it. Downgraded to STRONG RECOMMENDATION with an
  "engine enforcement on roadmap" note. Honesty > confidence
  theater.

---

## What's next (v3.2.0 outline, subject to change)

- Real MCP `sampling/createMessage` in `reflect()` (today returns
  a stubbed `sampling_supported: False`).
- Q&A vocabulary expansion: "who decided X", "when did we X",
  "compare X and Y", lineage queries.
- Recency-decay on relevance scoring (was attempted in 3.1.x but
  reverted — needs measurement infrastructure first).
- `do_not_revert` auto-soft-expire after N months unless
  re-affirmed via `apply_outcome("still valid")`.
