tenseleyflow/documentlanguagemodel / 15b524b

Browse files

Add determinism golden index

Authored by espadonne
SHA
15b524b009882f1a9987832462467a38feb6b34f
Parents
54e7e6a
Tree
b77dab1

8 changed files

StatusFile+-
A .determinism/lock.json 5 0
M docs/determinism.md 8 3
M scripts/regen-determinism-golden.py 17 6
M src/dlm/base_models/license.py 1 1
M src/dlm/lock/__init__.py 34 6
M src/dlm/lock/errors.py 18 0
A src/dlm/lock/golden_index.py 136 0
A tests/unit/lock/test_golden_index.py 128 0
.determinism/lock.jsonadded
@@ -0,0 +1,5 @@
1
+{
2
+  "goldens": [],
3
+  "lock_version": 1,
4
+  "updated_at": "2026-04-22T04:25:32"
5
+}
docs/determinism.mdmodified
@@ -19,7 +19,8 @@ training produces a byte-identical `adapter_model.safetensors`.
1919
 
2020
 Proved by `tests/integration/lock/test_determinism_golden.py`, which
2121
 runs two fresh training cycles on the tiny model and asserts the
22
-adapter SHAs match.
22
+adapter SHAs match. Approved tuple goldens are tracked at the repo
23
+level in `.determinism/lock.json`.
2324
 
2425
 ## What's in `dlm.lock`
2526
 
@@ -120,10 +121,14 @@ The script:
120121
 2. Runs the tiny-model training twice; confirms the two SHAs match.
121122
 3. Writes `tests/golden/determinism/tuple-<hash>.json` keyed by a
122123
    SHA-256 of the sorted version tuple + platform.
124
+4. Upserts `.determinism/lock.json` with the tuple path, adapter SHA,
125
+   platform, and pinned versions.
123126
 
124127
 Each tuple gets its own golden; the tuple file is keyed by content so
125
-running on a new platform simply writes a new golden file. The
126
-reviewer checks in the new golden alongside the dep bump.
128
+running on a new platform simply writes a new golden file. The repo-level
129
+index keeps the checked-in set explicit and avoids overloading the
130
+per-store `dlm.lock` name with a second meaning. The reviewer checks in
131
+the tuple file and the index update alongside the dep bump.
127132
 
128133
 ## Non-goals
129134
 
scripts/regen-determinism-golden.pymodified
@@ -17,8 +17,9 @@ Flow:
1717
    - `regenerated_at` — UTC timestamp
