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/plan_source.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""The plan-source seam — pluggable discovery of the (plan, phase) rows a plan view audits.
|
|
2
|
+
|
|
3
|
+
Why this exists
|
|
4
|
+
===============
|
|
5
|
+
|
|
6
|
+
DOS deliberately holds **no plan schema** (CLAUDE.md: "Phased-plan concepts are NOT
|
|
7
|
+
in this package"). `verify()` takes ``(plan, phase)`` positionally and answers from
|
|
8
|
+
git alone when no plan exists. So a screen that wants to show "the shape of the work
|
|
9
|
+
and how far it has shipped" cannot read a plan registry the kernel doesn't believe
|
|
10
|
+
in — it must instead ask a *declared* source for a flat list of candidate rows, then
|
|
11
|
+
let the **oracle** rule on each one's ship status. The plan is a row source; the truth
|
|
12
|
+
is `oracle.is_shipped`. That inversion is the whole point: a plan-status view built on
|
|
13
|
+
the plan's own self-report would be a self-narrating worker; one built on the oracle's
|
|
14
|
+
verdict is the kernel doing its job at the plan altitude.
|
|
15
|
+
|
|
16
|
+
This module is that row source — the **seam**, not a schema. It is the exact analogue
|
|
17
|
+
of `dos.judges` (the JUDGE-rung seam): a domain-neutral Protocol + a frozen value type
|
|
18
|
+
+ a fail-safe runner + a by-name resolver over an entry-point group + a single built-in
|
|
19
|
+
that ships in the kernel. Every host-specific bit (where plans live, what a phase
|
|
20
|
+
heading looks like, what a ship stamp reads as) is either CONFIG DATA the built-in
|
|
21
|
+
reads (`config.paths.plans_glob`) or a host's own `dos.plan_sources` plugin — never a
|
|
22
|
+
hardcoded `docs/_plans` literal in a kernel module.
|
|
23
|
+
|
|
24
|
+
The unit a source yields is a `PlanRow` — a domain-neutral
|
|
25
|
+
``{plan, phase, doc_path, claimed_status}`` quadruple. ``claimed_status`` is the
|
|
26
|
+
**plan's self-report** (the stamp it carries / "open" / "blocked") — the part DOS does
|
|
27
|
+
NOT believe; it is shown only to contrast against the oracle's verdict (the
|
|
28
|
+
believed-vs-adjudicated divergence the plan view is built around). A source NEVER
|
|
29
|
+
returns a ship verdict — that is `oracle.is_shipped`'s job alone, attached downstream.
|
|
30
|
+
|
|
31
|
+
Purity & layering
|
|
32
|
+
==================
|
|
33
|
+
|
|
34
|
+
Pure kernel, exactly like `judges`: a Protocol, one frozen value type, a built-in that
|
|
35
|
+
harvests markdown, and resolver/runner helpers. The built-in's markdown read is
|
|
36
|
+
**boundary I/O gathered when the source runs** — there is no verdict here to keep pure
|
|
37
|
+
(a row list is data, not an adjudication), but the discipline that matters carries
|
|
38
|
+
over: the source names no host (it reads `config.paths.plans_glob`, declared per
|
|
39
|
+
workspace), and **fail-to-empty** — `run_plan_source` converts any raise / bad return
|
|
40
|
+
into ``[]``, never a partial or fabricated row, so a broken source degrades the plan
|
|
41
|
+
view to its no-plan floor rather than inventing work. Entry-point discovery (the one
|
|
42
|
+
bit of registry I/O) happens at the call boundary in `active_plan_sources`, exactly as
|
|
43
|
+
`active_judges` / `active_predicates` do.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import re
|
|
49
|
+
import sys
|
|
50
|
+
from dataclasses import dataclass
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import Protocol, runtime_checkable
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# The row a source yields — domain-neutral, frozen, carries NO ship verdict.
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# The closed self-report vocabulary. A source reports what the PLAN claims about a
|
|
61
|
+
# phase — never a verified fact. `str`-valued so it round-trips through `--json`
|
|
62
|
+
# without a lookup table (the `gate_classify.Verdict` idiom). The oracle's verdict
|
|
63
|
+
# is a SEPARATE axis attached downstream; these never name "shipped-as-fact".
|
|
64
|
+
CLAIMED_SHIPPED = "shipped" # the plan stamps this phase as done (a `· SHIPPED` mark)
|
|
65
|
+
CLAIMED_BLOCKED = "blocked" # the plan marks it gated / soaking / awaiting
|
|
66
|
+
CLAIMED_OPEN = "open" # the plan lists it but claims no status
|
|
67
|
+
CLAIMED_UNKNOWN = "" # the source could not read a status off the plan
|
|
68
|
+
|
|
69
|
+
_CLAIMED_VALUES = frozenset({CLAIMED_SHIPPED, CLAIMED_BLOCKED, CLAIMED_OPEN, CLAIMED_UNKNOWN})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class PlanRow:
|
|
74
|
+
"""One candidate ``(plan, phase)`` the plan view will ask the oracle about.
|
|
75
|
+
|
|
76
|
+
Deliberately NOT a plan-schema node — it is the *flat* shape a plan view needs:
|
|
77
|
+
the positional ``(plan, phase)`` `oracle.is_shipped` takes, the ``doc_path`` the
|
|
78
|
+
row was harvested from (for drill-in / the oracle's doc-aware cross-check), and the
|
|
79
|
+
``claimed_status`` — the plan's OWN self-report (`shipped`/`blocked`/`open`), the
|
|
80
|
+
narration DOS distrusts and shows only to contrast against the oracle. ``lane`` is
|
|
81
|
+
an OPTIONAL hint a source may carry when the plan names the phase's lane; the plan
|
|
82
|
+
view uses it to join a row to a live lease / decision, and it is "" when unknown
|
|
83
|
+
(the join then falls back to lane-name matching downstream).
|
|
84
|
+
|
|
85
|
+
A `PlanRow` carries no ship verdict on purpose: the source's job is to enumerate
|
|
86
|
+
candidates honestly, not to rule on them. The ruling is the oracle's, attached in
|
|
87
|
+
`plan_board`.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
plan: str
|
|
91
|
+
phase: str
|
|
92
|
+
doc_path: str = ""
|
|
93
|
+
claimed_status: str = CLAIMED_UNKNOWN
|
|
94
|
+
lane: str = ""
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
return {
|
|
98
|
+
"plan": self.plan,
|
|
99
|
+
"phase": self.phase,
|
|
100
|
+
"doc_path": self.doc_path,
|
|
101
|
+
"claimed_status": self.claimed_status,
|
|
102
|
+
"lane": self.lane,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@runtime_checkable
|
|
107
|
+
class PlanSource(Protocol):
|
|
108
|
+
"""The contract a host implements to tell a plan view where its phases live.
|
|
109
|
+
|
|
110
|
+
``name`` is the token a CLI flag selects and `dos doctor` could list. ``rows`` is
|
|
111
|
+
handed the active ``config`` (read-only — it reads `config.paths.plans_glob` and
|
|
112
|
+
the workspace root; the type gives it nothing to mutate) and returns the candidate
|
|
113
|
+
`PlanRow`s, in a deterministic order (declaration / file order — the plan view
|
|
114
|
+
renders them as given).
|
|
115
|
+
|
|
116
|
+
A source MAY do I/O *inside* ``rows`` (glob the workspace, read markdown) — it is
|
|
117
|
+
the boundary read that turns "where are the plans" into data. The discipline that
|
|
118
|
+
keeps it honest is fail-to-empty (enforced by `run_plan_source`, not by trusting
|
|
119
|
+
the source) and naming no host (the built-in reads the declared glob, never a
|
|
120
|
+
literal), NOT purity — a row list is data, not an adjudication.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
name: str
|
|
124
|
+
|
|
125
|
+
def rows(self, config: object) -> list[PlanRow]:
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# The built-in markdown source — harvests `### N. PLAN PHASE` headings + a
|
|
131
|
+
# `· SHIPPED` / `SHIPPED` stamp from the files config.paths.plans_glob names.
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
# A numbered packet/plan heading: `### 1. IF IF4.1 — title…` or `## 3. AUTH P2: …`.
|
|
135
|
+
# The first token after the section number is the plan id (starts with a letter), the
|
|
136
|
+
# second is the phase id (the exact positional string the oracle takes). This is the
|
|
137
|
+
# SAME unambiguous shape `timeline._parse_packet_picks` harvests — lifted here as the
|
|
138
|
+
# generic default so a plan view and a dispatch timeline read the same heading grammar.
|
|
139
|
+
#
|
|
140
|
+
# Two guards keep the generic harvest CONSERVATIVE — it must under-harvest a foreign
|
|
141
|
+
# convention (→ "(no plans)" + the git floor, the honest degrade) rather than mine prose
|
|
142
|
+
# for phantom phases (the live-repo failure mode: `### Why never-stall` →
|
|
143
|
+
# `(Why, never-stall)`):
|
|
144
|
+
# * the trailing ``[—–\-:]`` separator is REQUIRED — a real plan heading titles its
|
|
145
|
+
# phase (`### 1. IF IF4.1 — split`); a plain numbered section header
|
|
146
|
+
# (`## 2. Next items`) has no separator after its second word.
|
|
147
|
+
# * the phase token must contain a DIGIT **and** a LETTER (`_looks_like_phase_id`) — a
|
|
148
|
+
# real phase id (`IF4.1`, `P2`, `1a`) carries both a series letter and an ordinal; a
|
|
149
|
+
# prose word (`items`, `Built`, `TOML`) has no digit, and a bare ordinal (the `2` in a
|
|
150
|
+
# prose `### 1. Phase 2 of 3 — done`) has no letter. Both are rejected — the single
|
|
151
|
+
# most effective false-positive cut.
|
|
152
|
+
# A repo whose plan docs use another shape (DOS's OWN `### Phase N:` / `- **1a.**` design
|
|
153
|
+
# docs do) ships a `dos.plan_sources` plugin — the kernel default does not guess.
|
|
154
|
+
_HEADING_RE = re.compile(
|
|
155
|
+
r"^#{2,4}\s+\d+\.\s+([A-Za-z][A-Za-z0-9_\-]*)\s+([A-Za-z0-9][A-Za-z0-9_\-./']*)\s*[—–\-:]",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# A bullet sub-phase row under a numbered heading: `- **IF4.2 — …`. The phase id is the
|
|
159
|
+
# bolded leading token; there is no plan id on the bullet, so it inherits the enclosing
|
|
160
|
+
# NUMBERED heading's plan (never a prose `###`). Like the heading, the token must look
|
|
161
|
+
# like a phase id (carry a digit) — without that guard a bolded design principle
|
|
162
|
+
# (`- **Rendering is downstream…`) harvests as a phantom phase (the live-repo failure).
|
|
163
|
+
_BULLET_RE = re.compile(
|
|
164
|
+
r"^\s*-\s+\*\*([A-Za-z0-9][A-Za-z0-9_\-./']*)\s*(?:[—\-–:]|\*\*)",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _looks_like_phase_id(token: str) -> bool:
|
|
169
|
+
"""True iff ``token`` has the generic phase-id shape: a LETTER and a DIGIT.
|
|
170
|
+
|
|
171
|
+
Every real phase id carries both — a series letter and an ordinal: `IF4.1`, `P2`,
|
|
172
|
+
`AUTH4`, `RS4`, `1a`, `MG3'-1`. Requiring both is the conservative false-positive cut
|
|
173
|
+
that keeps the generic default honest against prose the loose heading/bullet regex
|
|
174
|
+
would otherwise mine:
|
|
175
|
+
|
|
176
|
+
* a prose WORD (`items`, `Built`, `TOML`, `release`, `downstream`) has a letter but
|
|
177
|
+
no digit → rejected;
|
|
178
|
+
* a BARE ORDINAL (`2`, `3`) — the second token of a prose `### 1. Phase 2 of 3 —
|
|
179
|
+
done` heading, which would mis-harvest as the phantom phase `(Phase, 2)` — has a
|
|
180
|
+
digit but no letter → rejected.
|
|
181
|
+
|
|
182
|
+
The cost is the bare-ordinal `### Phase 6:` plan dialect (DOS's own design docs): its
|
|
183
|
+
phase token `6` is digit-only, so it is NOT harvested by the default. That is the
|
|
184
|
+
documented tradeoff — under-harvest a digit-less / bare-ordinal convention (ship a
|
|
185
|
+
`dos.plan_sources` plugin) rather than mine prose for phantom work."""
|
|
186
|
+
return any(c.isalpha() for c in token) and any(c.isdigit() for c in token)
|
|
187
|
+
|
|
188
|
+
# A heading section's own SHIPPED stamp lives in the lines under it until the next
|
|
189
|
+
# heading. We read the claimed status off the section text: a `SHIPPED` token (the
|
|
190
|
+
# universal stamp word `phase_shipped` keys on) ⇒ claimed shipped; a soak/gate/await
|
|
191
|
+
# word ⇒ claimed blocked; otherwise open. These are CLAIMED-only reads — the plan's
|
|
192
|
+
# narration, never a verified fact.
|
|
193
|
+
_STAMP_SHIPPED_RE = re.compile(r"\bSHIPPED\b")
|
|
194
|
+
_STAMP_BLOCKED_RE = re.compile(r"\b(?:SOAK|SOAKING|BLOCKED|AWAITING|GATED|DEFERRED)\b", re.IGNORECASE)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _claimed_status_for(section_text: str) -> str:
|
|
198
|
+
"""Read the plan's self-reported status off a phase's section text.
|
|
199
|
+
|
|
200
|
+
Pure. A `SHIPPED` token wins (the plan claims done); else a soak/blocked/await
|
|
201
|
+
word ⇒ blocked; else open. This is the plan's NARRATION — the part the plan view
|
|
202
|
+
distrusts and shows only to contrast against the oracle. An empty section ⇒ open
|
|
203
|
+
(the plan lists the phase but claims nothing).
|
|
204
|
+
"""
|
|
205
|
+
if _STAMP_SHIPPED_RE.search(section_text):
|
|
206
|
+
return CLAIMED_SHIPPED
|
|
207
|
+
if _STAMP_BLOCKED_RE.search(section_text):
|
|
208
|
+
return CLAIMED_BLOCKED
|
|
209
|
+
return CLAIMED_OPEN
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _harvest_markdown(text: str, doc_path: str) -> list[PlanRow]:
|
|
213
|
+
"""Parse one plan-doc's text into ordered PlanRows. Pure, no I/O.
|
|
214
|
+
|
|
215
|
+
Two recognised shapes, both coded GENERICALLY (no host directory or series literal)
|
|
216
|
+
and both gated on `_looks_like_phase_id` so prose is never mined for phantom phases:
|
|
217
|
+
|
|
218
|
+
* a numbered ``### N. PLAN PHASE — …`` heading yields a row (plan = first token,
|
|
219
|
+
phase = second), claimed status read from the lines under it (until the next
|
|
220
|
+
heading). This sets the enclosing plan for any bullets that follow.
|
|
221
|
+
* a bolded ``- **PHASE — …`` bullet INHERITS the most-recent numbered heading's
|
|
222
|
+
plan, yielding a sub-phase row, its claimed status read from the bullet's line.
|
|
223
|
+
|
|
224
|
+
A bullet with no preceding numbered heading is dropped (there is no honest plan id to
|
|
225
|
+
give it). De-duped on ``(plan, phase)`` preserving first-seen order. A doc with no
|
|
226
|
+
recognised heading yields ``[]`` — the conservative degrade (DOS's own `### Phase N:`
|
|
227
|
+
design-doc dialect lands here, and that is correct: it wants a `dos.plan_sources`
|
|
228
|
+
plugin, not a guess).
|
|
229
|
+
"""
|
|
230
|
+
lines = text.splitlines()
|
|
231
|
+
rows: list[PlanRow] = []
|
|
232
|
+
seen: set[tuple[str, str]] = set()
|
|
233
|
+
cur_plan = "" # the plan id from the most-recent NUMBERED heading (bullets inherit it)
|
|
234
|
+
|
|
235
|
+
# Pre-compute each heading line's index so a section's body is the slice up to the
|
|
236
|
+
# next heading — used to read a `### N. PLAN PHASE` row's claimed status.
|
|
237
|
+
heading_idx = [i for i, ln in enumerate(lines) if re.match(r"^#{2,4}\s+", ln)]
|
|
238
|
+
next_heading_after = {}
|
|
239
|
+
for pos, idx in enumerate(heading_idx):
|
|
240
|
+
nxt = heading_idx[pos + 1] if pos + 1 < len(heading_idx) else len(lines)
|
|
241
|
+
next_heading_after[idx] = nxt
|
|
242
|
+
|
|
243
|
+
def _add(plan: str, phase: str, claimed: str) -> None:
|
|
244
|
+
key = (plan, phase)
|
|
245
|
+
if not plan or not phase or key in seen or not _looks_like_phase_id(phase):
|
|
246
|
+
return
|
|
247
|
+
seen.add(key)
|
|
248
|
+
rows.append(PlanRow(plan=plan, phase=phase, doc_path=doc_path, claimed_status=claimed))
|
|
249
|
+
|
|
250
|
+
for i, line in enumerate(lines):
|
|
251
|
+
m = _HEADING_RE.match(line)
|
|
252
|
+
if m:
|
|
253
|
+
plan_tok, phase = m.group(1), m.group(2)
|
|
254
|
+
# Adopt the heading's first token as the enclosing plan ONLY when the heading
|
|
255
|
+
# itself harvested a real phase (its second token passed `_looks_like_phase_id`).
|
|
256
|
+
# A PROSE numbered heading (`### 1. The rationale — why`, second token `why` →
|
|
257
|
+
# no digit) must NOT scope the bullets below it: leaving `cur_plan` set to a
|
|
258
|
+
# prose word ("The") would let a following digit-bearing bullet inherit it as a
|
|
259
|
+
# phantom plan id (`(The, v2.0)`). Clearing it is the conservative degrade — the
|
|
260
|
+
# bullet is dropped (no honest plan), same under-harvest posture as the phase
|
|
261
|
+
# digit-guard. (Cost: a `### 1. IF overview` heading whose own second token is
|
|
262
|
+
# prose no longer scopes its IF bullets — ship a `dos.plan_sources` plugin for
|
|
263
|
+
# that dialect; the default does not guess a plan id off prose.)
|
|
264
|
+
if _looks_like_phase_id(phase):
|
|
265
|
+
cur_plan = plan_tok
|
|
266
|
+
body = "\n".join(lines[i + 1 : next_heading_after.get(i, i + 1)])
|
|
267
|
+
_add(cur_plan, phase, _claimed_status_for(line + "\n" + body))
|
|
268
|
+
else:
|
|
269
|
+
cur_plan = ""
|
|
270
|
+
continue
|
|
271
|
+
bm = _BULLET_RE.match(line)
|
|
272
|
+
if bm and cur_plan:
|
|
273
|
+
_add(cur_plan, bm.group(1), _claimed_status_for(line))
|
|
274
|
+
return rows
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class MarkdownPlanSource:
|
|
278
|
+
"""The built-in, always-available plan source: harvest the workspace's plan docs.
|
|
279
|
+
|
|
280
|
+
Globs ``config.paths.plans_glob`` (the declared, per-workspace plan location —
|
|
281
|
+
generic default ``docs/**/*-plan.md``, overridable in `dos.toml [paths]`) under the
|
|
282
|
+
workspace root, parses each matched markdown file for ``### N. PLAN PHASE`` headings
|
|
283
|
+
and ``- **PHASE`` bullet sub-phases, and reads each phase's CLAIMED status off its
|
|
284
|
+
section. Names no host directory — the glob is data.
|
|
285
|
+
|
|
286
|
+
The plan-source analogue of `judges.AbstainJudge` / the `text` renderer: a trusted
|
|
287
|
+
fallback a plugin can never shadow (`resolve_plan_source` resolves built-ins first),
|
|
288
|
+
and the honest zero of the seam — a workspace with no plugin still has a resolvable
|
|
289
|
+
source. A repo with no plans (or a non-markdown plan convention with no plugin)
|
|
290
|
+
yields ``[]``, which is the plan view's no-plan floor (the git-ships strip carries
|
|
291
|
+
the screen, exactly as `dos top` degrades).
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
name = "markdown"
|
|
295
|
+
|
|
296
|
+
def rows(self, config: object) -> list[PlanRow]:
|
|
297
|
+
paths = getattr(config, "paths", None)
|
|
298
|
+
if paths is None:
|
|
299
|
+
return []
|
|
300
|
+
root = Path(getattr(paths, "root", "."))
|
|
301
|
+
glob = str(getattr(paths, "plans_glob", "") or "")
|
|
302
|
+
if not glob:
|
|
303
|
+
return []
|
|
304
|
+
try:
|
|
305
|
+
matched = sorted(root.glob(glob))
|
|
306
|
+
except (OSError, ValueError):
|
|
307
|
+
return []
|
|
308
|
+
out: list[PlanRow] = []
|
|
309
|
+
for p in matched:
|
|
310
|
+
try:
|
|
311
|
+
if not p.is_file():
|
|
312
|
+
continue
|
|
313
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
314
|
+
except OSError:
|
|
315
|
+
continue
|
|
316
|
+
try:
|
|
317
|
+
rel = str(p.relative_to(root))
|
|
318
|
+
except ValueError:
|
|
319
|
+
rel = str(p)
|
|
320
|
+
out.extend(_harvest_markdown(text, rel))
|
|
321
|
+
return out
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def run_plan_source(source: PlanSource, config: object) -> list[PlanRow]:
|
|
325
|
+
"""Run one source, enforcing **fail-to-empty** + a clean, deduped row list.
|
|
326
|
+
|
|
327
|
+
The wrapper EVERY consumer calls instead of `source.rows(...)` directly — it makes
|
|
328
|
+
"a broken source degrades the plan view to its no-plan floor, never to fabricated
|
|
329
|
+
or partial rows" a structural guarantee rather than a hope:
|
|
330
|
+
|
|
331
|
+
* a source that **raises** (bad glob, unreadable tree, a bug) → ``[]``. Never
|
|
332
|
+
propagates; the plan view falls to its git-ships floor.
|
|
333
|
+
* a source that returns **anything that is not a list of `PlanRow`** → its
|
|
334
|
+
non-`PlanRow` items are dropped (a duck-typed look-alike never reaches the
|
|
335
|
+
oracle), and a non-iterable return → ``[]``.
|
|
336
|
+
|
|
337
|
+
The asymmetry note vs `judges.run_judge`: a judge fails to ABSTAIN (punt up the
|
|
338
|
+
ladder); a plan source fails to EMPTY (show no work). Both refuse to let a failure
|
|
339
|
+
fabricate an outcome — a judge never auto-CLEARS, a source never auto-INVENTS a
|
|
340
|
+
phase. Claimed-status values outside the closed set are normalised to UNKNOWN so a
|
|
341
|
+
plugin can't smuggle a free-text status into the divergence logic downstream.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
rows = source.rows(config)
|
|
345
|
+
except Exception: # fail-to-empty: a source that raises contributes nothing
|
|
346
|
+
return []
|
|
347
|
+
if not isinstance(rows, (list, tuple)):
|
|
348
|
+
return []
|
|
349
|
+
out: list[PlanRow] = []
|
|
350
|
+
for r in rows:
|
|
351
|
+
if not isinstance(r, PlanRow):
|
|
352
|
+
continue
|
|
353
|
+
if r.claimed_status not in _CLAIMED_VALUES:
|
|
354
|
+
out.append(PlanRow(plan=r.plan, phase=r.phase, doc_path=r.doc_path,
|
|
355
|
+
claimed_status=CLAIMED_UNKNOWN, lane=r.lane))
|
|
356
|
+
else:
|
|
357
|
+
out.append(r)
|
|
358
|
+
return out
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# Resolution — built-in first, then the `dos.plan_sources` entry-point group.
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
# The entry-point group a host/researcher registers a plan source under.
|
|
366
|
+
PLAN_SOURCE_ENTRY_POINT_GROUP = "dos.plan_sources"
|
|
367
|
+
|
|
368
|
+
# The built-in sources, resolvable by name and UNSHADOWABLE by a plugin (a plugin
|
|
369
|
+
# registering `markdown` cannot displace this one — built-ins resolve first). Only the
|
|
370
|
+
# generic markdown harvester ships in the kernel; a host's bespoke plan format is a
|
|
371
|
+
# plugin (the kernel has no host plan schema).
|
|
372
|
+
_BUILT_IN_PLAN_SOURCES: dict[str, type] = {
|
|
373
|
+
MarkdownPlanSource.name: MarkdownPlanSource,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _discover_entry_point_plan_sources(*, _stderr=None) -> list[tuple[str, PlanSource]]:
|
|
378
|
+
"""Find plan sources registered under the `dos.plan_sources` entry-point group.
|
|
379
|
+
|
|
380
|
+
A plugin registers ``name = "pkg.module:SourceClass"`` in its
|
|
381
|
+
``[project.entry-points."dos.plan_sources"]``. We load each, instantiate it if it
|
|
382
|
+
is a class, and return ``(entry_point_name, source)`` pairs sorted by name (stable,
|
|
383
|
+
deterministic order). A plugin that fails to load is skipped with a one-line stderr
|
|
384
|
+
note rather than crashing — the same posture `judges._discover_entry_point_judges`
|
|
385
|
+
takes (a broken third-party plugin is the operator's to fix, not a kernel fault).
|
|
386
|
+
"""
|
|
387
|
+
stderr = _stderr if _stderr is not None else sys.stderr
|
|
388
|
+
out: list[tuple[str, PlanSource]] = []
|
|
389
|
+
try:
|
|
390
|
+
from importlib.metadata import entry_points
|
|
391
|
+
except Exception: # pragma: no cover - importlib.metadata always present py3.11+
|
|
392
|
+
return out
|
|
393
|
+
try:
|
|
394
|
+
eps = entry_points(group=PLAN_SOURCE_ENTRY_POINT_GROUP)
|
|
395
|
+
except TypeError: # pragma: no cover - py<3.10 selectable-API fallback
|
|
396
|
+
eps = entry_points().get(PLAN_SOURCE_ENTRY_POINT_GROUP, []) # type: ignore[attr-defined]
|
|
397
|
+
except Exception: # pragma: no cover - defensive: never let discovery crash a call
|
|
398
|
+
return out
|
|
399
|
+
for ep in sorted(eps, key=lambda e: e.name):
|
|
400
|
+
try:
|
|
401
|
+
obj = ep.load()
|
|
402
|
+
source = obj() if isinstance(obj, type) else obj
|
|
403
|
+
except Exception as e: # pragma: no cover - depends on third-party plugin
|
|
404
|
+
print(
|
|
405
|
+
f"warning: plan source plugin {ep.name!r} failed to load ({e}); skipping",
|
|
406
|
+
file=stderr,
|
|
407
|
+
)
|
|
408
|
+
continue
|
|
409
|
+
out.append((ep.name, source))
|
|
410
|
+
return out
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def resolve_plan_source(name: str, *, _stderr=None) -> PlanSource:
|
|
414
|
+
"""Resolve a plan source by name: built-ins first, then `dos.plan_sources` plugins.
|
|
415
|
+
|
|
416
|
+
Built-ins (`markdown`) resolve FIRST and cannot be shadowed by a plugin of the same
|
|
417
|
+
name — the trusted-fallback guarantee, identical to `resolve_judge`. An unknown name
|
|
418
|
+
fails LOUD with the known list (it never silently degrades to `markdown`, which would
|
|
419
|
+
hide a typo'd selector): the caller asked for a specific source and getting a
|
|
420
|
+
different one silently is exactly the unannounced substitution the kernel refuses.
|
|
421
|
+
"""
|
|
422
|
+
if name in _BUILT_IN_PLAN_SOURCES:
|
|
423
|
+
return _BUILT_IN_PLAN_SOURCES[name]()
|
|
424
|
+
discovered = dict(_discover_entry_point_plan_sources(_stderr=_stderr))
|
|
425
|
+
if name in discovered:
|
|
426
|
+
return discovered[name]
|
|
427
|
+
known = sorted(set(_BUILT_IN_PLAN_SOURCES) | set(discovered))
|
|
428
|
+
raise ValueError(f"unknown plan source {name!r}; known: {', '.join(known)}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def active_plan_sources(*, _stderr=None) -> list[tuple[str, PlanSource]]:
|
|
432
|
+
"""Every resolvable source as ``(name, source)`` — built-ins THEN discovered plugins.
|
|
433
|
+
|
|
434
|
+
Does ENTRY-POINT DISCOVERY (I/O), so it is a call-boundary helper, never called
|
|
435
|
+
inside a row harvest (the `active_judges` discipline)."""
|
|
436
|
+
built = [(n, cls()) for n, cls in _BUILT_IN_PLAN_SOURCES.items()]
|
|
437
|
+
discovered = _discover_entry_point_plan_sources(_stderr=_stderr)
|
|
438
|
+
return built + discovered
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def active_plan_source_names(*, _stderr=None) -> list[str]:
|
|
442
|
+
"""The names of every active source (built-in + discovered) — what a `dos doctor`
|
|
443
|
+
listing or a `--plan-source` help text would show."""
|
|
444
|
+
return [name for name, _src in active_plan_sources(_stderr=_stderr)]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def default_rows(config: object, *, _stderr=None) -> list[PlanRow]:
|
|
448
|
+
"""The plan view's default row set: the built-in markdown source, run fail-safe.
|
|
449
|
+
|
|
450
|
+
The one call `plan_board.snapshot` makes when no explicit source/phase list was
|
|
451
|
+
given. Kept here (not in `plan_board`) so the "which source is the default" decision
|
|
452
|
+
lives with the seam, and so a future change to compose MULTIPLE active sources is a
|
|
453
|
+
one-line edit here rather than in the projection.
|
|
454
|
+
"""
|
|
455
|
+
return run_plan_source(MarkdownPlanSource(), config)
|