tenseleyflow/documentlanguagemodel / 57eef3f

Browse files

Hoist pending-plan I/O into dlm._pending shared base

Audit 12 F12.4: synth/pending.py and preference/pending.py were 90%+ identical — same JSON schema, same load/save/clear shape, same Section payload encoding (load tolerates the small per-domain field-set asymmetry). Move I/O to dlm._pending; each domain module shrinks to a thin typed shim that supplies (subdir, plan class, error class). Public API preserved (test imports still resolve including the private helpers); 470 tests pass. Net wins: one place to fix bugs in the I/O path; adding a third pending domain (e.g. harvest) is now a 30-line shim.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
57eef3f8cd41c7d240c168d9ad7f5a46383a55df
Parents
46a1078
Tree
a21bb9f

3 changed files

StatusFile+-
C src/dlm/_pending.py 0 0
M src/dlm/preference/pending.py 62 154
M src/dlm/synth/pending.py 62 161
src/dlm/synth/pending.py → src/dlm/_pending.pycopied (68% similarity)
@@ -1,4 +1,21 @@
1
-"""Persist staged auto-synth instruction sections between CLI steps."""
1
+"""Shared substrate for staged "pending plan" payloads under a store.
2
+
3
+Both `dlm preference mine`/`apply` and `dlm synth instructions`/`apply`
4
+need to stage a list of `Section` payloads on disk between two CLI
5
+invocations, then read them back in the apply step. Each domain stores
6
+the payload under a different store subdirectory and wraps validation
7
+errors in its own typed exception, but the I/O shape is identical.
8
+
9
+This module owns the I/O. The two domain modules
10
+(`dlm.preference.pending`, `dlm.synth.pending`) supply their own
11
+`PendingPlan` dataclass and error class via the small set of
12
+parameterized functions below.
13
+
14
+The on-disk format records the full optional-field surface of
15
+``Section`` so a domain that grows new optional fields tomorrow does
16
+not need to bump ``schema_version``: load-time defaults absorb the
17
+addition.
18
+"""
219
 
320
 from __future__ import annotations
421
 
@@ -10,7 +27,6 @@ from typing import TYPE_CHECKING, Any
1027
 
1128
 from dlm.doc.sections import Section, SectionType
1229
 from dlm.io.atomic import write_text as atomic_write_text
13
-from dlm.synth.errors import SynthError
1430
 
1531
 if TYPE_CHECKING:
1632
     from collections.abc import Sequence
@@ -18,22 +34,21 @@ if TYPE_CHECKING:
1834
     from dlm.store.paths import StorePath
1935
 
2036
 
21
-class PendingSynthPlanError(SynthError):
22
-    """Raised when the staged synth plan cannot be read or validated."""
37
+_SCHEMA_VERSION = 1
2338
 
2439
 
2540
 @dataclass(frozen=True)
26
-class PendingSynthPlan:
27
-    """One staged synth plan for a store."""
41
+class PendingSectionPlan:
42
+    """Generic staged plan payload — domain modules subclass this for typing."""
2843
 
2944
     source_path: Path
3045
     created_at: str
3146
     sections: tuple[Section, ...]
3247
 
3348
 
34
-def pending_plan_path(store: StorePath) -> Path:
35
-    """Path to the staged synth payload for `store`."""
36
-    return store.root / "synth" / "pending.json"
49
+def pending_plan_path(store: StorePath, *, subdir: str) -> Path:
50
+    """Path to the staged payload for `store` under the given subdir."""
51
+    return store.root / subdir / "pending.json"
3752
 
3853
 
