Skip to main content

harn_cli/commands/
mod.rs

1pub(crate) mod agents_conformance;
2pub(crate) mod bench;
3pub(crate) mod canon;
4pub(crate) mod check;
5pub(crate) mod codemod;
6pub(crate) mod config_cmd;
7pub(crate) mod conformance_helper;
8pub(crate) mod connect;
9pub(crate) mod connector;
10pub(crate) mod connector_schema_codegen;
11pub(crate) mod contracts;
12pub(crate) mod counterfactual;
13pub(crate) mod crystallize;
14pub mod demo;
15pub(crate) mod dev;
16pub(crate) mod diagnostics_catalog;
17pub(crate) mod dispatch_explain;
18pub(crate) mod doc;
19pub(crate) mod doctor;
20pub(crate) mod dump_highlight_keywords;
21pub(crate) mod dump_protocol_artifacts;
22pub(crate) mod dump_trigger_quickref;
23pub mod eval_coding_agent;
24pub(crate) mod eval_coding_agent_preset;
25pub mod eval_context;
26pub(crate) mod eval_model_selector;
27pub mod eval_prompt;
28pub(crate) mod eval_prompt_context;
29pub(crate) mod eval_scope_triage;
30pub mod eval_skill_gate;
31pub(crate) mod eval_tool_calls;
32pub(crate) mod explain;
33pub(crate) mod fix;
34pub mod flow;
35pub(crate) mod graph;
36pub(crate) mod guard;
37pub(crate) mod hardware;
38pub(crate) mod host;
39pub(crate) mod init;
40pub(crate) mod json_schemas;
41pub(crate) mod local;
42pub(crate) mod local_readiness;
43pub(crate) mod mcp;
44pub(crate) mod merge_captain;
45pub(crate) mod merge_captain_mock;
46pub(crate) mod models;
47pub mod orchestrator;
48pub mod pack;
49pub(crate) mod package_scaffold;
50pub(crate) mod parse_tokens;
51pub mod persona;
52pub mod persona_activation;
53pub mod persona_apply;
54pub mod persona_dispatch;
55pub mod persona_doctor;
56pub mod persona_prompt;
57pub mod persona_scaffold;
58pub mod persona_supervision;
59#[cfg(test)]
60pub(crate) mod persona_test_support;
61pub(crate) mod pg_codegen;
62pub mod playground;
63pub(crate) mod portal;
64pub mod precompile;
65pub(crate) mod protocol_conformance;
66pub(crate) mod provider;
67pub(crate) mod provider_capabilities;
68pub(crate) mod provider_limits;
69pub(crate) mod provider_report;
70pub(crate) mod provider_support;
71pub(crate) mod providers;
72pub(crate) mod quickstart;
73pub(crate) mod repl;
74pub(crate) mod replay;
75pub(crate) mod routes;
76pub(crate) mod rule;
77pub(crate) mod rules_cli;
78pub mod run;
79pub(crate) mod scaffold_common;
80pub(crate) mod scan;
81pub(crate) mod serve;
82pub(crate) mod session;
83pub(crate) mod skill;
84pub(crate) mod skills;
85pub(crate) mod supervisor;
86pub(crate) mod test;
87pub mod test_bench;
88pub(crate) mod test_worker;
89pub mod time;
90pub(crate) mod tool;
91pub(crate) mod tool_mode_parity;
92pub(crate) mod trace;
93pub mod trigger;
94pub(crate) mod trust;
95pub(crate) mod try_cmd;
96pub(crate) mod upgrade;
97pub(crate) mod usage;
98pub(crate) mod viz;
99pub(crate) mod workflow;
100
101use std::path::{Path, PathBuf};
102
103use ignore::WalkBuilder;
104
105/// Nearest-rank percentile over a pre-sorted slice. `quantile` is clamped to
106/// `[0, 1]`; returns `None` for an empty slice. Shared by the latency
107/// summaries in the eval and orchestrator stats commands.
108pub(crate) fn nearest_rank_percentile(sorted: &[u64], quantile: f64) -> Option<u64> {
109    if sorted.is_empty() {
110        return None;
111    }
112    let rank = ((sorted.len() as f64 * quantile.clamp(0.0, 1.0)).ceil() as usize)
113        .saturating_sub(1)
114        .min(sorted.len() - 1);
115    sorted.get(rank).copied()
116}
117
118const GENERATED_SOURCE_WALK_DIRS: &[&str] = &[
119    ".burin",
120    ".build",
121    ".claude",
122    ".codex",
123    ".git",
124    ".harn",
125    ".harn-runs",
126    ".next",
127    ".svelte-kit",
128    ".turbo",
129    ".venv",
130    "build",
131    "coverage",
132    "dist",
133    "node_modules",
134    "target",
135];
136
137pub(crate) fn should_skip_recursive_source_dir(dir: &Path) -> bool {
138    let Some(name) = dir.file_name().and_then(|name| name.to_str()) else {
139        return false;
140    };
141    GENERATED_SOURCE_WALK_DIRS.contains(&name) || name.starts_with(".harn-")
142}
143
144pub(crate) fn should_skip_recursive_source_file(file: &Path) -> bool {
145    let Some(name) = file.file_name().and_then(|name| name.to_str()) else {
146        return false;
147    };
148    name.starts_with(".harn-")
149}
150
151#[derive(Default)]
152pub(crate) struct SourceTargets {
153    pub(crate) harn: Vec<PathBuf>,
154    pub(crate) prompts: Vec<PathBuf>,
155}
156
157impl SourceTargets {
158    fn sort_and_dedup(&mut self) {
159        self.harn.sort();
160        self.harn.dedup();
161        self.prompts.sort();
162        self.prompts.dedup();
163    }
164}
165
166pub(crate) fn collect_source_targets(
167    targets: &[&str],
168    include_harn: bool,
169    include_prompts: bool,
170) -> SourceTargets {
171    let mut files = SourceTargets::default();
172    for target in targets {
173        let path = Path::new(target);
174        if path.is_dir() {
175            collect_source_targets_dir(path, include_harn, include_prompts, &mut files);
176        } else {
177            push_matching_source_target(path, include_harn, include_prompts, false, &mut files);
178        }
179    }
180    files.sort_and_dedup();
181    files
182}
183
184fn collect_source_targets_dir(
185    dir: &Path,
186    include_harn: bool,
187    include_prompts: bool,
188    files: &mut SourceTargets,
189) {
190    let root = dir.to_path_buf();
191    let mut walker = WalkBuilder::new(dir);
192    walker
193        .hidden(false)
194        .ignore(true)
195        .git_ignore(true)
196        .git_global(true)
197        .git_exclude(true)
198        .require_git(false)
199        .parents(true)
200        .follow_links(false)
201        .filter_entry(move |entry| {
202            let path = entry.path();
203            if path == root {
204                return true;
205            }
206            if path.is_dir() {
207                !should_skip_recursive_source_dir(path)
208            } else {
209                !should_skip_recursive_source_file(path)
210            }
211        });
212
213    for entry in walker.build().filter_map(Result::ok) {
214        let path = entry.path();
215        if entry
216            .file_type()
217            .is_some_and(|file_type| file_type.is_file())
218        {
219            push_matching_source_target(path, include_harn, include_prompts, true, files);
220        }
221    }
222}
223
224fn push_matching_source_target(
225    path: &Path,
226    include_harn: bool,
227    include_prompts: bool,
228    honor_skip_marker: bool,
229    files: &mut SourceTargets,
230) {
231    if include_prompts && is_harn_prompt_file(path) {
232        files.prompts.push(path.to_path_buf());
233    } else if include_harn && is_harn_program_file(path) {
234        let skip_marker = path.with_extension("conformance-skip");
235        if !honor_skip_marker || !skip_marker.exists() {
236            files.harn.push(path.to_path_buf());
237        }
238    }
239}
240
241pub(crate) fn is_harn_program_file(path: &Path) -> bool {
242    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
243        return false;
244    };
245    if name.ends_with(".harn.prompt") || name.ends_with(".prompt") {
246        return false;
247    }
248    name.ends_with(".harn") || name.ends_with(".harn.txt")
249}
250
251pub(crate) fn is_harn_prompt_file(path: &Path) -> bool {
252    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
253        return false;
254    };
255    name.ends_with(".harn.prompt") || name.ends_with(".prompt")
256}
257
258/// Recursively collect `.harn` files under `dir`, sorted by path. Files with a
259/// sibling `<name>.conformance-skip` marker are excluded — used to temporarily
260/// park tests that are tracking a known regression in an issue so `make test`
261/// + `harn test conformance` can stay green while the fix is in flight.
262pub(crate) fn collect_harn_files(dir: &Path, out: &mut Vec<PathBuf>) {
263    if let Ok(entries) = std::fs::read_dir(dir) {
264        let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
265        entries.sort_by_key(|e| e.path());
266        for entry in entries {
267            let path = entry.path();
268            if path.is_dir() {
269                if should_skip_recursive_source_dir(&path) {
270                    continue;
271                }
272                collect_harn_files(&path, out);
273            } else if should_skip_recursive_source_file(&path) {
274                continue;
275            } else if path.extension().is_some_and(|ext| ext == "harn") {
276                let skip_marker = path.with_extension("conformance-skip");
277                if skip_marker.exists() {
278                    continue;
279                }
280                out.push(path);
281            }
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::nearest_rank_percentile;
289
290    #[test]
291    fn percentile_handles_empty_and_bounds() {
292        assert_eq!(nearest_rank_percentile(&[], 0.5), None);
293        let data = [10u64, 20, 30, 40, 50];
294        assert_eq!(nearest_rank_percentile(&data, 0.0), Some(10));
295        assert_eq!(nearest_rank_percentile(&data, 0.5), Some(30));
296        assert_eq!(nearest_rank_percentile(&data, 1.0), Some(50));
297        // Out-of-range quantiles clamp instead of panicking.
298        assert_eq!(nearest_rank_percentile(&data, 2.0), Some(50));
299        assert_eq!(nearest_rank_percentile(&[7u64], 0.99), Some(7));
300    }
301}