dos-kernel 0.22.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. dos/__init__.py +261 -0
  2. dos/_bin/dos-hook.exe +0 -0
  3. dos/_filelock.py +255 -0
  4. dos/_job_policy.py +97 -0
  5. dos/_tree.py +145 -0
  6. dos/admission.py +433 -0
  7. dos/answer_shape.py +299 -0
  8. dos/arbiter.py +859 -0
  9. dos/archive_lock.py +266 -0
  10. dos/arg_provenance.py +814 -0
  11. dos/attest.py +472 -0
  12. dos/breaker.py +311 -0
  13. dos/churn.py +226 -0
  14. dos/claim_extract.py +229 -0
  15. dos/claim_ttl.py +150 -0
  16. dos/cli.py +8721 -0
  17. dos/commit_audit.py +666 -0
  18. dos/completion.py +466 -0
  19. dos/concurrency_class.py +154 -0
  20. dos/config.py +1380 -0
  21. dos/config_lint.py +464 -0
  22. dos/cooldown.py +390 -0
  23. dos/coverage.py +387 -0
  24. dos/dangling_intent.py +287 -0
  25. dos/data_class.py +397 -0
  26. dos/decisions.py +1274 -0
  27. dos/decisions_tui.py +251 -0
  28. dos/dispatch_top.py +740 -0
  29. dos/dispatch_top_tui.py +116 -0
  30. dos/drivers/__init__.py +40 -0
  31. dos/drivers/ci_status.py +630 -0
  32. dos/drivers/citation_resolve.py +703 -0
  33. dos/drivers/decision_stop.py +98 -0
  34. dos/drivers/export_file.py +173 -0
  35. dos/drivers/export_otlp.py +275 -0
  36. dos/drivers/export_statsd.py +242 -0
  37. dos/drivers/hook_dialects.py +391 -0
  38. dos/drivers/job.py +47 -0
  39. dos/drivers/llm_judge.py +360 -0
  40. dos/drivers/memory_recall.py +1231 -0
  41. dos/drivers/notify_slack.py +373 -0
  42. dos/drivers/notify_webhook.py +251 -0
  43. dos/drivers/operator_judge.py +114 -0
  44. dos/drivers/os_acceptance.py +228 -0
  45. dos/drivers/paste_log.py +132 -0
  46. dos/drivers/plan_scope.py +133 -0
  47. dos/drivers/self_improve.py +375 -0
  48. dos/drivers/similarity_judge.py +249 -0
  49. dos/drivers/state_diff.py +274 -0
  50. dos/drivers/supervisor.py +347 -0
  51. dos/drivers/watchdog.py +363 -0
  52. dos/drivers/workshop.py +160 -0
  53. dos/durable_schema.py +344 -0
  54. dos/effect_witness.py +393 -0
  55. dos/efficiency.py +318 -0
  56. dos/enforce.py +414 -0
  57. dos/enumerate.py +776 -0
  58. dos/env_print.py +378 -0
  59. dos/event_severity.py +258 -0
  60. dos/evidence.py +692 -0
  61. dos/exec_capability.py +256 -0
  62. dos/export_cursor.py +143 -0
  63. dos/exporter.py +320 -0
  64. dos/firing_label.py +353 -0
  65. dos/fleet_roll.py +226 -0
  66. dos/gate_classify.py +827 -0
  67. dos/gh4_coverage.py +179 -0
  68. dos/git_delta.py +122 -0
  69. dos/guard.py +215 -0
  70. dos/health.py +552 -0
  71. dos/help_summary.py +519 -0
  72. dos/home.py +934 -0
  73. dos/hook_binary.py +194 -0
  74. dos/hook_dialect.py +271 -0
  75. dos/hook_exit.py +191 -0
  76. dos/hook_install.py +437 -0
  77. dos/id_alloc.py +304 -0
  78. dos/improve.py +499 -0
  79. dos/intent_ledger.py +635 -0
  80. dos/interpret.py +176 -0
  81. dos/intervention.py +769 -0
  82. dos/intervention_eval.py +371 -0
  83. dos/journal_delta.py +308 -0
  84. dos/judge_eval.py +328 -0
  85. dos/judges.py +366 -0
  86. dos/lane_infer.py +127 -0
  87. dos/lane_journal.py +1001 -0
  88. dos/lane_lease.py +952 -0
  89. dos/lane_overlap.py +228 -0
  90. dos/lease_health.py +282 -0
  91. dos/lifecycle.py +211 -0
  92. dos/liveness.py +352 -0
  93. dos/lock_modes.py +185 -0
  94. dos/log_source.py +395 -0
  95. dos/loop_decide.py +1746 -0
  96. dos/marker_gate.py +254 -0
  97. dos/marker_sensor.py +396 -0
  98. dos/noop_streak.py +280 -0
  99. dos/notify.py +479 -0
  100. dos/observe.py +175 -0
  101. dos/oracle.py +1661 -0
  102. dos/overlap_eval.py +214 -0
  103. dos/overlap_policy.py +342 -0
  104. dos/packet_sidecar.py +267 -0
  105. dos/phase_shipped.py +1985 -0
  106. dos/pick_priority.py +225 -0
  107. dos/pickable.py +369 -0
  108. dos/picker_oracle.py +1037 -0
  109. dos/plan_board.py +513 -0
  110. dos/plan_board_tui.py +113 -0
  111. dos/plan_source.py +455 -0
  112. dos/posttool_sensor.py +528 -0
  113. dos/precursor_gate.py +499 -0
  114. dos/precursor_gate_eval.py +239 -0
  115. dos/preflight.py +825 -0
  116. dos/pretool_sensor.py +490 -0
  117. dos/proc_delta.py +181 -0
  118. dos/productivity.py +296 -0
  119. dos/provider_limit.py +242 -0
  120. dos/py.typed +4 -0
  121. dos/reason_morphology.py +299 -0
  122. dos/reasons.py +449 -0
  123. dos/reconcile.py +173 -0
  124. dos/recurring_wedge.py +206 -0
  125. dos/render.py +393 -0
  126. dos/result_state.py +468 -0
  127. dos/resume.py +578 -0
  128. dos/resume_evidence.py +293 -0
  129. dos/retention.py +344 -0
  130. dos/reward.py +372 -0
  131. dos/rewind.py +587 -0
  132. dos/rewind_evidence.py +168 -0
  133. dos/rewind_tokens.py +252 -0
  134. dos/run_id.py +342 -0
  135. dos/scope.py +520 -0
  136. dos/scope_source.py +382 -0
  137. dos/scout.py +982 -0
  138. dos/self_modify.py +209 -0
  139. dos/sibling_scan.py +569 -0
  140. dos/skills/EXAMPLES.md +584 -0
  141. dos/skills/dos-class-cycle/SKILL.md +107 -0
  142. dos/skills/dos-dispatch/SKILL.md +177 -0
  143. dos/skills/dos-dispatch-loop/SKILL.md +254 -0
  144. dos/skills/dos-goal-gate/SKILL.md +269 -0
  145. dos/skills/dos-next-up/SKILL.md +231 -0
  146. dos/skills/dos-promote/SKILL.md +114 -0
  147. dos/skills/dos-replan/SKILL.md +159 -0
  148. dos/skills/dos-replan-loop/SKILL.md +114 -0
  149. dos/skills/dos-self-improve/SKILL.md +213 -0
  150. dos/skills/dos-supervise-loop/SKILL.md +180 -0
  151. dos/skills/dos-unstick/SKILL.md +108 -0
  152. dos/skills/dos-witness-claim/SKILL.md +251 -0
  153. dos/stamp.py +1002 -0
  154. dos/state_health.py +387 -0
  155. dos/status.py +114 -0
  156. dos/stop_policy.py +334 -0
  157. dos/supervise.py +1014 -0
  158. dos/testwitness.py +392 -0
  159. dos/timeline.py +1027 -0
  160. dos/tokens.py +485 -0
  161. dos/tool_stream.py +393 -0
  162. dos/tool_stream_eval.py +226 -0
  163. dos/trace.py +524 -0
  164. dos/verdict.py +140 -0
  165. dos/verdict_cli.py +189 -0
  166. dos/verdict_journal.py +497 -0
  167. dos/verdict_rollup.py +217 -0
  168. dos/verdicts.py +181 -0
  169. dos/wedge_reason.py +282 -0
  170. dos_kernel-0.22.0.dist-info/METADATA +859 -0
  171. dos_kernel-0.22.0.dist-info/RECORD +178 -0
  172. dos_kernel-0.22.0.dist-info/WHEEL +5 -0
  173. dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
  174. dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
  175. dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
  176. dos_mcp/__init__.py +52 -0
  177. dos_mcp/py.typed +2 -0
  178. dos_mcp/server.py +779 -0
