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`]). Unlike the other leaf
5//! tools — which are flat-flag — `ct-okf` is **subcommand**-shaped (`ct okf
6//! search`, `ct okf roots add`, …), because its surface spans querying,
7//! root management, index maintenance, and authoring. The `ct-okf` bin is a
8//! parse-and-dispatch wrapper over this `Cli`.
9//!
10//! Global flags (`--json`, `--quiet`, `--base`, the walker vocabulary, …) are
11//! declared `global` so they may appear before or after the subcommand; the
12//! per-verb flags live on each subcommand's args struct.
13
14use std::path::PathBuf;
15
16use clap::{Args, Parser, Subcommand};
17
18use crate::explain::Format;
19use crate::pulse::HeartbeatOpts;
20
21#[derive(Parser, Debug)]
22#[command(
23 name = "ct-okf",
24 version,
25 about = "Author, query, and index Open Knowledge Format bundles across a project's content roots.",
26 long_about = "ct-okf manages Open Knowledge Format (OKF) knowledge for a project (also reachable \
27 as `ct okf`). It works over the project's configured content roots and keeps a \
28 lazily-maintained full-text index so `ct okf search` is always current. Pick a \
29 subcommand: search/find query, roots/index/init configure, validate/links check, \
30 show/add/mv/set/log/gen-index/script author. See `ct-okf --explain` for \
31 agent-oriented documentation."
32)]
33pub struct Cli {
34 #[command(subcommand)]
35 pub command: Option<Command>,
36
37 /// Search root for bundle-scoped verbs (validate/links/find/show), and the
38 /// directory project discovery starts from for search/index/roots/init.
39 #[arg(long, default_value = ".", global = true)]
40 pub base: PathBuf,
41
42 /// Limit to files whose name matches; '|'-separated alternatives.
43 #[arg(long, global = true)]
44 pub name: Option<String>,
45
46 /// Include dot-entries (names starting with '.').
47 #[arg(long, global = true)]
48 pub hidden: bool,
49
50 /// Follow symlinks while traversing.
51 #[arg(long, global = true)]
52 pub follow: bool,
53
54 /// Walk gitignored / .ignore files too (the .git directory is always skipped).
55 #[arg(long, global = true)]
56 pub no_ignore: bool,
57
58 /// Emit a structured JSON result instead of text.
59 #[arg(long, global = true)]
60 pub json: bool,
61
62 /// Like --json, but pretty-printed (indented).
63 #[arg(long, global = true)]
64 pub json_pretty: bool,
65
66 /// Suppress informational output (exit status and --emit still report).
67 #[arg(long, global = true)]
68 pub quiet: bool,
69
70 /// Abort with exit 2 if the run exceeds SECS seconds (fractional allowed).
71 #[arg(long, value_name = "SECS", global = true)]
72 pub timeout: Option<f64>,
73
74 #[command(flatten)]
75 pub heartbeat: HeartbeatOpts,
76
77 /// Print agent usage docs (md or json) and exit.
78 #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
79 pub explain: Option<Format>,
80}
81
82/// The `ct-okf` verbs.
83#[derive(Subcommand, Debug)]
84pub enum Command {
85 /// Full-text search the project's OKF content roots (auto-updates the index first).
86 Search(SearchArgs),
87 /// List concepts by metadata (--type / --tag) in the --base bundle.
88 Find(FindArgs),
89 /// Manage the project's OKF content roots.
90 Roots(RootsArgs),
91 /// Maintain the search index.
92 Index(IndexArgs),
93 /// Onboard: discover content roots and record them (optionally write markers).
94 Init(InitArgs),
95 /// Judge the --base bundle's OKF conformance (framed verdict).
96 Validate(CheckArgs),
97 /// Report broken bundle cross-links (framed verdict).
98 Links(CheckArgs),
99 /// Print one concept's frontmatter.
100 Show(ShowArgs),
101 /// Scaffold a new concept file (alias: new).
102 #[command(alias = "new")]
103 Add(AddArgs),
104 /// Move/rename a concept, fixing bundle cross-links (alias: rename).
105 #[command(alias = "rename")]
106 Mv(MvArgs),
107 /// Set or update a scalar frontmatter field on a concept.
108 Set(SetArgs),
109 /// Prepend a dated entry to a bundle's log.md.
110 Log(LogArgs),
111 /// (Re)generate a bundle directory's index.md from its concepts.
112 GenIndex(GenIndexArgs),
113 /// Run a .ctb batch of OKF mutations atomically.
114 Script(ScriptArgs),
115}
116
117/// Framing options shared by the check verbs (`validate`, `links`).
118#[derive(Args, Debug, Default)]
119pub struct Framing {
120 /// Print a `== QUESTION ==` banner before the check.
121 #[arg(long)]
122 pub question: Option<String>,
123 /// Classify the violation count: any|none|N|=N|+N|-N (default none).
124 #[arg(long)]
125 pub expect: Option<String>,
126 /// Expand a template to stdout after the check (tokens {RESULT} {COUNT} …).
127 #[arg(long, alias = "emit-stdout")]
128 pub emit: Option<String>,
129 /// Expand a template to stderr after the check.
130 #[arg(long)]
131 pub emit_stderr: Option<String>,
132}
133
134#[derive(Args, Debug)]
135pub struct SearchArgs {
136 /// Query terms. Supports `term`, `term*` (prefix), `term~`/`term~2` (fuzzy), and `/regex/`.
137 #[arg(value_name = "QUERY", required = true)]
138 pub query: Vec<String>,
139 /// Maximum number of hits to return.
140 #[arg(long, default_value_t = 20)]
141 pub limit: usize,
142 /// Only hits of this exact OKF type.
143 #[arg(long = "type", value_name = "TYPE")]
144 pub type_: Option<String>,
145 /// Only hits carrying all of these tags.
146 #[arg(long, value_delimiter = ',')]
147 pub tag: Vec<String>,
148}
149
150#[derive(Args, Debug)]
151pub struct FindArgs {
152 /// Filter to this exact OKF type.
153 #[arg(long = "type", value_name = "TYPE")]
154 pub type_: Option<String>,
155 /// Filter to concepts carrying all of these tags.
156 #[arg(long, value_delimiter = ',')]
157 pub tag: Vec<String>,
158}
159
160#[derive(Args, Debug)]
161pub struct RootsArgs {
162 #[command(subcommand)]
163 pub action: RootsCmd,
164}
165
166#[derive(Subcommand, Debug)]
167pub enum RootsCmd {
168 /// List the configured/detected content roots and how each was detected.
169 List,
170 /// Register a directory as a content root (records it in .ct/okf.jsonc).
171 Add {
172 /// The directory to add (project-relative or absolute).
173 dir: PathBuf,
174 /// Also drop a `.okf` marker file in the directory.
175 #[arg(long)]
176 marker: bool,
177 },
178 /// Unregister a content root from .ct/okf.jsonc.
179 Rm {
180 /// The directory to remove.
181 dir: PathBuf,
182 },
183 /// Discover candidate roots by scanning for OKF concepts.
184 Scan {
185 /// Record discovered roots in config and drop `.okf` markers.
186 #[arg(long)]
187 write: bool,
188 },
189}
190
191#[derive(Args, Debug)]
192pub struct IndexArgs {
193 #[command(subcommand)]
194 pub action: IndexCmd,
195}
196
197#[derive(Subcommand, Debug)]
198pub enum IndexCmd {
199 /// Report index freshness (docs, segments, tombstones, pending changes).
200 Status,
201 /// Show the effective provider/include/exclude indexing plan.
202 Scopes {
203 /// Include derived defaults and hard exclusions in the report (the default plan is always effective).
204 #[arg(long)]
205 effective: bool,
206 },
207 /// Explain why one path is included in or excluded from the index.
208 Why {
209 /// File whose effective indexing decision should be explained.
210 path: PathBuf,
211 },
212 /// Preview or materialize the conservative derived indexing configuration.
213 Init {
214 /// Print the derived configuration without writing it (the default).
215 #[arg(long)]
216 dry_run: bool,
217 /// Write the derived configuration to .ct/index.jsonc.
218 #[arg(long, conflicts_with = "dry_run")]
219 write: bool,
220 },
221 /// Inspect or control the opportunistic filesystem watcher.
222 Watch {
223 #[command(subcommand)]
224 action: WatchCmd,
225 },
226 /// Reconcile the index against the content roots now.
227 Update,
228 /// Merge segments and drop tombstones, reclaiming space.
229 Condense,
230 /// Discard and rebuild the index from scratch.
231 Rebuild,
232}
233
234#[derive(Subcommand, Debug)]
235pub enum WatchCmd {
236 /// Report watcher health and its last update metrics.
237 Status,
238 /// Start the per-project watcher if one is not already healthy.
239 Start,
240 /// Ask the per-project watcher to exit.
241 Stop,
242 /// Internal foreground daemon body (normally started automatically).
243 #[command(hide = true)]
244 Run,
245}
246
247#[derive(Args, Debug)]
248pub struct InitArgs {
249 /// Also drop a `.okf` marker file in each discovered root.
250 #[arg(long)]
251 pub marker: bool,
252}
253
254#[derive(Args, Debug)]
255pub struct CheckArgs {
256 /// Also count broken bundle-relative cross-links as violations.
257 #[arg(long)]
258 pub strict: bool,
259 #[command(flatten)]
260 pub framing: Framing,
261}
262
263#[derive(Args, Debug)]
264pub struct ShowArgs {
265 /// The concept file to show.
266 pub path: PathBuf,
267}
268
269#[derive(Args, Debug)]
270pub struct AddArgs {
271 /// The concept file to create (must not exist).
272 pub path: PathBuf,
273 /// The concept's OKF type (required).
274 #[arg(long = "type", value_name = "TYPE")]
275 pub type_: String,
276 /// Human title (defaults to the file stem).
277 #[arg(long)]
278 pub title: Option<String>,
279 /// One-sentence description.
280 #[arg(long)]
281 pub description: Option<String>,
282 /// Tags (comma-separated or repeated).
283 #[arg(long, value_delimiter = ',')]
284 pub tag: Vec<String>,
285}
286
287#[derive(Args, Debug)]
288pub struct MvArgs {
289 /// The concept file to move.
290 pub src: PathBuf,
291 /// The destination path.
292 pub dst: PathBuf,
293}
294
295#[derive(Args, Debug)]
296pub struct SetArgs {
297 /// FIELD=VALUE to set on the concept's frontmatter.
298 #[arg(value_name = "FIELD=VALUE")]
299 pub spec: String,
300 /// The concept file to edit.
301 #[arg(long)]
302 pub file: PathBuf,
303}
304
305#[derive(Args, Debug)]
306pub struct LogArgs {
307 /// The log message.
308 #[arg(value_name = "MESSAGE")]
309 pub message: String,
310 /// Entry label (default Update).
311 #[arg(long = "kind", value_name = "LABEL")]
312 pub kind: Option<String>,
313}
314
315#[derive(Args, Debug)]
316pub struct GenIndexArgs {
317 /// Scaffold an absent index.md declaring okf_version instead of listing concepts.
318 #[arg(long)]
319 pub scaffold: bool,
320}
321
322#[derive(Args, Debug)]
323pub struct ScriptArgs {
324 /// The .ctb script of new/set/log/index/init items.
325 pub path: PathBuf,
326 /// Print the plan and write nothing.
327 #[arg(long)]
328 pub dry_run: bool,
329 /// Directive prefix for script lines (default '#%').
330 #[arg(long, value_name = "STR")]
331 pub fence: Option<String>,
332}