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/reason_morphology.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""The reason-class *morphology* — rung 2 of the recognizer ladder, as data.
|
|
2
|
+
|
|
3
|
+
This is the hackability seam that closes the deeper half of the picker oracle's
|
|
4
|
+
blind spot (`docs/105`). `picker_oracle.resolve_cause` maps an emitted
|
|
5
|
+
`reason_class` token onto a closed `NoPickCause` so the oracle can grade a
|
|
6
|
+
NO-PICK. Its first two rungs are **exact string match** — the frozen
|
|
7
|
+
`REASON_CLASS_MAP` (the closed `wedge_reason` enum) and the workspace
|
|
8
|
+
`ReasonRegistry` (`dos.toml [reasons]`). But `reason_class` tokens are
|
|
9
|
+
*LLM-authored compounds* — an open, effectively-infinite set
|
|
10
|
+
(`PLAN_ID_COLLISION_FALSE_SHIPPED`,
|
|
11
|
+
`DL_LANE_STALE_STAMP_BODY_VS_META_DRIFT_PLUS_SHIP_ORACLE_SERIES_COLLISION`, …).
|
|
12
|
+
Exact equality over an infinite generator is brittle by construction: every novel
|
|
13
|
+
compound falls to `UNCLASSIFIED`, even when its category is obvious.
|
|
14
|
+
|
|
15
|
+
The tell measured on `job`'s corpus (`docs/105` §1): each token's **category is
|
|
16
|
+
legible in its morphology** — `*FALSE_SHIP*` is a stale-claim shape, `*OPERATOR*`
|
|
17
|
+
is an operator-gate, `*INFLIGHT*` is a stale-claim (the inflight alias), `*SOAK*`
|
|
18
|
+
/`*GATE*` is a gate. So this module is the **rung-2 recognizer**: an ordered set
|
|
19
|
+
of `(substring → category)` rules, declared as DATA, that classifies the legible
|
|
20
|
+
tail the exact rungs miss. It is the direct analogue of `stamp.StampConvention` —
|
|
21
|
+
the host declares *what a stale-claim token looks like in its dialect*; the kernel
|
|
22
|
+
keeps the closed `NoPickCause` set and every cross-check downstream of it
|
|
23
|
+
(`docs/76` §3's line, applied to the reason-class recognizer instead of the
|
|
24
|
+
ship-subject grammar).
|
|
25
|
+
|
|
26
|
+
The category strings a rule may emit are exactly `reasons.KNOWN_CATEGORIES`
|
|
27
|
+
(`TRUE_DRAIN` / `OPERATOR_GATE` / `STALE_CLAIM` / `MISROUTE` / `UNCLASSIFIED`),
|
|
28
|
+
which equal the `NoPickCause` member values by construction. A rule emitting
|
|
29
|
+
anything else is a host mistake surfaced loudly at load.
|
|
30
|
+
|
|
31
|
+
Pure stdlib — no third-party imports, no I/O — so `picker_oracle` imports it as a
|
|
32
|
+
leaf, the same way it imports `stamp` / `wedge_reason`. The aliases the GENERIC
|
|
33
|
+
ruleset encodes mirror the ones already documented in `reasons.py`'s built-in
|
|
34
|
+
`BASE_REASONS` table (`INFLIGHT → STALE_CLAIM`, `SOAK/LEASE_HELD → OPERATOR_GATE`),
|
|
35
|
+
so the morphological rung and the exact rung agree on category by construction.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# The category vocabulary a morphology rule may emit. Kept as a module constant
|
|
45
|
+
# (not imported from `reasons`/`picker_oracle`) so this leaf stays pure-stdlib and
|
|
46
|
+
# dependency-free; it is pinned EQUAL to `reasons.KNOWN_CATEGORIES` by
|
|
47
|
+
# `tests/test_reason_morphology.py` so the two can never drift. `UNCLASSIFIED` is
|
|
48
|
+
# included so a rule may *deliberately* map a recognized-but-undecidable shape to
|
|
49
|
+
# the honest floor (rare, but legal).
|
|
50
|
+
KNOWN_CATEGORIES: frozenset[str] = frozenset({
|
|
51
|
+
"TRUE_DRAIN", "OPERATOR_GATE", "STALE_CLAIM", "MISROUTE", "UNCLASSIFIED",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class MorphologyRule:
|
|
57
|
+
"""One ``(substring → category)`` rule of the rung-2 recognizer.
|
|
58
|
+
|
|
59
|
+
Fields:
|
|
60
|
+
substring
|
|
61
|
+
A case-insensitive substring of the reason-class token. Matched with a
|
|
62
|
+
plain ``in`` (uppercased on both sides) — NOT a regex — so a host
|
|
63
|
+
declares a token *shape* (``FALSE_SHIP``), never a pattern. Empty is a
|
|
64
|
+
host mistake (it would match everything); rejected at construction of
|
|
65
|
+
the ruleset.
|
|
66
|
+
category
|
|
67
|
+
The `NoPickCause` value-string this shape maps to. Must be one of
|
|
68
|
+
``KNOWN_CATEGORIES``; anything else raises at ruleset construction (the
|
|
69
|
+
closed-enum discipline — a typo'd category is surfaced, not silently
|
|
70
|
+
carried as an unknown that resolves to UNCLASSIFIED anyway).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
substring: str
|
|
74
|
+
category: str
|
|
75
|
+
|
|
76
|
+
def matches(self, token_upper: str) -> bool:
|
|
77
|
+
"""True iff this rule's (uppercased) substring occurs in ``token_upper``.
|
|
78
|
+
|
|
79
|
+
The caller passes the already-uppercased token so a ruleset scan does not
|
|
80
|
+
re-uppercase per rule. Pure."""
|
|
81
|
+
return self.substring.upper() in token_upper
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class MorphologyRuleset:
|
|
86
|
+
"""An ORDERED set of `MorphologyRule`s — the rung-2 recognizer, as data.
|
|
87
|
+
|
|
88
|
+
Order is load-bearing: `classify` returns the FIRST matching rule's category
|
|
89
|
+
(first-match-wins), so a more-specific shape must precede a more-general one
|
|
90
|
+
(e.g. ``STALE_STAMP`` before a bare ``STAMP``, or ``INFLIGHT`` before ``GATE``
|
|
91
|
+
for a token like ``LANE_ALL_SHIPPED_INFLIGHT_OR_SOAK_GATED`` where the author's
|
|
92
|
+
intent is the in-flight state, not the gate). Because the rung *reports itself*
|
|
93
|
+
(`picker_oracle` records ``cause_source="morphological"`` and the matched
|
|
94
|
+
rule), the precedence a given order encodes is auditable, never buried.
|
|
95
|
+
|
|
96
|
+
Construction validates every rule (non-empty substring, known category) so a
|
|
97
|
+
malformed ruleset fails at load — the same loud-on-malformed posture
|
|
98
|
+
`stamp.convention_from_table` takes.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
rules: tuple[MorphologyRule, ...] = ()
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
for r in self.rules:
|
|
105
|
+
if not r.substring or not r.substring.strip():
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"MorphologyRule.substring must be a non-empty string "
|
|
108
|
+
"(an empty substring would match every token)"
|
|
109
|
+
)
|
|
110
|
+
if r.category not in KNOWN_CATEGORIES:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"MorphologyRule.category {r.category!r} is not a known "
|
|
113
|
+
f"category; must be one of {sorted(KNOWN_CATEGORIES)}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def classify(self, token: str | None) -> tuple[str, str] | None:
|
|
117
|
+
"""Return ``(category, matched_substring)`` for the first matching rule.
|
|
118
|
+
|
|
119
|
+
Returns ``None`` when no rule matches (the caller then falls through to the
|
|
120
|
+
``UNCLASSIFIED`` floor — rung 3). The matched substring is returned so the
|
|
121
|
+
caller can record *which* rule fired (the self-reporting `docs/105` §3.1
|
|
122
|
+
requires). Pure; ``None``/empty token → ``None``.
|
|
123
|
+
"""
|
|
124
|
+
if not token:
|
|
125
|
+
return None
|
|
126
|
+
token_upper = token.upper().strip()
|
|
127
|
+
if not token_upper:
|
|
128
|
+
return None
|
|
129
|
+
for r in self.rules:
|
|
130
|
+
if r.matches(token_upper):
|
|
131
|
+
return (r.category, r.substring.upper())
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# -- serialization (parity with StampConvention; crosses no subprocess today
|
|
135
|
+
# but kept symmetric so a future grep-rung-style boundary is free) --------
|
|
136
|
+
def to_list(self) -> list[dict]:
|
|
137
|
+
"""Plain-data form — a JSON-serializable ordered list of rule dicts."""
|
|
138
|
+
return [{"substring": r.substring, "category": r.category} for r in self.rules]
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_list(cls, data: object) -> "MorphologyRuleset":
|
|
142
|
+
"""Rebuild from `to_list` form. Tolerant of a missing/empty value (→ empty
|
|
143
|
+
ruleset); a malformed entry raises via `__post_init__` / `_rule_from`."""
|
|
144
|
+
if not data:
|
|
145
|
+
return cls(())
|
|
146
|
+
if not isinstance(data, (list, tuple)):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"[reasons.morphology] must be a list of rules, got "
|
|
149
|
+
f"{type(data).__name__}"
|
|
150
|
+
)
|
|
151
|
+
return cls(tuple(_rule_from(item) for item in data))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _rule_from(item: object) -> MorphologyRule:
|
|
155
|
+
"""Coerce one parsed TOML/JSON entry into a `MorphologyRule`, raising on shape.
|
|
156
|
+
|
|
157
|
+
Accepts a ``{substring, category}`` table (the canonical form) or a
|
|
158
|
+
two-element ``[substring, category]`` pair (a terse TOML form). Anything else
|
|
159
|
+
is a host mistake surfaced loudly, mirroring `stamp._str_tuple`'s posture.
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(item, dict):
|
|
162
|
+
try:
|
|
163
|
+
sub = item["substring"]
|
|
164
|
+
cat = item["category"]
|
|
165
|
+
except KeyError as e:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"[reasons.morphology] rule {item!r} is missing required key {e}"
|
|
168
|
+
) from None
|
|
169
|
+
elif isinstance(item, (list, tuple)) and len(item) == 2:
|
|
170
|
+
sub, cat = item[0], item[1]
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"[reasons.morphology] rule must be a {{substring, category}} table or "
|
|
174
|
+
f"a [substring, category] pair, got {item!r}"
|
|
175
|
+
)
|
|
176
|
+
if not isinstance(sub, str) or not isinstance(cat, str):
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"[reasons.morphology] rule {item!r}: substring and category must both "
|
|
179
|
+
f"be strings"
|
|
180
|
+
)
|
|
181
|
+
# `MorphologyRule` is validated for emptiness/known-category by the ruleset's
|
|
182
|
+
# __post_init__; build it here and let that fire.
|
|
183
|
+
return MorphologyRule(substring=sub, category=cat)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# The GENERIC, domain-free default ruleset — the kernel ships this so EVERY host
|
|
188
|
+
# gets the legible-tail recovery out of the box, with NO host lanes baked in
|
|
189
|
+
# (`APPLY_LANE_*` / `TAILOR_LANE_*` are a host's; they are NOT here — a host
|
|
190
|
+
# declares those via `dos.toml [reasons.morphology]`). Every substring below is a
|
|
191
|
+
# DOMAIN-FREE distrust shape, and every category matches the alias intuitions
|
|
192
|
+
# already encoded in `reasons.BASE_REASONS` (so rung-2 agrees with rung-1).
|
|
193
|
+
#
|
|
194
|
+
# Order is most-specific-first. The category for each shape:
|
|
195
|
+
# *FALSE_SHIP* / *FALSE_SHIPPED* -> STALE_CLAIM (claims-shipped-but-not: the
|
|
196
|
+
# diagnostic picker-bug shape — a stamp the
|
|
197
|
+
# ship oracle would refuse)
|
|
198
|
+
# *STALE_STAMP* / *STAMP_DRIFT* -> STALE_CLAIM (a stamp present but stale)
|
|
199
|
+
# *OPERATOR* -> OPERATOR_GATE (operator-attended / decision-pending)
|
|
200
|
+
# *SOAK* -> OPERATOR_GATE (soak window open)
|
|
201
|
+
# *INFLIGHT* / *IN_FLIGHT* -> STALE_CLAIM (the inflight alias — matches
|
|
202
|
+
# reasons.py: LANE_ALL_INFLIGHT_* -> STALE_CLAIM)
|
|
203
|
+
# *LEASE_HELD* -> OPERATOR_GATE (held by a live loop — the LEASE_HELD alias)
|
|
204
|
+
# *GATE* -> OPERATOR_GATE (a generic gate; AFTER soak/inflight
|
|
205
|
+
# so a *...INFLIGHT...GATE* token reads as inflight)
|
|
206
|
+
# *DRAIN* / *DRAINED* -> TRUE_DRAIN (the lane is drained — genuinely nothing)
|
|
207
|
+
# *NO_DISPATCHABLE* / *NOTHING* -> TRUE_DRAIN (no dispatchable phase)
|
|
208
|
+
# *MISROUTE* / *MIS_ROUT* -> MISROUTE (finding routed to the wrong lane)
|
|
209
|
+
#
|
|
210
|
+
# Note `STALE_STAMP` precedes nothing it would shadow, but `DRAIN` is placed AFTER
|
|
211
|
+
# the stamp/inflight shapes so `STALE_STAMP_LANE_DRAINED` reads as STALE_CLAIM (the
|
|
212
|
+
# stamp is the salient defect), not TRUE_DRAIN. This precedence is the auditable
|
|
213
|
+
# judgment `docs/105` §3.2 calls out; it is DATA precisely so a host can reorder it.
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
GENERIC_REASON_MORPHOLOGY = MorphologyRuleset((
|
|
216
|
+
MorphologyRule("FALSE_SHIPPED", "STALE_CLAIM"),
|
|
217
|
+
MorphologyRule("FALSE_SHIP", "STALE_CLAIM"),
|
|
218
|
+
MorphologyRule("STALE_STAMP", "STALE_CLAIM"),
|
|
219
|
+
MorphologyRule("STAMP_DRIFT", "STALE_CLAIM"),
|
|
220
|
+
MorphologyRule("STAMPING_DRIFT", "STALE_CLAIM"),
|
|
221
|
+
MorphologyRule("OPERATOR", "OPERATOR_GATE"),
|
|
222
|
+
MorphologyRule("SOAK", "OPERATOR_GATE"),
|
|
223
|
+
MorphologyRule("INFLIGHT", "STALE_CLAIM"),
|
|
224
|
+
MorphologyRule("IN_FLIGHT", "STALE_CLAIM"),
|
|
225
|
+
MorphologyRule("LEASE_HELD", "OPERATOR_GATE"),
|
|
226
|
+
MorphologyRule("GATE", "OPERATOR_GATE"),
|
|
227
|
+
MorphologyRule("DRAINED", "TRUE_DRAIN"),
|
|
228
|
+
MorphologyRule("DRAIN", "TRUE_DRAIN"),
|
|
229
|
+
MorphologyRule("NO_DISPATCHABLE", "TRUE_DRAIN"),
|
|
230
|
+
MorphologyRule("MISROUTE", "MISROUTE"),
|
|
231
|
+
MorphologyRule("MIS_ROUT", "MISROUTE"),
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# The EMPTY ruleset — an explicit "no morphological rung" value. A workspace that
|
|
236
|
+
# wants only the two exact rungs (the pre-`docs/105` behavior) can set this, and a
|
|
237
|
+
# `[reasons.morphology] = []` table degrades to it. Distinct from
|
|
238
|
+
# GENERIC_REASON_MORPHOLOGY: empty means "rung 2 is off", generic means "rung 2 with
|
|
239
|
+
# the kernel's domain-free defaults".
|
|
240
|
+
NO_REASON_MORPHOLOGY = MorphologyRuleset(())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# The declarative on-ramp: read a `[reasons.morphology]` list out of dos.toml.
|
|
245
|
+
# Mirrors `stamp.load_from_toml` / `reasons.load_from_toml`: a present list
|
|
246
|
+
# OVERRIDES the base (declaring your morphology means declaring it, not appending
|
|
247
|
+
# to the kernel's); absent/empty degrades to the base; present-but-malformed
|
|
248
|
+
# raises. The TOML shape (an array of tables, the natural TOML form for an ordered
|
|
249
|
+
# list of rules):
|
|
250
|
+
#
|
|
251
|
+
# [[reasons.morphology]]
|
|
252
|
+
# substring = "APPLY_LANE_BLOCKED_MESH" # a host's own shape
|
|
253
|
+
# category = "OPERATOR_GATE"
|
|
254
|
+
#
|
|
255
|
+
# [[reasons.morphology]]
|
|
256
|
+
# substring = "RESPAWN"
|
|
257
|
+
# category = "TRUE_DRAIN"
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def load_from_toml(
|
|
262
|
+
path: Path | str, *, base: MorphologyRuleset = GENERIC_REASON_MORPHOLOGY
|
|
263
|
+
) -> MorphologyRuleset:
|
|
264
|
+
"""Build a `MorphologyRuleset` from a `dos.toml`'s `[reasons.morphology]` list.
|
|
265
|
+
|
|
266
|
+
Returns ``base`` unchanged when the file is absent, has no
|
|
267
|
+
`[reasons.morphology]` key, or `tomllib` is unavailable — the declarative path
|
|
268
|
+
is additive, so a missing config degrades to the supplied base (the kernel's
|
|
269
|
+
generic ruleset), never an error. A *present but malformed* list raises
|
|
270
|
+
(`from_list` / `_rule_from`), because a host that declared its morphology wrong
|
|
271
|
+
wants it surfaced. Mirrors `stamp.load_from_toml` exactly.
|
|
272
|
+
|
|
273
|
+
Override (not merge) semantics: a host that declares a `[reasons.morphology]`
|
|
274
|
+
gets EXACTLY its rules, not its rules plus the kernel's generic ones. To keep
|
|
275
|
+
the generic shapes a host re-lists the ones it wants — declaring your
|
|
276
|
+
morphology means declaring it. (A future `extend = true` key could add merge
|
|
277
|
+
semantics; out of scope for `docs/105`.)
|
|
278
|
+
"""
|
|
279
|
+
p = Path(path)
|
|
280
|
+
if not p.exists():
|
|
281
|
+
return base
|
|
282
|
+
try:
|
|
283
|
+
import tomllib # py3.11+
|
|
284
|
+
except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
|
|
285
|
+
try:
|
|
286
|
+
import tomli as tomllib # type: ignore
|
|
287
|
+
except ModuleNotFoundError:
|
|
288
|
+
return base
|
|
289
|
+
data = tomllib.loads(p.read_text(encoding="utf-8-sig"))
|
|
290
|
+
reasons_tbl = data.get("reasons")
|
|
291
|
+
if not isinstance(reasons_tbl, dict):
|
|
292
|
+
return base
|
|
293
|
+
morph = reasons_tbl.get("morphology")
|
|
294
|
+
if morph is None:
|
|
295
|
+
return base
|
|
296
|
+
# An explicit empty list means "turn rung 2 off" → NO_REASON_MORPHOLOGY, not base.
|
|
297
|
+
if isinstance(morph, (list, tuple)) and len(morph) == 0:
|
|
298
|
+
return NO_REASON_MORPHOLOGY
|
|
299
|
+
return MorphologyRuleset.from_list(morph)
|