1use std::path::{Path, PathBuf};
25use std::sync::{Arc, Mutex};
26
27use bock_air::{AIRNode, NodeKind};
28use bock_ai::{
29 compute_key, node_kind_name, AiCache, AiError, AiProvider, Decision, DecisionType,
30 GenerateRequest, GenerateResponse, ManifestWriter, ModuleContext, RuleCache,
31 StrictnessPolicy,
32};
33use bock_types::{AIRModule, Strictness};
34use chrono::Utc;
35
36use crate::profile::{classify_node, TargetProfile};
37
38#[derive(Debug, Clone)]
42pub struct SynthesisConfig {
43 pub confidence_threshold: f64,
45 pub deterministic_fallback: bool,
47 pub strictness: Strictness,
49 pub auto_pin: bool,
51 pub module_path: PathBuf,
53}
54
55impl Default for SynthesisConfig {
56 fn default() -> Self {
57 Self {
58 confidence_threshold: 0.75,
59 deterministic_fallback: true,
60 strictness: Strictness::Development,
61 auto_pin: false,
62 module_path: PathBuf::new(),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
71pub enum SynthesisOutcome {
72 Accepted {
75 code: String,
77 confidence: f64,
79 from_cache: bool,
82 },
83 RuleApplied {
86 code: String,
88 rule_id: String,
90 node_kind: String,
92 confidence: f64,
94 },
95 RejectedLowConfidence {
97 confidence: f64,
99 },
100 RejectedVerification {
102 error: String,
104 },
105 ProviderError {
107 message: String,
109 },
110 ProductionUnpinned,
113}
114
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub struct SynthesisStats {
120 pub total_nodes: usize,
122 pub flagged_nodes: usize,
124 pub ai_calls: usize,
126 pub accepted: usize,
128 pub cache_hits: usize,
130 pub rejected_low_confidence: usize,
132 pub rejected_verification: usize,
134 pub provider_errors: usize,
136 pub fallback_triggered: usize,
138 pub production_unpinned: usize,
140 pub rule_applied: usize,
142}
143
144#[must_use]
152pub fn needs_ai_synthesis(target: &TargetProfile, node: &AIRNode) -> bool {
153 let Some(hint) = classify_node(node) else {
154 return false;
155 };
156 target.ai_hints.contains(&hint)
157}
158
159pub fn verify_generated(target_id: &str, code: &str) -> Result<(), String> {
175 if code.trim().is_empty() {
176 return Err("generated code is empty".into());
177 }
178 if target_id == "python" || target_id == "py" {
179 return Ok(());
180 }
181 check_bracket_balance(code)
182}
183
184fn check_bracket_balance(code: &str) -> Result<(), String> {
185 let mut stack: Vec<char> = Vec::new();
186 let mut chars = code.chars().peekable();
187 while let Some(c) = chars.next() {
188 match c {
189 '"' => skip_until(&mut chars, '"'),
190 '\'' => skip_until(&mut chars, '\''),
191 '/' if chars.peek() == Some(&'/') => {
192 for next in chars.by_ref() {
193 if next == '\n' {
194 break;
195 }
196 }
197 }
198 '(' | '[' | '{' => stack.push(c),
199 ')' => match stack.pop() {
200 Some('(') => {}
201 _ => return Err("unbalanced `)`".into()),
202 },
203 ']' => match stack.pop() {
204 Some('[') => {}
205 _ => return Err("unbalanced `]`".into()),
206 },
207 '}' => match stack.pop() {
208 Some('{') => {}
209 _ => return Err("unbalanced `}`".into()),
210 },
211 _ => {}
212 }
213 }
214 if !stack.is_empty() {
215 return Err(format!("unclosed `{}`", stack.last().unwrap()));
216 }
217 Ok(())
218}
219
220fn skip_until(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, delim: char) {
221 while let Some(next) = chars.next() {
222 if next == '\\' {
223 chars.next();
224 } else if next == delim {
225 return;
226 }
227 }
228}
229
230pub struct AiSynthesisDriver {
239 provider: Option<Arc<dyn AiProvider>>,
240 cache: Option<AiCache>,
241 manifest: Option<Arc<Mutex<ManifestWriter>>>,
242 rule_cache: Option<RuleCache>,
243 config: SynthesisConfig,
244}
245
246impl AiSynthesisDriver {
247 #[must_use]
251 pub fn deterministic(config: SynthesisConfig) -> Self {
252 Self {
253 provider: None,
254 cache: None,
255 manifest: None,
256 rule_cache: None,
257 config,
258 }
259 }
260
261 #[must_use]
264 pub fn new(
265 provider: Arc<dyn AiProvider>,
266 cache: Option<AiCache>,
267 manifest: Option<Arc<Mutex<ManifestWriter>>>,
268 config: SynthesisConfig,
269 ) -> Self {
270 Self {
271 provider: Some(provider),
272 cache,
273 manifest,
274 rule_cache: None,
275 config,
276 }
277 }
278
279 #[must_use]
287 pub fn with_rule_cache(mut self, rules: RuleCache) -> Self {
288 self.rule_cache = Some(rules);
289 self
290 }
291
292 #[must_use]
294 pub fn rule_cache(&self) -> Option<&RuleCache> {
295 self.rule_cache.as_ref()
296 }
297
298 #[must_use]
300 pub fn manifest(&self) -> Option<&Arc<Mutex<ManifestWriter>>> {
301 self.manifest.as_ref()
302 }
303
304 #[must_use]
306 pub fn config(&self) -> &SynthesisConfig {
307 &self.config
308 }
309
310 pub async fn synthesize_module(
318 &self,
319 module: &AIRModule,
320 target: &TargetProfile,
321 ctx: &ModuleContext,
322 ) -> Result<SynthesisStats, bock_ai::ManifestError> {
323 let mut stats = SynthesisStats::default();
324
325 if self.provider.is_none() {
327 walk_module(module, &mut |n| {
328 stats.total_nodes += 1;
329 if needs_ai_synthesis(target, n) {
330 stats.flagged_nodes += 1;
331 stats.fallback_triggered += 1;
332 }
333 });
334 return Ok(stats);
335 }
336
337 let mut flagged: Vec<AIRNode> = Vec::new();
340 walk_module(module, &mut |n| {
341 stats.total_nodes += 1;
342 if needs_ai_synthesis(target, n) {
343 stats.flagged_nodes += 1;
344 flagged.push(n.clone());
345 }
346 });
347
348 for node in &flagged {
349 let outcome = self.synthesize_one(node, target, ctx).await;
350 self.account_outcome(&outcome, &mut stats);
351 match &outcome {
352 SynthesisOutcome::Accepted {
353 code,
354 confidence,
355 from_cache,
356 } => {
357 self.record_decision(node, target, code, *confidence, *from_cache)?;
358 }
359 SynthesisOutcome::RuleApplied {
360 code,
361 rule_id,
362 node_kind,
363 confidence,
364 } => {
365 self.record_rule_applied(
366 node, target, code, rule_id, node_kind, *confidence,
367 )?;
368 }
369 _ => {}
370 }
371 }
372
373 Ok(stats)
374 }
375
376 fn account_outcome(&self, outcome: &SynthesisOutcome, stats: &mut SynthesisStats) {
377 match outcome {
378 SynthesisOutcome::RuleApplied { .. } => {
379 stats.rule_applied += 1;
380 }
381 SynthesisOutcome::Accepted {
382 from_cache: true, ..
383 } => {
384 stats.ai_calls += 1;
385 stats.accepted += 1;
386 stats.cache_hits += 1;
387 }
388 SynthesisOutcome::Accepted { .. } => {
389 stats.ai_calls += 1;
390 stats.accepted += 1;
391 }
392 SynthesisOutcome::RejectedLowConfidence { .. } => {
393 stats.ai_calls += 1;
394 stats.rejected_low_confidence += 1;
395 stats.fallback_triggered += 1;
396 }
397 SynthesisOutcome::RejectedVerification { .. } => {
398 stats.ai_calls += 1;
399 stats.rejected_verification += 1;
400 stats.fallback_triggered += 1;
401 }
402 SynthesisOutcome::ProviderError { .. } => {
403 stats.ai_calls += 1;
404 stats.provider_errors += 1;
405 if self.config.deterministic_fallback {
406 stats.fallback_triggered += 1;
407 }
408 }
409 SynthesisOutcome::ProductionUnpinned => {
410 stats.production_unpinned += 1;
411 if self.config.deterministic_fallback {
412 stats.fallback_triggered += 1;
413 }
414 }
415 }
416 }
417
418 async fn synthesize_one(
419 &self,
420 node: &AIRNode,
421 target: &TargetProfile,
422 ctx: &ModuleContext,
423 ) -> SynthesisOutcome {
424 if let Some(rule) = self.lookup_rule(node, target) {
430 return rule;
431 }
432
433 let request = build_request(node, target, ctx, self.config.strictness);
434 let (response, from_cache) = match self.call_generate(&request).await {
435 Ok(Some(pair)) => pair,
436 Ok(None) => {
437 return SynthesisOutcome::ProductionUnpinned;
441 }
442 Err(e) => {
443 return SynthesisOutcome::ProviderError {
444 message: format!("{e}"),
445 };
446 }
447 };
448
449 let accept = from_cache || response.confidence >= self.config.confidence_threshold;
450 if !accept {
451 return SynthesisOutcome::RejectedLowConfidence {
452 confidence: response.confidence,
453 };
454 }
455
456 if let Err(err) = verify_generated(&target.id, &response.code) {
457 return SynthesisOutcome::RejectedVerification { error: err };
458 }
459
460 SynthesisOutcome::Accepted {
461 code: response.code,
462 confidence: response.confidence,
463 from_cache,
464 }
465 }
466
467 fn lookup_rule(&self, node: &AIRNode, target: &TargetProfile) -> Option<SynthesisOutcome> {
468 let cache = self.rule_cache.as_ref()?;
469 let production_only = matches!(self.config.strictness, Strictness::Production);
470 let rule = cache.lookup(&target.id, node, production_only).ok().flatten()?;
471 Some(SynthesisOutcome::RuleApplied {
472 code: rule.template.clone(),
473 rule_id: rule.id.clone(),
474 node_kind: rule.node_kind.clone(),
475 confidence: rule.confidence,
476 })
477 }
478
479 async fn call_generate(
480 &self,
481 request: &GenerateRequest,
482 ) -> Result<Option<(GenerateResponse, bool)>, AiError> {
483 let provider = self
484 .provider
485 .as_ref()
486 .ok_or_else(|| AiError::Unavailable("no provider configured".into()))?;
487
488 let cache_key = self.build_cache_key(provider.model_id(), request);
492 if let Some(cache) = &self.cache {
493 if let Some(resp) = cache.get::<_, GenerateResponse>(&cache_key) {
494 return Ok(Some((resp, true)));
495 }
496 }
497
498 let policy = StrictnessPolicy::for_level(self.config.strictness);
503 if !policy.allow_build_ai {
504 return Ok(None);
505 }
506
507 let resp = provider.generate(request).await?;
508 if let Some(cache) = &self.cache {
509 let _ = cache.put(&cache_key, &resp);
510 }
511 Ok(Some((resp, false)))
512 }
513
514 fn build_cache_key(&self, model_id: String, request: &GenerateRequest) -> CacheKey {
515 let prior: Vec<(String, String)> = request
516 .prior_decisions
517 .iter()
518 .map(|d| (d.decision.clone(), d.choice.clone()))
519 .collect();
520 CacheKey {
525 mode: "generate",
526 model_id,
527 target_id: request.target.id.clone(),
528 module_path: request.module_context.module_path.clone(),
529 imports: request.module_context.imports.clone(),
530 siblings: request.module_context.siblings.clone(),
531 annotations: request.module_context.annotations.clone(),
532 prior_decisions: prior,
533 node_debug: format!("{:?}", request.node),
534 }
535 }
536
537 fn record_rule_applied(
538 &self,
539 node: &AIRNode,
540 target: &TargetProfile,
541 _code: &str,
542 rule_id: &str,
543 rule_kind: &str,
544 confidence: f64,
545 ) -> Result<(), bock_ai::ManifestError> {
546 let Some(manifest) = &self.manifest else {
547 return Ok(());
548 };
549 let mut mw = manifest
550 .lock()
551 .expect("manifest writer mutex poisoned");
552
553 let model_id = self
554 .provider
555 .as_ref()
556 .map_or_else(|| "deterministic".into(), |p| p.model_id());
557 let id = rule_decision_id(node, target, rule_id);
558 mw.record(Decision {
559 id,
560 module: self.config.module_path.clone(),
561 target: Some(target.id.clone()),
562 decision_type: DecisionType::RuleApplied,
563 choice: format!("rule {rule_id} matched pattern {rule_kind}"),
564 alternatives: Vec::new(),
565 reasoning: Some(format!(
566 "local rule cache hit for {rule_kind}; no AI call issued"
567 )),
568 model_id,
569 confidence,
570 pinned: true,
571 pin_reason: Some("rule-applied".into()),
572 pinned_at: Some(Utc::now()),
573 pinned_by: Some("rule-cache".into()),
574 superseded_by: None,
575 timestamp: Utc::now(),
576 });
577 Ok(())
578 }
579
580 fn record_decision(
581 &self,
582 node: &AIRNode,
583 target: &TargetProfile,
584 code: &str,
585 confidence: f64,
586 from_cache: bool,
587 ) -> Result<(), bock_ai::ManifestError> {
588 let Some(manifest) = &self.manifest else {
589 return Ok(());
590 };
591 let mut mw = manifest
592 .lock()
593 .expect("manifest writer mutex poisoned");
594
595 let id = decision_id(node, target);
596 let policy = StrictnessPolicy::for_level(self.config.strictness);
597 let pinned = from_cache
603 || policy.auto_pin_default
604 || (matches!(self.config.strictness, Strictness::Development) && self.config.auto_pin);
605 let pin_reason = if from_cache {
606 Some("cache-replay".into())
607 } else if policy.auto_pin_default {
608 Some("production-auto".into())
609 } else if pinned {
610 Some("auto-pin".into())
611 } else {
612 None
613 };
614
615 let model_id = self
616 .provider
617 .as_ref()
618 .map_or_else(|| "deterministic".into(), |p| p.model_id());
619
620 mw.record(Decision {
621 id,
622 module: self.config.module_path.clone(),
623 target: Some(target.id.clone()),
624 decision_type: DecisionType::Codegen,
625 choice: code.into(),
626 alternatives: Vec::new(),
627 reasoning: None,
628 model_id,
629 confidence,
630 pinned,
631 pin_reason,
632 pinned_at: pinned.then(Utc::now),
633 pinned_by: pinned.then(|| "auto".into()),
634 superseded_by: None,
635 timestamp: Utc::now(),
636 });
637 Ok(())
638 }
639}
640
641pub async fn synthesize_and_flush(
649 driver: &AiSynthesisDriver,
650 module: &AIRModule,
651 target: &TargetProfile,
652 ctx: &ModuleContext,
653) -> Result<SynthesisStats, bock_ai::ManifestError> {
654 let stats = driver.synthesize_module(module, target, ctx).await?;
655 if let Some(m) = driver.manifest() {
656 let mut guard = m.lock().expect("manifest writer mutex poisoned");
657 guard.flush()?;
658 }
659 Ok(stats)
660}
661
662#[derive(serde::Serialize)]
665struct CacheKey {
666 mode: &'static str,
667 model_id: String,
668 target_id: String,
669 module_path: String,
670 imports: Vec<String>,
671 siblings: Vec<String>,
672 annotations: Vec<String>,
673 prior_decisions: Vec<(String, String)>,
674 node_debug: String,
675}
676
677fn walk_module<F: FnMut(&AIRNode)>(module: &AIRModule, f: &mut F) {
681 walk_node(module, f);
682}
683
684fn walk_node<F: FnMut(&AIRNode)>(node: &AIRNode, f: &mut F) {
685 f(node);
686 match &node.kind {
687 NodeKind::Module { imports, items, .. } => {
688 for n in imports {
689 walk_node(n, f);
690 }
691 for n in items {
692 walk_node(n, f);
693 }
694 }
695 NodeKind::FnDecl {
696 params,
697 return_type,
698 body,
699 ..
700 } => {
701 for p in params {
702 walk_node(p, f);
703 }
704 if let Some(rt) = return_type {
705 walk_node(rt, f);
706 }
707 walk_node(body, f);
708 }
709 NodeKind::ClassDecl { methods, .. } => {
710 for m in methods {
711 walk_node(m, f);
712 }
713 }
714 NodeKind::TraitDecl { methods, .. } => {
715 for m in methods {
716 walk_node(m, f);
717 }
718 }
719 NodeKind::ImplBlock { methods, .. } => {
720 for m in methods {
721 walk_node(m, f);
722 }
723 }
724 NodeKind::EnumDecl { variants, .. } => {
725 for v in variants {
726 walk_node(v, f);
727 }
728 }
729 NodeKind::EffectDecl { operations, .. } => {
730 for op in operations {
731 walk_node(op, f);
732 }
733 }
734 NodeKind::Block { stmts, tail } => {
735 for s in stmts {
736 walk_node(s, f);
737 }
738 if let Some(t) = tail {
739 walk_node(t, f);
740 }
741 }
742 NodeKind::If {
743 condition,
744 then_block,
745 else_block,
746 ..
747 } => {
748 walk_node(condition, f);
749 walk_node(then_block, f);
750 if let Some(e) = else_block {
751 walk_node(e, f);
752 }
753 }
754 NodeKind::For {
755 pattern,
756 iterable,
757 body,
758 } => {
759 walk_node(pattern, f);
760 walk_node(iterable, f);
761 walk_node(body, f);
762 }
763 NodeKind::While { condition, body } => {
764 walk_node(condition, f);
765 walk_node(body, f);
766 }
767 NodeKind::Loop { body } => walk_node(body, f),
768 NodeKind::LetBinding {
769 pattern, value, ty, ..
770 } => {
771 walk_node(pattern, f);
772 walk_node(value, f);
773 if let Some(t) = ty {
774 walk_node(t, f);
775 }
776 }
777 NodeKind::Match { scrutinee, arms } => {
778 walk_node(scrutinee, f);
779 for a in arms {
780 walk_node(a, f);
781 }
782 }
783 NodeKind::MatchArm {
784 pattern,
785 guard,
786 body,
787 } => {
788 walk_node(pattern, f);
789 if let Some(g) = guard {
790 walk_node(g, f);
791 }
792 walk_node(body, f);
793 }
794 NodeKind::HandlingBlock { body, .. } => walk_node(body, f),
795 NodeKind::BinaryOp { left, right, .. } => {
796 walk_node(left, f);
797 walk_node(right, f);
798 }
799 NodeKind::UnaryOp { operand, .. } => walk_node(operand, f),
800 NodeKind::Call { callee, args, .. } => {
801 walk_node(callee, f);
802 for a in args {
803 walk_node(&a.value, f);
804 }
805 }
806 NodeKind::MethodCall { receiver, args, .. } => {
807 walk_node(receiver, f);
808 for a in args {
809 walk_node(&a.value, f);
810 }
811 }
812 NodeKind::Lambda { params, body } => {
813 for p in params {
814 walk_node(p, f);
815 }
816 walk_node(body, f);
817 }
818 NodeKind::Return { value } | NodeKind::Break { value } => {
819 if let Some(v) = value {
820 walk_node(v, f);
821 }
822 }
823 NodeKind::Assign { target, value, .. } => {
824 walk_node(target, f);
825 walk_node(value, f);
826 }
827 NodeKind::FieldAccess { object, .. } => walk_node(object, f),
828 NodeKind::Index { object, index } => {
829 walk_node(object, f);
830 walk_node(index, f);
831 }
832 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
833 walk_node(left, f);
834 walk_node(right, f);
835 }
836 NodeKind::Await { expr } | NodeKind::Propagate { expr } => walk_node(expr, f),
837 NodeKind::Move { expr } | NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } => {
838 walk_node(expr, f);
839 }
840 NodeKind::Guard {
841 let_pattern,
842 condition,
843 else_block,
844 } => {
845 if let Some(p) = let_pattern {
846 walk_node(p, f);
847 }
848 walk_node(condition, f);
849 walk_node(else_block, f);
850 }
851 NodeKind::Param {
852 pattern,
853 ty,
854 default,
855 } => {
856 walk_node(pattern, f);
857 if let Some(t) = ty {
858 walk_node(t, f);
859 }
860 if let Some(d) = default {
861 walk_node(d, f);
862 }
863 }
864 NodeKind::ListLiteral { elems }
865 | NodeKind::SetLiteral { elems }
866 | NodeKind::TupleLiteral { elems } => {
867 for e in elems {
868 walk_node(e, f);
869 }
870 }
871 NodeKind::MapLiteral { entries } => {
872 for e in entries {
873 walk_node(&e.key, f);
874 walk_node(&e.value, f);
875 }
876 }
877 NodeKind::RecordConstruct { fields, spread, .. } => {
878 for fld in fields {
879 if let Some(v) = &fld.value {
880 walk_node(v, f);
881 }
882 }
883 if let Some(s) = spread {
884 walk_node(s, f);
885 }
886 }
887 NodeKind::Range { lo, hi, .. } => {
888 walk_node(lo, f);
889 walk_node(hi, f);
890 }
891 NodeKind::ResultConstruct { value: Some(v), .. } => walk_node(v, f),
892 NodeKind::TypeNamed { args, .. } => {
893 for a in args {
894 walk_node(a, f);
895 }
896 }
897 NodeKind::TypeTuple { elems } => {
898 for e in elems {
899 walk_node(e, f);
900 }
901 }
902 NodeKind::TypeFunction { params, ret, .. } => {
903 for p in params {
904 walk_node(p, f);
905 }
906 walk_node(ret, f);
907 }
908 NodeKind::TypeOptional { inner } => walk_node(inner, f),
909 NodeKind::TypeAlias { ty, .. } => walk_node(ty, f),
910 NodeKind::ConstDecl { ty, value, .. } => {
911 walk_node(ty, f);
912 walk_node(value, f);
913 }
914 NodeKind::ModuleHandle { handler, .. } => walk_node(handler, f),
915 NodeKind::PropertyTest { body, .. } => walk_node(body, f),
916 NodeKind::ConstructorPat { fields, .. } => {
917 for fld in fields {
918 walk_node(fld, f);
919 }
920 }
921 NodeKind::RecordPat { fields, .. } => {
922 for fld in fields {
923 if let Some(p) = &fld.pattern {
924 walk_node(p, f);
925 }
926 }
927 }
928 NodeKind::TuplePat { elems } => {
929 for e in elems {
930 walk_node(e, f);
931 }
932 }
933 NodeKind::ListPat { elems, rest } => {
934 for e in elems {
935 walk_node(e, f);
936 }
937 if let Some(r) = rest {
938 walk_node(r, f);
939 }
940 }
941 NodeKind::OrPat { alternatives } => {
942 for a in alternatives {
943 walk_node(a, f);
944 }
945 }
946 NodeKind::GuardPat { pattern, guard } => {
947 walk_node(pattern, f);
948 walk_node(guard, f);
949 }
950 NodeKind::RangePat { lo, hi, .. } => {
951 walk_node(lo, f);
952 walk_node(hi, f);
953 }
954 _ => {}
955 }
956}
957
958fn build_request(
961 node: &AIRNode,
962 target: &TargetProfile,
963 ctx: &ModuleContext,
964 strictness: Strictness,
965) -> GenerateRequest {
966 GenerateRequest {
967 node: node.clone(),
968 target: flatten_profile(target),
969 module_context: ctx.clone(),
970 prior_decisions: Vec::new(),
971 strictness,
972 }
973}
974
975fn flatten_profile(target: &TargetProfile) -> bock_ai::TargetProfile {
976 use std::collections::HashMap;
977 let mut capabilities = HashMap::new();
978 capabilities.insert(
979 "memory_model".into(),
980 format!("{}", target.capabilities.memory_model),
981 );
982 capabilities.insert(
983 "async_model".into(),
984 format!("{}", target.capabilities.async_model),
985 );
986 capabilities.insert(
987 "generics".into(),
988 format!("{}", target.capabilities.generics),
989 );
990 capabilities.insert(
991 "pattern_matching".into(),
992 format!("{}", target.capabilities.pattern_matching),
993 );
994 capabilities.insert(
995 "algebraic_types".into(),
996 format!("{}", target.capabilities.algebraic_types),
997 );
998 capabilities.insert(
999 "string_interpolation".into(),
1000 format!("{}", target.capabilities.string_interpolation),
1001 );
1002 capabilities.insert("traits".into(), format!("{}", target.capabilities.traits));
1003 let mut conventions = HashMap::new();
1004 conventions.insert("naming".into(), format!("{}", target.conventions.naming));
1005 conventions.insert(
1006 "error_handling".into(),
1007 format!("{}", target.conventions.error_handling),
1008 );
1009 conventions.insert(
1010 "file_extension".into(),
1011 target.conventions.file_extension.clone(),
1012 );
1013 bock_ai::TargetProfile {
1014 id: target.id.clone(),
1015 display_name: target.display_name.clone(),
1016 capabilities,
1017 conventions,
1018 }
1019}
1020
1021fn decision_id(node: &AIRNode, target: &TargetProfile) -> String {
1024 #[derive(serde::Serialize)]
1025 struct Keyed<'a> {
1026 target: &'a str,
1027 node_debug: String,
1028 }
1029 let keyed = Keyed {
1030 target: &target.id,
1031 node_debug: format!("{node:?}"),
1032 };
1033 compute_key(&keyed).unwrap_or_else(|_| format!("{:x}", node.id))
1034}
1035
1036fn rule_decision_id(node: &AIRNode, target: &TargetProfile, rule_id: &str) -> String {
1039 #[derive(serde::Serialize)]
1040 struct Keyed<'a> {
1041 kind: &'static str,
1042 target: &'a str,
1043 rule_id: &'a str,
1044 node_kind: &'a str,
1045 node_id: u32,
1046 }
1047 let keyed = Keyed {
1048 kind: "rule_applied",
1049 target: &target.id,
1050 rule_id,
1051 node_kind: node_kind_name(&node.kind),
1052 node_id: node.id,
1053 };
1054 compute_key(&keyed).unwrap_or_else(|_| format!("rule-{rule_id}-{:x}", node.id))
1055}
1056
1057#[must_use]
1060pub fn cache_at(project_root: &Path) -> AiCache {
1061 AiCache::new(project_root)
1062}