Verification: P0.8.2 η Three-Zone Retention

Task: P0.8.2 Branch: feature/p0-8-2-retention Commit: 617a67cd (Step 4 — feat(p0-8-2): η three-zone retention (hot/warm/cold)) Date: 2026-04-17


1. Test evidence

1.1 Retention-only run (hermetic)

$ npm test -- --testPathPattern='domains/proof/retention'

Test Suites: 1 passed, 1 total
Tests:       51 passed, 51 total
Snapshots:   0 total
Time:        18.6 s

All 51 tests in src/__tests__/domains/proof/retention.test.ts pass.

1.2 Full-suite run

$ npm test

Test Suites: 1 failed, 17 passed, 18 total
Tests:       1 failed, 830 passed, 831 total
Time:        47.8 s

The single failing test is startup — subprocess smoke › tsx src/server.ts boots and logs [Startup] Phase 1. It passes cleanly when run in isolation:

$ npm test -- --testPathPattern='startup'
Test Suites: 1 passed, 1 total
Tests:       40 passed, 40 total
Time:        21.7 s

This is the documented pre-existing subprocess-smoke flakiness noted in the memory log for Wave E (project_r75_phase0_wave_e_2026_04_17.md → “Pre-existing intermittent startup-subprocess smoke test flakiness in CI”). Root cause: subprocess spawn + Phase 1 stdout timing under concurrent Jest worker pressure; not caused by this task.

Retention-only tests: 51 / 0 failures. Full-suite minus the flake: 830 / 0 real failures. No retention-related regressions. thought_records migration-rebuild preserves P0.7.2 trail tests (all trail/repository tests still pass at 48/48).

1.3 Coverage (retention module)

retention.ts     | 91.25% stmts | 77.14% branch | 100% funcs | 91.25% lines

Uncovered lines are defensive error-path throws: RetentionIntegrityError branches for hot/warm rows with NULL content, the unreachable “cold-going-backwards” guard. These are correctness guards, not reachable through the public API.

2. Lint evidence

$ npm run lint

(no output — clean)

Zero ESLint errors. Zero warnings.

3. Build evidence

$ npm run build

(no output — clean)

TypeScript tsc produces no diagnostics.

4. Acceptance criteria checklist

# AC (task-breakdown §P0.8.2) Evidence
1 Hot zone: last 100 records — full content in DB computeZone tests (positions 1..100 → hot); archiveRecord — hot zone (no-op) suite confirms content preserved.
2 Warm zone: records 101–1000 — content compressed (JSON → gzip → base64) archiveRecord — hot → warm suite: asserts zone='warm', content=NULL, content_compressed != NULL, and gunzipContent(content_compressed) === original.
3 Cold zone: records 1001+ — content hash only (full content deleted) archiveRecord — warm → cold + hot → cold direct suites: zone='cold', content=NULL, content_compressed=NULL, content_hash=sha256(original).
4 archiveRecord(id) moves record to next zone based on age/position All archive suites plus archiveRecord — errors cover happy-path + unknown-id.
5 retrieveRecord(id) decompresses if Warm, returns hash stub if Cold retrieveRecord suite: hot returns content, warm decompresses, cold returns hash_only with content_available: false, unknown id returns null.
6 Test: hot → warm → cold transitions; verify content availability per zone end-to-end hot → warm → cold sequence (AC6) single test walks the full lifecycle and checks both zone transitions and content_available flip.

All 6 acceptance criteria satisfied with direct test coverage.

5. Invariant checklist

