Coverage for src/keel/flows.py: 100%
23 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 18:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 18:07 +0000
1"""Canonical command-flow registry: the ordered phases of every keel command.
3The ship command's shape is the fixed :data:`keel.model.BACKBONE` (s0–s12). The
4other commands have their own shapes — a CI poll, an audit scan→report, a
5work-block loop, a triage queue pass. This module is the single source of truth
6for all of them, so any consumer (notably ``keel-visual``) can render or reason
7about a command's structure without re-deriving it from the adapter prose.
9Each flow is an ordered tuple of :class:`Phase`. A phase ``kind`` is one of:
11* ``normal`` — an ordinary step
12* ``gate`` — a pass/fail quality gate (e.g. the ship test gate)
13* ``loop`` — a repeating/iterating phase (a fix loop, a per-item work block)
14* ``merge`` — the literal merge
15* ``report`` — a terminal summary/output phase
17Pure data: no I/O. ``ship`` reuses the backbone verbatim (its gate/loop kinds are
18kept exactly as the rest of keel models them — gates at s8/s10, loop at s9) so
19nothing about the ship flow changes here.
20"""
22from __future__ import annotations
24from dataclasses import dataclass
26from .model import BACKBONE
28SCHEMA_VERSION = "keel.flows.v1"
30PHASE_KINDS = ("normal", "gate", "loop", "merge", "report")
33@dataclass(frozen=True)
34class Phase:
35 """One phase in a command's flow."""
37 id: str
38 name: str
39 kind: str = "normal"
42# Ship's own gate/loop/merge semantics, keyed by backbone step id. Kept identical
43# to how the rest of keel (and keel-visual) already treats the backbone.
44_SHIP_KINDS = {"s8": "gate", "s9": "loop", "s10": "merge"}
47def _ship_flow() -> tuple[Phase, ...]:
48 return tuple(
49 Phase(step.id, step.name, _SHIP_KINDS.get(step.id, "normal")) for step in BACKBONE
50 )
53def _flow(*phases: tuple[str, str, str]) -> tuple[Phase, ...]:
54 return tuple(Phase(pid, name, kind) for pid, name, kind in phases)
57#: The canonical flow for each keel command, in execution order.
58FLOWS: dict[str, tuple[Phase, ...]] = {
59 "ship": _ship_flow(),
60 "ci-check": _flow(
61 ("poll", "poll", "normal"),
62 ("report", "report", "report"),
63 ),
64 "coverage": _flow(
65 ("orient", "orient", "normal"),
66 ("areas", "areas", "normal"),
67 ("baseline", "baseline", "normal"),
68 ("head", "head", "normal"),
69 ("delta", "delta", "normal"),
70 ("hotspots", "hotspots", "normal"),
71 ("post", "post", "report"),
72 ),
73 "deps-audit": _flow(
74 ("orient", "orient", "normal"),
75 ("tracking", "tracking", "normal"),
76 ("scan", "scan", "normal"),
77 ("drift", "drift", "normal"),
78 ("report", "report", "normal"),
79 ("post", "post", "report"),
80 ),
81 "flake-audit": _flow(
82 ("orient", "orient", "normal"),
83 ("evidence", "evidence", "normal"),
84 ("aggregate", "aggregate", "normal"),
85 ("classify", "classify", "normal"),
86 ("dedupe", "dedupe", "normal"),
87 ("report", "report", "normal"),
88 ("open", "open", "report"),
89 ),
90 "implement": _flow(
91 ("config", "config", "normal"),
92 ("fetch", "fetch", "normal"),
93 ("branch", "branch", "normal"),
94 ("resolve", "resolve", "normal"),
95 ("codename", "codename", "normal"),
96 ("delegate", "delegate", "normal"),
97 ("report", "report", "report"),
98 ),
99 "morning": _flow(
100 ("config", "config", "normal"),
101 ("deferrals", "deferrals", "normal"),
102 ("shipped", "shipped", "normal"),
103 ("health", "health", "normal"),
104 ("enrichment", "enrichment", "normal"),
105 ("window", "window", "normal"),
106 ("output", "output", "report"),
107 ),
108 "overnight": _flow(
109 ("config", "config", "normal"),
110 ("preflight", "preflight", "normal"),
111 ("queue", "queue", "normal"),
112 ("loop", "work block", "loop"),
113 ("report", "report", "report"),
114 ),
115 "pr-loop": _flow(
116 ("config", "config", "normal"),
117 ("find", "find", "normal"),
118 ("open", "open", "normal"),
119 ("read", "read", "normal"),
120 ("categorize", "categorize", "normal"),
121 ("fix", "fix", "loop"),
122 ("review", "review", "normal"),
123 ("post", "post", "normal"),
124 ("recheck", "recheck", "normal"),
125 ("collect", "collect", "normal"),
126 ("handoff", "handoff", "report"),
127 ),
128 "regression": _flow(
129 ("orient", "orient", "normal"),
130 ("preflight", "preflight", "normal"),
131 ("fanout", "fan out", "loop"),
132 ("aggregate", "aggregate", "normal"),
133 ("dedupe", "dedupe", "normal"),
134 ("open", "open", "normal"),
135 ("report", "report", "report"),
136 ),
137 "review-all-day": _flow(
138 ("config", "config", "normal"),
139 ("parse", "parse", "normal"),
140 ("commits", "commits", "normal"),
141 ("decide", "decide", "normal"),
142 ("classify", "classify", "loop"),
143 ("open", "open", "normal"),
144 ("report", "report", "report"),
145 ),
146 "review-cycle": _flow(
147 ("config", "config", "normal"),
148 ("validate", "validate", "normal"),
149 ("loop", "loop", "loop"),
150 ("reviewers", "reviewers", "normal"),
151 ("post", "post", "normal"),
152 ("fixloop", "fix loop", "loop"),
153 ("report", "report", "report"),
154 ),
155 "stale-prs": _flow(
156 ("orient", "orient", "normal"),
157 ("list", "list", "normal"),
158 ("classify", "classify", "normal"),
159 ("triage", "triage", "normal"),
160 ("post", "post", "normal"),
161 ("rebase", "rebase", "normal"),
162 ("summary", "summary", "report"),
163 ),
164 "triage": _flow(
165 ("config", "config", "normal"),
166 ("find", "find", "normal"),
167 ("tier", "tier", "normal"),
168 ("classify", "classify", "loop"),
169 ("rank", "rank", "normal"),
170 ("apply", "apply", "normal"),
171 ("summary", "summary", "report"),
172 ),
173 "work-block": _flow(
174 ("config", "config", "normal"),
175 ("snapshot", "snapshot", "normal"),
176 ("loop", "work block", "loop"),
177 ("report", "report", "report"),
178 ),
179 "wrap": _flow(
180 ("config", "config", "normal"),
181 ("sanity", "sanity", "normal"),
182 ("gates", "gates", "gate"),
183 ("commit", "commit", "normal"),
184 ("push", "push", "normal"),
185 ("recap", "recap", "report"),
186 ),
187}
189#: The default flow when a command is unknown — the ship backbone.
190DEFAULT_COMMAND = "ship"
193def command_names() -> tuple[str, ...]:
194 """Return every known command name, in a stable sorted order."""
195 return tuple(sorted(FLOWS))
198def flow_for(command: str) -> tuple[Phase, ...]:
199 """Return the ordered phases for ``command`` (the ship backbone if unknown)."""
200 return FLOWS.get(command, FLOWS[DEFAULT_COMMAND])
203def is_known(command: str) -> bool:
204 """Return whether ``command`` has a dedicated flow (vs falling back to ship)."""
205 return command in FLOWS