3954
 def save_pending_plan(
@@ -41,17 +56,18 @@ def save_pending_plan(
4156
     *,
4257
     source_path: Path,
4358
     sections: Sequence[Section],
44
-) -> PendingSynthPlan:
45
-    """Persist `sections` as the staged synth plan for `store`."""
46
-    plan = PendingSynthPlan(
59
+    subdir: str,
60
+    plan_cls: type[PendingSectionPlan],
61
+) -> PendingSectionPlan:
62
+    plan = plan_cls(
4763
         source_path=source_path.resolve(),
4864
         created_at=_utcnow(),
4965
         sections=tuple(sections),
5066
     )
51
-    path = pending_plan_path(store)
67
+    path = pending_plan_path(store, subdir=subdir)
5268
     path.parent.mkdir(parents=True, exist_ok=True)
5369
     payload = {
54
-        "schema_version": 1,
70
+        "schema_version": _SCHEMA_VERSION,
5571
         "source_path": str(plan.source_path),
5672
         "created_at": plan.created_at,
5773
         "sections": [_section_to_payload(section) for section in plan.sections],
@@ -60,52 +76,60 @@ def save_pending_plan(
6076
     return plan
6177
 
6278
 
63
-def load_pending_plan(store: StorePath) -> PendingSynthPlan | None:
64
-    """Return the staged synth plan for `store`, or None when absent."""
65
-    path = pending_plan_path(store)
79
+def load_pending_plan(
80
+    store: StorePath,
81
+    *,
82
+    subdir: str,
83
+    plan_cls: type[PendingSectionPlan],
84
+    error_cls: type[Exception],
85
+    label: str,
86
+) -> PendingSectionPlan | None:
87
+    """Return the staged plan, or None when absent. Raises `error_cls` on corruption.
88
+
89
+    `label` names the domain in error messages ("preference plan", "synth plan").
90
+    """
91
+    path = pending_plan_path(store, subdir=subdir)
6692
     if not path.exists():
6793
         return None
6894
     try:
6995
         raw = json.loads(path.read_text(encoding="utf-8"))
7096
     except OSError as exc:
71
-        raise PendingSynthPlanError(f"could not read staged synth plan: {exc}") from exc
97
+        raise error_cls(f"could not read staged {label}: {exc}") from exc
7298
     except json.JSONDecodeError as exc:
73
-        raise PendingSynthPlanError(f"staged synth plan is not valid JSON: {exc}") from exc
99
+        raise error_cls(f"staged {label} is not valid JSON: {exc}") from exc
74100
 
75101
     if not isinstance(raw, dict):
76
-        raise PendingSynthPlanError("staged synth plan must be a JSON object")
77
-    if raw.get("schema_version") != 1:
78
-        raise PendingSynthPlanError(
79
-            f"unsupported staged synth plan schema_version={raw.get('schema_version')!r}"
80
-        )
102
+        raise error_cls(f"staged {label} must be a JSON object")
103
+    if raw.get("schema_version") != _SCHEMA_VERSION:
104
+        raise error_cls(f"unsupported staged {label} schema_version={raw.get('schema_version')!r}")
81105
 
82106
     source_path = raw.get("source_path")
83107
     created_at = raw.get("created_at")
84108
     sections_raw = raw.get("sections")
85109
     if not isinstance(source_path, str) or not source_path:
86
-        raise PendingSynthPlanError("staged synth plan is missing source_path")
110
+        raise error_cls(f"staged {label} is missing source_path")
87111
     if not isinstance(created_at, str) or not created_at:
88
-        raise PendingSynthPlanError("staged synth plan is missing created_at")
112
+        raise error_cls(f"staged {label} is missing created_at")
89113
     if not isinstance(sections_raw, list):
90
-        raise PendingSynthPlanError("staged synth plan is missing sections")
114
+        raise error_cls(f"staged {label} is missing sections")
91115
 
92116
     sections: list[Section] = []
93117
     for idx, entry in enumerate(sections_raw):
94118
         try:
95119
             sections.append(_section_from_payload(entry))
96120
         except (TypeError, ValueError, KeyError) as exc:
97
-            raise PendingSynthPlanError(f"invalid section payload at index {idx}: {exc}") from exc
121
+            raise error_cls(f"invalid section payload at index {idx}: {exc}") from exc
98122
 
99
-    return PendingSynthPlan(
123
+    return plan_cls(
100124
         source_path=Path(source_path),
101125
         created_at=created_at,
102126
         sections=tuple(sections),
103127
     )
104128
 
105129
 
106
-def clear_pending_plan(store: StorePath) -> bool:
107
-    """Delete the staged synth plan for `store`. Returns True iff it existed."""
108
-    path = pending_plan_path(store)
130
+def clear_pending_plan(store: StorePath, *, subdir: str) -> bool:
131
+    """Delete the staged plan; True iff it existed."""
132
+    path = pending_plan_path(store, subdir=subdir)
109133
     if not path.exists():
110134
         return False
111135
     path.unlink()
src/dlm/preference/pending.pymodified
@@ -5,42 +5,61 @@ but the follow-up `dlm preference apply` still needs a stable plan to
55
 write. This module stores the mined `Section` payloads under the store
66
 root so the reviewed plan can be applied later without re-running
77
 sampling or judging.
8
+
9
+I/O is shared with `dlm.synth.pending` via `dlm._pending`.
810
 """
911
 
1012
 from __future__ import annotations
1113
 
12
-import json
1314
 from dataclasses import dataclass
14
-from datetime import UTC, datetime
15
-from pathlib import Path
16
-from typing import TYPE_CHECKING, Any
17
-
18
-from dlm.doc.sections import Section, SectionType
19
-from dlm.io.atomic import write_text as atomic_write_text
15
+from typing import TYPE_CHECKING
16
+
17
+from dlm._pending import (
18
+    PendingSectionPlan,
19
+    _optional_float,
20
+    _optional_int,
21
+    _optional_str,
22
+    _section_from_payload,
23
+    _section_to_payload,
24
+)
25
+from dlm._pending import (
26
+    clear_pending_plan as _clear,
27
+)
28
+from dlm._pending import (
29
+    load_pending_plan as _load,
30
+)
31
+from dlm._pending import (
32
+    pending_plan_path as _path,
33
+)
34
+from dlm._pending import (
35
+    save_pending_plan as _save,
36
+)
2037
 from dlm.preference.errors import PreferenceMiningError
2138
 
2239
 if TYPE_CHECKING:
2340
     from collections.abc import Sequence
41
+    from pathlib import Path
2442
 
43
+    from dlm.doc.sections import Section
2544
     from dlm.store.paths import StorePath
2645
 
2746
 
47
+_SUBDIR = "preference"
48
+_LABEL = "preference plan"
49
+
50
+
2851
 class PendingPreferencePlanError(PreferenceMiningError):
2952
     """Raised when the staged preference plan cannot be read or validated."""
3053
 
3154
 
3255
 @dataclass(frozen=True)
33
-class PendingPreferencePlan:
56
+class PendingPreferencePlan(PendingSectionPlan):
3457
     """One staged preference-mine plan for a store."""
3558
 
36
-    source_path: Path
37
-    created_at: str
38
-    sections: tuple[Section, ...]
39
-
4059
 
4160
 def pending_plan_path(store: StorePath) -> Path:
4261
     """Path to the staged preference-mine payload for `store`."""
43
-    return store.root / "preference" / "pending.json"
62
+    return _path(store, subdir=_SUBDIR)
4463
 
4564
 
4665
 def save_pending_plan(
@@ -50,154 +69,43 @@ def save_pending_plan(
5069
     sections: Sequence[Section],
5170
 ) -> PendingPreferencePlan:
5271
     """Persist `sections` as the staged plan for `store`."""
53
-    plan = PendingPreferencePlan(
54
-        source_path=source_path.resolve(),
55
-        created_at=_utcnow(),
56
-        sections=tuple(sections),
72
+    return _save(  # type: ignore[return-value]
73
+        store,
74
+        source_path=source_path,
75
+        sections=sections,
76
+        subdir=_SUBDIR,
77
+        plan_cls=PendingPreferencePlan,
5778
     )
58
-    path = pending_plan_path(store)
59
-    path.parent.mkdir(parents=True, exist_ok=True)
60
-    payload = {
61
-        "schema_version": 1,
62
-        "source_path": str(plan.source_path),
63
-        "created_at": plan.created_at,
64
-        "sections": [_section_to_payload(section) for section in plan.sections],
65
-    }
66
-    atomic_write_text(path, json.dumps(payload, indent=2, sort_keys=True) + "\n")
67
-    return plan
6879
 
6980
 
7081
 def load_pending_plan(store: StorePath) -> PendingPreferencePlan | None:
7182
     """Return the staged plan for `store`, or None when absent."""
72
-    path = pending_plan_path(store)
73
-    if not path.exists():
74
-        return None
75
-    try:
76
-        raw = json.loads(path.read_text(encoding="utf-8"))
77
-    except OSError as exc:
78
-        raise PendingPreferencePlanError(f"could not read staged preference plan: {exc}") from exc
79
-    except json.JSONDecodeError as exc:
80
-        raise PendingPreferencePlanError(
81
-            f"staged preference plan is not valid JSON: {exc}"
82
-        ) from exc
83
-
84
-    if not isinstance(raw, dict):
85
-        raise PendingPreferencePlanError("staged preference plan must be a JSON object")
86
-    if raw.get("schema_version") != 1:
87
-        raise PendingPreferencePlanError(
88
-            f"unsupported staged preference plan schema_version={raw.get('schema_version')!r}"
89
-        )
90
-
91
-    source_path = raw.get("source_path")
92
-    created_at = raw.get("created_at")
93
-    sections_raw = raw.get("sections")
94
-    if not isinstance(source_path, str) or not source_path:
95
-        raise PendingPreferencePlanError("staged preference plan is missing source_path")
96
-    if not isinstance(created_at, str) or not created_at:
97
-        raise PendingPreferencePlanError("staged preference plan is missing created_at")
98
-    if not isinstance(sections_raw, list):
99
-        raise PendingPreferencePlanError("staged preference plan is missing sections")
100
-
101
-    sections: list[Section] = []
102
-    for idx, entry in enumerate(sections_raw):
103
-        try:
104
-            sections.append(_section_from_payload(entry))
105
-        except (TypeError, ValueError, KeyError) as exc:
106
-            raise PendingPreferencePlanError(
107
-                f"invalid section payload at index {idx}: {exc}"
108
-            ) from exc
109
-
110
-    return PendingPreferencePlan(
111
-        source_path=Path(source_path),
112
-        created_at=created_at,
113
-        sections=tuple(sections),
83
+    return _load(  # type: ignore[return-value]
84
+        store,
85
+        subdir=_SUBDIR,
86
+        plan_cls=PendingPreferencePlan,
87
+        error_cls=PendingPreferencePlanError,
88
+        label=_LABEL,
11489
     )
11590
 
11691
 
11792
 def clear_pending_plan(store: StorePath) -> bool:
11893
     """Delete the staged plan for `store`. Returns True iff it existed."""
119
-    path = pending_plan_path(store)
120
-    if not path.exists():
121
-        return False
122
-    path.unlink()
123
-    return True
124
-
125
-
126
-def _utcnow() -> str:
127
-    return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
128
-
129
-
130
-def _section_to_payload(section: Section) -> dict[str, Any]:
131
-    return {
132
-        "type": section.type.value,
133
-        "content": section.content,
134
-        "start_line": section.start_line,
135
-        "adapter": section.adapter,
136
-        "tags": dict(section.tags),
137
-        "auto_harvest": section.auto_harvest,
138
-        "harvest_source": section.harvest_source,
139
-        "auto_mined": section.auto_mined,
140
-        "judge_name": section.judge_name,
141
-        "judge_score_chosen": section.judge_score_chosen,
142
-        "judge_score_rejected": section.judge_score_rejected,
143
-        "mined_at": section.mined_at,
144
-        "mined_run_id": section.mined_run_id,
145
-        "media_path": section.media_path,
146
-        "media_alt": section.media_alt,
147
-        "media_blob_sha": section.media_blob_sha,
148
-        "media_transcript": section.media_transcript,
149
-    }
150
-
151
-
152
-def _section_from_payload(raw: object) -> Section:
153
-    if not isinstance(raw, dict):
154
-        raise TypeError(f"expected object, got {type(raw).__name__}")
155
-    section_type = SectionType(str(raw["type"]))
156
-    tags = raw.get("tags", {})
157
-    if not isinstance(tags, dict):
158
-        raise TypeError("tags must be an object")
159
-    if not all(isinstance(k, str) and isinstance(v, str) for k, v in tags.items()):
160
-        raise TypeError("tags keys and values must be strings")
161
-    return Section(
162
-        type=section_type,
163
-        content=str(raw["content"]),
164
-        start_line=int(raw.get("start_line", 0)),
165
-        adapter=_optional_str(raw.get("adapter")),
166
-        tags=dict(tags),
167
-        auto_harvest=bool(raw.get("auto_harvest", False)),
168
-        harvest_source=_optional_str(raw.get("harvest_source")),
169
-        auto_mined=bool(raw.get("auto_mined", False)),
170
-        judge_name=_optional_str(raw.get("judge_name")),
171
-        judge_score_chosen=_optional_float(raw.get("judge_score_chosen")),
172
-        judge_score_rejected=_optional_float(raw.get("judge_score_rejected")),
173
-        mined_at=_optional_str(raw.get("mined_at")),
174
-        mined_run_id=_optional_int(raw.get("mined_run_id")),
175
-        media_path=_optional_str(raw.get("media_path")),
176
-        media_alt=_optional_str(raw.get("media_alt")),
177
-        media_blob_sha=_optional_str(raw.get("media_blob_sha")),
178
-        media_transcript=_optional_str(raw.get("media_transcript")),
179
-    )
180
-
181
-
182
-def _optional_str(value: object) -> str | None:
183
-    if value is None:
184
-        return None
185
-    if not isinstance(value, str):
186
-        raise TypeError(f"expected string or null, got {type(value).__name__}")
187
-    return value
188
-
189
-
190
-def _optional_float(value: object) -> float | None:
191
-    if value is None:
192
-        return None
193
-    if isinstance(value, bool) or not isinstance(value, int | float):
194
-        raise TypeError(f"expected float or null, got {type(value).__name__}")
195
-    return float(value)
196
-
197
-
198
-def _optional_int(value: object) -> int | None:
199
-    if value is None:
200
-        return None
201
-    if isinstance(value, bool) or not isinstance(value, int):
202
-        raise TypeError(f"expected int or null, got {type(value).__name__}")
203
-    return value
94
+    return _clear(store, subdir=_SUBDIR)
95
+
96
+
97
+# Re-export private I/O helpers so test modules that reached into the
98
+# original implementation continue to work without import churn.
99
+__all__ = [
100
+    "PendingPreferencePlan",
101
+    "PendingPreferencePlanError",
102
+    "_optional_float",
103
+    "_optional_int",
104
+    "_optional_str",
105
+    "_section_from_payload",
106
+    "_section_to_payload",
107
+    "clear_pending_plan",
108
+    "load_pending_plan",
109
+    "pending_plan_path",
110
+    "save_pending_plan",
111
+]
src/dlm/synth/pending.pymodified
@@ -1,39 +1,59 @@
1
-"""Persist staged auto-synth instruction sections between CLI steps."""
1
+"""Persist staged auto-synth instruction sections between CLI steps.
2
+
3
+I/O is shared with `dlm.preference.pending` via `dlm._pending`.
4
+"""
25
 
36
 from __future__ import annotations
47
 
5
-import json
68
 from dataclasses import dataclass
7
-from datetime import UTC, datetime
8
-from pathlib import Path
9
-from typing import TYPE_CHECKING, Any
10
-
11
-from dlm.doc.sections import Section, SectionType
12
-from dlm.io.atomic import write_text as atomic_write_text
9
+from typing import TYPE_CHECKING
10
+
11
+from dlm._pending import (
12
+    PendingSectionPlan,
13
+    _optional_float,
14
+    _optional_int,
15
+    _optional_str,
16
+    _section_from_payload,
17
+    _section_to_payload,
18
+)
19
+from dlm._pending import (
20
+    clear_pending_plan as _clear,
21
+)
22
+from dlm._pending import (
23
+    load_pending_plan as _load,
24
+)
25
+from dlm._pending import (
26
+    pending_plan_path as _path,
27
+)
28
+from dlm._pending import (
29
+    save_pending_plan as _save,
30
+)
1331
 from dlm.synth.errors import SynthError
1432
 
1533
 if TYPE_CHECKING:
1634
     from collections.abc import Sequence
35
+    from pathlib import Path
1736
 
37
+    from dlm.doc.sections import Section
1838
     from dlm.store.paths import StorePath
1939
 
2040
 
41
+_SUBDIR = "synth"
42
+_LABEL = "synth plan"
43
+
44
+
2145
 class PendingSynthPlanError(SynthError):
2246
     """Raised when the staged synth plan cannot be read or validated."""
2347
 
2448
 
2549
 @dataclass(frozen=True)
26
-class PendingSynthPlan:
50
+class PendingSynthPlan(PendingSectionPlan):
2751
     """One staged synth plan for a store."""
2852
 
29
-    source_path: Path
30
-    created_at: str
31
-    sections: tuple[Section, ...]
32
-
3353
 
3454
 def pending_plan_path(store: StorePath) -> Path:
3555
     """Path to the staged synth payload for `store`."""
36
-    return store.root / "synth" / "pending.json"
56
+    return _path(store, subdir=_SUBDIR)
3757
 
3858
 
3959
 def save_pending_plan(
@@ -43,160 +63,41 @@ def save_pending_plan(
4363
     sections: Sequence[Section],
4464
 ) -> PendingSynthPlan:
4565
     """Persist `sections` as the staged synth plan for `store`."""
46
-    plan = PendingSynthPlan(
47
-        source_path=source_path.resolve(),
48
-        created_at=_utcnow(),
49
-        sections=tuple(sections),
66
+    return _save(  # type: ignore[return-value]
67
+        store,
68
+        source_path=source_path,
69
+        sections=sections,
70
+        subdir=_SUBDIR,
71
+        plan_cls=PendingSynthPlan,
5072
     )
51
-    path = pending_plan_path(store)
52
-    path.parent.mkdir(parents=True, exist_ok=True)
53
-    payload = {
54
-        "schema_version": 1,
55
-        "source_path": str(plan.source_path),
56
-        "created_at": plan.created_at,
57
-        "sections": [_section_to_payload(section) for section in plan.sections],
58
-    }
59
-    atomic_write_text(path, json.dumps(payload, indent=2, sort_keys=True) + "\n")
60
-    return plan
6173
 
6274
 
6375
 def load_pending_plan(store: StorePath) -> PendingSynthPlan | None:
6476
     """Return the staged synth plan for `store`, or None when absent."""
65
-    path = pending_plan_path(store)
66
-    if not path.exists():
67
-        return None
68
-    try:
69
-        raw = json.loads(path.read_text(encoding="utf-8"))
70
-    except OSError as exc:
71
-        raise PendingSynthPlanError(f"could not read staged synth plan: {exc}") from exc
72
-    except json.JSONDecodeError as exc:
73
-        raise PendingSynthPlanError(f"staged synth plan is not valid JSON: {exc}") from exc
74
-
75
-    if not isinstance(raw, dict):
76
-        raise PendingSynthPlanError("staged synth plan must be a JSON object")
77
-    if raw.get("schema_version") != 1:
78
-        raise PendingSynthPlanError(
79
-            f"unsupported staged synth plan schema_version={raw.get('schema_version')!r}"
80
-        )
81
-
82
-    source_path = raw.get("source_path")
83
-    created_at = raw.get("created_at")
84
-    sections_raw = raw.get("sections")
85
-    if not isinstance(source_path, str) or not source_path:
86
-        raise PendingSynthPlanError("staged synth plan is missing source_path")
87
-    if not isinstance(created_at, str) or not created_at:
88
-        raise PendingSynthPlanError("staged synth plan is missing created_at")
89
-    if not isinstance(sections_raw, list):
90
-        raise PendingSynthPlanError("staged synth plan is missing sections")
91
-
92
-    sections: list[Section] = []
93
-    for idx, entry in enumerate(sections_raw):
94
-        try:
95
-            sections.append(_section_from_payload(entry))
96
-        except (TypeError, ValueError, KeyError) as exc:
97
-            raise PendingSynthPlanError(f"invalid section payload at index {idx}: {exc}") from exc
98
-
99
-    return PendingSynthPlan(
100
-        source_path=Path(source_path),
101
-        created_at=created_at,
102
-        sections=tuple(sections),
77
+    return _load(  # type: ignore[return-value]
78
+        store,
79
+        subdir=_SUBDIR,
80
+        plan_cls=PendingSynthPlan,
81
+        error_cls=PendingSynthPlanError,
82
+        label=_LABEL,
10383
     )
10484
 
10585
 
10686
 def clear_pending_plan(store: StorePath) -> bool:
10787
     """Delete the staged synth plan for `store`. Returns True iff it existed."""
108
-    path = pending_plan_path(store)
109
-    if not path.exists():
110
-        return False
111
-    path.unlink()
112
-    return True
113
-
114
-
115
-def _utcnow() -> str:
116
-    return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
117
-
118
-
119
-def _section_to_payload(section: Section) -> dict[str, Any]:
120
-    return {
121
-        "type": section.type.value,
122
-        "content": section.content,
123
-        "start_line": section.start_line,
124
-        "adapter": section.adapter,
125
-        "tags": dict(section.tags),
126
-        "auto_harvest": section.auto_harvest,
127
-        "harvest_source": section.harvest_source,
128
-        "auto_mined": section.auto_mined,
129
-        "judge_name": section.judge_name,
130
-        "judge_score_chosen": section.judge_score_chosen,
131
-        "judge_score_rejected": section.judge_score_rejected,
132
-        "mined_at": section.mined_at,
133
-        "mined_run_id": section.mined_run_id,
134
-        "auto_synth": section.auto_synth,
135
-        "synth_teacher": section.synth_teacher,
136
-        "synth_strategy": section.synth_strategy,
137
-        "synth_at": section.synth_at,
138
-        "source_section_id": section.source_section_id,
139
-        "media_path": section.media_path,
140
-        "media_alt": section.media_alt,
141
-        "media_blob_sha": section.media_blob_sha,
142
-        "media_transcript": section.media_transcript,
143
-    }
144
-
145
-
146
-def _section_from_payload(raw: object) -> Section:
147
-    if not isinstance(raw, dict):
148
-        raise TypeError(f"expected object, got {type(raw).__name__}")
149
-    section_type = SectionType(str(raw["type"]))
150
-    tags = raw.get("tags", {})
151
-    if not isinstance(tags, dict):
152
-        raise TypeError("tags must be an object")
153
-    if not all(isinstance(k, str) and isinstance(v, str) for k, v in tags.items()):
154
-        raise TypeError("tags keys and values must be strings")
155
-    return Section(
156
-        type=section_type,
157
-        content=str(raw["content"]),
158
-        start_line=int(raw.get("start_line", 0)),
159
-        adapter=_optional_str(raw.get("adapter")),
160
-        tags=dict(tags),
161
-        auto_harvest=bool(raw.get("auto_harvest", False)),
162
-        harvest_source=_optional_str(raw.get("harvest_source")),
163
-        auto_mined=bool(raw.get("auto_mined", False)),
164
-        judge_name=_optional_str(raw.get("judge_name")),
165
-        judge_score_chosen=_optional_float(raw.get("judge_score_chosen")),
166
-        judge_score_rejected=_optional_float(raw.get("judge_score_rejected")),
167
-        mined_at=_optional_str(raw.get("mined_at")),
168
-        mined_run_id=_optional_int(raw.get("mined_run_id")),
169
-        auto_synth=bool(raw.get("auto_synth", False)),
170
-        synth_teacher=_optional_str(raw.get("synth_teacher")),
171
-        synth_strategy=_optional_str(raw.get("synth_strategy")),
172
-        synth_at=_optional_str(raw.get("synth_at")),
173
-        source_section_id=_optional_str(raw.get("source_section_id")),
174
-        media_path=_optional_str(raw.get("media_path")),
175
-        media_alt=_optional_str(raw.get("media_alt")),
176
-        media_blob_sha=_optional_str(raw.get("media_blob_sha")),
177
-        media_transcript=_optional_str(raw.get("media_transcript")),
178
-    )
179
-
180
-
181
-def _optional_str(value: object) -> str | None:
182
-    if value is None:
183
-        return None
184
-    if not isinstance(value, str):
185
-        raise TypeError(f"expected string or null, got {type(value).__name__}")
186
-    return value
187
-
188
-
189
-def _optional_float(value: object) -> float | None:
190
-    if value is None:
191
-        return None
192
-    if isinstance(value, bool) or not isinstance(value, int | float):
193
-        raise TypeError(f"expected float or null, got {type(value).__name__}")
194
-    return float(value)
195
-
196
-
197
-def _optional_int(value: object) -> int | None:
198
-    if value is None:
199
-        return None
200
-    if isinstance(value, bool) or not isinstance(value, int):
201
-        raise TypeError(f"expected int or null, got {type(value).__name__}")
202
-    return value
88
+    return _clear(store, subdir=_SUBDIR)
89
+
90
+
91
+__all__ = [
92
+    "PendingSynthPlan",
93
+    "PendingSynthPlanError",
94
+    "_optional_float",
95
+    "_optional_int",
96
+    "_optional_str",
97
+    "_section_from_payload",
98
+    "_section_to_payload",
99
+    "clear_pending_plan",
100
+    "load_pending_plan",
101
+    "pending_plan_path",
102
+    "save_pending_plan",
103
+]