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.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- 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
|
+
}
|