coding_tools/cli/ct_okf.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-okf` command grammar (see [`crate::cli`]); the `ct-okf` bin is a
5//! thin parse-and-dispatch wrapper over this `Cli`.
6
7use std::path::PathBuf;
8
9use clap::Parser;
10
11use crate::explain::Format;
12use crate::pulse::HeartbeatOpts;
13
14#[derive(Parser, Debug)]
15#[command(
16 name = "ct-okf",
17 version,
18 about = "Author and query Open Knowledge Format (OKF) bundles of Markdown concepts.",
19 long_about = "ct-okf works with OKF v0.1 bundles — directory trees of Markdown concepts whose \
20 YAML frontmatter carries a required `type` plus optional metadata (also reachable \
21 as `ct okf`). Read-only verbs: --validate (a conformance verdict), --list (query \
22 concepts by --type/--tag), --show (one concept's metadata), --links (cross-link \
23 report / broken-link verdict). Authoring verbs (these write): --new, --init, \
24 --index, --log, --set. See `ct-okf --explain` for agent-oriented documentation."
25)]
26pub struct Cli {
27 /// Bundle root (or single concept root) to operate on.
28 #[arg(long, default_value = ".")]
29 pub base: PathBuf,
30
31 /// Limit selection to files whose name matches; '|'-separated alternatives, each substring->glob->regex promoted and anchored.
32 #[arg(long)]
33 pub name: Option<String>,
34
35 /// Include dot-entries (names starting with '.'); default skips them.
36 #[arg(long)]
37 pub hidden: bool,
38
39 /// Follow symlinks while traversing.
40 #[arg(long)]
41 pub follow: bool,
42
43 /// Walk gitignored / .ignore files too (the .git directory is always skipped); by default the walk skips what git would.
44 #[arg(long)]
45 pub no_ignore: bool,
46
47 // ----- verbs (choose exactly one) -----
48 /// Check the bundle for OKF conformance and report a verdict (the default verb when no other is given).
49 #[arg(long)]
50 pub validate: bool,
51
52 /// List the bundle's concepts with their metadata; filter with --type / --tag.
53 #[arg(long)]
54 pub list: bool,
55
56 /// Show one concept's frontmatter (give the concept path).
57 #[arg(long, value_name = "PATH")]
58 pub show: Option<PathBuf>,
59
60 /// Report the bundle's cross-links; with --strict, fail on a broken bundle-relative link.
61 #[arg(long)]
62 pub links: bool,
63
64 /// Scaffold a new concept at PATH (requires --type); refuses to overwrite.
65 #[arg(long, value_name = "PATH")]
66 pub new: Option<PathBuf>,
67
68 /// Scaffold a bundle root index.md (declaring okf_version) if absent.
69 #[arg(long)]
70 pub init: bool,
71
72 /// (Re)generate index.md for --base from the concepts' frontmatter.
73 #[arg(long)]
74 pub index: bool,
75
76 /// Prepend a dated entry to the bundle's log.md (use --log-kind to label it).
77 #[arg(long, value_name = "MESSAGE")]
78 pub log: Option<String>,
79
80 /// Set or update a frontmatter field on the --file concept: FIELD=VALUE.
81 #[arg(long, value_name = "FIELD=VALUE")]
82 pub set: Option<String>,
83
84 /// Run a .ctb script of new/set/log/index/init items atomically: simulate the whole batch, write only if every op succeeds.
85 #[arg(long, value_name = "PATH")]
86 pub script: Option<PathBuf>,
87
88 /// With --script: simulate and print the plan, but write nothing.
89 #[arg(long)]
90 pub dry_run: bool,
91
92 /// With --script: the directive prefix for script lines (default "#%").
93 #[arg(long, value_name = "STR")]
94 pub fence: Option<String>,
95
96 // ----- authoring / filtering parameters -----
97 /// The concept `type` for --new; also filters --list to this type.
98 #[arg(long = "type", value_name = "TYPE")]
99 pub type_: Option<String>,
100
101 /// With --new: the concept title.
102 #[arg(long)]
103 pub title: Option<String>,
104
105 /// With --new: the concept description (one sentence).
106 #[arg(long)]
107 pub description: Option<String>,
108
109 /// With --new: tags (comma-separated); also filters --list to concepts carrying all given tags.
110 #[arg(long, value_delimiter = ',')]
111 pub tag: Vec<String>,
112
113 /// With --log: the entry label (e.g. Update, Creation). Default: Update.
114 #[arg(long, value_name = "LABEL")]
115 pub log_kind: Option<String>,
116
117 /// With --set: the concept file to edit.
118 #[arg(long, value_name = "PATH")]
119 pub file: Option<PathBuf>,
120
121 /// With --validate / --links: also treat broken bundle-relative links as failures.
122 #[arg(long)]
123 pub strict: bool,
124
125 // ----- framed verdict (for --validate / --links) -----
126 /// Question this check answers, framing it as a test; printed as a "== ... ==" banner unless --quiet.
127 #[arg(long)]
128 pub question: Option<String>,
129
130 /// Verdict expectation over the violation count: any|none|N|=N|+N|-N. Default: none (every concept conforms / no broken links).
131 #[arg(long)]
132 pub expect: Option<String>,
133
134 /// Template written to stdout after a check. Tokens: {RESULT} {QUESTION} {COUNT} {TOTAL} {BASE} {MATCHES} ({COUNT} is the violation count).
135 #[arg(long, alias = "emit-stdout")]
136 pub emit: Option<String>,
137
138 /// Template written to stderr after a check (same tokens as --emit).
139 #[arg(long)]
140 pub emit_stderr: Option<String>,
141
142 /// Suppress informational output; report via exit status (and --emit, which still fires).
143 #[arg(long)]
144 pub quiet: bool,
145
146 /// Emit a structured JSON result instead of text.
147 #[arg(long)]
148 pub json: bool,
149
150 /// Like `--json`, but pretty-printed (indented).
151 #[arg(long)]
152 pub json_pretty: bool,
153
154 /// Abort with exit 2 if the run exceeds SECS seconds (fractional allowed).
155 #[arg(long, value_name = "SECS")]
156 pub timeout: Option<f64>,
157
158 #[command(flatten)]
159 pub heartbeat: HeartbeatOpts,
160
161 /// Print agent usage docs (md or json) and exit.
162 #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
163 pub explain: Option<Format>,
164}