1pub async fn run_cli() -> anyhow::Result<()> {
2 tracing_subscriber::fmt().with_env_filter("warn").init();
3 let cli = Cli::parse();
4 let repo = cli.repo.clone();
5 match cli.command {
6 Command::Init { repo: command_repo } => {
7 let repo = resolve_repo(&repo, command_repo);
8 std::fs::create_dir_all(repo.join(".ok"))?;
9 OkConfig::write_default(repo.join("ok.toml"))?;
10 print_text_or_json(
11 cli.json,
12 "Open Kioku is ready.\n\nNext:\n ok index\n ok doctor\n ok mcp install cursor\n\nIf this is useful, star the repo:\nhttps://github.com/shivyadavus/open-kioku",
13 &serde_json::json!({"status":"initialized"}),
14 )?;
15 }
16 Command::Index {
17 repo: command_repo,
18 with_scip,
19 mode,
20 workspace,
21 from_snapshot,
22 } => {
23 let repo = resolve_repo(&repo, command_repo);
24 let mode = parse_index_mode(&mode)?;
25 if mode == IndexMode::CrossProject {
26 let workspace = workspace.ok_or_else(|| {
27 anyhow::anyhow!("--workspace is required for cross-project indexing")
28 })?;
29 let report = build_cross_project_workspace(&workspace)?;
30 if cli.json {
31 println!("{}", serde_json::to_string_pretty(&report)?);
32 } else {
33 print_workspace_link_report(&report);
34 }
35 return Ok(());
36 }
37 if from_snapshot.as_deref() == Some("auto") {
38 match snapshot_import(&repo) {
39 Ok(report) => {
40 if cli.json {
41 println!("{}", serde_json::to_string_pretty(&report)?);
42 } else {
43 println!(
44 "Imported snapshot from {} and rebuilt search index",
45 report.artifact_path.display()
46 );
47 for warning in &report.warnings {
48 println!("warning: {warning}");
49 }
50 }
51 return Ok(());
52 }
53 Err(err) => {
54 eprintln!("snapshot import unavailable; falling back to full index: {err}");
55 }
56 }
57 }
58 let snapshot = index_repo_with_scip_mode(&repo, with_scip.as_deref(), mode)?;
59 if cli.json {
60 println!("{}", serde_json::to_string_pretty(&snapshot.manifest)?);
61 } else {
62 println!(
63 "Indexed {} files, {} symbols, {} chunks in {} mode",
64 snapshot.manifest.file_count,
65 snapshot.manifest.symbol_count,
66 snapshot.manifest.chunk_count,
67 snapshot.manifest.index_mode
68 );
69 if let Some(scip) = &snapshot.scip {
70 println!(
71 "SCIP: mode {:?}, imported {} index(es), {} exact references",
72 scip.mode,
73 scip.imported_paths.len(),
74 scip.exact_references
75 );
76 for attempt in &scip.generator_attempts {
77 println!(
78 "SCIP {}: {:?} - {}",
79 attempt.language, attempt.status, attempt.message
80 );
81 }
82 }
83 }
84 }
85 Command::Snapshot { command } => match command {
86 SnapshotCommand::Export { quality } => {
87 let report = snapshot_export(&repo, quality)?;
88 if cli.json {
89 println!("{}", serde_json::to_string_pretty(&report)?);
90 } else {
91 println!(
92 "Exported {} snapshot to {}",
93 report.quality,
94 report.artifact_path.display()
95 );
96 println!(
97 "Metadata: {} ({} -> {} bytes)",
98 report.metadata_path.display(),
99 report.metadata.original_size_bytes,
100 report.metadata.compressed_size_bytes
101 );
102 }
103 }
104 SnapshotCommand::Import => {
105 let report = snapshot_import(&repo)?;
106 if cli.json {
107 println!("{}", serde_json::to_string_pretty(&report)?);
108 } else {
109 println!("Imported snapshot from {}", report.artifact_path.display());
110 if report.rebuilt_search {
111 println!("Rebuilt Tantivy search index from imported SQLite index");
112 }
113 for warning in &report.warnings {
114 println!("warning: {warning}");
115 }
116 }
117 }
118 SnapshotCommand::Doctor => {
119 let report = snapshot_doctor(&repo);
120 if cli.json {
121 println!("{}", serde_json::to_string_pretty(&report)?);
122 } else {
123 print_snapshot_doctor_report(&report);
124 }
125 }
126 },
127 Command::Watch { repo: command_repo } => {
128 let repo = resolve_repo(&repo, command_repo);
129 open_kioku_watch::watch_repo(&repo)?;
130 }
131 Command::Status {
132 repo: command_repo,
133 markdown,
134 write,
135 exit_code,
136 } => {
137 let repo = resolve_repo(&repo, command_repo);
138 let manifest = load_index_manifest(&repo)?;
139 let doctor = if markdown || write.is_some() || exit_code {
140 Some(doctor_report(&repo))
141 } else {
142 None
143 };
144 if markdown || write.is_some() {
145 let doctor_ref = doctor
146 .as_ref()
147 .expect("doctor report should be available for status snapshot");
148 let rendered = render_status_markdown(&repo, manifest.as_ref(), doctor_ref);
149 if let Some(path) = write {
150 fs::write(&path, rendered)?;
151 if cli.json {
152 println!(
153 "{}",
154 serde_json::to_string_pretty(&serde_json::json!({
155 "ok": doctor_ref.ok,
156 "path": path,
157 }))?
158 );
159 } else {
160 println!("Wrote Open Kioku status snapshot to {}", path.display());
161 }
162 } else {
163 println!("{rendered}");
164 }
165 } else if cli.json {
166 println!("{}", serde_json::to_string_pretty(&manifest)?);
167 } else if let Some(manifest) = manifest {
168 println!(
169 "Healthy index: {} files, {} symbols, {} skipped, mode {}, indexed at {}",
170 manifest.file_count,
171 manifest.symbol_count,
172 manifest.quality.skipped_paths.len(),
173 manifest.index_mode,
174 manifest.indexed_at
175 );
176 } else {
177 println!("No index found. Run `ok index .`.");
178 }
179 if exit_code && !doctor.as_ref().map(|report| report.ok).unwrap_or(true) {
180 anyhow::bail!("Open Kioku status has failing readiness checks");
181 }
182 }
183 Command::Doctor {
184 repo: command_repo,
185 format,
186 } => {
187 let repo = resolve_repo(&repo, command_repo);
188 let report = doctor_report(&repo);
189 let ok = report.ok;
190 if cli.json || format == DoctorFormat::Json {
191 println!("{}", serde_json::to_string_pretty(&report)?);
192 } else {
193 println!("Open Kioku doctor for {}", report.repo.display());
194 for check in &report.checks {
195 let marker = match check.status {
196 CheckStatus::Pass => "[ok]",
197 CheckStatus::Warn => "[warn]",
198 CheckStatus::Fail => "[fail]",
199 };
200 println!("{marker:<6} {:<16} {}", check.name, check.message);
201 }
202 let passes = report
203 .checks
204 .iter()
205 .filter(|c| matches!(c.status, CheckStatus::Pass))
206 .count();
207 let warns = report
208 .checks
209 .iter()
210 .filter(|c| matches!(c.status, CheckStatus::Warn))
211 .count();
212 let fails = report
213 .checks
214 .iter()
215 .filter(|c| matches!(c.status, CheckStatus::Fail))
216 .count();
217 println!(
218 "\n{} checks passed, {} warnings, {} failures",
219 passes, warns, fails
220 );
221
222 if !report.next_steps.is_empty() {
223 println!("\nNext steps:");
224 for step in &report.next_steps {
225 println!("- {step}");
226 }
227 }
228 }
229 if !ok {
230 std::process::exit(1);
231 }
232 }
233 Command::Setup { command } => match command {
234 SetupCommand::Audit {
235 repo: command_repo,
236 markdown,
237 write,
238 exit_code,
239 } => {
240 let repo = resolve_repo(&repo, command_repo);
241 let report = setup_audit_report(&repo);
242 if markdown || write.is_some() {
243 let rendered = render_setup_audit_markdown(&report);
244 if let Some(path) = write {
245 fs::write(&path, rendered)?;
246 if cli.json {
247 println!(
248 "{}",
249 serde_json::to_string_pretty(&serde_json::json!({
250 "ok": report.ok,
251 "path": path,
252 }))?
253 );
254 } else {
255 println!("Wrote Open Kioku setup audit to {}", path.display());
256 }
257 } else {
258 println!("{rendered}");
259 }
260 } else if cli.json {
261 println!("{}", serde_json::to_string_pretty(&report)?);
262 } else {
263 print_setup_audit_report(&report);
264 }
265 if exit_code && !report.ok {
266 anyhow::bail!("Open Kioku setup audit has failing checks");
267 }
268 }
269 },
270 Command::Graph { command } => match command {
271 GraphCommand::Query {
272 dsl,
273 limit,
274 max_depth,
275 timeout_ms,
276 format,
277 } => {
278 let store = open_store(&repo)?;
279 let ast = open_kioku_graph::query::parse_graph_query(&dsl)?;
280 let options = open_kioku_graph::query::GraphQueryOptions {
281 limit,
282 max_depth,
283 deadline_ms: timeout_ms,
284 ..Default::default()
285 };
286 let result = open_kioku_graph::query::execute_graph_query(
287 &store as &dyn open_kioku_storage::GraphStore,
288 &ast,
289 options,
290 )?;
291 if format.to_lowercase() == "json" {
292 println!("{}", serde_json::to_string_pretty(&result)?);
293 } else {
294 println!("{:?}", result);
295 }
296 }
297 GraphCommand::Schema { format } => {
298 let store = open_store(&repo).ok();
299 let manifest = store
300 .as_ref()
301 .and_then(|store| open_kioku_storage::MetadataStore::manifest(store).ok())
302 .flatten();
303 let schema = open_kioku_graph::schema::current_schema_with_manifest(
304 store
305 .as_ref()
306 .map(|s| s as &dyn open_kioku_storage::GraphStore),
307 manifest.as_ref(),
308 );
309 if format.to_lowercase() == "markdown" {
310 let mut lines = vec![
311 format!("# Open Kioku Evidence Graph Schema v{}", schema.version),
312 "".to_string(),
313 ];
314
315 if !schema.feature_flags.is_empty() {
316 lines.push("## Supported Features".to_string());
317 for feature in &schema.feature_flags {
318 lines.push(format!("- `{}`", feature));
319 }
320 lines.push("".to_string());
321 }
322
323 if !schema.query_features.is_empty() {
324 lines.push("## Query Features".to_string());
325 for feature in &schema.query_features {
326 lines.push(format!("- `{}`", feature));
327 }
328 lines.push("".to_string());
329 }
330
331 if !schema.evidence_source_types.is_empty() {
332 lines.push("## Evidence Source Types".to_string());
333 for source_type in &schema.evidence_source_types {
334 lines.push(format!("- `{}`", source_type));
335 }
336 lines.push("".to_string());
337 }
338
339 if !schema.optional_evidence.is_empty() {
340 lines.push("## Optional Evidence Availability".to_string());
341 for evidence in &schema.optional_evidence {
342 lines.push(format!(
343 "- `{}`: {} (count: {})",
344 evidence.name, evidence.status, evidence.evidence_count
345 ));
346 for caveat in &evidence.caveats {
347 lines.push(format!(" - caveat: {}", caveat));
348 }
349 }
350 lines.push("".to_string());
351 }
352
353 lines.push("## Node Types".to_string());
354 for node in &schema.node_types {
355 let status = if node.stable {
356 "Stable"
357 } else {
358 "Experimental"
359 };
360 lines.push(format!("### {} ({})", node.name, status));
361 lines.push(node.description.clone());
362 if !node.required_fields.is_empty() {
363 lines.push(
364 "- **Required**: ".to_string() + &node.required_fields.join(", "),
365 );
366 }
367 if !node.optional_fields.is_empty() {
368 lines.push(
369 "- **Optional**: ".to_string() + &node.optional_fields.join(", "),
370 );
371 }
372 lines.push("".to_string());
373 }
374
375 lines.push("## Edge Types".to_string());
376 for edge in &schema.edge_types {
377 let status = if edge.stable {
378 "Stable"
379 } else {
380 "Experimental"
381 };
382 lines.push(format!("### {} ({})", edge.name, status));
383 lines.push(edge.description.clone());
384 lines.push(format!("- **Sources**: {}", edge.source_types.join(", ")));
385 lines.push(format!("- **Targets**: {}", edge.target_types.join(", ")));
386 if !edge.required_evidence.is_empty() {
387 lines.push(format!(
388 "- **Evidence**: {}",
389 edge.required_evidence.join(", ")
390 ));
391 }
392 lines.push("".to_string());
393 }
394
395 println!("{}", lines.join("\n"));
396 } else {
397 println!("{}", serde_json::to_string_pretty(&schema)?);
398 }
399 }
400 },
401 Command::Demo { path, force } => {
402 let repo = demo_repo_path(path.clone())?;
403 let report = build_demo_repo(&repo, force)?;
404 if cli.json {
405 println!("{}", serde_json::to_string_pretty(&report)?);
406 } else {
407 let rel_path = if let Some(ref p) = path {
408 p.display().to_string()
409 } else {
410 "./open-kioku-demo".to_string()
411 };
412 println!("Open Kioku is ready.\n");
413 println!("Next:");
414 println!(" ok demo --force");
415 println!(" ok --repo {} plan token --format markdown\n", rel_path);
416 println!("If this is useful, star the repo:");
417 println!("https://github.com/shivyadavus/open-kioku");
418 }
419 }
420 Command::Search {
421 query,
422 limit,
423 kind,
424 explain_ranking,
425 semantic,
426 hybrid,
427 } => {
428 let store = open_store(&repo)?;
429 let results = if matches!(kind, SearchKind::Graph) {
430 graph_search(&repo, &query, limit)?
431 } else if semantic {
432 semantic_search(&repo, &store, &query, limit)?
433 } else if hybrid {
434 hybrid_search(&repo, &store, &query, limit)?
435 } else {
436 search(&repo, &store, &query, limit)?
437 };
438 output(cli.json, &results, || {
439 for result in &results {
440 println!(
441 "{}:{} {:.2} {}",
442 result.path.display(),
443 result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
444 result.score,
445 result.snippet
446 );
447 if explain_ranking {
448 let signals = top_score_signals(result, 3);
449 if signals.is_empty() {
450 println!(" ranking: no dominant signals");
451 } else {
452 println!(" ranking: {}", signals.join(", "));
453 }
454 }
455 }
456 })?;
457 }
458 Command::Semantic { command } => match command {
459 SemanticCommand::Status { repo: command_repo } => {
460 let repo = absolutize(&resolve_repo(&repo, command_repo))?;
461 let store = open_store(&repo)?;
462 let config = OkConfig::load_from_repo(&repo)?;
463 let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
464 let status = manager.status();
465 output(cli.json, &status, || print_semantic_status(&status))?;
466 }
467 SemanticCommand::Index { repo: command_repo } => {
468 let repo = absolutize(&resolve_repo(&repo, command_repo))?;
469 let store = open_store(&repo)?;
470 let mut config = OkConfig::load_from_repo(&repo)?;
471 config.semantic.enabled = true;
472 let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
473 let report = manager.index()?;
474 output(cli.json, &report, || {
475 println!(
476 "Semantic index ready: {} vectors, {} reused, {} embedded",
477 report.status.vector_count, report.reused_embeddings, report.embedded_count
478 );
479 })?;
480 }
481 SemanticCommand::Rebuild { repo: command_repo } => {
482 let repo = absolutize(&resolve_repo(&repo, command_repo))?;
483 let store = open_store(&repo)?;
484 let mut config = OkConfig::load_from_repo(&repo)?;
485 config.semantic.enabled = true;
486 let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
487 let report = manager.rebuild()?;
488 output(cli.json, &report, || {
489 println!(
490 "Semantic index rebuilt: {} vectors, {} embedded",
491 report.status.vector_count, report.embedded_count
492 );
493 })?;
494 }
495 SemanticCommand::Clean {
496 repo: command_repo,
497 include_cache,
498 } => {
499 let repo = absolutize(&resolve_repo(&repo, command_repo))?;
500 let store = open_store(&repo)?;
501 let config = OkConfig::load_from_repo(&repo)?;
502 let manager = SemanticIndexManager::new(&repo, &store, &config.semantic);
503 manager.clean(include_cache)?;
504 println!("Semantic artifacts removed.");
505 }
506 },
507 Command::Symbol { command } => {
508 let store = open_store(&repo)?;
509 let engine = SymbolEngine::new(&store);
510 match command {
511 SymbolCommand::Find { name } => output(cli.json, &engine.find(&name, 50)?, || {})?,
512 SymbolCommand::Definition { name } => {
513 output(cli.json, &engine.definition(&name)?, || {})?
514 }
515 SymbolCommand::Refs { name } => {
516 output(cli.json, &engine.references(&name, 50)?, || {})?
517 }
518 }
519 }
520 Command::Explain { command } => {
521 let store = open_store(&repo)?;
522 match command {
523 ExplainCommand::File { path } => {
524 let file = store.get_file_by_path(&path)?;
525 let chunks = if let Some(file) = &file {
526 store.chunks_for_file(&file.id)?
527 } else {
528 Vec::new()
529 };
530 let symbols = if let Some(file) = &file {
531 store.symbols_for_file(&file.id)?
532 } else {
533 Vec::new()
534 };
535 output(
536 cli.json,
537 &serde_json::json!({"file": file, "chunks": chunks, "symbols": symbols}),
538 || {
539 if let Some(f) = &file {
540 println!(
541 "{} ({:?}, {} bytes)",
542 path.display(),
543 f.language,
544 f.size_bytes
545 );
546 }
547 println!("{} chunks, {} symbols indexed", chunks.len(), symbols.len());
548 for symbol in &symbols {
549 let range = symbol
550 .range
551 .as_ref()
552 .map(|r| format!(":{}–{}", r.start, r.end))
553 .unwrap_or_default();
554 println!(" {:?} {}{}", symbol.kind, symbol.name, range);
555 }
556 },
557 )?;
558 }
559 ExplainCommand::Symbol { name } => {
560 let symbol = SymbolEngine::new(&store).definition(&name)?;
561 output(cli.json, &symbol, || {})?;
562 }
563 }
564 }
565 Command::Impact(args) => {
566 let store = open_store(&repo)?;
567 let index_dir = default_index_dir(&repo);
568 let search_index = if TantivySearchIndex::exists(&index_dir) {
569 Some(TantivySearchIndex::open_or_create(&index_dir)?)
570 } else {
571 None
572 };
573 let engine = ImpactEngine::new(&store)
574 .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
575 .with_history_store(Some(&store));
576 let architecture_policy = configured_architecture_policy_report(&repo, &store)?;
577
578 if let Some(since) = args.since.as_deref() {
579 let changed = changed_ranges_since(&repo, since)?;
580 let mut reports = Vec::new();
581 for file in changed.iter().filter_map(|change| change.new_path.as_ref()) {
582 let mut report = engine.for_file(file)?;
583 report.architecture_policy = architecture_policy.clone();
584 reports.push(report);
585 }
586 if cli.json {
587 println!(
588 "{}",
589 serde_json::to_string_pretty(&serde_json::json!({
590 "since": since,
591 "changed_files": changed,
592 "impact_reports": reports,
593 }))?
594 );
595 } else {
596 println!("Changed files since {since}:");
597 for change in &changed {
598 println!(" {}", render_changed_range(change));
599 }
600 for report in &reports {
601 println!("\nImpact target: {}", report.target);
602 println!(
603 "Risk: {} ({:.2})",
604 report.risk_report.level, report.risk_report.score
605 );
606 }
607 }
608 return Ok(());
609 }
610
611 let mut report = if let Some(path) = args.file {
612 let normalized = normalize_to_repo_relative(&repo, &path);
613 engine.for_file(&normalized)?
614 } else if let Some(symbol) = args.symbol {
615 let definition = SymbolEngine::new(&store).definition(&symbol)?;
616 let files = store.list_files(usize::MAX, 0)?;
617 let file = files.iter().find(|file| file.id == definition.file_id);
618 let path_to_use = file
619 .map(|file| file.path.as_path())
620 .unwrap_or(Path::new(&symbol));
621 let normalized = normalize_to_repo_relative(&repo, path_to_use);
622 engine.for_file(&normalized)?
623 } else {
624 anyhow::bail!("provide --file or --symbol");
625 };
626 report.architecture_policy = architecture_policy;
627 output(cli.json, &report, || {
628 println!("Impact target: {}", report.target);
629 println!(
630 "Risk: {} ({:.2})",
631 report.risk_report.level, report.risk_report.score
632 );
633 println!("\nDirect impacts ({}):", report.direct_impacts.len());
634 for result in &report.direct_impacts {
635 println!(
636 " {}:{} ({:.2})",
637 result.path.display(),
638 result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
639 result.score
640 );
641 }
642 if !report.indirect_impacts.is_empty() {
643 println!("\nIndirect impacts ({}):", report.indirect_impacts.len());
644 for result in report.indirect_impacts.iter().take(5) {
645 println!(
646 " {}:{} ({:.2})",
647 result.path.display(),
648 result.line_range.as_ref().map(|r| r.start).unwrap_or(0),
649 result.score
650 );
651 }
652 }
653 })?;
654 }
655 Command::Path { from, to } => {
656 let store = open_store(&repo)?;
657 let from = resolve_graph_node(&store, &from)?;
658 let to = resolve_graph_node(&store, &to)?;
659 let path = store.shortest_path(&from, &to, 12)?;
660 output(cli.json, &path, || {
661 if path.is_empty() {
662 println!("No dependency path found.");
663 } else {
664 for edge in &path {
665 println!("{} -> {} {:?}", edge.from, edge.to, edge.edge_type);
666 }
667 }
668 })?;
669 }
670 Command::Tests { changed } => {
671 let store = open_store(&repo)?;
672 output(
673 cli.json,
674 &TestSelector::new(&store).for_changed_path_with_evidence(&changed, 20)?,
675 || {},
676 )?;
677 }
678 Command::Context {
679 task,
680 format,
681 compressed,
682 } => {
683 let store = open_store(&repo)?;
684 let pack = build_context_pack(&repo, &store, &task, 20)?;
685 if compressed {
686 let compressed = ContextHandleStore::open_repo(&repo)?.compress_pack(&pack)?;
687 if cli.json || format == ContextPackFormat::Json {
688 println!("{}", serde_json::to_string_pretty(&compressed)?);
689 } else if format == ContextPackFormat::Toon {
690 println!(
691 "{}",
692 open_kioku_format::render_compressed_context_toon(&compressed)
693 );
694 } else {
695 println!("{}", serde_json::to_string_pretty(&compressed)?);
696 }
697 } else {
698 let rendered = format.render(&pack)?;
699 println!("{}", rendered);
700 }
701 }
702 Command::RetrieveContext { handle } => {
703 let retrieved =
704 ContextHandleStore::open_repo(&repo)?.retrieve(&ContextHandleId::new(handle))?;
705 output(cli.json, &retrieved, || {
706 if let Some(retrieved) = &retrieved {
707 println!("{}", retrieved.original);
708 } else {
709 println!("No context handle found.");
710 }
711 })?;
712 }
713 Command::Plan {
714 task,
715 format,
716 limit,
717 since,
718 verify_evidence,
719 } => {
720 let store = open_store(&repo)?;
721 let task = if let Some(since) = since.as_deref() {
722 task_with_changed_ranges(&repo, &task, since)?
723 } else {
724 task
725 };
726 let context = build_context_pack(&repo, &store, &task, limit)?;
727 let index_dir = default_index_dir(&repo);
728 let search_index = if TantivySearchIndex::exists(&index_dir) {
729 Some(TantivySearchIndex::open_or_create(&index_dir)?)
730 } else {
731 None
732 };
733 let report = PlanEngine::new(&store as &dyn OkStore)
734 .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
735 .with_history_store(Some(&store))
736 .with_memory_facts(RepoMemoryStore::open_repo(&repo)?.search(&task, 8)?)
737 .plan_from_context(&task, limit, context)?;
738 let format = if cli.json { PlanFormat::Json } else { format };
739 println!("{}", format.render(&report)?);
740 verify_plan_evidence(&report, verify_evidence)?;
741 }
742 Command::VerifyBoundary {
743 plan,
744 changed,
745 evidence_refs,
746 } => {
747 let report = load_saved_plan(&plan)?;
748 let outcome = verify_saved_plan_boundary(&report, &changed, &evidence_refs)?;
749 if cli.json {
750 println!("{}", serde_json::to_string_pretty(&outcome)?);
751 } else {
752 println!(
753 "Boundary verification passed for {} changed file(s)",
754 outcome.changed_files.len()
755 );
756 for warning in &outcome.warnings {
757 eprintln!("{warning}");
758 }
759 }
760 }
761 Command::Verify {
762 plan,
763 diff,
764 git,
765 since_plan,
766 changed,
767 evidence_refs,
768 traceability_strict,
769 check_api_surface,
770 check_deps,
771 run_commands,
772 write_attestation,
773 } => {
774 let store = open_store(&repo)?;
775 let report = load_saved_plan(&plan)?;
776 let mut changed = changed;
777 let unified_diff = if let Some(since) = since_plan.as_deref() {
778 for change in changed_ranges_since(&repo, since)? {
779 if let Some(path) = change.new_path.or(change.old_path) {
780 changed.push(path);
781 }
782 }
783 verify_diff_since(&repo, diff.as_deref(), since)?
784 } else {
785 verify_diff_input(&repo, diff.as_deref(), git)?
786 };
787 let index_dir = default_index_dir(&repo);
788 let search_index = if TantivySearchIndex::exists(&index_dir) {
789 Some(TantivySearchIndex::open_or_create(&index_dir)?)
790 } else {
791 None
792 };
793 let architecture_policy = load_architecture_policy(&repo)?;
794 let check_dependency_delta = check_deps || architecture_policy.is_some();
795 let contract_store =
796 write_attestation.then(|| FsContractStore::new(repo.join(".ok/contracts")));
797 let verification = ChangeVerifier::new(&store as &dyn OkStore)
798 .with_search_index(search_index.as_ref().map(|idx| idx as &dyn SearchIndex))
799 .with_contract_store(
800 contract_store
801 .as_ref()
802 .map(|store| store as &dyn ContractStore),
803 )
804 .verify(
805 &repo,
806 &report,
807 VerifyChangeInput {
808 changed_files: changed,
809 unified_diff,
810 evidence_refs,
811 run_commands,
812 write_attestation,
813 validation_attestations: Vec::new(),
814 traceability_strict,
815 check_api_surface,
816 check_dependency_delta,
817 architecture_policy,
818 suppress_plan_validation_pending: false,
819 },
820 )?;
821 if cli.json {
822 println!("{}", serde_json::to_string_pretty(&verification)?);
823 } else {
824 print_verify_report(&verification);
825 }
826 if matches!(
827 verification.verdict,
828 open_kioku_patch::VerificationVerdict::Fail
829 ) {
830 anyhow::bail!("change verification failed");
831 }
832 }
833 Command::Contract { command } => {
834 handle_contract_command(cli.json, &repo, command)?;
835 }
836 Command::Bench(args) => {
837 let min_precision = args.quality_min_precision_at_1;
838 let report = run_bench(args)?;
839 if cli.json {
840 println!("{}", serde_json::to_string_pretty(&report)?);
841 } else {
842 print_bench_report(&report);
843 }
844 if let Some(quality) = &report.quality {
845 if quality.precision_at_1 < min_precision {
846 anyhow::bail!(
847 "quality precision@1 {:.3} is below required {:.3}",
848 quality.precision_at_1,
849 min_precision
850 );
851 }
852 }
853 }
854 Command::WorkflowBench(args) => {
855 let min_context_recall = args.min_context_recall;
856 let min_verification_accuracy = args.min_verification_accuracy;
857 let min_cases = args.min_cases;
858 let report = run_workflow_bench(args)?;
859 if cli.json {
860 println!("{}", serde_json::to_string_pretty(&report)?);
861 } else {
862 print_workflow_bench_report(&report);
863 }
864 if report.case_count < min_cases {
865 anyhow::bail!(
866 "workflow benchmark loaded {} cases, below required {}",
867 report.case_count,
868 min_cases
869 );
870 }
871 if report.workflow.context_recall_at_k < min_context_recall {
872 anyhow::bail!(
873 "workflow context recall@{} {:.3} is below required {:.3}",
874 report.limit,
875 report.workflow.context_recall_at_k,
876 min_context_recall
877 );
878 }
879 if report.workflow.verification_verdict_accuracy < min_verification_accuracy {
880 anyhow::bail!(
881 "workflow verification accuracy {:.3} is below required {:.3}",
882 report.workflow.verification_verdict_accuracy,
883 min_verification_accuracy
884 );
885 }
886 }
887 Command::ContractBench(args) => {
888 let min_cases = args.min_cases;
889 let min_verdict_accuracy = args.min_verdict_accuracy;
890 let min_verification_precision = args.min_verification_precision;
891 let min_boundary_precision = args.min_boundary_precision;
892 let min_boundary_recall = args.min_boundary_recall;
893 let min_toon_reduction = args.min_toon_reduction;
894 let report = run_contract_bench(args)?;
895 if cli.json {
896 println!("{}", serde_json::to_string_pretty(&report)?);
897 } else {
898 print_contract_bench_report(&report);
899 }
900 if report.case_count < min_cases {
901 anyhow::bail!(
902 "contract benchmark loaded {} cases, below required {}",
903 report.case_count,
904 min_cases
905 );
906 }
907 if !report.failures.is_empty() {
908 anyhow::bail!(
909 "contract benchmark failed {} case expectation(s): {}",
910 report.failures.len(),
911 report.failures.join(", ")
912 );
913 }
914 if report.summary.verdict_accuracy < min_verdict_accuracy {
915 anyhow::bail!(
916 "contract verdict accuracy {:.3} is below required {:.3}",
917 report.summary.verdict_accuracy,
918 min_verdict_accuracy
919 );
920 }
921 if report.summary.verification_precision < min_verification_precision {
922 anyhow::bail!(
923 "contract verification precision {:.3} is below required {:.3}",
924 report.summary.verification_precision,
925 min_verification_precision
926 );
927 }
928 if report.summary.boundary_precision < min_boundary_precision {
929 anyhow::bail!(
930 "contract boundary precision {:.3} is below required {:.3}",
931 report.summary.boundary_precision,
932 min_boundary_precision
933 );
934 }
935 if report.summary.boundary_recall < min_boundary_recall {
936 anyhow::bail!(
937 "contract boundary recall {:.3} is below required {:.3}",
938 report.summary.boundary_recall,
939 min_boundary_recall
940 );
941 }
942 if report.summary.min_toon_reduction < min_toon_reduction {
943 anyhow::bail!(
944 "contract TOON reduction {:.3} is below required {:.3}",
945 report.summary.min_toon_reduction,
946 min_toon_reduction
947 );
948 }
949 }
950 Command::Eval(args) => {
951 let min_recall = args.min_recall_at_k;
952 let min_mrr = args.min_mrr;
953 let report = run_eval(args)?;
954 if cli.json {
955 println!("{}", serde_json::to_string_pretty(&report)?);
956 } else {
957 print_eval_report(&report);
958 }
959 if report.summary.search_recall_at_k < min_recall {
960 anyhow::bail!(
961 "eval search recall@{} {:.3} is below required {:.3}",
962 report.limit,
963 report.summary.search_recall_at_k,
964 min_recall
965 );
966 }
967 if report.summary.search_mrr < min_mrr {
968 anyhow::bail!(
969 "eval MRR {:.3} is below required {:.3}",
970 report.summary.search_mrr,
971 min_mrr
972 );
973 }
974 }
975 Command::Prove(args) => {
976 let format = if cli.json {
977 ProveFormat::Json
978 } else {
979 args.format
980 };
981 let report = run_proof(args)?;
982 if matches!(format, ProveFormat::Json) {
983 println!("{}", serde_json::to_string_pretty(&report)?);
984 } else {
985 println!("{}", render_proof_markdown(&report));
986 println!("\nShareable proof generated.");
987 println!("Repo: https://github.com/shivyadavus/open-kioku");
988 }
989 }
990 Command::Architecture { command } => match command {
991 ArchitectureCommand::Policy { command } => {
992 handle_architecture_policy_command(cli.json, &repo, command)?;
993 }
994 ArchitectureCommand::Detect => {
995 let store = open_store(&repo)?;
996 let summary = ArchitectureDetector::new(&store, None).detect()?;
997 output(cli.json, &summary, || {})?;
998 }
999 ArchitectureCommand::Boundaries => {
1000 let store = open_store(&repo)?;
1001 let summary = ArchitectureDetector::new(&store, None).detect()?;
1002 output(cli.json, &summary.components, || {})?;
1003 }
1004 ArchitectureCommand::Violations => {
1005 let store = open_store(&repo)?;
1006 let summary = ArchitectureDetector::new(&store, None).detect()?;
1007 output(cli.json, &summary.violations, || {})?;
1008 }
1009 ArchitectureCommand::Bench(args) => {
1010 let min_precision = args.min_precision;
1011 let min_recall = args.min_recall;
1012 let max_p95_ms = args.max_p95_ms;
1013 let report = run_architecture_policy_bench(args)?;
1014 if cli.json {
1015 println!("{}", serde_json::to_string_pretty(&report)?);
1016 } else {
1017 print_architecture_policy_bench_report(&report);
1018 }
1019 if report.summary.precision < min_precision {
1020 anyhow::bail!(
1021 "architecture policy benchmark precision {:.3} is below required {:.3}",
1022 report.summary.precision,
1023 min_precision
1024 );
1025 }
1026 if report.summary.recall < min_recall {
1027 anyhow::bail!(
1028 "architecture policy benchmark recall {:.3} is below required {:.3}",
1029 report.summary.recall,
1030 min_recall
1031 );
1032 }
1033 if let Some(max_p95_ms) = max_p95_ms {
1034 if report.p95_policy_check_ms > max_p95_ms {
1035 anyhow::bail!(
1036 "architecture policy p95 {:.2}ms exceeds required {:.2}ms",
1037 report.p95_policy_check_ms,
1038 max_p95_ms
1039 );
1040 }
1041 }
1042 }
1043 ArchitectureCommand::Fleet { workspace } => {
1044 let report = load_fleet_architecture_report(&workspace)?;
1045 if cli.json {
1046 println!("{}", serde_json::to_string_pretty(&report)?);
1047 } else {
1048 print_fleet_architecture_report(&report);
1049 }
1050 }
1051 },
1052 Command::History { command } => {
1053 let store = open_store(&repo)?;
1054 match command {
1055 HistoryCommand::Similar {
1056 task,
1057 paths,
1058 symbols,
1059 limit,
1060 } => {
1061 let query = SimilarChangeQuery {
1062 task,
1063 paths,
1064 symbols,
1065 };
1066 let report = store.similar_changes(&query, limit)?;
1067 if cli.json {
1068 println!("{}", serde_json::to_string_pretty(&report)?);
1069 } else {
1070 print_similar_change_report(&report);
1071 }
1072 }
1073 HistoryCommand::Churn {
1074 path,
1075 module,
1076 symbol,
1077 } => {
1078 let provided = usize::from(path.is_some())
1079 + usize::from(module.is_some())
1080 + usize::from(symbol.is_some());
1081 if provided != 1 {
1082 anyhow::bail!("provide exactly one of --path, --module, or --symbol");
1083 }
1084 let summary = if let Some(path) = path {
1085 store.churn_for_file(&path)?
1086 } else if let Some(module) = module {
1087 store.churn_for_module(&module)?
1088 } else if let Some(query) = symbol {
1089 let symbol = resolve_provenance_symbol(&store, &query)?;
1090 store.churn_for_symbol(&symbol.id)?
1091 } else {
1092 unreachable!("exactly one churn target was checked above");
1093 };
1094 if cli.json {
1095 println!("{}", serde_json::to_string_pretty(&summary)?);
1096 } else {
1097 print_churn_summary(&summary);
1098 }
1099 }
1100 HistoryCommand::Ownership { path } => {
1101 let components = ownership_components(&repo, &store, &path)?;
1102 let memory_facts = ownership_memory_facts(&repo, &path, &components)?;
1103 let report =
1104 open_kioku_git::ownership_for_path(open_kioku_git::OwnershipInput {
1105 repo: &repo,
1106 path: &path,
1107 history: &store,
1108 memory_facts: &memory_facts,
1109 components,
1110 })?;
1111 if cli.json {
1112 println!("{}", serde_json::to_string_pretty(&report)?);
1113 } else {
1114 print_ownership_report(&report);
1115 }
1116 }
1117 HistoryCommand::Reviewers { path } => {
1118 let components = ownership_components(&repo, &store, &path)?;
1119 let memory_facts = ownership_memory_facts(&repo, &path, &components)?;
1120 let ownership =
1121 open_kioku_git::ownership_for_path(open_kioku_git::OwnershipInput {
1122 repo: &repo,
1123 path: &path,
1124 history: &store,
1125 memory_facts: &memory_facts,
1126 components,
1127 })?;
1128 let report = open_kioku_git::suggest_reviewers(
1129 open_kioku_git::ReviewerSuggestionInput {
1130 path: &path,
1131 history: &store,
1132 ownership: Some(&ownership),
1133 },
1134 )?;
1135 if cli.json {
1136 println!("{}", serde_json::to_string_pretty(&report)?);
1137 } else {
1138 print_reviewer_suggestion_report(&report);
1139 }
1140 }
1141 HistoryCommand::ReviewersBench(args) => {
1142 let min_accuracy = args.min_accuracy;
1143 let report = run_reviewer_bench(&repo, args)?;
1144 if cli.json {
1145 println!("{}", serde_json::to_string_pretty(&report)?);
1146 } else {
1147 print_reviewer_bench_report(&report);
1148 }
1149 if report.accuracy < min_accuracy {
1150 anyhow::bail!(
1151 "reviewer benchmark accuracy {:.3} is below required {:.3}",
1152 report.accuracy,
1153 min_accuracy
1154 );
1155 }
1156 }
1157 HistoryCommand::SimilarBench(args) => {
1158 let min_recall_at_5 = args.min_recall_at_5;
1159 let report = run_similar_history_bench(&repo, args)?;
1160 if cli.json {
1161 println!("{}", serde_json::to_string_pretty(&report)?);
1162 } else {
1163 print_similar_history_bench_report(&report);
1164 }
1165 if report.recall_at_5 < min_recall_at_5 {
1166 anyhow::bail!(
1167 "similar-history benchmark Top-5 recall {:.3} is below required {:.3}",
1168 report.recall_at_5,
1169 min_recall_at_5
1170 );
1171 }
1172 }
1173 HistoryCommand::Bench(args) => {
1174 let min_reviewer_accuracy = args.min_reviewer_accuracy;
1175 let min_similar_recall_at_5 = args.min_similar_recall_at_5;
1176 let max_similar_p95_ms = args.max_similar_p95_ms;
1177 let max_lookup_p95_ms = args.max_lookup_p95_ms;
1178 let report = run_history_bench(&repo, args)?;
1179 if cli.json {
1180 println!("{}", serde_json::to_string_pretty(&report)?);
1181 } else {
1182 print_history_bench_report(&report);
1183 }
1184 if report.reviewer_accuracy < min_reviewer_accuracy {
1185 anyhow::bail!(
1186 "history benchmark reviewer accuracy {:.3} is below required {:.3}",
1187 report.reviewer_accuracy,
1188 min_reviewer_accuracy
1189 );
1190 }
1191 if report.similar_recall_at_5 < min_similar_recall_at_5 {
1192 anyhow::bail!(
1193 "history benchmark similar-change Top-5 recall {:.3} is below required {:.3}",
1194 report.similar_recall_at_5,
1195 min_similar_recall_at_5
1196 );
1197 }
1198 if report.similar_p95_ms > max_similar_p95_ms {
1199 anyhow::bail!(
1200 "history benchmark similar-change p95 latency {:.3} ms exceeds {:.3} ms",
1201 report.similar_p95_ms,
1202 max_similar_p95_ms
1203 );
1204 }
1205 if report.ownership_churn_p95_ms > max_lookup_p95_ms {
1206 anyhow::bail!(
1207 "history benchmark ownership/churn p95 latency {:.3} ms exceeds {:.3} ms",
1208 report.ownership_churn_p95_ms,
1209 max_lookup_p95_ms
1210 );
1211 }
1212 if !report.failures.is_empty() {
1213 anyhow::bail!(
1214 "history benchmark had {} failing public API case(s)",
1215 report.failures.len()
1216 );
1217 }
1218 }
1219 HistoryCommand::Provenance {
1220 path,
1221 symbol,
1222 limit,
1223 } => {
1224 if let Some(path) = path {
1225 let provenance = store.provenance_for_path(&path, limit)?;
1226 if cli.json {
1227 println!("{}", serde_json::to_string_pretty(&provenance)?);
1228 } else {
1229 print_file_provenance(&provenance);
1230 }
1231 } else if let Some(query) = symbol {
1232 let symbol = resolve_provenance_symbol(&store, &query)?;
1233 let provenance = store.provenance_for_symbol(&symbol.id, limit)?;
1234 if cli.json {
1235 println!("{}", serde_json::to_string_pretty(&provenance)?);
1236 } else {
1237 print_symbol_provenance(&provenance);
1238 }
1239 }
1240 }
1241 }
1242 }
1243 Command::Patch { command } => {
1244 let config = OkConfig::load_from_repo(&repo)?;
1245 let store = open_store(&repo)?;
1246 let planner = PatchPlanner::new(&config, &store as &dyn OkStore);
1247 match command {
1248 PatchCommand::Plan { task } => output(cli.json, &planner.plan(&task)?, || {})?,
1249 PatchCommand::Review { id } => {
1250 let response = serde_json::json!({
1251 "id": id,
1252 "status": "requires_stored_patch_plan",
1253 "message": "patch review requires a stored patch plan"
1254 });
1255 print_text_or_json(
1256 cli.json,
1257 &format!("patch review requires stored patch plan id={id}"),
1258 &response,
1259 )?;
1260 }
1261 PatchCommand::Apply { id, approved } => {
1262 anyhow::bail!("patch apply is policy gated and requires a stored diff; id={id} approved={approved}");
1263 }
1264 }
1265 }
1266 Command::Memory { command } => {
1267 let memory = RepoMemoryStore::open_repo(&repo)?;
1268 match command {
1269 MemoryCommand::Remember {
1270 text,
1271 source,
1272 confidence,
1273 } => {
1274 let fact = memory.remember(&text, &source, confidence.into())?;
1275 output(cli.json, &fact, || {
1276 println!(
1277 "{}",
1278 serde_json::to_string_pretty(&fact).unwrap_or_default()
1279 );
1280 })?;
1281 }
1282 MemoryCommand::Search { query, limit } => {
1283 let results = memory.search(&query, limit)?;
1284 output(cli.json, &results, || {
1285 if results.is_empty() {
1286 println!("No repo memory matched.");
1287 } else {
1288 for result in &results {
1289 println!(
1290 "{:.2} {} [{}]",
1291 result.score, result.fact.text, result.fact.source
1292 );
1293 }
1294 }
1295 })?;
1296 }
1297 MemoryCommand::Recent { limit } => {
1298 let facts = memory.recent(limit)?;
1299 output(cli.json, &facts, || {
1300 for fact in &facts {
1301 println!("{} [{}]", fact.text, fact.source);
1302 }
1303 })?;
1304 }
1305 }
1306 }
1307 Command::Mcp { command } => match command {
1308 McpCommand::Install { client, repo } => {
1309 let repo = absolutize(&repo)?;
1310 let snippet = mcp_install_snippet(client, &repo);
1311 if cli.json {
1312 println!("{}", serde_json::to_string_pretty(&snippet)?);
1313 } else {
1314 println!("{}", snippet["instructions"].as_str().unwrap_or_default());
1315 if let Some(config_text) = snippet["config_text"].as_str() {
1316 println!("{config_text}");
1317 } else if let Ok(config) = serde_json::to_string_pretty(&snippet["config"]) {
1318 println!("{config}");
1319 }
1320 }
1321 }
1322 McpCommand::Serve {
1323 repo,
1324 read_only,
1325 allow_write,
1326 approval_required,
1327 allow_command,
1328 deny_network,
1329 hide_experimental,
1330 } => {
1331 let mut config = OkConfig::load_from_repo(&repo)?;
1332 config.mcp.mode = if read_only && !allow_write {
1333 "read-only".into()
1334 } else {
1335 "write".into()
1336 };
1337 config.security.allow_write = allow_write;
1338 config.security.approval_required = approval_required;
1339 config.security.deny_network = deny_network;
1340 config.mcp.hide_experimental = hide_experimental;
1341 if !allow_command.is_empty() {
1342 config.commands.allow = allow_command;
1343 }
1344 open_kioku_mcp::serve_stdio(repo, config).await?;
1345 }
1346 },
1347 Command::Scip { command } => match command {
1348 ScipCommand::Doctor { repo: command_repo } => {
1349 let repo = resolve_repo(&repo, command_repo);
1350 let config = OkConfig::load_from_repo(&repo)?;
1351 let snapshot = scip_setup_report(&repo, &config);
1352 if cli.json {
1353 println!("{}", serde_json::to_string_pretty(&snapshot)?);
1354 } else {
1355 print_scip_setup_report(&snapshot);
1356 }
1357 }
1358 ScipCommand::Setup { repo: command_repo } => {
1359 let repo = resolve_repo(&repo, command_repo);
1360 let config = OkConfig::load_from_repo(&repo)?;
1361 let snapshot = scip_setup_report(&repo, &config);
1362 if cli.json {
1363 println!("{}", serde_json::to_string_pretty(&snapshot)?);
1364 } else {
1365 print_scip_setup_report(&snapshot);
1366 println!("\nTo generate where installed:");
1367 println!(
1368 " ok index {} --with-scip auto",
1369 shell_quote(&repo.display().to_string())
1370 );
1371 println!("\nOpen Kioku will never install SCIP indexers unless a future explicit install flag enables it.");
1372 }
1373 }
1374 },
1375 }
1376 Ok(())
1377}