Invariant (contract §5) Evidence
I1 — forward-only (no backwards producible) computeZone is a pure function of position; archiveRecord threads the computed target into a forward-only UPDATE. The backwards-from-cold branch in retention.ts throws RetentionIntegrityError and is unreachable through the public API.
I2 — idempotency archiveRecord — idempotency (I2) suite: warm + cold second-call returns changed:false with row bytes unchanged.
I3 — chain-integrity preservation archiveRecord — chain integrity (I3, I4) suite: snapshot-and-compare on id, type, task_id, agent_id, timestamp, prev_hash, hash, created_at.
I4 — row preservation (no DELETE) row preservation (I4): COUNT(*) unchanged after any archival.
I5 — position stability within transaction Implicit in SQLite semantics (rowid monotonicity) — no separate test; getRecordPosition tests prove correct positioning.
I6 — gzip round-trip gzipContent / gunzipContent round-trip (I6) suite: empty, ASCII, unicode, 72-KiB, base64 format regex.
I7 — content_hash stability warm → cold preserves all chain fields + content_hash stability (I7) compares warm-transition content_hash with cold-transition content_hash — byte-identical.

6. 5-step artefacts shipped

Step File Commit
1. Audit docs/audits/p0-8-2-retention-audit.md 38742b9a
2. Contract docs/contracts/p0-8-2-retention-contract.md 5e3c0ac3
3. Packet docs/packets/p0-8-2-retention-packet.md 4ab5cd9f
4. Implement src/db/migrations/005_retention.sql, src/domains/proof/retention.ts, src/__tests__/domains/proof/retention.test.ts 617a67cd
5. Verify docs/verification/p0-8-2-retention-verification.md (this commit)

7. Schema changes summary

New migration: src/db/migrations/005_retention.sql.

  • Rebuilds thought_records table via canonical SQLite “create new, copy rows, drop old, rename” pattern to drop the NOT NULL on content.
  • Adds 3 nullable columns: zone, content_compressed, content_hash.
  • Re-creates the two indexes from 003: idx_trail_task, idx_trail_prev.
  • No new index on zone in this migration (deferred; audit §4 notes a future task may add idx_trail_zone for bulk passes).

No source file modifications beyond the three new files. thought_records.sql (003) is not edited — the 005 migration handles the column change in an additive way. src/db/index.ts migration runner already auto-discovers 005 from the filename prefix.

8. Interpretation choices (for PR reviewer)

  • Position vs TTL: used position (1..100 Hot / 101..1000 Warm / 1001+ Cold) per task spec, overriding the HERITAGE TTL-based interpretation in the eta-proof-store-extraction.md. Documented in audit §1.3.
  • Per-task positioning: a record’s position is evaluated within its own task_id chain (ordered by rowid DESC). This matches P0.7.2’s per-task chain-linkage semantics and means a 50-record chain is entirely Hot even if a sibling chain has 5000 records.
  • NULL zone = Hot: records written by P0.7.2 createThoughtRecord have no zone column written at insert time (P0.7.2 didn’t know about retention). retrieveRecord treats zone IS NULL as 'hot' so existing records behave correctly without a backfill.
  • content_hash column distinct from hash: hash commits to the 6-field subset (already includes content); content_hash is a convenience column for the Cold zone’s “hash-only” read path, since hash alone doesn’t expose a content-level digest isolated from the other record fields.
  • No MCP tool: task spec does not register one. retention.ts is a library-level surface; a later P0.8.x task may wrap it in a tool.
  • content now nullable: required by AC3. SQLite can’t mutate NOT NULL in place; we rebuilt the table in 005.

9. Deviations from packet

None. Implementation follows the packet §3 skeleton, the Zod + error schemas in contract §2, and the migration in packet §2 (with an expansion from 3 ALTER statements to a table-rebuild + 3 columns + 2 indexes). The rebuild was required because SQLite can’t drop a NOT NULL constraint via ALTER; this was flagged in audit §5 and spelled out in the final 005 migration.

10. Blockers / residual risks

  • Startup subprocess smoke flake — pre-existing, documented, not caused by this task.
  • No index on zone — fine for single-record archival; a bulk retention pass would benefit. Deferred to future task.
  • No scheduled retention passarchiveRecord is called explicitly by callers. A cron-style iterator is out of P0.8.2 scope.
  • Retention is position-based only — time-based (TTL) retention is the donor approach and can be layered on later as a separate dimension if needed.
  • No unarchive path — forward-only by design.

Ready for PR.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.