1818
    - `dlm_sha256` — hash of the synthetic training doc (reproducible
1919
      across runs when the factory's ULID seed is pinned)
20
-5. Compare against the prior golden (if one existed) and print a diff.
21
-6. Exit non-zero unless `--approve` is passed. The default is
20
+5. Upsert `.determinism/lock.json` with the checked-in tuple metadata.
21
+6. Compare against the prior golden (if one existed) and print a diff.
22
+7. Exit non-zero unless `--approve` is passed. The default is
2223
    dry-run-and-report so a stray script invocation doesn't silently
2324
    overwrite a baseline.
2425
 
@@ -26,10 +27,9 @@ Usage:
2627
     uv run python scripts/regen-determinism-golden.py           # dry run
2728
     uv run python scripts/regen-determinism-golden.py --approve # write
2829
 
29
-The matching root-level `dlm.lock` (distinct from the per-store
30
-`dlm.lock`) records which tuples have a checked-in golden. CI computes
31
-the current golden and fails iff that lock asserts a tuple has a
32
-golden but the on-disk file differs (catches silent drift on dep bump).
30
+The matching repo-level `.determinism/lock.json` records which tuples
31
+have a checked-in golden. It is distinct from the per-store
32
+`dlm.lock`, which captures one training run's determinism contract.
3333
 """
3434
 
3535
 from __future__ import annotations
@@ -140,9 +140,12 @@ def main() -> int:
140140
 
141141
     import tempfile
142142
 
143
+    from dlm.lock.golden_index import GOLDEN_INDEX_RELATIVE_PATH, upsert_golden_index
144
+
143145
     versions = _current_versions()
144146
     filename = _tuple_filename(versions)
145147
     target = _GOLDEN_DIR / filename
148
+    golden_relpath = target.relative_to(_REPO_ROOT).as_posix()
146149
     prior = None
147150
     if target.is_file():
148151
         try:
@@ -192,7 +195,15 @@ def main() -> int:
192195
 
193196
     _GOLDEN_DIR.mkdir(parents=True, exist_ok=True)
194197
     target.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
198
+    upsert_golden_index(
199
+        _REPO_ROOT,
200
+        golden_relpath=golden_relpath,
201
+        adapter_sha256=sha_a,
202
+        platform=payload["platform"],
203
+        pinned_versions=versions,
204
+    )
195205
     print(f"[wrote] {target.relative_to(_REPO_ROOT)}")
206
+    print(f"[wrote] {GOLDEN_INDEX_RELATIVE_PATH}")
196207
     return 0
197208
 
198209
 
src/dlm/base_models/license.pymodified
@@ -11,7 +11,7 @@ an `accept_license` flag against the spec.
1111
 - `manifest.json.license_acceptance`: the per-store durable record;
1212
   read on every subsequent `dlm train` to verify the acceptance
1313
   fingerprint is still present.
14
-- Repo-level `dlm.lock.license_acceptance`: the determinism-contract
14
+- Per-store `dlm.lock.license_acceptance`: the determinism-contract
1515
   mirror; divergence between the two triggers a lock re-check.
1616
 
1717
 The interactive prompt in `dlm init` lives in the CLI layer; this
src/dlm/lock/__init__.pymodified
@@ -1,10 +1,11 @@
11
 """Per-store `dlm.lock` — determinism contract for one `.dlm`.
22
 
3
-Separate from the repo-level `uv.lock` (tool-dep pins) and from the
4
-`manifest.json` (training run narrative). The store-level `dlm.lock`
5
-pins the tuple `(torch, transformers, peft, trl, bitsandbytes,
6
-accelerate, llama.cpp tag, cuda/rocm, hardware_tier, seed,
7
-determinism_flags, determinism_class)` and carries:
3
+Separate from the repo-level `uv.lock` (tool-dep pins), from the
4
+repo-level determinism-golden index at `.determinism/lock.json`, and
5
+from `manifest.json` (training run narrative). The store-level
6
+`dlm.lock` pins the tuple `(torch, transformers, peft, trl,
7
+bitsandbytes, accelerate, llama.cpp tag, cuda/rocm, hardware_tier,
8
+seed, determinism_flags, determinism_class)` and carries:
89
 
910
 - the hash of the `.dlm` source at the time the lock was written
1011
 - the base-model revision + content hash
@@ -20,7 +21,24 @@ table in `policy.py`.
2021
 from __future__ import annotations
2122
 
2223
 from dlm.lock.builder import build_lock, hardware_tier_from_backend, hash_dlm_file
23
-from dlm.lock.errors import LockError, LockSchemaError, LockValidationError, LockWriteError
24
+from dlm.lock.errors import (
25
+    GoldenIndexSchemaError,
26
+    GoldenIndexWriteError,
27
+    LockError,
28
+    LockSchemaError,
29
+    LockValidationError,
30
+    LockWriteError,
31
+)
32
+from dlm.lock.golden_index import (
33
+    CURRENT_GOLDEN_INDEX_VERSION,
34
+    GOLDEN_INDEX_RELATIVE_PATH,
35
+    DeterminismGoldenEntry,
36
+    DeterminismGoldenIndex,
37
+    golden_index_path,
38
+    load_golden_index,
39
+    upsert_golden_index,
40
+    write_golden_index,
41
+)
2442
 from dlm.lock.policy import Severity, classify_mismatches
2543
 from dlm.lock.schema import CURRENT_LOCK_VERSION, LOCK_FILENAME, DlmLock
2644
 from dlm.lock.validator import LockDecision, LockMode, validate_lock
@@ -28,6 +46,12 @@ from dlm.lock.writer import load_lock, lock_path, write_lock
2846
 
2947
 __all__ = [
3048
     "CURRENT_LOCK_VERSION",
49
+    "CURRENT_GOLDEN_INDEX_VERSION",
50
+    "GOLDEN_INDEX_RELATIVE_PATH",
51
+    "DeterminismGoldenEntry",
52
+    "DeterminismGoldenIndex",
53
+    "GoldenIndexSchemaError",
54
+    "GoldenIndexWriteError",
3155
     "LOCK_FILENAME",
3256
     "DlmLock",
3357
     "LockDecision",
@@ -40,9 +64,13 @@ __all__ = [
4064
     "build_lock",
4165
     "classify_mismatches",
4266
     "hardware_tier_from_backend",
67
+    "golden_index_path",
4368
     "hash_dlm_file",
4469
     "load_lock",
70
+    "load_golden_index",
4571
     "lock_path",
72
+    "upsert_golden_index",
4673
     "validate_lock",
74
+    "write_golden_index",
4775
     "write_lock",
4876
 ]
src/dlm/lock/errors.pymodified
@@ -51,3 +51,21 @@ class LockValidationError(LockError):
5151
         self.reasons = list(reasons)
5252
         joined = "; ".join(reasons)
5353
         super().__init__(f"{path}: lock validation failed ({joined})")
54
+
55
+
56
+class GoldenIndexSchemaError(LockError):
57
+    """Repo-level determinism-golden index is unreadable or schema-invalid."""
58
+
59
+    def __init__(self, path: Path, reason: str) -> None:
60
+        self.path = path
61
+        self.reason = reason
62
+        super().__init__(f"{path}: {reason}")
63
+
64
+
65
+class GoldenIndexWriteError(LockError):
66
+    """Programmer error on the repo-level determinism-golden index write path."""
67
+
68
+    def __init__(self, *, path: Path, reason: str) -> None:
69
+        self.path = path
70
+        self.reason = reason
71
+        super().__init__(f"{path}: write refused: {reason}")
src/dlm/lock/golden_index.pyadded
@@ -0,0 +1,136 @@
1
+"""Repo-level index of checked-in determinism goldens.
2
+
3
+Separate from the per-store `dlm.lock`: this file tracks which
4
+runtime tuples have an approved golden under `tests/golden/determinism/`.
5
+The canonical path is `.determinism/lock.json`.
6
+"""
7
+
8
+from __future__ import annotations
9
+
10
+import json
11
+from collections.abc import Mapping
12
+from datetime import UTC, datetime
13
+from pathlib import Path
14
+from typing import Final
15
+
16
+from pydantic import BaseModel, ConfigDict, Field
17
+
18
+from dlm.io.atomic import write_text
19
+from dlm.lock.errors import GoldenIndexSchemaError, GoldenIndexWriteError
20
+
21
+GOLDEN_INDEX_RELATIVE_PATH: Final[str] = ".determinism/lock.json"
22
+CURRENT_GOLDEN_INDEX_VERSION: Final[int] = 1
23
+
24
+
25
+class DeterminismGoldenEntry(BaseModel):
26
+    """One approved tuple golden tracked at repo scope."""
27
+
28
+    model_config = ConfigDict(extra="forbid", frozen=True)
29
+
30
+    golden_relpath: str = Field(
31
+        ...,
32
+        pattern=r"^tests/golden/determinism/tuple-[0-9a-f]{16}\.json$",
33
+    )
34
+    adapter_sha256: str = Field(..., pattern=r"^[0-9a-f]{64}$")
35
+    platform: str = Field(..., min_length=1)
36
+    pinned_versions: dict[str, str] = Field(default_factory=dict)
37
+
38
+
39
+class DeterminismGoldenIndex(BaseModel):
40
+    """Checked-in set of approved determinism goldens."""
41
+
42
+    model_config = ConfigDict(extra="forbid", frozen=True)
43
+
44
+    lock_version: int = Field(CURRENT_GOLDEN_INDEX_VERSION, ge=1)
45
+    updated_at: datetime
46
+    goldens: tuple[DeterminismGoldenEntry, ...] = ()
47
+
48
+
49
+def golden_index_path(repo_root: Path) -> Path:
50
+    """Return `<repo_root>/.determinism/lock.json`."""
51
+
52
+    return repo_root / GOLDEN_INDEX_RELATIVE_PATH
53
+
54
+
55
+def write_golden_index(repo_root: Path, index: DeterminismGoldenIndex) -> Path:
56
+    """Atomically persist the repo-level determinism-golden index."""
57
+
58
+    target = golden_index_path(repo_root)
59
+    if index.lock_version != CURRENT_GOLDEN_INDEX_VERSION:
60
+        raise GoldenIndexWriteError(
61
+            path=target,
62
+            reason=(
63
+                f"lock_version={index.lock_version!r} != writer's "
64
+                f"CURRENT_GOLDEN_INDEX_VERSION={CURRENT_GOLDEN_INDEX_VERSION}"
65
+            ),
66
+        )
67
+    target.parent.mkdir(parents=True, exist_ok=True)
68
+    payload = index.model_dump(mode="json")
69
+    text = json.dumps(payload, indent=2, sort_keys=True) + "\n"
70
+    write_text(target, text)
71
+    return target
72
+
73
+
74
+def load_golden_index(repo_root: Path) -> DeterminismGoldenIndex | None:
75
+    """Read `.determinism/lock.json`, returning `None` when absent."""
76
+
77
+    path = golden_index_path(repo_root)
78
+    if not path.is_file():
79
+        return None
80
+
81
+    try:
82
+        raw = path.read_text(encoding="utf-8")
83
+    except OSError as exc:
84
+        raise GoldenIndexSchemaError(path, f"unreadable: {exc}") from exc
85
+
86
+    try:
87
+        payload = json.loads(raw)
88
+    except json.JSONDecodeError as exc:
89
+        raise GoldenIndexSchemaError(path, f"invalid JSON: {exc}") from exc
90
+
91
+    if not isinstance(payload, dict):
92
+        raise GoldenIndexSchemaError(
93
+            path,
94
+            f"top-level JSON must be an object, got {type(payload).__name__}",
95
+        )
96
+
97
+    version = payload.get("lock_version")
98
+    if version != CURRENT_GOLDEN_INDEX_VERSION:
99
+        raise GoldenIndexSchemaError(
100
+            path,
101
+            f"unsupported lock_version {version!r} (reader expects {CURRENT_GOLDEN_INDEX_VERSION})",
102
+        )
103
+
104
+    try:
105
+        return DeterminismGoldenIndex.model_validate(payload)
106
+    except Exception as exc:
107
+        raise GoldenIndexSchemaError(path, f"schema validation: {exc}") from exc
108
+
109
+
110
+def upsert_golden_index(
111
+    repo_root: Path,
112
+    *,
113
+    golden_relpath: str,
114
+    adapter_sha256: str,
115
+    platform: str,
116
+    pinned_versions: Mapping[str, str],
117
+) -> Path:
118
+    """Insert or replace one tuple golden in `.determinism/lock.json`."""
119
+
120
+    current = load_golden_index(repo_root)
121
+    entries = {} if current is None else {entry.golden_relpath: entry for entry in current.goldens}
122
+    entries[golden_relpath] = DeterminismGoldenEntry(
123
+        golden_relpath=golden_relpath,
124
+        adapter_sha256=adapter_sha256,
125
+        platform=platform,
126
+        pinned_versions=dict(sorted(pinned_versions.items())),
127
+    )
128
+    updated = DeterminismGoldenIndex(
129
+        updated_at=_utcnow(),
130
+        goldens=tuple(sorted(entries.values(), key=lambda entry: entry.golden_relpath)),
131
+    )
132
+    return write_golden_index(repo_root, updated)
133
+
134
+
135
+def _utcnow() -> datetime:
136
+    return datetime.now(UTC).replace(tzinfo=None, microsecond=0)
tests/unit/lock/test_golden_index.pyadded
@@ -0,0 +1,128 @@
1
+"""Repo-level determinism-golden index I/O."""
2
+
3
+from __future__ import annotations
4
+
5
+from datetime import UTC, datetime
6
+from pathlib import Path
7
+
8
+import pytest
9
+
10
+from dlm.lock.errors import GoldenIndexSchemaError
11
+from dlm.lock.golden_index import (
12
+    GOLDEN_INDEX_RELATIVE_PATH,
13
+    DeterminismGoldenEntry,
14
+    DeterminismGoldenIndex,
15
+    golden_index_path,
16
+    load_golden_index,
17
+    upsert_golden_index,
18
+    write_golden_index,
19
+)
20
+
21
+
22
+def _index(*entries: DeterminismGoldenEntry) -> DeterminismGoldenIndex:
23
+    return DeterminismGoldenIndex(
24
+        updated_at=datetime(2026, 4, 22, 4, 25, 32, tzinfo=UTC),
25
+        goldens=entries,
26
+    )
27
+
28
+
29
+def _entry(
30
+    *,
31
+    golden_relpath: str = "tests/golden/determinism/tuple-0123456789abcdef.json",
32
+    adapter_sha256: str = "a" * 64,
33
+    platform: str = "darwin-arm64",
34
+) -> DeterminismGoldenEntry:
35
+    return DeterminismGoldenEntry(
36
+        golden_relpath=golden_relpath,
37
+        adapter_sha256=adapter_sha256,
38
+        platform=platform,
39
+        pinned_versions={"peft": "0.14.0", "torch": "2.5.1"},
40
+    )
41
+
42
+
43
+class TestGoldenIndexPath:
44
+    def test_returns_repo_relative_path(self, tmp_path: Path) -> None:
45
+        assert golden_index_path(tmp_path) == tmp_path / GOLDEN_INDEX_RELATIVE_PATH
46
+
47
+
48
+class TestWriteGoldenIndex:
49
+    def test_writes_readable_json(self, tmp_path: Path) -> None:
50
+        written = write_golden_index(tmp_path, _index(_entry()))
51
+        assert written.is_file()
52
+        text = written.read_text(encoding="utf-8")
53
+        assert text.endswith("\n")
54
+        assert text.index('"golden_relpath"') < text.index('"platform"')
55
+
56
+    def test_round_trip_equal(self, tmp_path: Path) -> None:
57
+        original = _index(_entry())
58
+        write_golden_index(tmp_path, original)
59
+        loaded = load_golden_index(tmp_path)
60
+        assert loaded == original
61
+
62
+
63
+class TestLoadGoldenIndex:
64
+    def test_missing_file_returns_none(self, tmp_path: Path) -> None:
65
+        assert load_golden_index(tmp_path) is None
66
+
67
+    def test_invalid_json_raises(self, tmp_path: Path) -> None:
68
+        golden_index_path(tmp_path).parent.mkdir(parents=True)
69
+        golden_index_path(tmp_path).write_text("{not valid", encoding="utf-8")
70
+        with pytest.raises(GoldenIndexSchemaError, match="invalid JSON"):
71
+            load_golden_index(tmp_path)
72
+
73
+    def test_non_object_top_level_raises(self, tmp_path: Path) -> None:
74
+        golden_index_path(tmp_path).parent.mkdir(parents=True)
75
+        golden_index_path(tmp_path).write_text("[]", encoding="utf-8")
76
+        with pytest.raises(GoldenIndexSchemaError, match="must be an object"):
77
+            load_golden_index(tmp_path)
78
+
79
+    def test_newer_version_is_rejected(self, tmp_path: Path) -> None:
80
+        golden_index_path(tmp_path).parent.mkdir(parents=True)
81
+        golden_index_path(tmp_path).write_text('{"lock_version": 99}', encoding="utf-8")
82
+        with pytest.raises(GoldenIndexSchemaError, match="unsupported lock_version"):
83
+            load_golden_index(tmp_path)
84
+
85
+
86
+class TestUpsertGoldenIndex:
87
+    def test_creates_index_when_absent(self, tmp_path: Path) -> None:
88
+        upsert_golden_index(
89
+            tmp_path,
90
+            golden_relpath="tests/golden/determinism/tuple-0123456789abcdef.json",
91
+            adapter_sha256="a" * 64,
92
+            platform="darwin-arm64",
93
+            pinned_versions={"torch": "2.5.1", "peft": "0.14.0"},
94
+        )
95
+        loaded = load_golden_index(tmp_path)
96
+        assert loaded is not None
97
+        assert [entry.golden_relpath for entry in loaded.goldens] == [
98
+            "tests/golden/determinism/tuple-0123456789abcdef.json"
99
+        ]
100
+
101
+    def test_overwrites_existing_entry_and_sorts(self, tmp_path: Path) -> None:
102
+        write_golden_index(
103
+            tmp_path,
104
+            _index(
105
+                _entry(golden_relpath="tests/golden/determinism/tuple-ffffffffffffffff.json"),
106
+                _entry(
107
+                    golden_relpath="tests/golden/determinism/tuple-aaaaaaaaaaaaaaaa.json",
108
+                    adapter_sha256="b" * 64,
109
+                ),
110
+            ),
111
+        )
112
+
113
+        upsert_golden_index(
114
+            tmp_path,
115
+            golden_relpath="tests/golden/determinism/tuple-ffffffffffffffff.json",
116
+            adapter_sha256="c" * 64,
117
+            platform="linux-x86_64",
118
+            pinned_versions={"torch": "2.6.0"},
119
+        )
120
+
121
+        loaded = load_golden_index(tmp_path)
122
+        assert loaded is not None
123
+        assert [entry.golden_relpath for entry in loaded.goldens] == [
124
+            "tests/golden/determinism/tuple-aaaaaaaaaaaaaaaa.json",
125
+            "tests/golden/determinism/tuple-ffffffffffffffff.json",
126
+        ]
127
+        assert loaded.goldens[1].adapter_sha256 == "c" * 64
128
+        assert loaded.goldens[1].platform == "linux-x86_64"