dos/durable_schema.py ADDED
@@ -0,0 +1,344 @@
1
+ """The schema-evolution floor — every durable record declares its own format (docs/107 §6).
2
+
3
+ > **The substrate survives the kernel changing because each record declares its
4
+ > own format and the reader refuses what it cannot soundly read — the same
5
+ > distrust posture the kernel takes toward agents, turned on its own history.**
6
+
7
+ `CLAUDE.md` calls DOS a "durable substrate," and the whole resumable-work design
8
+ (docs/107) assumes a record written by `v0.6` stays *readable and resumable* by
9
+ `v0.9`. Nothing in the kernel enforced that. A journal entry, a `run.json`, a
10
+ checkpoint payload, an intent-ledger line written under one kernel version is read
11
+ back under another — and when the format moved in between, the reader either
12
+ silently misparses it (the worst outcome: resuming from a *misread* intent) or
13
+ crashes. This module is the contract that forecloses both.
14
+
15
+ It is the **time axis** of the same closed-enum-as-data discipline `dos.reasons` /
16
+ `dos.stamp` apply to the *workspace* axis (see `docs/HACKING.md`): the format is
17
+ data the record *declares*, not a constant the reading code *assumes*. Three
18
+ disciplines (docs/107 §6), all policy/format — none a new syscall:
19
+
20
+ 1. **Every durable record carries a `schema:` tag.** Already true of `run.json`
21
+ (`home.SCHEMA`); this generalizes it to *every* persisted record and gives it
22
+ one shape: ``{family, version}`` (a string family name + an int version),
23
+ declared by the WRITER. A reader keys its parse on the record's own tag,
24
+ never on "what kernel version am I."
25
+ 2. **Evolution is additive and forward-compatible by default.** A new *field* is
26
+ optional-with-a-default (the `ProgressEvidence` dataclass-default idiom), so a
27
+ newer reader sees an older record's *absence* of a field as the default, and
28
+ an older reader sees a newer record's *extra* field as ignorable. A new *op*
29
+ in a closed vocabulary is skipped by an older replay fold (the
30
+ `lane_journal._STATE_MUTATING_OPS` gate already does this). **Additive
31
+ evolution does NOT bump the version** — that is the whole point: the version
32
+ is the *non-additive*, break-the-reader signal, reserved for a genuine shape
33
+ change. So a reader's `understands` ceiling rarely moves.
34
+ 3. **A non-additively-newer record is refuse-don't-guess.** When a record's
35
+ `version` exceeds what the reader understands for that family, the reader must
36
+ **refuse to interpret it** — a typed `UNREADABLE_NEWER` classification a
37
+ caller surfaces as `UNRESUMABLE`/`INDETERMINATE`, never a silent best-effort
38
+ parse. This is the kernel's whole reflex — *when you can't verify, refuse;
39
+ don't fabricate* — applied to its own persisted past.
40
+
41
+ This module is PURE stdlib (a near-leaf, like `reasons`/`stamp`): it stamps a tag,
42
+ reads a tag, and classifies one record's readability against a declared ceiling.
43
+ The actual reads/writes live in the durable surfaces (`intent_ledger`,
44
+ `lane_journal`, `run_id`); they call `tag()` when they write and `classify()` when
45
+ they read. A genuine breaking migration is an explicit operator-run fold (a `dos …
46
+ migrate`, the `compact()` shape — a pure old→new transform), never an implicit
47
+ in-place reinterpretation; this module supplies the *detection* (the reader knows
48
+ it cannot read the record) that makes such a fold necessary rather than silent.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import enum
54
+ from dataclasses import dataclass
55
+ from typing import Any, Mapping
56
+
57
+ # The key every durable record carries. A single name so a grep over the
58
+ # persisted surfaces ("which records declare a schema?") has one answer, and so a
59
+ # reader never guesses where the tag lives. Value shape: ``{"family": str,
60
+ # "version": int}`` — `family` names the record kind (one per durable surface),
61
+ # `version` is the WRITER's format version (bumped ONLY on a non-additive change).
62
+ SCHEMA_KEY = "schema"
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class SchemaTag:
67
+ """A durable record's self-declared format: ``(family, version)``.
68
+
69
+ `family` — the record kind, a stable string (e.g. ``"intent-ledger"``,
70
+ ``"lane-journal"``, ``"run"``). One family per durable surface; the reader
71
+ matches on it so two surfaces' versions never collide.
72
+ `version` — the WRITER's format version, a 1-based int. Bumped ONLY on a
73
+ NON-additive change (a removed/renamed/retyped field, a changed semantic).
74
+ An additive change (a new optional field, a new op) does NOT bump it — that
75
+ is what keeps an older reader forward-compatible without a migration.
76
+
77
+ `str`-mirroring `to_dict`/`from_obj` so it round-trips through a JSONL line
78
+ losslessly, the `RunId.to_dict` idiom.
79
+ """
80
+
81
+ family: str
82
+ version: int = 1
83
+
84
+ def __post_init__(self) -> None:
85
+ # An EMPTY family is permitted at the TAG level — it is the legacy
86
+ # bare-int sentinel (`home.SCHEMA`'s `"schema": 1`, which predates
87
+ # families), parsed by `from_obj` and bridged to a named reader by
88
+ # `classify`. A WRITER must still name a family: `tag()` rejects an empty
89
+ # one (see its body), so a fresh record is never untyped — only a record
90
+ # read back from the pre-family past is.
91
+ if not isinstance(self.family, str):
92
+ raise ValueError("a schema family must be a string")
93
+ if self.version < 1:
94
+ raise ValueError("a schema version is 1-based (got {!r})".format(self.version))
95
+
96
+ def to_dict(self) -> dict:
97
+ return {"family": self.family, "version": self.version}
98
+
99
+ @classmethod
100
+ def from_obj(cls, obj: Any) -> "SchemaTag | None":
101
+ """Parse a tag out of a record's ``schema`` value. None if absent/malformed.
102
+
103
+ Tolerant by design — a record with NO tag, or a tag of the wrong shape, is
104
+ not a crash here: it yields ``None``, and `classify` maps that to the
105
+ explicit `UNTAGGED` classification a caller decides how to treat (the
106
+ torn-tail / `_CORRUPT` posture, lifted to the tag axis). Two accepted
107
+ shapes:
108
+ * the canonical ``{"family": "...", "version": N}`` dict, and
109
+ * a legacy BARE INT (``"schema": 1`` — the `home.SCHEMA` shape that
110
+ predates families): read as family ``""`` at that version, so an
111
+ untyped-family reader can still gate on the integer (see
112
+ `classify`'s `family=""` wildcard).
113
+ """
114
+ if isinstance(obj, bool): # bool is an int subclass — exclude it explicitly
115
+ return None
116
+ if isinstance(obj, int):
117
+ # Legacy bare-int tag (home.SCHEMA): no family, just a version.
118
+ try:
119
+ return cls(family="", version=int(obj))
120
+ except ValueError:
121
+ return None
122
+ if isinstance(obj, Mapping):
123
+ fam = obj.get("family")
124
+ ver = obj.get("version")
125
+ if not isinstance(fam, str) or isinstance(ver, bool) or not isinstance(ver, int):
126
+ return None
127
+ try:
128
+ return cls(family=fam, version=ver)
129
+ except ValueError:
130
+ return None
131
+ return None
132
+
133
+
134
+ class Readability(str, enum.Enum):
135
+ """How a reader may treat one durable record, given its declared schema tag.
136
+
137
+ `str`-valued so it round-trips a `--json` token without a lookup table
138
+ (`Liveness` / `gate_classify.Verdict` idiom). The whole point is the asymmetry
139
+ between READABLE/IGNORABLE (proceed) and UNREADABLE_NEWER (refuse) — the
140
+ refuse-don't-guess floor.
141
+ """
142
+
143
+ READABLE = "READABLE" # version ≤ the reader's ceiling — parse it
144
+ UNREADABLE_NEWER = "UNREADABLE_NEWER" # version > the ceiling — REFUSE (don't guess)
145
+ UNTAGGED = "UNTAGGED" # no/malformed tag — caller decides (legacy floor)
146
+ WRONG_FAMILY = "WRONG_FAMILY" # a tag for a DIFFERENT family — not this reader's record
147
+
148
+ def __str__(self) -> str: # pragma: no cover - trivial
149
+ return self.value
150
+
151
+ @property
152
+ def is_soundly_readable(self) -> bool:
153
+ """True iff a reader may parse the record's body without guessing.
154
+
155
+ READABLE only. UNREADABLE_NEWER is the refuse case; WRONG_FAMILY is not
156
+ this reader's record at all; UNTAGGED is the legacy floor the CALLER must
157
+ decide on explicitly (a fold over a mixed old/new file treats it
158
+ permissively as the family's v1; a strict reader may refuse) — so neither
159
+ is "soundly readable" without a caller policy, and this property stays
160
+ conservative (the safe direction for a durability guard).
161
+ """
162
+ return self is Readability.READABLE
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class ReadabilityVerdict:
167
+ """The typed result of `classify` — the classification + the record's own tag.
168
+
169
+ Carries `tag` (what the record DECLARED, or None when UNTAGGED/WRONG-shaped)
170
+ and `ceiling` (what the reader UNDERSTANDS) so a surfaced refusal is legible:
171
+ "this `intent-ledger` record is v3 but this kernel reads ≤ v2 — run
172
+ `dos runs migrate`," not a bare "can't read it." The `reason` is that
173
+ one-liner. `to_dict` is the `--json` shape (the `LivenessVerdict.to_dict`
174
+ idiom).
175
+ """
176
+
177
+ readability: Readability
178
+ reason: str
179
+ family: str
180
+ ceiling: int
181
+ tag: SchemaTag | None = None
182
+
183
+ def to_dict(self) -> dict:
184
+ return {
185
+ "readability": self.readability.value,
186
+ "reason": self.reason,
187
+ "family": self.family,
188
+ "ceiling": self.ceiling,
189
+ "tag": self.tag.to_dict() if self.tag is not None else None,
190
+ }
191
+
192
+
193
+ def tag(family: str, version: int = 1) -> dict:
194
+ """The ``{"schema": {"family", "version"}}`` fragment a WRITER merges into a record.
195
+
196
+ Pure constructor — the write-side half of the contract. A durable surface's
197
+ entry builder does ``{**durable_schema.tag("intent-ledger", INTENT_LEDGER_SCHEMA), …}``
198
+ so every persisted record self-declares its format with one call and one shape.
199
+ A WRITER must name a family (an empty one is the legacy READ-side sentinel, not
200
+ a legal write) and a 1-based version — both raise here, surfaced loudly the way
201
+ a malformed `[stamp]` table is: a writer that stamps a bad tag is a kernel bug,
202
+ not silent data.
203
+ """
204
+ if not family:
205
+ raise ValueError("a written schema tag must name a family (empty is the legacy read-only sentinel)")
206
+ return {SCHEMA_KEY: SchemaTag(family=family, version=version).to_dict()}
207
+
208
+
209
+ def classify(
210
+ record: Mapping[str, Any],
211
+ *,
212
+ family: str,
213
+ understands: int,
214
+ ) -> ReadabilityVerdict:
215
+ """Classify whether THIS reader may soundly parse one durable `record`. PURE.
216
+
217
+ The read-side half of the contract, and the refuse-don't-guess gate. The
218
+ reader declares the `family` it is reading and `understands` — the MAX version
219
+ of that family it knows how to parse (its ceiling). The verdict:
220
+
221
+ * **READABLE** — the record's tag is this family at a version ≤ `understands`.
222
+ Parse the body. (The additive-evolution contract means a `v1` reader
223
+ reading a `v1` record still ignores any *extra fields* a later writer
224
+ added — that is the body parser's job, not this gate's; this gate only
225
+ decides "is the VERSION within my ceiling.")
226
+ * **UNREADABLE_NEWER** — the tag is this family but at a version GREATER than
227
+ `understands`: a non-additive change this kernel predates. **Refuse** — the
228
+ caller surfaces `UNRESUMABLE`/`INDETERMINATE`, never a best-effort parse of
229
+ a shape it does not know. This is the §6 floor.
230
+ * **WRONG_FAMILY** — the tag names a DIFFERENT family. Not this reader's
231
+ record (e.g. a lane-journal entry handed to an intent-ledger reader); the
232
+ caller skips it rather than misreading it as its own.
233
+ * **UNTAGGED** — no tag, or a malformed one. The legacy floor: records that
234
+ predate the tag contract, or a torn write. The caller decides — a tolerant
235
+ replay over a file that mixes pre-tag and tagged records treats UNTAGGED as
236
+ the family's implicit v1 (and `is_soundly_readable` stays False so the
237
+ decision is never implicit). A `family=""` ceiling reader (the legacy
238
+ bare-int `home.SCHEMA` case) treats a bare-int tag of the right version as
239
+ READABLE — the back-compat bridge.
240
+
241
+ Conservative on every unknown — the safe direction for a *durability* guard, the
242
+ `WorkspaceFacts(None)`-is-conservative and `git_delta`-degrades-to-empty rule:
243
+ when in doubt about whether a record is soundly readable, do not claim it is.
244
+ """
245
+ parsed = SchemaTag.from_obj(record.get(SCHEMA_KEY))
246
+ if parsed is None:
247
+ return ReadabilityVerdict(
248
+ readability=Readability.UNTAGGED,
249
+ reason=(
250
+ f"record carries no {SCHEMA_KEY!r} tag (or a malformed one) — "
251
+ f"a pre-tag/legacy or torn record; caller decides (treated as "
252
+ f"{family!r} v1 by a tolerant fold, refused by a strict reader)"
253
+ ),
254
+ family=family,
255
+ ceiling=understands,
256
+ tag=None,
257
+ )
258
+ # Family match. A reader declaring family "" is the legacy bare-int gate: it
259
+ # accepts a bare-int (family-"") tag, and ALSO any family at its version (it is
260
+ # the "I only care about the integer version" reader). A named-family reader
261
+ # accepts ONLY its own family; a bare-int tag (family "") handed to a named
262
+ # reader is treated as that reader's family by version (the home.SCHEMA bridge:
263
+ # a legacy untyped record is the named surface's record at that version).
264
+ if parsed.family and family and parsed.family != family:
265
+ return ReadabilityVerdict(
266
+ readability=Readability.WRONG_FAMILY,
267
+ reason=(
268
+ f"record declares family {parsed.family!r} but this reader reads "
269
+ f"{family!r} — not this reader's record; skip it"
270
+ ),
271
+ family=family,
272
+ ceiling=understands,
273
+ tag=parsed,
274
+ )
275
+ if parsed.version > understands:
276
+ return ReadabilityVerdict(
277
+ readability=Readability.UNREADABLE_NEWER,
278
+ reason=(
279
+ f"record is {family or parsed.family!r} v{parsed.version} but this "
280
+ f"kernel reads ≤ v{understands} — a non-additive format change this "
281
+ f"version predates; REFUSING to guess (run the explicit migration "
282
+ f"fold, never a best-effort parse)"
283
+ ),
284
+ family=family,
285
+ ceiling=understands,
286
+ tag=parsed,
287
+ )
288
+ return ReadabilityVerdict(
289
+ readability=Readability.READABLE,
290
+ reason=(
291
+ f"{family or parsed.family!r} v{parsed.version} ≤ ceiling v{understands} "
292
+ f"— soundly readable"
293
+ ),
294
+ family=family,
295
+ ceiling=understands,
296
+ tag=parsed,
297
+ )
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # The structured-refusal surface (docs/115 primitive 4). The refuse-don't-guess
302
+ # floor above is a per-reader READ GATE; this is the token + wire shape that lets
303
+ # it surface through the kernel's CLOSED refusal vocabulary, carrying the supported
304
+ # set the way MCP's `UnsupportedProtocolVersionError(-32004)` carries
305
+ # `{supported, requested}` (a normative MUST DOS's durable_schema predates).
306
+ # ---------------------------------------------------------------------------
307
+
308
+ # The `reason_class` token a refusal carries when a durable record is unreadable
309
+ # because its schema version is newer than this kernel understands. Declared in
310
+ # `dos.reasons.BASE_REASONS` (category MISROUTE — a record this kernel can't soundly
311
+ # parse is work to route elsewhere, the SELF_MODIFY sibling), so it is emittable /
312
+ # verifiable / refusable / `dos man wedge`-documented like every other refuse.
313
+ SCHEMA_UNREADABLE_REASON = "SCHEMA_UNREADABLE"
314
+
315
+
316
+ def unreadable_refusal_payload(verdict: "ReadabilityVerdict") -> dict:
317
+ """Render an UNREADABLE_NEWER verdict as the MCP `{supported, requested}` shape.
318
+
319
+ PURE. Turns the read-gate's `ReadabilityVerdict` into the structured-refusal
320
+ payload a caller (a resuming successor, a cross-version fleet member, the MCP
321
+ server) gets WITH the refusal, so it can re-negotiate or migrate instead of
322
+ failing blind. The shape mirrors MCP's `-32004` body:
323
+
324
+ * ``reason_class`` — the closed-vocabulary token (``SCHEMA_UNREADABLE``);
325
+ * ``family`` — which durable surface the record belongs to;
326
+ * ``requested`` — the record's own declared version (what it needs);
327
+ * ``supported`` — ``[1 .. ceiling]``, the versions THIS kernel can read
328
+ (the "supported set" — MCP returns the same so the caller
329
+ knows what to fall back to);
330
+ * ``detail`` — the legible one-liner (`ReadabilityVerdict.reason`).
331
+
332
+ Defensive on the non-newer cases (a caller should only render this for an
333
+ UNREADABLE_NEWER verdict): `requested` falls back to the ceiling when the
334
+ record carried no parseable tag, so the payload is always well-formed.
335
+ """
336
+ requested = verdict.tag.version if verdict.tag is not None else verdict.ceiling
337
+ supported = list(range(1, verdict.ceiling + 1))
338
+ return {
339
+ "reason_class": SCHEMA_UNREADABLE_REASON,
340
+ "family": verdict.family,
341
+ "requested": requested,
342
+ "supported": supported,
343
+ "detail": verdict.reason,
344
+ }