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