Skip to content
C.W.K.
Lesson 02 of 05 · published

A Hash That Freezes a File

~15 min · contract-versioning, tests, sha-256, xfail

Level 0Loose Note
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Frozen Means a Test Fails

"Do not edit the base class casually" is a sentence every codebase has and few keep. Soul Vault keeps it with arithmetic. The discipline test tests/test_root_discipline.py holds one constant, ROOT_SHA256, the SHA-256 of soulvault/root.py as last reviewed. The first test, T-ROOT-FROZEN, hashes the file on disk and compares. Change one character of the root (a comment, a blank line, a rule) and the suite goes red with a message that says what to do instead:

soulvault/root.py changed. The root is never edited to fit a child: a root change is a versioned contract change (bump CONTRACT_VERSION, implement it in every child, re-pin ROOT_SHA256 = ...)

The test also asserts root.CONTRACT_VERSION == 7. So a deliberate change has three visible parts in the same commit: a new contract version, a new pinned hash, and the children updated to meet it. An accidental change has none of them and cannot pass.

Seven Versions in Two Days

Freezing a file does not mean it never changes. It means every change is a named event with a reason. The root's header keeps the list:

  • v1 the first contract.
  • v2 (the first independent review): a guest's own instructions are required like a soul's, and a new soul's identity file must name its slug, so every backend can find the vault again by the same rule.
  • v3 (census to zero): every implementation declares where it keeps soul-vault bytes on this Mac (local_footprint), so an app can fence its own file tools without knowing a vault path.
  • v4: an identity document's text and its revision come from one read (a write between a listing and a read had produced a revision that did not name the text), and a note that is not UTF-8 is integrity_error by name in every reader.
  • v5 (what the second implementation needed from every vault): a write says why (reason); a store can be sealed for a pointer transition; a create may carry the token under which its caller saw the note absent; a request that already completed answers with its first result.
  • v6 (forward-only retirement): the retired implementation's examples removed from the root's comments; no rule changed.
  • v7: no write deletes the note that identifies a soul or a guest.

Notice what is not on the list: nothing a single child wanted for itself. Each version is something every soul vault must do. A need that only one child has stays in that child.

Tests First, as Expected Failures

Version 7 shows the cleanest way to change a frozen contract. The rule (never delete the identity note) was approved on the evening of 2026-11-10. Before touching the root, eight tests describing the new behaviour were committed as strict expected failures (pytest's xfail(strict=True)), and the suite was run on the v6 root: eight tests, eight expected failures. Then the rule went into the root, the hash was re-pinned, the version bumped, and the markers removed in the same change; the eight tests now pass. A strict expected failure is useful because it also fails if the test unexpectedly passes, so it proves the tests really tested the missing rule rather than something already true.

A small human footnote: the todo that asked for this rule called it "contract v6". By the time it was built, v6 had been taken an hour earlier by the retirement. The number is assigned when the change lands, not when it is requested, which is one more reason the version lives in the file and nowhere else.

Who Reviews a Root Change

The release gate in the architecture document asks for more than green tests: after a build, an independent, maximum-effort review by a brain other than the one that built it. A frozen file concentrates risk; every implementation inherits a mistake in it. The hash makes changes visible. The second brain makes them examined.

Code

T-ROOT-FROZEN (trimmed from tests/test_root_discipline.py)·python
import hashlib
from pathlib import Path
from soulvault import root

PACKAGE = Path(root.__file__).resolve().parent

# The reviewed root. Re-pin only together with a CONTRACT_VERSION bump.
ROOT_SHA256 = "de48234325a2a60304aa0f878184a26bf05c3993b5aab5334a7934ede7e14d2d"

def test_root_frozen():
    digest = hashlib.sha256((PACKAGE / "root.py").read_bytes()).hexdigest()
    assert digest == ROOT_SHA256, (
        "soulvault/root.py changed. The root is never edited to fit a child: "
        "a root change is a versioned contract change (bump CONTRACT_VERSION, "
        f"implement it in every child, re-pin ROOT_SHA256 = {digest!r})")
    assert root.CONTRACT_VERSION == 7
Describing a contract change before making it (pattern)·python
import pytest

# Committed BEFORE the root changes. strict=True: an unexpected pass is a failure,
# so these must really exercise the rule that is still missing.
@pytest.mark.xfail(strict=True, reason="contract v7: identity note is never deleted")
def test_delete_note_refuses_identity_note(vault, fixture_soul, actor):
    ident = fixture_soul.identity
    rev = vault.read_note(ident).revision
    with pytest.raises(VaultError) as err:
        vault.delete_note(ident, expected_revision=rev, actor=actor, request_id="t-1")
    assert err.value.code == "policy_denied"

# Then: change root.py, bump CONTRACT_VERSION, re-pin ROOT_SHA256,
# and delete the xfail markers in the same commit.

External links

Exercise

Pick one file in a project whose rules every other module depends on. Add a test that pins its SHA-256 next to a version constant, with a failure message that explains the change procedure. Then make one real change the right way: write the tests for the new behaviour as strict expected failures first, run them, change the file, bump the version, re-pin, and remove the markers in one commit.
Hint
Write the failure message for the person who will hit it six months from now. 'Hash mismatch' teaches nothing; 'this is a contract change: bump X, update every implementation, re-pin' tells them exactly what the project expects.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.