Selvedge v0.3.11 — Active memory v2 + a tamper-evident log
Abandoned alternatives are first-class, and the log can prove itself. The active-memory release: the expires_when column that shipped dormant in v0.3.8 gets its evaluator, reject joins revert so paths never taken are recorded the same way paths rolled back are, and prior_attempts reads both as stated outcomes instead of inferring them.
Riding along, pulled forward from the verifiable-claims phase: a tamper-evident hash chain over the event log. A store whose differentiator is retaining rejected decisions should be able to show the retention is intact — so selvedge verify now recomputes a SHA-256 chain over every logged event and names the exact sequence number of a row that was edited, deleted, or reordered out-of-band. Honest scope, stated in the module itself: this detects casual and accidental modification and produces an independently verifiable export; it is not proof against a motivated local attacker, who controls the file and can recompute every digest.
Added
- Tamper-evident hash chain (
selvedge-chain/1, design (d) of the design doc). Every logged event gets a SHA-256 chain record in a sidecarevent_chaintable, written in the same transaction, over a canonicalized 17-field core — every events column exceptgit_commit, which is late-bound by the post-commit hook and independently checkable against git. Legitimate mutators append boundary records instead of breaking the chain:migrate-paths --applyappends anamendrecord, and the destructive-gated prune appends atombstonecounting the chained rows it deleted — so a gated prune verifies clean while a silentsqlite3deletion of a chained row fails verification. Zero new dependencies (stdlibhashlib/json), no migration of theeventstable, genesis is automatic on first write after upgrade. - Two new
selvedge verifychecks.chain_intact(must-fail) recomputes the chain end to end — hashes,prev_hashthreading,seqcontiguity, tombstone accounting; nothing legitimate can trip it.chain_coverage(warn-only) counts events rows with no chain record — expected on every upgraded install, so it warns without breaking CI. Pre-genesis rows are unchained, not invalid. - Chain attestation manifest in
selvedge verify --json(chain_manifest): storage mechanism, chain algorithm, canonical-form version, verification-procedure reference — the four fields MCP SEP-3004 §2.7 asks a self-attesting store to publish, producible here precisely because the verifier ships in the package. (Selvedge assessed itself against that draft contract and declares its divergences in the design doc; it does not claim conformance.) expires_whenevaluation inselvedge stale. A decision can carry a machine-checkable expiry condition in a closed four-shape grammar —library:NAME>=VERSION,entity:PATH:changes,date:ISO,manual:LABEL— validated at write time on every path; values outside the grammar are rejected, not stored. Evaluation is local-only, no network, no LLM: installed distribution metadata, the event log itself, and the clock. Alibrary:condition whose package isn't locally observable presents asmanual_reviewrather than a guess.change_type="reject". Records "we considered this and decided against it" without writing the change — the counterpart torevertfor paths never taken. No new MCP tool. Adoption defended on three surfaces: a worked example in thelog_changedocstring, a prompt-block sentence telling agents to log rejections, and a reject-specific validator rule nudging the reasoning to name what was chosen instead.stale_whencapture nudge. Arejectorrevertlogged with neitherstale_whennorexpires_whengets a suggestion to record the invalidating condition — the reason a decision was made is also the condition under which it should die. Warns, never rejects. Digest rows whosestale_whenhas since matched present as re-examine rather than a bare reverted — presentation only; the stored verdict never mutates.selvedge supersedegains-d/--diff,--revisit-after, and--expires-when(#31), with the same semantics asselvedge log, routed through the same size-bound and secret-shape checks as every other write path.
Changed
prior_attemptsconfidence tiers. An attempt closed by an explicitrejectorrevertnow reportsconfidence: "exact"— the outcome is stated in the log, not inferred; the v0.3.7 proximity heuristic drops to tiebreaker. No API shape change, but results move: callers filtering onproximity_highshould also acceptexact.- PreCompact reminder contracts, pinned. Repeat-fire stability is now asserted byte-for-byte (and the reminder goes quiet once the
log_changelands), and the reminder distinguishes "edited with no log" from "log exists but was truncated" — the two cases have different fixes. - SessionStart digest selection order is a documented contract, pinned by tests including a seeded fixture for the digest-boundary race (a supersede landing between seed and render yields a digest naming exactly one effective verdict). Deliberately dumb ranking — overdue-ness and recency, nothing semantic.
selvedge promptblock now tells agents to log rejections and reverts with their invalidating conditions; the bundled skill is regenerated from it.
Fixed
- An id-less supersede no longer re-opens every earlier revert on the path (#30). The timestamp fallback in
prior_attemptsmatched a supersede-without-link against every earlier closing removal; it now mirrorslog_supersede's own auto-link rule and re-opens at most the single removal it would have auto-linked to at write time.
Notes
- Test suite 984 → 1114. The phase's soft budget is ≤40 new tests and this release exceeds it deliberately, which the budget discipline requires calling out: the overrun is the tamper-evidence chain pulled forward from the verifiable-claims phase, on top of the
expires_whengrammar suite and the hook-contract fixtures the v0.3.10 release-thread feedback committed this release to. Coverage 89.4%, flat. The chain checks are mutation-verified — deleting theprev_hash/seqthreading checks or the pagination makes the suite red. - Schema tax 4445/5450 tokens (was 3705/3800):
log_changegained theexpires_whenparameter andget_stale_decisionsdocuments the three surfacing flags. The core tool surface is 2.2% of a 200k-token window. - The chain makes no claim about rows that predate it — including whether they still exist. A
chain sealcommand for opt-in retroactive coverage is future work, per the design doc. Selvedge's own dogfood store crossed its genesis with this release: the decisions behind v0.3.11 are the first chained rows, including a realchange_type="reject".
Install: pip install selvedge==0.3.11
PyPI: https://pypi.org/project/selvedge/
Docs: https://github.com/masondelan/selvedge/blob/main/docs/getting-started.md