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 /// Reconcile the index against the content roots now.
202 Update,
203 /// Merge segments and drop tombstones, reclaiming space.
204 Condense,
205 /// Discard and rebuild the index from scratch.
206 Rebuild,
207}
208
209#[derive(Args, Debug)]
210pub struct InitArgs {
211 /// Also drop a `.okf` marker file in each discovered root.
212 #[arg(long)]
213 pub marker: bool,
214}
215
216#[derive(Args, Debug)]
217pub struct CheckArgs {
218 /// Also count broken bundle-relative cross-links as violations.
219 #[arg(long)]
220 pub strict: bool,
221 #[command(flatten)]
222 pub framing: Framing,
223}
224
225#[derive(Args, Debug)]
226pub struct ShowArgs {
227 /// The concept file to show.
228 pub path: PathBuf,
229}
230
231#[derive(Args, Debug)]
232pub struct AddArgs {
233 /// The concept file to create (must not exist).
234 pub path: PathBuf,
235 /// The concept's OKF type (required).
236 #[arg(long = "type", value_name = "TYPE")]
237 pub type_: String,
238 /// Human title (defaults to the file stem).
239 #[arg(long)]
240 pub title: Option<String>,
241 /// One-sentence description.
242 #[arg(long)]
243 pub description: Option<String>,
244 /// Tags (comma-separated or repeated).
245 #[arg(long, value_delimiter = ',')]
246 pub tag: Vec<String>,
247}
248
249#[derive(Args, Debug)]
250pub struct MvArgs {
251 /// The concept file to move.
252 pub src: PathBuf,
253 /// The destination path.
254 pub dst: PathBuf,
255}
256
257#[derive(Args, Debug)]
258pub struct SetArgs {
259 /// FIELD=VALUE to set on the concept's frontmatter.
260 #[arg(value_name = "FIELD=VALUE")]
261 pub spec: String,
262 /// The concept file to edit.
263 #[arg(long)]
264 pub file: PathBuf,
265}
266
267#[derive(Args, Debug)]
268pub struct LogArgs {
269 /// The log message.
270 #[arg(value_name = "MESSAGE")]
271 pub message: String,
272 /// Entry label (default Update).
273 #[arg(long = "kind", value_name = "LABEL")]
274 pub kind: Option<String>,
275}
276
277#[derive(Args, Debug)]
278pub struct GenIndexArgs {
279 /// Scaffold an absent index.md declaring okf_version instead of listing concepts.
280 #[arg(long)]
281 pub scaffold: bool,
282}
283
284#[derive(Args, Debug)]
285pub struct ScriptArgs {
286 /// The .ctb script of new/set/log/index/init items.
287 pub path: PathBuf,
288 /// Print the plan and write nothing.
289 #[arg(long)]
290 pub dry_run: bool,
291 /// Directive prefix for script lines (default '#%').
292 #[arg(long, value_name = "STR")]
293 pub fence: Option<String>,
294}