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/trace.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""`dos trace <run_id>` — the cross-surface join, as a read-only projection (docs/137).
|
|
2
|
+
|
|
3
|
+
DOS has a correlation **spine** (`run_id` + `parent_id`/`root_id`, sortable,
|
|
4
|
+
lineage-carrying — `run_id.py`, docs/64) and three durable surfaces that compose:
|
|
5
|
+
the WAL (`lane_journal`), the intent ledger (`intent.jsonl`, docs/107), and git.
|
|
6
|
+
What it lacked was a **verb to walk from one part to the others** — "show me
|
|
7
|
+
everything this run touched: its lineage, the lanes it held / was refused, the
|
|
8
|
+
steps it claimed vs the kernel verified, the commits it landed." This module is
|
|
9
|
+
that walk.
|
|
10
|
+
|
|
11
|
+
It is a **read-only projection**, never a store — the same posture as
|
|
12
|
+
`dos decisions` / `dos top` / `dos plan` (`decisions.py`'s module doc states the
|
|
13
|
+
contract): it stores nothing of its own, takes no lease, mints no belief, and
|
|
14
|
+
adjudicates *nothing new*. `build_trace` joins surfaces that already persist their
|
|
15
|
+
own truth, by the id that already exists (`run_id`) — it does NOT mint a second
|
|
16
|
+
parallel `correlation_id`/`trace_id`/UUID (that would be a second spine to keep in
|
|
17
|
+
sync, the `memory-is-an-unverified-agent` failure at the identity layer). Delete
|
|
18
|
+
this module and you lose the reader, not any data.
|
|
19
|
+
|
|
20
|
+
**The join keys are the existing ones, nothing fabricated** (docs/137 non-goal):
|
|
21
|
+
|
|
22
|
+
run_id spine ↔ intent ledger ↔ WAL (after docs/118 Size S stamped it
|
|
23
|
+
onto the ACQUIRE — the *grant* side, completing what the
|
|
24
|
+
*refuse* side already had)
|
|
25
|
+
(loop_ts, lane) within the WAL (a lease's identity)
|
|
26
|
+
SHA intent ledger (`start_sha`, `STEP_VERIFIED.sha`) ↔ git
|
|
27
|
+
|
|
28
|
+
**The honesty discipline is inherited verbatim** (docs/103 / docs/118): the
|
|
29
|
+
*adjudicated* columns (verified steps, commits, refusals) come from git ancestry +
|
|
30
|
+
the WAL, never the agent's self-report; `claimed` is shown as *believed* and
|
|
31
|
+
labelled as such, beside the verified column, with the residual (declared −
|
|
32
|
+
verified) visible. A lease event that carries no `run_id` (a pre-Build-1 ACQUIRE,
|
|
33
|
+
or a writer that didn't pass one) is surfaced under an explicit `(unattributed)`
|
|
34
|
+
note — NEVER silently dropped and NEVER guessed onto this run by a time window
|
|
35
|
+
(the docs/118 "fail toward no-match" rule). `trace` over a workspace that never
|
|
36
|
+
journaled with run-ids therefore honestly reports "0 attributable lease events"
|
|
37
|
+
rather than a fabricated lane list.
|
|
38
|
+
|
|
39
|
+
Pure-stdlib, read-only — the readers (`run_id.read_run_json`,
|
|
40
|
+
`intent_ledger.read_all`, `lane_journal.read_all`, `git_delta.commits_since`) are
|
|
41
|
+
the only I/O, reused verbatim so `trace` defines no new read; the assembly +
|
|
42
|
+
ranking + render are pure and are the unit-test surface (mirrors
|
|
43
|
+
`decisions.collect_decisions` / `timeline.build_timeline`).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import io
|
|
49
|
+
import sys
|
|
50
|
+
from dataclasses import dataclass, field
|
|
51
|
+
|
|
52
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
53
|
+
try:
|
|
54
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
55
|
+
except Exception: # pragma: no cover
|
|
56
|
+
pass
|
|
57
|
+
elif not isinstance(sys.stdout, io.TextIOWrapper): # pragma: no cover
|
|
58
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
from dos import config as _config
|
|
61
|
+
from dos import git_delta as _git_delta
|
|
62
|
+
from dos import intent_ledger as _intent
|
|
63
|
+
from dos import lane_journal as _lj
|
|
64
|
+
from dos import run_id as _run_id
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# The joined value objects — each a pure projection of one surface, plus the
|
|
69
|
+
# frame that carries all four. Frozen + `to_dict()` so `--json` round-trips
|
|
70
|
+
# (mirrors `decisions.Decision`).
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class LeaseEvent:
|
|
76
|
+
"""One WAL event attributed to this run — a row in the lanes column.
|
|
77
|
+
|
|
78
|
+
`op` is the journal op (ACQUIRE/RELEASE/HEARTBEAT/SCAVENGE/REFUSE/HALT);
|
|
79
|
+
`lane` + `loop_ts` are the `(loop_ts, lane)` lease identity; `reason` is the
|
|
80
|
+
recorded prose; `attributed_by` names HOW this event was tied to the run —
|
|
81
|
+
`lease.run_id` (the docs/118 grant-side join), `entry.run_id` (a refuse/halt
|
|
82
|
+
that carried it), so a reader can see the join is real, not a time guess.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
op: str
|
|
86
|
+
lane: str
|
|
87
|
+
loop_ts: str
|
|
88
|
+
ts: str
|
|
89
|
+
reason: str
|
|
90
|
+
attributed_by: str # "lease.run_id" | "entry.run_id"
|
|
91
|
+
reason_class: str = ""
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict:
|
|
94
|
+
return {
|
|
95
|
+
"op": self.op,
|
|
96
|
+
"lane": self.lane,
|
|
97
|
+
"loop_ts": self.loop_ts,
|
|
98
|
+
"ts": self.ts,
|
|
99
|
+
"reason": self.reason,
|
|
100
|
+
"reason_class": self.reason_class,
|
|
101
|
+
"attributed_by": self.attributed_by,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class StepRow:
|
|
107
|
+
"""One declared step, with its believed (claimed) vs adjudicated (verified) state.
|
|
108
|
+
|
|
109
|
+
The epistemic surface (docs/107 §3.2): `claimed_sha` is the agent's distrusted
|
|
110
|
+
self-report; `verified_sha`/`verified_via` is the kernel's minted belief (the
|
|
111
|
+
only "done"). `state` is the folded verdict: VERIFIED (kernel-confirmed),
|
|
112
|
+
CLAIMED (self-reported but not confirmed — the residual a resume would re-do),
|
|
113
|
+
or PENDING (declared, no claim yet).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
step_id: str
|
|
117
|
+
state: str # "VERIFIED" | "CLAIMED" | "PENDING"
|
|
118
|
+
claimed_sha: str = ""
|
|
119
|
+
verified_sha: str = ""
|
|
120
|
+
verified_via: str = ""
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict:
|
|
123
|
+
return {
|
|
124
|
+
"step_id": self.step_id,
|
|
125
|
+
"state": self.state,
|
|
126
|
+
"claimed_sha": self.claimed_sha,
|
|
127
|
+
"verified_sha": self.verified_sha,
|
|
128
|
+
"verified_via": self.verified_via,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class TraceFrame:
|
|
134
|
+
"""The full cross-surface join for one run — what every reader/renderer consumes.
|
|
135
|
+
|
|
136
|
+
Assembled by `build_trace`. Carries the four surfaces side by side:
|
|
137
|
+
* spine — the run's own id + lineage (parent/root/process), + the
|
|
138
|
+
ancestors/descendants found by a `root_id` scan.
|
|
139
|
+
* intent — the declared goal/plan/phase + `start_sha`, and the step rows
|
|
140
|
+
(claimed-vs-verified, the residual).
|
|
141
|
+
* wal — the lease events attributed to this run + the count of
|
|
142
|
+
ACQUIREs seen on the workspace that carried NO run_id
|
|
143
|
+
(`unattributed_acquires`, the honest "couldn't join" tally).
|
|
144
|
+
* git — the commits since `start_sha` (the forward git delta).
|
|
145
|
+
Every list is empty-on-missing-surface (the reader degrades, never crashes).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
run_id: str
|
|
149
|
+
found: bool # was there ANY surface for this run_id?
|
|
150
|
+
# spine
|
|
151
|
+
run_json: dict = field(default_factory=dict)
|
|
152
|
+
parent_id: str = ""
|
|
153
|
+
root_id: str = ""
|
|
154
|
+
process_id: str = ""
|
|
155
|
+
ancestors: tuple[str, ...] = () # run_ids up the lineage (parent → root)
|
|
156
|
+
descendants: tuple[str, ...] = () # run_ids whose parent chain reaches this one
|
|
157
|
+
# intent
|
|
158
|
+
has_intent: bool = False
|
|
159
|
+
goal: str = ""
|
|
160
|
+
plan: str = ""
|
|
161
|
+
phase: str = ""
|
|
162
|
+
start_sha: str = ""
|
|
163
|
+
steps: tuple[StepRow, ...] = ()
|
|
164
|
+
corrupt_ledger_lines: int = 0
|
|
165
|
+
unreadable_newer: bool = False
|
|
166
|
+
# wal
|
|
167
|
+
lease_events: tuple[LeaseEvent, ...] = ()
|
|
168
|
+
unattributed_acquires: int = 0
|
|
169
|
+
# git
|
|
170
|
+
commits: tuple[dict, ...] = ()
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def residual(self) -> tuple[str, ...]:
|
|
174
|
+
"""Declared steps NOT kernel-verified — the resume residual (docs/107)."""
|
|
175
|
+
return tuple(s.step_id for s in self.steps if s.state != "VERIFIED")
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict:
|
|
178
|
+
return {
|
|
179
|
+
"run_id": self.run_id,
|
|
180
|
+
"found": self.found,
|
|
181
|
+
"spine": {
|
|
182
|
+
"run_json": self.run_json,
|
|
183
|
+
"parent_id": self.parent_id,
|
|
184
|
+
"root_id": self.root_id,
|
|
185
|
+
"process_id": self.process_id,
|
|
186
|
+
"ancestors": list(self.ancestors),
|
|
187
|
+
"descendants": list(self.descendants),
|
|
188
|
+
},
|
|
189
|
+
"intent": {
|
|
190
|
+
"has_intent": self.has_intent,
|
|
191
|
+
"goal": self.goal,
|
|
192
|
+
"plan": self.plan,
|
|
193
|
+
"phase": self.phase,
|
|
194
|
+
"start_sha": self.start_sha,
|
|
195
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
196
|
+
"residual": list(self.residual),
|
|
197
|
+
"corrupt_ledger_lines": self.corrupt_ledger_lines,
|
|
198
|
+
"unreadable_newer": self.unreadable_newer,
|
|
199
|
+
},
|
|
200
|
+
"wal": {
|
|
201
|
+
"lease_events": [e.to_dict() for e in self.lease_events],
|
|
202
|
+
"unattributed_acquires": self.unattributed_acquires,
|
|
203
|
+
},
|
|
204
|
+
"git": {"commits": list(self.commits)},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Surface readers — each returns its slice; all degrade to empty on a missing /
|
|
210
|
+
# malformed surface (the `decisions.py` defensive-loader posture).
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _read_spine(run_id: str, cfg) -> dict:
|
|
215
|
+
"""The run's own `run.json` (or {} if the run-dir/stamp is absent)."""
|
|
216
|
+
run_dir = _intent.run_dir_for(run_id, cfg=cfg)
|
|
217
|
+
return _run_id.read_run_json(run_dir) or {}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _scan_lineage(run_id: str, root_id: str, cfg) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
221
|
+
"""(ancestors, descendants) for `run_id` from a single scan of the run-dirs.
|
|
222
|
+
|
|
223
|
+
Reads every sibling run-dir's `run.json` once and builds the parent map, then:
|
|
224
|
+
* ancestors — walk `parent_id` up from this run to its root.
|
|
225
|
+
* descendants — every run whose ancestor chain passes through this run.
|
|
226
|
+
Pure over the materialized map. Cheap (one stat+read per run-dir) and OPTIONAL —
|
|
227
|
+
a missing runs/ dir yields ((), ()), the same empty degrade as every reader.
|
|
228
|
+
The `root_id` narrows the scan to this run's tree when set.
|
|
229
|
+
"""
|
|
230
|
+
runs_dir = cfg.paths.fanout_runs
|
|
231
|
+
if not runs_dir.exists():
|
|
232
|
+
return ((), ())
|
|
233
|
+
parent_of: dict[str, str] = {}
|
|
234
|
+
root_of: dict[str, str] = {}
|
|
235
|
+
try:
|
|
236
|
+
children = list(runs_dir.iterdir())
|
|
237
|
+
except OSError:
|
|
238
|
+
return ((), ())
|
|
239
|
+
for d in children:
|
|
240
|
+
if not d.is_dir():
|
|
241
|
+
continue
|
|
242
|
+
data = _run_id.read_run_json(d)
|
|
243
|
+
if not isinstance(data, dict):
|
|
244
|
+
continue
|
|
245
|
+
rid = str(data.get("run_id") or d.name)
|
|
246
|
+
parent_of[rid] = str(data.get("parent_id") or "")
|
|
247
|
+
root_of[rid] = str(data.get("root_id") or "")
|
|
248
|
+
|
|
249
|
+
# Ancestors: walk parent links up from this run (guard against a cycle).
|
|
250
|
+
ancestors: list[str] = []
|
|
251
|
+
seen: set[str] = {run_id}
|
|
252
|
+
cur = parent_of.get(run_id, "")
|
|
253
|
+
while cur and cur not in seen:
|
|
254
|
+
ancestors.append(cur)
|
|
255
|
+
seen.add(cur)
|
|
256
|
+
cur = parent_of.get(cur, "")
|
|
257
|
+
|
|
258
|
+
# Descendants: a run is below us iff walking ITS parent chain reaches us.
|
|
259
|
+
def _reaches(rid: str) -> bool:
|
|
260
|
+
hops = 0
|
|
261
|
+
cur = parent_of.get(rid, "")
|
|
262
|
+
local: set[str] = set()
|
|
263
|
+
while cur and cur not in local and hops < 10000:
|
|
264
|
+
if cur == run_id:
|
|
265
|
+
return True
|
|
266
|
+
local.add(cur)
|
|
267
|
+
cur = parent_of.get(cur, "")
|
|
268
|
+
hops += 1
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
descendants = sorted(
|
|
272
|
+
rid for rid in parent_of
|
|
273
|
+
if rid != run_id and _reaches(rid)
|
|
274
|
+
)
|
|
275
|
+
return (tuple(ancestors), tuple(descendants))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _read_intent(run_id: str, cfg) -> tuple[_intent.LedgerState, tuple[StepRow, ...]]:
|
|
279
|
+
"""Replay the run's intent ledger → (LedgerState, step rows).
|
|
280
|
+
|
|
281
|
+
The step rows fold claimed-vs-verified into a single per-step verdict: a step
|
|
282
|
+
in `verified` is VERIFIED (the minted belief); else in `claimed` is CLAIMED
|
|
283
|
+
(self-reported, not confirmed — the residual); else PENDING. Declared order is
|
|
284
|
+
preserved; a claimed/verified step NOT in `declared_steps` (a run that claimed
|
|
285
|
+
a step it never declared) is appended after, so nothing is hidden.
|
|
286
|
+
"""
|
|
287
|
+
entries = _intent.read_all(run_id, cfg=cfg)
|
|
288
|
+
state = _intent.replay(entries)
|
|
289
|
+
rows: list[StepRow] = []
|
|
290
|
+
seen: set[str] = set()
|
|
291
|
+
|
|
292
|
+
def _row_for(sid: str) -> StepRow:
|
|
293
|
+
v = state.verified.get(sid)
|
|
294
|
+
if v is not None:
|
|
295
|
+
return StepRow(step_id=sid, state="VERIFIED",
|
|
296
|
+
claimed_sha=state.claimed.get(sid, ""),
|
|
297
|
+
verified_sha=v.sha, verified_via=v.via)
|
|
298
|
+
if sid in state.claimed:
|
|
299
|
+
return StepRow(step_id=sid, state="CLAIMED",
|
|
300
|
+
claimed_sha=state.claimed.get(sid, ""))
|
|
301
|
+
return StepRow(step_id=sid, state="PENDING")
|
|
302
|
+
|
|
303
|
+
for sid in state.declared_steps:
|
|
304
|
+
rows.append(_row_for(sid))
|
|
305
|
+
seen.add(sid)
|
|
306
|
+
# Claimed/verified steps the run never declared — surface them, don't hide.
|
|
307
|
+
for sid in list(state.verified.keys()) + list(state.claimed.keys()):
|
|
308
|
+
if sid not in seen:
|
|
309
|
+
rows.append(_row_for(sid))
|
|
310
|
+
seen.add(sid)
|
|
311
|
+
return state, tuple(rows)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _read_wal(run_id: str, cfg) -> tuple[tuple[LeaseEvent, ...], int]:
|
|
315
|
+
"""(lease events attributed to this run, count of unattributed ACQUIREs).
|
|
316
|
+
|
|
317
|
+
Joins the WAL to the spine on `run_id`, two ways:
|
|
318
|
+
* a lease event (ACQUIRE/RELEASE/HEARTBEAT/SCAVENGE) whose reconstructed
|
|
319
|
+
live-lease carries `run_id` (the docs/118 grant-side join) — attributed
|
|
320
|
+
via "lease.run_id".
|
|
321
|
+
* a REFUSE/HALT entry whose top-level `run_id` matches — attributed via
|
|
322
|
+
"entry.run_id" (the refuse side already carried it).
|
|
323
|
+
|
|
324
|
+
An ACQUIRE seen on the workspace that carries NO `run_id` is COUNTED (so the
|
|
325
|
+
operator sees the join's coverage) but NEVER attributed to this run by time —
|
|
326
|
+
the docs/118 "fail toward no-match" rule. RELEASE/HEARTBEAT carry no lease body
|
|
327
|
+
of their own, so they are attributed via the live lease they fold against:
|
|
328
|
+
`replay` is what propagates the ACQUIRE's `run_id` onto the live lease, so we
|
|
329
|
+
match a RELEASE/HEARTBEAT by its `(loop_ts, lane)` identity belonging to a
|
|
330
|
+
run-attributed live lease.
|
|
331
|
+
"""
|
|
332
|
+
path = cfg.paths.lane_journal
|
|
333
|
+
try:
|
|
334
|
+
entries = _lj.read_all(path)
|
|
335
|
+
except Exception:
|
|
336
|
+
return ((), 0)
|
|
337
|
+
|
|
338
|
+
# First pass: which (loop_ts, lane) identities belong to THIS run, learned from
|
|
339
|
+
# any ACQUIRE that carries the run_id (on the nested lease or inline). This lets
|
|
340
|
+
# a later RELEASE/HEARTBEAT — which has no lease body — be attributed by identity.
|
|
341
|
+
identities: set[tuple[str, str]] = set()
|
|
342
|
+
unattributed_acquires = 0
|
|
343
|
+
for e in entries:
|
|
344
|
+
if str(e.get("op") or "") != _lj.OP_ACQUIRE:
|
|
345
|
+
continue
|
|
346
|
+
lease = e.get("lease") if isinstance(e.get("lease"), dict) else e
|
|
347
|
+
rid = str((lease.get("run_id") if isinstance(lease, dict) else "") or e.get("run_id") or "")
|
|
348
|
+
if rid == run_id:
|
|
349
|
+
identities.add(_lj._lease_identity(e))
|
|
350
|
+
elif not rid:
|
|
351
|
+
unattributed_acquires += 1
|
|
352
|
+
|
|
353
|
+
out: list[LeaseEvent] = []
|
|
354
|
+
for e in entries:
|
|
355
|
+
op = str(e.get("op") or "")
|
|
356
|
+
ts = str(e.get("ts") or "")
|
|
357
|
+
reason = str(e.get("reason") or "")
|
|
358
|
+
if op in (_lj.OP_REFUSE, _lj.OP_HALT):
|
|
359
|
+
# The refuse/halt side already carries run_id at the top level.
|
|
360
|
+
if str(e.get("run_id") or "") != run_id:
|
|
361
|
+
continue
|
|
362
|
+
out.append(LeaseEvent(
|
|
363
|
+
op=op, lane=str(e.get("lane") or ""),
|
|
364
|
+
loop_ts=str(e.get("loop_ts") or ""), ts=ts, reason=reason,
|
|
365
|
+
attributed_by="entry.run_id",
|
|
366
|
+
reason_class=str(e.get("reason_class") or ""),
|
|
367
|
+
))
|
|
368
|
+
continue
|
|
369
|
+
if op in (_lj.OP_ACQUIRE, _lj.OP_RELEASE, _lj.OP_HEARTBEAT,
|
|
370
|
+
_lj.OP_SCAVENGE):
|
|
371
|
+
ident = _lj._lease_identity(e)
|
|
372
|
+
if ident not in identities:
|
|
373
|
+
continue # not this run's lease (or an unattributed ACQUIRE)
|
|
374
|
+
out.append(LeaseEvent(
|
|
375
|
+
op=op, lane=ident[1], loop_ts=ident[0], ts=ts, reason=reason,
|
|
376
|
+
attributed_by="lease.run_id",
|
|
377
|
+
))
|
|
378
|
+
return (tuple(out), unattributed_acquires)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def build_trace(run_id: str, config=None) -> TraceFrame:
|
|
382
|
+
"""Assemble the full cross-surface join for `run_id` (docs/137). Read-only.
|
|
383
|
+
|
|
384
|
+
Joins the spine (`run.json` + lineage), the intent ledger (claimed-vs-verified),
|
|
385
|
+
the WAL (lease events attributed by `run_id`), and git (commits since
|
|
386
|
+
`start_sha`) — every join by an id that already exists, nothing fabricated.
|
|
387
|
+
`found` is True iff ANY surface had something for this run (a stamp, a ledger,
|
|
388
|
+
or a WAL event); a wholly unknown run_id yields `found=False` and empty slices.
|
|
389
|
+
"""
|
|
390
|
+
cfg = config if config is not None else _config.active()
|
|
391
|
+
|
|
392
|
+
spine = _read_spine(run_id, cfg)
|
|
393
|
+
parent_id = str(spine.get("parent_id") or "")
|
|
394
|
+
root_id = str(spine.get("root_id") or run_id)
|
|
395
|
+
process_id = str(spine.get("process_id") or "")
|
|
396
|
+
ancestors, descendants = _scan_lineage(run_id, root_id, cfg)
|
|
397
|
+
|
|
398
|
+
state, steps = _read_intent(run_id, cfg)
|
|
399
|
+
lease_events, unattributed = _read_wal(run_id, cfg)
|
|
400
|
+
commits = tuple(_git_delta.commits_since(state.start_sha, root=cfg.root)) \
|
|
401
|
+
if state.start_sha else ()
|
|
402
|
+
|
|
403
|
+
found = bool(spine) or state.has_intent or bool(lease_events) \
|
|
404
|
+
or bool(state.claimed) or bool(state.verified)
|
|
405
|
+
|
|
406
|
+
return TraceFrame(
|
|
407
|
+
run_id=run_id,
|
|
408
|
+
found=found,
|
|
409
|
+
run_json=spine,
|
|
410
|
+
parent_id=parent_id,
|
|
411
|
+
root_id=root_id,
|
|
412
|
+
process_id=process_id,
|
|
413
|
+
ancestors=ancestors,
|
|
414
|
+
descendants=descendants,
|
|
415
|
+
has_intent=state.has_intent,
|
|
416
|
+
goal=state.goal,
|
|
417
|
+
plan=state.plan,
|
|
418
|
+
phase=state.phase,
|
|
419
|
+
start_sha=state.start_sha,
|
|
420
|
+
steps=steps,
|
|
421
|
+
corrupt_ledger_lines=state.corrupt_lines,
|
|
422
|
+
unreadable_newer=state.unreadable_newer,
|
|
423
|
+
lease_events=lease_events,
|
|
424
|
+
unattributed_acquires=unattributed,
|
|
425
|
+
commits=commits,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
# Rendering — the plain text floor (provenance-first) + JSON.
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
_STEP_GLYPH = {"VERIFIED": "OK ", "CLAIMED": "?? ", "PENDING": "···"}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def render_text(t: TraceFrame) -> str:
|
|
438
|
+
"""The operator-facing walk: lineage → intent → lanes → commits.
|
|
439
|
+
|
|
440
|
+
Provenance-first: who the run is, what it tried, what it touched, what it
|
|
441
|
+
actually shipped — believed (claimed) beside adjudicated (verified). Reuses the
|
|
442
|
+
small-column idiom of `decisions.render_list_plain` / `timeline.render_text`.
|
|
443
|
+
"""
|
|
444
|
+
out: list[str] = []
|
|
445
|
+
out.append(f"# trace · {t.run_id}")
|
|
446
|
+
if not t.found:
|
|
447
|
+
out.append(f" no surface found for run {t.run_id} "
|
|
448
|
+
f"(no run.json, no intent ledger, no attributed WAL event)")
|
|
449
|
+
return "\n".join(out)
|
|
450
|
+
|
|
451
|
+
# --- spine / lineage ---------------------------------------------------
|
|
452
|
+
out.append("")
|
|
453
|
+
out.append("## spine")
|
|
454
|
+
out.append(f" process {t.process_id or '-'}")
|
|
455
|
+
out.append(f" parent {t.parent_id or '-(root)'}")
|
|
456
|
+
out.append(f" root {t.root_id or t.run_id}"
|
|
457
|
+
+ (" (this run is the root)" if t.root_id in ("", t.run_id) else ""))
|
|
458
|
+
if t.ancestors:
|
|
459
|
+
out.append(f" ancestry {' → '.join(t.ancestors)}")
|
|
460
|
+
if t.descendants:
|
|
461
|
+
shown = list(t.descendants[:6])
|
|
462
|
+
more = f" (+{len(t.descendants) - 6} more)" if len(t.descendants) > 6 else ""
|
|
463
|
+
out.append(f" children {', '.join(shown)}{more}")
|
|
464
|
+
|
|
465
|
+
# --- intent (claimed vs verified) -------------------------------------
|
|
466
|
+
out.append("")
|
|
467
|
+
out.append("## intent")
|
|
468
|
+
if not t.has_intent:
|
|
469
|
+
out.append(" (no INTENT declared — this run recorded no goal/plan/steps)")
|
|
470
|
+
else:
|
|
471
|
+
if t.goal:
|
|
472
|
+
out.append(f" goal {t.goal[:120]}")
|
|
473
|
+
if t.plan or t.phase:
|
|
474
|
+
out.append(f" target {t.plan or '-'} :: {t.phase or '-'}")
|
|
475
|
+
out.append(f" start {t.start_sha or '-'}")
|
|
476
|
+
if t.steps:
|
|
477
|
+
n_ver = sum(1 for s in t.steps if s.state == "VERIFIED")
|
|
478
|
+
out.append(f" steps {n_ver}/{len(t.steps)} kernel-verified "
|
|
479
|
+
f"(believed-claimed shown beside adjudicated-verified):")
|
|
480
|
+
for s in t.steps:
|
|
481
|
+
glyph = _STEP_GLYPH.get(s.state, s.state)
|
|
482
|
+
detail = ""
|
|
483
|
+
if s.state == "VERIFIED":
|
|
484
|
+
detail = f"sha={s.verified_sha[:10]} via={s.verified_via or '?'}"
|
|
485
|
+
elif s.state == "CLAIMED":
|
|
486
|
+
detail = f"claimed sha={s.claimed_sha[:10]} — NOT kernel-verified"
|
|
487
|
+
out.append(f" {glyph} {s.step_id:<22} {detail}")
|
|
488
|
+
if t.residual:
|
|
489
|
+
out.append(f" residual {len(t.residual)} step(s) not verified: "
|
|
490
|
+
f"{', '.join(t.residual[:8])}"
|
|
491
|
+
+ (" …" if len(t.residual) > 8 else ""))
|
|
492
|
+
if t.corrupt_ledger_lines:
|
|
493
|
+
note = " (incl. a record this kernel is too OLD to read — migrate)" \
|
|
494
|
+
if t.unreadable_newer else ""
|
|
495
|
+
out.append(f" ⚠ ledger {t.corrupt_ledger_lines} corrupt/unreadable line(s){note}")
|
|
496
|
+
|
|
497
|
+
# --- WAL (lanes held / refused) ---------------------------------------
|
|
498
|
+
out.append("")
|
|
499
|
+
out.append("## lanes (WAL)")
|
|
500
|
+
if not t.lease_events:
|
|
501
|
+
out.append(" (no lease event attributed to this run by run_id)")
|
|
502
|
+
else:
|
|
503
|
+
header = f" {'op':<10} {'lane':<12} {'loop_ts':<22} via"
|
|
504
|
+
out.append(header)
|
|
505
|
+
out.append(" " + "-" * (len(header) - 2))
|
|
506
|
+
for e in t.lease_events:
|
|
507
|
+
out.append(f" {e.op:<10} {(e.lane or '-'):<12} "
|
|
508
|
+
f"{(e.loop_ts or '-'):<22} {e.attributed_by}")
|
|
509
|
+
if t.unattributed_acquires:
|
|
510
|
+
out.append(f" note: {t.unattributed_acquires} ACQUIRE(s) on this workspace "
|
|
511
|
+
f"carried NO run_id and were NOT attributed by time "
|
|
512
|
+
f"(stamp --run-id to join them; docs/137)")
|
|
513
|
+
|
|
514
|
+
# --- git --------------------------------------------------------------
|
|
515
|
+
out.append("")
|
|
516
|
+
out.append("## commits since start")
|
|
517
|
+
if not t.commits:
|
|
518
|
+
out.append(f" (none since {t.start_sha or '-'})")
|
|
519
|
+
else:
|
|
520
|
+
for c in t.commits[:20]:
|
|
521
|
+
out.append(f" {str(c.get('sha',''))[:10]} {str(c.get('subject',''))[:72]}")
|
|
522
|
+
if len(t.commits) > 20:
|
|
523
|
+
out.append(f" (+{len(t.commits) - 20} more)")
|
|
524
|
+
return "\n".join(out)
|
dos/verdict.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""The typed-verdict contract — one ABI the distrust verbs share (docs/86).
|
|
2
|
+
|
|
3
|
+
`liveness` and `scope` are two instances of the *same* shape, and `verify`'s
|
|
4
|
+
`ShipVerdict` is a third that drifted. This module NAMES that shape as a contract
|
|
5
|
+
so a third party can add a verdict the way you add a device driver — and so the
|
|
6
|
+
CLI, the `dos decisions` queue, and the MCP server can treat every verdict
|
|
7
|
+
uniformly instead of special-casing each verb.
|
|
8
|
+
|
|
9
|
+
It is deliberately written **after** there were two real instances to generalize
|
|
10
|
+
from (`liveness.classify`, `scope.classify`) — the "generalize last" discipline
|
|
11
|
+
(docs/86 §4): a contract invented before its instances is machinery imported
|
|
12
|
+
ahead of need. This module adds NO behavior to the existing verbs; it is a pure
|
|
13
|
+
description (Protocols) plus a conformance check a future verdict registry can
|
|
14
|
+
call. Nothing here is imported by `liveness`/`scope`/`oracle` — the arrow points
|
|
15
|
+
the other way (a consumer reads this; the verbs don't depend on it).
|
|
16
|
+
|
|
17
|
+
The contract, in one line:
|
|
18
|
+
|
|
19
|
+
classify(Evidence, Policy) -> Verdict[V]
|
|
20
|
+
|
|
21
|
+
where the pieces are exactly what `liveness`/`scope` already are:
|
|
22
|
+
|
|
23
|
+
Evidence — a frozen dataclass of facts GATHERED BY THE CALLER (no I/O inside
|
|
24
|
+
the verdict). The git read / config read / clock lives at the CLI
|
|
25
|
+
boundary; `classify` receives the already-gathered facts. This is
|
|
26
|
+
what lets a verdict be replay-tested on frozen fixtures.
|
|
27
|
+
Policy — a frozen dataclass of thresholds, declarable per-workspace in
|
|
28
|
+
`dos.toml [<verb>]` (the `[liveness]`/`[scope]` seam), defaults
|
|
29
|
+
GENERIC. "Mechanism is kernel, knobs are config."
|
|
30
|
+
Verdict — the typed answer: a CLOSED-ENUM `verdict` (never a bare bool), a
|
|
31
|
+
one-line operator-facing `reason`, the echoed `evidence`, and a
|
|
32
|
+
`to_dict()` for the JSON/MCP/renderer seam. The provenance — which
|
|
33
|
+
rung/signal answered — is carried IN the verdict (liveness's
|
|
34
|
+
`evidence`, scope's spill lists, `ShipVerdict.source`).
|
|
35
|
+
|
|
36
|
+
This is the three kernel design laws fused into one type: *typed verdict over
|
|
37
|
+
binary gate* (the closed enum), *evidence over narrative* (Evidence is gathered
|
|
38
|
+
from artifacts, never the agent's claim), and *the give lives in provenance*
|
|
39
|
+
(docs/76 — the rung, not the adjudication, is what flexes).
|
|
40
|
+
|
|
41
|
+
SCOPE — the contract is for the EPISTEMIC verbs only: the ones that answer "is
|
|
42
|
+
this claim about ground-truth state true?" (`verify`, `liveness`, `scope`, and
|
|
43
|
+
the candidates `acceptance`/`identity`). `arbitrate()` and `spawn/reap` share the
|
|
44
|
+
`classify` *shape* (state-in → typed-out, pure) but their output is an EFFECT
|
|
45
|
+
decision (acquire/refuse) or an IDENTITY record, not a belief about the world —
|
|
46
|
+
they are cousins, not members. Forcing them under this Protocol would make it a
|
|
47
|
+
god-type that means nothing; the boundary is part of the contract.
|
|
48
|
+
|
|
49
|
+
The four-gate registration test a verdict must pass to be a kernel verb (docs/85
|
|
50
|
+
§2, enforced by a future `dos.verdicts.register`): (1) it answers a claim about
|
|
51
|
+
ground-truth state; (2) its evidence is unforgeable by the agent; (3) it is
|
|
52
|
+
domain-free; (4) its verdict is a mechanical closed enum. Fail (1) → it is an
|
|
53
|
+
advisory JUDGE (`drivers/llm_judge`), not a verdict. Fail (3) → it is a driver
|
|
54
|
+
oracle on the seam, not a kernel verb. Open set of verbs, CLOSED shape per verb —
|
|
55
|
+
the closed-enum-as-data hackability pattern (HACKING.md) lifted from vocabularies
|
|
56
|
+
to verdicts.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
from typing import Any, Protocol, runtime_checkable
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@runtime_checkable
|
|
65
|
+
class TypedVerdict(Protocol):
|
|
66
|
+
"""The shape every distrust verdict returns. `liveness.LivenessVerdict` and
|
|
67
|
+
`scope.ScopeVerdict` satisfy it structurally (no inheritance needed — this is
|
|
68
|
+
a duck-typed Protocol).
|
|
69
|
+
|
|
70
|
+
verdict — a `str`-valued Enum member (the closed verdict vocabulary). The
|
|
71
|
+
`str` base lets it round-trip through a CLI token / exit-code map
|
|
72
|
+
without a lookup table; `.value` is the wire form.
|
|
73
|
+
reason — a one-line, operator-facing summary of WHY this verdict, naming
|
|
74
|
+
the driving evidence (legible distrust — the renderer seam).
|
|
75
|
+
to_dict — the JSON shape: the verdict, the reason, and the evidence behind
|
|
76
|
+
it, so `--output json` / an MCP tool emits the answer AND its
|
|
77
|
+
grounds in one object.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def verdict(self) -> Any: # a str-valued enum member
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def reason(self) -> str:
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@runtime_checkable
|
|
93
|
+
class Classifier(Protocol):
|
|
94
|
+
"""A pure verdict function: `classify(evidence, policy) -> TypedVerdict`.
|
|
95
|
+
|
|
96
|
+
The shared signature of `liveness.classify` and `scope.classify`. `policy`
|
|
97
|
+
carries a default, so a caller may invoke `classify(evidence)`; a verb's
|
|
98
|
+
`gather()` (the boundary I/O) produces the `evidence` argument. A `Classifier`
|
|
99
|
+
MUST NOT perform I/O — that is the property the conformance check below cannot
|
|
100
|
+
enforce statically but every kernel verb's test suite poisons `subprocess`/
|
|
101
|
+
`open`/`time` to prove (see `test_liveness`/`test_scope`).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __call__(self, evidence: Any, policy: Any = ..., /) -> TypedVerdict:
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def conforms(verdict_obj: Any) -> bool:
|
|
109
|
+
"""True when `verdict_obj` satisfies the `TypedVerdict` contract at runtime.
|
|
110
|
+
|
|
111
|
+
The check a future `dos.verdicts.register` runs on a third party's verdict
|
|
112
|
+
sample before exposing it as a `dos <verb>` subcommand / a decisions-queue row
|
|
113
|
+
/ an MCP tool. Structural, not nominal: any object with a `str`-enum `verdict`,
|
|
114
|
+
a `str` `reason`, and a callable `to_dict()` whose result is JSON-shaped passes
|
|
115
|
+
— no base class required.
|
|
116
|
+
|
|
117
|
+
Note this validates the *verdict value's* shape, which is gate (4) of the
|
|
118
|
+
four-gate test (a mechanical closed enum + a legible reason). Gates (1)–(3)
|
|
119
|
+
— ground-truth claim, unforgeable evidence, domain-free — are properties of
|
|
120
|
+
the verb's DESIGN that a runtime check cannot see; they stay a review
|
|
121
|
+
responsibility (the contract documents them so the reviewer has the checklist).
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(verdict_obj, TypedVerdict):
|
|
124
|
+
return False
|
|
125
|
+
# `verdict` must be a str-valued enum member (the closed vocabulary), not a
|
|
126
|
+
# bare bool or free string — the "typed verdict over binary gate" law.
|
|
127
|
+
import enum
|
|
128
|
+
|
|
129
|
+
v = verdict_obj.verdict
|
|
130
|
+
if not isinstance(v, enum.Enum) or not isinstance(v, str):
|
|
131
|
+
return False
|
|
132
|
+
if not isinstance(verdict_obj.reason, str):
|
|
133
|
+
return False
|
|
134
|
+
try:
|
|
135
|
+
d = verdict_obj.to_dict()
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
138
|
+
if not isinstance(d, dict) or "verdict" not in d:
|
|
139
|
+
return False
|
|
140
|
+
return True
|