Skip to main content

majordomus_cli/cli/
local.rs

1//! Why a command of the command line is not itself a capability.
2//!
3//! # The question this answers
4//!
5//! Every capability of this executable is projected to HTTP and to the OpenAPI document,
6//! and most of them to MCP. The command line is the one projection that also carries
7//! commands of its own: starting a server, writing a generated file, rendering a value for
8//! a person. Left alone, that is how an operation goes missing from the API — not by
9//! decision, but because nobody noticed it had.
10//!
11//! So the rule is not *every command is a capability*. It is: **a command is the
12//! projection of a capability, or it says here why it is not, and the reason is checked.**
13//! [`LOCAL`] is that statement, and [`crate::quality::parity`] is the check. A command in
14//! neither place is a finding; an entry here naming a command that no longer exists is a
15//! finding; an entry here for a command that *is* bound to a capability is a finding. None
16//! of the three can be reached by forgetting.
17//!
18//! # Where it lives
19//!
20//! Beside the clap declaration it is about, for the same reason
21//! [`super::EXAMPLES`] does: the person adding a command is looking at this file, and a
22//! statement kept anywhere else is a statement that goes stale in a refactor nobody
23//! connects to it.
24//!
25//! ```
26//! use majordomus_cli::cli::local::{LocalReason, LOCAL};
27//!
28//! // starting a server is not an operation that can answer over a socket
29//! let serve = LOCAL.iter().find(|l| l.command == "serve").unwrap();
30//! assert!(matches!(serve.reason, LocalReason::ProcessLifecycle));
31//!
32//! // and a command that renders a capability names the capability it renders
33//! let list = LOCAL.iter().find(|l| l.command == "web list").unwrap();
34//! assert_eq!(list.reason.renders(), Some("web.surfaces"));
35//! ```
36
37/// Why a command belongs to the command line alone.
38///
39/// The variants are the four structural reasons this executable has, and there is no
40/// fifth for "not projected yet": a read that belongs in the API and is missing from it
41/// is a defect, and a vocabulary that can express it politely is a vocabulary that will.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LocalReason {
44    /// The command runs a default subcommand when none is given: it is that command's
45    /// other name, not an operation of its own. The target is the full command, as a
46    /// person types it after `majordomus`.
47    Alias(&'static str),
48    /// What the command produces is a running process, not a value. There is nothing for a
49    /// request/response projection to return, and the caller of such a projection would be
50    /// asking the server to become a different server.
51    ProcessLifecycle,
52    /// The command writes into the repository. Every projection of the registry is
53    /// read-only — that is the executable's first promise — so a command that writes is
54    /// offered to the person who runs the executable and to nobody over a network.
55    WritesRepository,
56    /// The command answers about the working copy and the shell that invoked it. A caller
57    /// over HTTP or MCP does not share either, so the same question asked over a socket
58    /// would be a different question with the same name, which is worse than not asking it.
59    SessionLocal,
60    /// The command renders, for a person at a terminal, the value the named capability
61    /// answers. The operation is in the API; this is its terminal rendering. The domain
62    /// logic is shared — the command and the capability read the same resolution — and
63    /// only the shaping of the output is the command's own.
64    RendersCapability(&'static str),
65}
66
67impl LocalReason {
68    /// The capability this command renders, when it renders one.
69    ///
70    /// ```
71    /// use majordomus_cli::cli::local::LocalReason;
72    /// assert_eq!(LocalReason::RendersCapability("web.surfaces").renders(), Some("web.surfaces"));
73    /// assert_eq!(LocalReason::ProcessLifecycle.renders(), None);
74    /// ```
75    pub fn renders(self) -> Option<&'static str> {
76        match self {
77            LocalReason::RendersCapability(id) => Some(id),
78            _ => None,
79        }
80    }
81
82    /// The command this one is another name for, when it is.
83    ///
84    /// ```
85    /// use majordomus_cli::cli::local::LocalReason;
86    /// assert_eq!(LocalReason::Alias("why list").aliases(), Some("why list"));
87    /// assert_eq!(LocalReason::SessionLocal.aliases(), None);
88    /// ```
89    pub fn aliases(self) -> Option<&'static str> {
90        match self {
91            LocalReason::Alias(target) => Some(target),
92            _ => None,
93        }
94    }
95
96    /// The short word a report and the generated reference show.
97    ///
98    /// ```
99    /// use majordomus_cli::cli::local::LocalReason;
100    /// assert_eq!(LocalReason::WritesRepository.as_str(), "writes_repository");
101    /// assert_eq!(LocalReason::Alias("why list").as_str(), "alias");
102    /// ```
103    pub fn as_str(self) -> &'static str {
104        match self {
105            LocalReason::Alias(_) => "alias",
106            LocalReason::ProcessLifecycle => "process_lifecycle",
107            LocalReason::WritesRepository => "writes_repository",
108            LocalReason::SessionLocal => "session_local",
109            LocalReason::RendersCapability(_) => "renders_capability",
110        }
111    }
112}
113
114/// One command of the command line that is not itself a capability, and why.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct LocalCommand {
117    /// The command as a person types it after `majordomus`: `bench baseline update`.
118    pub command: &'static str,
119    /// Why it is not a capability.
120    pub reason: LocalReason,
121    /// One sentence about this command in particular. The reason says which of four
122    /// structural cases it is; this says what a reader of *this* command needs to know.
123    pub note: &'static str,
124}
125
126/// Every command of the command line that is not the projection of a capability.
127///
128/// Checked in both directions by [`crate::quality::parity`]: a runnable command that is
129/// neither bound to a capability nor listed here is a finding, and an entry here that
130/// names no command, or names one that is bound, is a finding too.
131///
132/// ```
133/// use majordomus_cli::cli::local::LOCAL;
134/// // every entry says something specific, not just which case it is
135/// assert!(LOCAL.iter().all(|l| l.note.split_whitespace().count() >= 6));
136/// ```
137pub const LOCAL: &[LocalCommand] = &[
138    // ---------------------------------------------------------------- servers
139    LocalCommand {
140        command: "mcp",
141        reason: LocalReason::ProcessLifecycle,
142        note: "becomes the stdio MCP server for the client that spawned it; the capabilities it then serves are the projection.",
143    },
144    LocalCommand {
145        command: "serve",
146        reason: LocalReason::ProcessLifecycle,
147        note: "becomes the HTTP server that serves every capability over the API this command exists to start.",
148    },
149    LocalCommand {
150        command: "serve ensure",
151        reason: LocalReason::ProcessLifecycle,
152        note: "starts the HTTP server for this checkout as a process of its own when none answers, and waits until it is ready.",
153    },
154    LocalCommand {
155        command: "serve stop",
156        reason: LocalReason::ProcessLifecycle,
157        note: "ends the server this checkout's lease names by signalling the process, which nothing served by that process could do to itself.",
158    },
159    // ---------------------------------------------------------------- writers
160    LocalCommand {
161        command: "generate",
162        reason: LocalReason::WritesRepository,
163        note: "writes the committed projections of the registry into the working tree, which a read-only API never offers.",
164    },
165    LocalCommand {
166        command: "economics measure",
167        reason: LocalReason::WritesRepository,
168        note: "records a counted measurement of the context compiler into the benchmark's evidence directory, which a read-only API never offers.",
169    },
170    LocalCommand {
171        command: "economics run",
172        reason: LocalReason::WritesRepository,
173        note: "spends provider usage running real harness sessions and records their usage and gate verdicts as evidence; a deliberate act of a person, never a request.",
174    },
175    LocalCommand {
176        command: "economics references",
177        reason: LocalReason::SessionLocal,
178        note: "builds disposable workspaces on this machine and runs each task's tests in them; a remote caller shares neither the machine nor the toolchain.",
179    },
180    LocalCommand {
181        command: "bench",
182        reason: LocalReason::WritesRepository,
183        note: "times every capability through every transport and writes the results; a measurement served over a transport would be measuring itself.",
184    },
185    LocalCommand {
186        command: "bench baseline update",
187        reason: LocalReason::WritesRepository,
188        note: "accepts a measurement as the baseline of this platform by writing it into the layer, which is a deliberate act of a person.",
189    },
190    LocalCommand {
191        command: "web compose",
192        reason: LocalReason::WritesRepository,
193        note: "assembles the publication into a destination directory, writing every surface's artifact into it.",
194    },
195    LocalCommand {
196        command: "web report tests",
197        reason: LocalReason::WritesRepository,
198        note: "writes the test report surface from a run's output, which is a producer of the web root rather than a reading of it.",
199    },
200    LocalCommand {
201        command: "web report benchmarks",
202        reason: LocalReason::WritesRepository,
203        note: "writes the benchmark report surface from a results file, which is a producer of the web root rather than a reading of it.",
204    },
205    LocalCommand {
206        command: "web report ui",
207        reason: LocalReason::WritesRepository,
208        note: "writes the UI conformance report surface from a probe's output, which is a producer of the web root rather than a reading of it.",
209    },
210    LocalCommand {
211        command: "web report ui-snapshot",
212        reason: LocalReason::WritesRepository,
213        note: "writes the UI's snapshot surface, one file per screen and panel read in-process, which is a producer of the web root rather than a reading of it.",
214    },
215    LocalCommand {
216        command: "web report cockpit-snapshot",
217        reason: LocalReason::WritesRepository,
218        note: "writes the Cockpit's snapshot surface, one file per page read in-process, and leads the UI snapshot's links to those pages, which is a producer of the web root rather than a reading of it.",
219    },
220    LocalCommand {
221        command: "prs",
222        reason: LocalReason::Alias("prs status"),
223        note: "with no subcommand it answers `prs status`, the ranked integration queue.",
224    },
225    LocalCommand {
226        command: "prs plan",
227        reason: LocalReason::RendersCapability("integration.queue"),
228        note: "renders the queue's next merge, next refresh and lanes for a person; the value is `integration.queue`'s.",
229    },
230    LocalCommand {
231        command: "prs brief",
232        reason: LocalReason::RendersCapability("integration.queue"),
233        note: "renders the last `integration.queue` answer this checkout built, summarised in one line for `majordomus context`, without building a queue again.",
234    },
235    LocalCommand {
236        command: "prs refresh",
237        reason: LocalReason::WritesRepository,
238        note: "observes the forge over the network, fetches every open head into refs/majordomus/prs/ and records the observation; the shared server never reaches the network (ADR 0101).",
239    },
240    LocalCommand {
241        command: "prs drain",
242        reason: LocalReason::WritesRepository,
243        note: "merges pull requests on the forge and pushes master into their branches, under the base branch's integration lease; a mutation of the repository and its remote.",
244    },
245    LocalCommand {
246        command: "prs repair",
247        reason: LocalReason::WritesRepository,
248        note: "with --apply, brings master into one named pull request's branch and pushes it under the base branch's integration lease, the act recorded on the integration trail first: a mutation of the repository and its remote. Its default dry run decides on the recorded observation and changes nothing.",
249    },
250    LocalCommand {
251        command: "prs cleanup",
252        reason: LocalReason::WritesRepository,
253        note: "closes pull requests on the forge whose work is provably on master, only with --apply.",
254    },
255    LocalCommand {
256        command: "pack build",
257        reason: LocalReason::WritesRepository,
258        note: "writes the pack's index, shards and manifest under tmp/packs/ (or --out), after planning with pack.plan and before verifying with pack.verify.",
259    },
260    LocalCommand {
261        command: "offload derive",
262        reason: LocalReason::WritesRepository,
263        note: "stages the files a derive on a declared host changed into the caller's worktree, after a round trip over the caller's own ssh credentials.",
264    },
265    LocalCommand {
266        command: "worktree create",
267        reason: LocalReason::WritesRepository,
268        note: "creates a branch and a linked worktree on disk at the path the topology derives for it.",
269    },
270    LocalCommand {
271        command: "worktree ensure",
272        reason: LocalReason::WritesRepository,
273        note: "creates the canonical worktree of a branch when it is absent, which is the same write made idempotent.",
274    },
275    LocalCommand {
276        command: "worktree repair",
277        reason: LocalReason::WritesRepository,
278        note: "rewrites the git administrative files of a worktree whose links no longer resolve.",
279    },
280    LocalCommand {
281        command: "worktree remove",
282        reason: LocalReason::WritesRepository,
283        note: "removes a linked worktree from disk and from the repository's administrative data.",
284    },
285    LocalCommand {
286        command: "worktree cleanup",
287        reason: LocalReason::WritesRepository,
288        note: "prunes the worktrees git still records and whose directories are gone.",
289    },
290    // ---------------------------------------------------------------- the caller's own checkout
291    LocalCommand {
292        command: "offload probe",
293        reason: LocalReason::SessionLocal,
294        note: "measures the declared hosts over the invoking user's ssh keys and network, which a networked caller does not share.",
295    },
296    LocalCommand {
297        command: "worktree root",
298        reason: LocalReason::SessionLocal,
299        note: "answers where the repository containing the current directory begins, which a networked caller does not share.",
300    },
301    LocalCommand {
302        command: "worktree guard",
303        reason: LocalReason::SessionLocal,
304        note: "decides whether the commit being made in this checkout may proceed, and is invoked by this checkout's pre-commit hook.",
305    },
306    // ---------------------------------------------------------------- other names
307    LocalCommand {
308        command: "commit",
309        reason: LocalReason::Alias("commit plan"),
310        note: "plans when no subcommand follows it, because planning is what a person wants when they ask about committing; the two are one operation typed two ways.",
311    },
312    LocalCommand {
313        command: "why",
314        reason: LocalReason::Alias("why list"),
315        note: "runs the catalogue listing when no subcommand follows it, so the two are one operation typed two ways.",
316    },
317    LocalCommand {
318        command: "web",
319        reason: LocalReason::Alias("web list"),
320        note: "runs the surface listing when no subcommand follows it, so the two are one operation typed two ways.",
321    },
322    LocalCommand {
323        command: "distribution",
324        reason: LocalReason::Alias("distribution show"),
325        note: "runs the model listing when no subcommand follows it, so the two are one operation typed two ways.",
326    },
327    LocalCommand {
328        command: "worktree",
329        reason: LocalReason::Alias("worktree status"),
330        note: "runs the status of this checkout when no subcommand follows it, so the two are one operation typed two ways.",
331    },
332    // ---------------------------------------------------------------- renderings of a capability
333    LocalCommand {
334        command: "capabilities schema",
335        reason: LocalReason::RendersCapability("capabilities.describe"),
336        note: "prints the canonical input or output schema that capabilities.describe already carries on the descriptor it returns.",
337    },
338    LocalCommand {
339        command: "offload hosts",
340        reason: LocalReason::RendersCapability("objects.get"),
341        note: "prints the derive-offload declaration, the object objects.get serves as majordomus://derive-offload/<id>, with its defaults filled in.",
342    },
343    LocalCommand {
344        command: "guard hook",
345        reason: LocalReason::RendersCapability("guard.edit"),
346        note: "answers guard.edit for the path a provider's pre-tool payload on stdin names, without building the index because it runs before every edit, and says the answer as the exit code the provider reads: 2 refuses the tool.",
347    },
348    LocalCommand {
349        command: "mesh status",
350        reason: LocalReason::RendersCapability("mesh.status"),
351        note: "asks this checkout's running server for mesh.status and renders it: the mesh lives in the server's memory, so an in-process answer would truthfully say only that this process runs no mesh.",
352    },
353    LocalCommand {
354        command: "mesh nodes",
355        reason: LocalReason::RendersCapability("mesh.nodes"),
356        note: "asks this checkout's running server for mesh.nodes and renders it, for the same reason as mesh status: the registry is the server's, not this process's.",
357    },
358    LocalCommand {
359        command: "mesh peers",
360        reason: LocalReason::RendersCapability("mesh.peers"),
361        note: "asks this checkout's running server for mesh.peers and renders the machine, runtime and session tree: links and the journal are the server's memory.",
362    },
363    LocalCommand {
364        command: "mesh peer",
365        reason: LocalReason::RendersCapability("mesh.peer"),
366        note: "asks this checkout's running server for mesh.peer and renders one runtime; exits 10 when the runtime is not known there.",
367    },
368    LocalCommand {
369        command: "mesh state",
370        reason: LocalReason::RendersCapability("mesh.state"),
371        note: "asks this checkout's running server for mesh.state and renders the folded cooperation state and its digest.",
372    },
373    LocalCommand {
374        command: "monitor",
375        reason: LocalReason::RendersCapability("monitor.stream"),
376        note: "asks this checkout's running server for monitor.stream and renders a page, or follows it.",
377    },
378    LocalCommand {
379        command: "mesh events",
380        reason: LocalReason::RendersCapability("mesh.events"),
381        note: "asks this checkout's running server for mesh.events and renders one page of the journal.",
382    },
383    LocalCommand {
384        command: "mesh verify",
385        reason: LocalReason::RendersCapability("mesh.verify"),
386        note: "runs mesh.verify on this checkout's running server — the rounds are the server's links — and exits 10 when a check or a round fails.",
387    },
388    LocalCommand {
389        command: "mesh claim",
390        reason: LocalReason::RendersCapability("mesh.claim"),
391        note: "runs mesh.claim on this checkout's running server, whose journal replicates it; exits 10 with the conflicting claims when it is refused.",
392    },
393    LocalCommand {
394        command: "mesh release",
395        reason: LocalReason::RendersCapability("mesh.release"),
396        note: "runs mesh.release on this checkout's running server, the only process that holds the claim.",
397    },
398    LocalCommand {
399        command: "mesh session open",
400        reason: LocalReason::RendersCapability("mesh.session.open"),
401        note: "runs mesh.session.open on this checkout's running server, whose journal replicates the session.",
402    },
403    LocalCommand {
404        command: "mesh session close",
405        reason: LocalReason::RendersCapability("mesh.session.close"),
406        note: "runs mesh.session.close on this checkout's running server.",
407    },
408    LocalCommand {
409        command: "mesh handover publish",
410        reason: LocalReason::RendersCapability("mesh.handover.publish"),
411        note: "runs mesh.handover.publish on this checkout's running server, which reads the record and replicates it.",
412    },
413    LocalCommand {
414        command: "mesh handover consume",
415        reason: LocalReason::RendersCapability("mesh.handover.consume"),
416        note: "runs mesh.handover.consume on this checkout's running server, which holds the replicated handover and writes the local record.",
417    },
418    LocalCommand {
419        command: "mesh review request",
420        reason: LocalReason::RendersCapability("mesh.review.request"),
421        note: "runs mesh.review.request on this checkout's running server, whose link table knows which peers carry reviews.",
422    },
423    LocalCommand {
424        command: "mesh review answer",
425        reason: LocalReason::RendersCapability("mesh.review.answer"),
426        note: "runs mesh.review.answer on this checkout's running server, whose journal holds the request.",
427    },
428    LocalCommand {
429        command: "mesh review withdraw",
430        reason: LocalReason::RendersCapability("mesh.review.withdraw"),
431        note: "runs mesh.review.withdraw on this checkout's running server, whose journal holds the request.",
432    },
433    LocalCommand {
434        command: "capabilities validate",
435        reason: LocalReason::RendersCapability("repository.info"),
436        note: "reports the registry's own validation, which repository.info answers as the diagnostics of the process that built it.",
437    },
438    LocalCommand {
439        command: "bench coverage",
440        reason: LocalReason::RendersCapability("capabilities.list"),
441        note: "reports which capabilities are benchmark targets and which are waived, from the benchmark policy capabilities.list carries on every descriptor.",
442    },
443    LocalCommand {
444        command: "web list",
445        reason: LocalReason::RendersCapability("web.surfaces"),
446        note: "prints the resolved topology web.surfaces answers, in the same route-precedence order and from the same resolution.",
447    },
448    LocalCommand {
449        command: "web explain",
450        reason: LocalReason::RendersCapability("web.surfaces"),
451        note: "prints one surface of that same resolved topology, with the provenance of every value on it.",
452    },
453    LocalCommand {
454        command: "web validate",
455        reason: LocalReason::RendersCapability("web.surfaces"),
456        note: "prints the findings web.surfaces already carries, and can additionally require each artifact to be present on this disk.",
457    },
458    LocalCommand {
459        command: "web manifest",
460        reason: LocalReason::RendersCapability("web.surfaces"),
461        note: "prints the routing manifest derived from the same resolved topology, for a static host to consume.",
462    },
463    LocalCommand {
464        command: "distribution validate",
465        reason: LocalReason::RendersCapability("distribution.model"),
466        note: "prints the findings of the distribution model that distribution.model returns with it.",
467    },
468    LocalCommand {
469        command: "distribution targets",
470        reason: LocalReason::RendersCapability("distribution.model"),
471        note: "prints the platforms of the model distribution.model returns, one line each.",
472    },
473    LocalCommand {
474        command: "distribution matrix",
475        reason: LocalReason::RendersCapability("distribution.model"),
476        note: "prints those same platforms in the shape a CI job's matrix consumes.",
477    },
478    LocalCommand {
479        command: "distribution metadata",
480        reason: LocalReason::RendersCapability("distribution.model"),
481        note: "prints the packaging metadata of the model, which is the same value seen field by field.",
482    },
483    LocalCommand {
484        command: "worktree list",
485        reason: LocalReason::RendersCapability("worktree.topology"),
486        note: "prints the registered worktrees the topology holds, one line each.",
487    },
488    LocalCommand {
489        command: "worktree path",
490        reason: LocalReason::RendersCapability("worktree.topology"),
491        note: "prints the canonical path the topology derives for a branch, for a shell to interpolate.",
492    },
493    LocalCommand {
494        command: "worktree validate",
495        reason: LocalReason::RendersCapability("worktree.topology"),
496        note: "prints the topology's findings and exits non-zero on one, which is the same value read as a verdict.",
497    },
498    LocalCommand {
499        command: "worktree doctor",
500        reason: LocalReason::RendersCapability("worktree.topology"),
501        note: "prints those findings with the command that repairs each one beside it.",
502    },
503    LocalCommand {
504        command: "worktree branches",
505        reason: LocalReason::RendersCapability("worktree.topology"),
506        note: "prints the branches of the repository with the worktree each belongs in, derived from the same topology.",
507    },
508    // ---------------------------------------------------------------- the command graph
509    // Three landings — the command graph (#117), the environment and executions (#142) and
510    // the release module — each added runnable commands without saying here why they were
511    // not capabilities, and the parity check found all twenty-one at once the day a CI run
512    // finished. Every entry below was read off the command's own dispatch; none is a guess.
513    LocalCommand {
514        command: "commands",
515        reason: LocalReason::Alias("commands list"),
516        note: "with nothing after it, lists the graph.",
517    },
518    LocalCommand {
519        command: "commands list",
520        reason: LocalReason::RendersCapability("commands.list"),
521        note: "prints the command graph as a table, one row per command with where it is projected.",
522    },
523    LocalCommand {
524        command: "commands show",
525        reason: LocalReason::RendersCapability("commands.get"),
526        note: "prints one command's node: its arguments, its origin, its projections.",
527    },
528    LocalCommand {
529        command: "commands explain",
530        reason: LocalReason::RendersCapability("commands.get"),
531        note: "prints the same node with the provenance of every fact beside it.",
532    },
533    LocalCommand {
534        command: "commands graph",
535        reason: LocalReason::RendersCapability("commands.graph"),
536        note: "prints the whole graph document, the value the capability answers.",
537    },
538    LocalCommand {
539        command: "commands bridge",
540        reason: LocalReason::WritesRepository,
541        note: "writes the workflow bridge the graph derives into .ai/local/cache/, which a read-only API never offers.",
542    },
543    // ---------------------------------------------------------------- completion
544    LocalCommand {
545        command: "completion",
546        reason: LocalReason::Alias("completion init"),
547        note: "with nothing after it, prints the integration for the current shell.",
548    },
549    LocalCommand {
550        command: "completion init",
551        reason: LocalReason::SessionLocal,
552        note: "prints the shell snippet that turns TAB into a query of this executable; which shell is a fact of the terminal that asked.",
553    },
554    LocalCommand {
555        command: "completion query",
556        reason: LocalReason::SessionLocal,
557        note: "answers the candidates for a partial command line the shell is holding, which no caller over a socket has.",
558    },
559    LocalCommand {
560        command: "completion install",
561        reason: LocalReason::WritesRepository,
562        note: "writes the integration into the person's shell rc file, backing up what was there; a deliberate act at a terminal.",
563    },
564    // ---------------------------------------------------------------- environment
565    LocalCommand {
566        command: "env",
567        reason: LocalReason::Alias("env status"),
568        note: "with nothing after it, reports the checkout.",
569    },
570    LocalCommand {
571        command: "env status",
572        reason: LocalReason::RendersCapability("environment.status"),
573        note: "prints what this checkout is, as a table.",
574    },
575    LocalCommand {
576        command: "env explain",
577        reason: LocalReason::RendersCapability("environment.explain"),
578        note: "prints the same facts with where each one was read from.",
579    },
580    LocalCommand {
581        command: "env preflight",
582        reason: LocalReason::RendersCapability("environment.preflight"),
583        note: "prints what is proven about this checkout, verdict by verdict with its evidence; it resolves fast from the cached rule tally unless --full asks it to count.",
584    },
585    LocalCommand {
586        command: "env banner",
587        reason: LocalReason::SessionLocal,
588        note: "prints the one-line banner direnv shows on entering this worktree; it is about the shell that just arrived.",
589    },
590    LocalCommand {
591        command: "env export",
592        reason: LocalReason::SessionLocal,
593        note: "prints the environment variables for the shell to evaluate, which is the shell's own state and nobody else's.",
594    },
595    LocalCommand {
596        command: "env enter",
597        reason: LocalReason::SessionLocal,
598        note: "is the whole of entering this repository as one call (ADR 0043) — the assignments, the banner, the bridge refreshed and the runtime ensured — and every part of it is about the shell that just arrived, which no caller over a socket shares.",
599    },
600    // ---------------------------------------------------------------- executions
601    LocalCommand {
602        command: "executions",
603        reason: LocalReason::Alias("executions list"),
604        note: "with nothing after it, lists the executions.",
605    },
606    // ---------------------------------------------------------------- product
607    LocalCommand {
608        command: "product",
609        reason: LocalReason::Alias("product list"),
610        note: "with nothing after it, lists the features.",
611    },
612    // ---------------------------------------------------------------- release
613    LocalCommand {
614        command: "release",
615        reason: LocalReason::Alias("release changelog"),
616        note: "with nothing after it, prints the changelog.",
617    },
618    LocalCommand {
619        command: "release changelog",
620        reason: LocalReason::RendersCapability("release.changelog"),
621        note: "prints the changelog as Markdown, the same document the capability answers as data.",
622    },
623    LocalCommand {
624        command: "release version",
625        reason: LocalReason::RendersCapability("release.version"),
626        note: "prints the declared and the tool's version and whether they agree.",
627    },
628    LocalCommand {
629        command: "release analyze",
630        reason: LocalReason::RendersCapability("release.analysis"),
631        note: "prints what the public contract did since the last release and the smallest version it allows.",
632    },
633    LocalCommand {
634        command: "release next",
635        reason: LocalReason::RendersCapability("release.next"),
636        note: "prints the version a landing of HEAD onto the trunk would allocate, and why.",
637    },
638    LocalCommand {
639        command: "release bump",
640        reason: LocalReason::WritesRepository,
641        note: "raises the version in the one place it is authored, and the lock's record of it; the one writer, and a deliberate act.",
642    },
643];
644
645/// The entry for a command, by the words a person types after `majordomus`.
646///
647/// ```
648/// use majordomus_cli::cli::local::find;
649/// assert!(find("serve").is_some());
650/// assert!(find("capabilities list").is_none(), "that one is a capability");
651/// ```
652pub fn find(command: &str) -> Option<&'static LocalCommand> {
653    LOCAL.iter().find(|l| l.command == command)
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use std::collections::BTreeSet;
660
661    #[test]
662    fn no_command_is_classified_twice() {
663        let mut seen = BTreeSet::new();
664        for l in LOCAL {
665            assert!(seen.insert(l.command), "{} is listed twice", l.command);
666        }
667    }
668
669    #[test]
670    fn every_alias_names_a_command_that_is_itself_accounted_for() {
671        // an alias of an unclassified command would move the question rather than answer it
672        for l in LOCAL {
673            let Some(target) = l.reason.aliases() else {
674                continue;
675            };
676            assert_ne!(target, l.command, "{} aliases itself", l.command);
677            // the target is either a capability's command or classified in its own right;
678            // parity proves the first, and this proves the entry is not circular
679            assert!(
680                find(target).is_none_or(|t| t.reason.aliases().is_none()),
681                "{} aliases {target}, which is itself an alias",
682                l.command
683            );
684        }
685    }
686
687    #[test]
688    fn a_rendering_names_a_capability_and_never_a_command() {
689        for l in LOCAL {
690            let Some(id) = l.reason.renders() else {
691                continue;
692            };
693            assert!(
694                id.contains('.') && !id.contains(' '),
695                "{} renders '{id}', which is not a capability id",
696                l.command
697            );
698        }
699    }
700}