1use chrono::{DateTime, Utc};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fmt;
6use std::path::{Path, PathBuf};
7
8macro_rules! id_type {
9 ($name:ident) => {
10 #[derive(
11 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
12 )]
13 pub struct $name(pub String);
14
15 impl $name {
16 pub fn new(value: impl Into<String>) -> Self {
17 Self(value.into())
18 }
19 }
20
21 impl fmt::Display for $name {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.write_str(&self.0)
24 }
25 }
26 };
27}
28
29id_type!(RepositoryId);
30id_type!(FileId);
31id_type!(FileVersionId);
32id_type!(SymbolId);
33id_type!(NodeId);
34id_type!(EdgeId);
35id_type!(PatchId);
36id_type!(EvidenceId);
37id_type!(MemoryFactId);
38id_type!(ContextHandleId);
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
41#[serde(rename_all = "snake_case")]
42pub enum Confidence {
43 Low,
44 Medium,
45 High,
46 Exact,
47}
48
49impl Confidence {
50 pub fn score(self) -> f32 {
51 match self {
52 Self::Low => 0.35,
53 Self::Medium => 0.6,
54 Self::High => 0.85,
55 Self::Exact => 1.0,
56 }
57 }
58
59 pub fn from_score(score: f32) -> Self {
60 if score >= 0.95 {
61 Self::Exact
62 } else if score >= 0.75 {
63 Self::High
64 } else if score >= 0.55 {
65 Self::Medium
66 } else {
67 Self::Low
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
73pub struct ConfidenceBreakdown {
74 pub overall_enum: Confidence,
75 pub overall_score: f32,
76 pub components: Vec<ScoreComponent>,
77 pub blockers: Vec<String>,
78 pub caveats: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82pub struct NegativeEvidence {
83 pub query: String,
84 pub scope: String,
85 pub inspected_sources: Vec<String>,
86 pub reason: String,
87 pub confidence: f32,
88 pub suggested_next_probe: Option<String>,
89}
90
91impl Default for ConfidenceBreakdown {
92 fn default() -> Self {
93 Self {
94 overall_enum: Confidence::Low,
95 overall_score: 0.0,
96 components: Vec::new(),
97 blockers: Vec::new(),
98 caveats: Vec::new(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, Default)]
104pub struct ConfidenceSignalInput {
105 pub primary_file_count: usize,
106 pub evidence_count: usize,
107 pub exact_reference_count: usize,
108 pub validation_count: usize,
109 pub validation_with_command_count: usize,
110 pub negative_evidence_count: usize,
111 pub allowed_file_count: usize,
112 pub runtime_signal_count: usize,
113}
114
115impl ConfidenceBreakdown {
116 pub fn from_signals(input: ConfidenceSignalInput) -> Self {
117 let mut blockers = Vec::new();
118 let mut caveats = Vec::new();
119
120 if input.primary_file_count == 0 {
121 blockers.push("no primary context matched the task".into());
122 }
123 if input.negative_evidence_count > 0 {
124 blockers.push(format!(
125 "{} negative evidence signal(s) lowered confidence",
126 input.negative_evidence_count
127 ));
128 }
129 if input.exact_reference_count == 0 {
130 caveats.push("exact symbol/reference evidence is absent".into());
131 }
132 if input.validation_count == 0 {
133 caveats.push("no validation target was selected".into());
134 } else if input.validation_with_command_count == 0 {
135 caveats.push("validation targets require manual commands".into());
136 }
137 if input.runtime_signal_count == 0 {
138 caveats.push("runtime corroboration is absent".into());
139 }
140 if input.allowed_file_count == 0 {
141 caveats.push("change boundary has no allowed files".into());
142 } else if input.allowed_file_count > 8 {
143 caveats.push("change boundary is broad".into());
144 }
145
146 let evidence_target = input.primary_file_count.max(1) * 2;
147 let evidence_density = if input.primary_file_count == 0 {
148 0.0
149 } else {
150 (input.evidence_count as f32 / evidence_target.max(4) as f32).min(1.0)
151 };
152 if evidence_density < 0.5 {
153 caveats.push("evidence density is thin".into());
154 }
155
156 let exact_reference = if input.exact_reference_count > 0 {
157 1.0
158 } else {
159 0.25
160 };
161 let validation_availability = if input.validation_count > 0 { 1.0 } else { 0.2 };
162 let negative_evidence = if input.negative_evidence_count == 0 {
163 1.0
164 } else if input.negative_evidence_count <= 2 {
165 0.3
166 } else {
167 0.1
168 };
169 let boundary_tightness = if input.primary_file_count == 0 {
170 0.0
171 } else if input.allowed_file_count == 0 {
172 0.3
173 } else if input.allowed_file_count <= 3 {
174 1.0
175 } else if input.allowed_file_count <= 8
176 && input.allowed_file_count <= input.primary_file_count.max(1) * 2
177 {
178 0.85
179 } else {
180 0.45
181 };
182 let runtime_corroboration = if input.runtime_signal_count > 0 {
183 1.0
184 } else {
185 0.25
186 };
187 let test_coverage = if input.validation_count == 0 {
188 0.2
189 } else if input.validation_with_command_count > 0 {
190 1.0
191 } else {
192 0.6
193 };
194
195 let mut components = vec![
196 confidence_component(
197 "evidence_density",
198 evidence_density,
199 0.20,
200 "amount of independent indexed evidence near the selected context",
201 ),
202 confidence_component(
203 "exact_references",
204 exact_reference,
205 0.20,
206 "explicit exact symbol references or SCIP signals",
207 ),
208 confidence_component(
209 "validation_availability",
210 validation_availability,
211 0.15,
212 "presence of validation targets for the likely change",
213 ),
214 confidence_component(
215 "negative_evidence",
216 negative_evidence,
217 0.15,
218 "absence of low-confidence, missing-anchor, or no-match evidence",
219 ),
220 confidence_component(
221 "boundary_tightness",
222 boundary_tightness,
223 0.15,
224 "how narrowly allowed edit files bound the proposed change",
225 ),
226 confidence_component(
227 "runtime_corroboration",
228 runtime_corroboration,
229 0.05,
230 "runtime traces, incidents, or error signals that support the context",
231 ),
232 confidence_component(
233 "test_coverage",
234 test_coverage,
235 0.10,
236 "selected tests with runnable commands",
237 ),
238 ];
239 components.sort_by(|a, b| a.signal.cmp(&b.signal));
240 let mut overall_score = score_component_total(&components).clamp(0.0, 1.0);
241 if input.primary_file_count == 0 {
242 overall_score = overall_score.min(0.35);
243 }
244 if input.exact_reference_count == 0
245 && input.validation_count == 0
246 && input.runtime_signal_count == 0
247 {
248 overall_score = overall_score.min(0.55);
249 }
250 if input.negative_evidence_count > 0 {
251 overall_score = overall_score.min(0.60);
252 }
253
254 blockers.sort();
255 blockers.dedup();
256 caveats.sort();
257 caveats.dedup();
258 if !caveats.is_empty() {
259 overall_score = overall_score.min(0.94);
260 }
261
262 Self {
263 overall_enum: Confidence::from_score(overall_score),
264 overall_score,
265 components,
266 blockers,
267 caveats,
268 }
269 }
270}
271
272fn confidence_component(
273 signal: &'static str,
274 value: f32,
275 weight: f32,
276 rationale: &'static str,
277) -> ScoreComponent {
278 ScoreComponent::new(
279 signal,
280 value,
281 value,
282 weight,
283 value * weight,
284 Vec::new(),
285 rationale,
286 )
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
290pub struct LineRange {
291 pub start: u32,
292 pub end: u32,
293}
294
295impl LineRange {
296 pub fn single(line: u32) -> Self {
297 Self {
298 start: line,
299 end: line,
300 }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
305pub struct FileRange {
306 pub path: PathBuf,
307 pub line_range: Option<LineRange>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
311#[serde(rename_all = "snake_case")]
312pub enum EvidenceSourceType {
313 TreeSitter,
314 Scip,
315 Lsp,
316 Regex,
317 Lexical,
318 Semantic,
319 Runtime,
320 GitHistory,
321 StaticAnalysis,
322 ExternalIntegration,
323 Heuristic,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
327pub struct Evidence {
328 pub id: EvidenceId,
329 pub source: String,
330 pub source_type: EvidenceSourceType,
331 pub file_range: Option<FileRange>,
332 pub symbol_id: Option<SymbolId>,
333 pub confidence: Confidence,
334 pub message: String,
335 pub indexed_at: DateTime<Utc>,
336}
337
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
339pub struct ScoreComponent {
340 pub signal: String,
341 pub raw_value: f32,
342 pub normalized_value: f32,
343 pub weight: f32,
344 pub contribution: f32,
345 pub evidence_ids: Vec<String>,
346 pub rationale: String,
347}
348
349impl ScoreComponent {
350 pub fn new(
351 signal: impl Into<String>,
352 raw_value: f32,
353 normalized_value: f32,
354 weight: f32,
355 contribution: f32,
356 evidence_ids: Vec<String>,
357 rationale: impl Into<String>,
358 ) -> Self {
359 Self {
360 signal: signal.into(),
361 raw_value,
362 normalized_value,
363 weight,
364 contribution,
365 evidence_ids,
366 rationale: rationale.into(),
367 }
368 }
369
370 pub fn single(
371 signal: impl Into<String>,
372 score: f32,
373 evidence_ids: Vec<String>,
374 rationale: impl Into<String>,
375 ) -> Self {
376 Self::new(
377 signal,
378 score,
379 score.clamp(0.0, 1.0),
380 1.0,
381 score,
382 evidence_ids,
383 rationale,
384 )
385 }
386
387 pub fn adjustment(
388 signal: impl Into<String>,
389 contribution: f32,
390 evidence_ids: Vec<String>,
391 rationale: impl Into<String>,
392 ) -> Self {
393 Self::new(
394 signal,
395 contribution,
396 contribution.clamp(-1.0, 1.0),
397 1.0,
398 contribution,
399 evidence_ids,
400 rationale,
401 )
402 }
403}
404
405pub fn score_component_total(components: &[ScoreComponent]) -> f32 {
406 components
407 .iter()
408 .map(|component| component.contribution)
409 .sum()
410}
411
412pub fn reconcile_score_breakdown(
413 score: f32,
414 components: &mut Vec<ScoreComponent>,
415 fallback_signal: &str,
416 evidence_ids: Vec<String>,
417 rationale: &str,
418) {
419 if components.is_empty() {
420 components.push(ScoreComponent::single(
421 fallback_signal,
422 score,
423 evidence_ids,
424 rationale,
425 ));
426 return;
427 }
428
429 let delta = score - score_component_total(components);
430 if delta.abs() > 0.001 {
431 components.push(ScoreComponent::adjustment(
432 "score_reconciliation",
433 delta,
434 evidence_ids,
435 format!("adjusted component total to match surfaced score: {rationale}"),
436 ));
437 }
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
441pub struct Repository {
442 pub id: RepositoryId,
443 pub name: String,
444 pub root: PathBuf,
445 pub branch: Option<String>,
446 pub commit: Option<String>,
447 pub indexed_at: Option<DateTime<Utc>>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
451pub struct Commit {
452 pub sha: String,
453 pub message: Option<String>,
454 pub authored_at: Option<DateTime<Utc>>,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
458pub struct Branch {
459 pub name: String,
460 pub head: Option<String>,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
464#[serde(rename_all = "snake_case")]
465pub enum Language {
466 Rust,
467 Java,
468 TypeScript,
469 JavaScript,
470 Python,
471 Go,
472 Yaml,
473 Json,
474 Toml,
475 Sql,
476 Markdown,
477 Text,
478 Unknown,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
482pub struct File {
483 pub id: FileId,
484 pub repository_id: RepositoryId,
485 pub path: PathBuf,
486 pub language: Language,
487 pub size_bytes: u64,
488 pub content_hash: String,
489 pub is_generated: bool,
490 pub is_vendor: bool,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
494pub struct FileVersion {
495 pub id: FileVersionId,
496 pub file_id: FileId,
497 pub commit: Option<String>,
498 pub content_hash: String,
499 pub indexed_at: DateTime<Utc>,
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
503#[serde(rename_all = "snake_case")]
504pub enum SymbolKind {
505 Module,
506 Package,
507 Class,
508 Trait,
509 Interface,
510 Function,
511 Method,
512 Field,
513 Variable,
514 Constant,
515 Endpoint,
516 DatabaseTable,
517 Test,
518 Unknown,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
522pub struct Symbol {
523 pub id: SymbolId,
524 pub name: String,
525 pub qualified_name: String,
526 pub kind: SymbolKind,
527 pub file_id: FileId,
528 pub range: Option<LineRange>,
529 pub language: Language,
530 pub confidence: Confidence,
531 pub provenance: EvidenceSourceType,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
535pub struct SymbolOccurrence {
536 pub symbol_id: SymbolId,
537 pub file_id: FileId,
538 pub range: Option<LineRange>,
539 pub is_definition: bool,
540 pub confidence: Confidence,
541 pub provenance: EvidenceSourceType,
542}
543
544pub type Reference = SymbolOccurrence;
545pub type Definition = SymbolOccurrence;
546
547#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
548pub struct Import {
549 pub file_id: FileId,
550 pub imported: String,
551 pub range: Option<LineRange>,
552 pub confidence: Confidence,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
556pub struct AnalysisFact {
557 pub id: String,
558 pub file_id: FileId,
559 pub symbol_id: Option<SymbolId>,
560 pub target: String,
561 pub target_kind: GraphNodeType,
562 pub edge_type: GraphEdgeType,
563 pub range: Option<LineRange>,
564 pub confidence: Confidence,
565 pub source: String,
566 pub source_type: EvidenceSourceType,
567 pub message: String,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
571pub struct CodeChunk {
572 pub id: String,
573 pub file_id: FileId,
574 pub range: LineRange,
575 pub language: Language,
576 pub text: String,
577 pub symbol_id: Option<SymbolId>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
581pub struct Diagnostic {
582 pub severity: String,
583 pub message: String,
584 pub file_range: Option<FileRange>,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
588pub struct TestTarget {
589 pub id: String,
590 pub name: String,
591 pub file_id: FileId,
592 pub range: Option<LineRange>,
593 pub command: Option<String>,
594 pub confidence: Confidence,
595 pub reason: String,
596 #[serde(default)]
597 pub evidence_refs: Vec<String>,
598 #[serde(default)]
599 pub score_breakdown: Vec<ScoreComponent>,
600}
601
602impl TestTarget {
603 pub fn reconcile_score_breakdown(&mut self) {
604 if self.evidence_refs.is_empty() {
605 self.evidence_refs.push(format!("test:{}", self.id));
606 }
607 reconcile_score_breakdown(
608 self.confidence.score(),
609 &mut self.score_breakdown,
610 "test_confidence",
611 self.evidence_refs.clone(),
612 &self.reason,
613 );
614 }
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
618pub struct BuildTarget {
619 pub id: String,
620 pub name: String,
621 pub command: String,
622 pub files: Vec<FileId>,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
626pub struct RuntimeSignal {
627 pub id: String,
628 pub kind: String,
629 pub message: String,
630 pub file_range: Option<FileRange>,
631 pub occurred_at: Option<DateTime<Utc>>,
632 pub confidence: Confidence,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
636pub struct Owner {
637 pub name: String,
638 pub email: Option<String>,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
642pub struct ArchitectureComponent {
643 pub id: String,
644 pub name: String,
645 pub paths: Vec<String>,
646 pub evidence: Vec<Evidence>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
650pub struct IndexManifest {
651 pub repository: Repository,
652 pub file_count: usize,
653 pub symbol_count: usize,
654 pub chunk_count: usize,
655 pub indexed_at: DateTime<Utc>,
656 pub schema_version: u32,
657 #[serde(default)]
658 pub quality: IndexQuality,
659}
660
661#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
662pub struct IndexQuality {
663 pub scip_enabled: bool,
664 pub scip_mode: String,
665 pub scip_indexes_imported: usize,
666 pub scip_symbols: usize,
667 pub scip_occurrences: usize,
668 pub scip_exact_references: usize,
669 pub test_count: usize,
670 pub import_count: usize,
671 #[serde(default)]
672 pub build_systems: Vec<String>,
673 #[serde(default)]
674 pub codeql_databases: usize,
675 #[serde(default)]
676 pub coverage_reports: usize,
677 #[serde(default)]
678 pub junit_reports: usize,
679 #[serde(default)]
680 pub static_analysis_facts: usize,
681 #[serde(default)]
682 pub runtime_analysis_facts: usize,
683 #[serde(default)]
684 pub git_history_facts: usize,
685 #[serde(default)]
686 pub semantic_provider_notes: Vec<String>,
687 pub quality_notes: Vec<String>,
688}
689
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
691#[serde(rename_all = "snake_case")]
692pub enum GraphNodeType {
693 File,
694 Directory,
695 Module,
696 Package,
697 Class,
698 Trait,
699 Interface,
700 Function,
701 Method,
702 Field,
703 Endpoint,
704 DatabaseTable,
705 Collection,
706 Queue,
707 Topic,
708 ConfigKey,
709 Test,
710 BuildTarget,
711 RuntimeError,
712 Ticket,
713 PullRequest,
714 ArchitectureComponent,
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
718#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
719pub enum GraphEdgeType {
720 Contains,
721 Defines,
722 References,
723 Calls,
724 Implements,
725 Extends,
726 Imports,
727 DependsOn,
728 ExposesEndpoint,
729 CallsEndpoint,
730 ReadsConfig,
731 WritesConfig,
732 ReadsTable,
733 WritesTable,
734 PublishesEvent,
735 ConsumesEvent,
736 Tests,
737 OwnedBy,
738 ChangedBy,
739 FailedIn,
740 MentionedIn,
741 RelatedToTicket,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
745pub struct GraphNode {
746 pub id: NodeId,
747 pub node_type: GraphNodeType,
748 pub label: String,
749 pub file_id: Option<FileId>,
750 pub symbol_id: Option<SymbolId>,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
754pub struct GraphEdge {
755 pub id: EdgeId,
756 pub from: NodeId,
757 pub to: NodeId,
758 pub edge_type: GraphEdgeType,
759 pub evidence: Evidence,
760}
761
762#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
763pub struct SearchResult {
764 pub path: PathBuf,
765 pub line_range: Option<LineRange>,
766 pub snippet: String,
767 pub symbol: Option<Symbol>,
768 pub score: f32,
769 pub match_reason: String,
770 pub evidence: Vec<String>,
771 #[serde(default)]
772 pub evidence_refs: Vec<String>,
773 pub confidence: f32,
774 #[serde(default)]
775 pub score_breakdown: Vec<ScoreComponent>,
776}
777
778impl SearchResult {
779 pub fn derived_evidence_ids(&self) -> Vec<String> {
780 if !self.evidence_refs.is_empty() {
781 return self.evidence_refs.clone();
782 }
783 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len())
784 }
785
786 pub fn reconcile_score_breakdown(&mut self) {
787 if self.evidence_refs.is_empty() {
788 self.evidence_refs =
789 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len());
790 }
791 reconcile_score_breakdown(
792 self.score,
793 &mut self.score_breakdown,
794 "search_score",
795 self.evidence_refs.clone(),
796 &self.match_reason,
797 );
798 }
799
800 pub fn add_score_component(&mut self, component: ScoreComponent) {
801 self.score_breakdown.push(component);
802 }
803}
804
805pub fn search_result_evidence_ids(
806 path: &Path,
807 line_range: &Option<LineRange>,
808 evidence_len: usize,
809) -> Vec<String> {
810 let range = line_range
811 .as_ref()
812 .map(|range| format!("{}-{}", range.start, range.end))
813 .unwrap_or_else(|| "unknown".into());
814 let count = evidence_len.max(1);
815 (0..count)
816 .map(|index| format!("search:{}:{range}:{index}", path.display()))
817 .collect()
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
821pub struct EntityLink {
822 pub kind: String,
823 pub value: String,
824 pub file_range: Option<FileRange>,
825 pub confidence: Confidence,
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
829pub struct MemoryFact {
830 pub id: MemoryFactId,
831 pub text: String,
832 pub source: String,
833 pub confidence: Confidence,
834 pub entities: Vec<EntityLink>,
835 pub created_at: DateTime<Utc>,
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
839pub struct MemorySearchResult {
840 pub fact: MemoryFact,
841 pub score: f32,
842 pub match_reason: String,
843 pub evidence: Vec<String>,
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
847pub struct ContextHandle {
848 pub id: ContextHandleId,
849 pub kind: String,
850 pub summary: String,
851 pub file_range: Option<FileRange>,
852 pub entities: Vec<EntityLink>,
853 pub original_tokens_estimate: usize,
854 pub compressed_tokens_estimate: usize,
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
858pub struct CompressedContextPack {
859 pub task: String,
860 pub summary: String,
861 pub handles: Vec<ContextHandle>,
862 pub original_tokens_estimate: usize,
863 pub compressed_tokens_estimate: usize,
864 pub compression_ratio: f32,
865 pub evidence: Vec<Evidence>,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
869pub struct RiskReport {
870 pub level: String,
871 pub score: f32,
872 pub reasons: Vec<String>,
873}
874
875#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
876pub struct BoundaryFileRule {
877 pub path: PathBuf,
878 pub reason: String,
879 #[serde(default)]
880 pub evidence_refs: Vec<String>,
881 #[serde(default)]
882 pub symbols: Vec<String>,
883}
884
885#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
886pub struct BoundaryForbiddenRule {
887 pub pattern: String,
888 pub reason: String,
889 #[serde(default)]
890 pub evidence_refs: Vec<String>,
891}
892
893#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
894pub struct BoundaryExpansionRequirement {
895 pub reason: String,
896 #[serde(default)]
897 pub required_evidence_refs: Vec<String>,
898}
899
900#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
901pub struct BoundarySignalHooks {
902 #[serde(default)]
903 pub architecture_components: Vec<String>,
904 #[serde(default)]
905 pub ownership_sources: Vec<String>,
906 #[serde(default)]
907 pub cochange_sources: Vec<String>,
908}
909
910#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
911pub struct ChangeBoundary {
912 pub allowed_files: Vec<PathBuf>,
913 pub caution_files: Vec<PathBuf>,
914 pub forbidden_files: Vec<PathBuf>,
915 #[serde(default)]
916 pub evidence_refs: Vec<String>,
917 #[serde(default)]
918 pub allowed_symbols: Vec<String>,
919 #[serde(default)]
920 pub allowed_rules: Vec<BoundaryFileRule>,
921 #[serde(default)]
922 pub caution_rules: Vec<BoundaryFileRule>,
923 #[serde(default)]
924 pub forbidden_rules: Vec<BoundaryForbiddenRule>,
925 #[serde(default)]
926 pub expansion_requirements: Vec<BoundaryExpansionRequirement>,
927 #[serde(default)]
928 pub signal_hooks: BoundarySignalHooks,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
932pub struct ValidationPlan {
933 pub commands: Vec<String>,
934 pub tests: Vec<TestTarget>,
935 pub requires_approval: bool,
936 pub evidence: Vec<Evidence>,
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
940pub struct ImpactReport {
941 pub target: String,
942 pub direct_impacts: Vec<SearchResult>,
943 pub indirect_impacts: Vec<SearchResult>,
944 pub risk_report: RiskReport,
945 pub evidence: Vec<Evidence>,
946 #[serde(default)]
947 pub score_breakdown: Vec<ScoreComponent>,
948}
949
950impl ImpactReport {
951 pub fn reconcile_score_breakdown(&mut self) {
952 reconcile_score_breakdown(
953 self.risk_report.score,
954 &mut self.score_breakdown,
955 "impact_risk",
956 self.evidence
957 .iter()
958 .map(|evidence| evidence.id.0.clone())
959 .collect(),
960 "impact risk score",
961 );
962 for result in &mut self.direct_impacts {
963 result.reconcile_score_breakdown();
964 }
965 for result in &mut self.indirect_impacts {
966 result.reconcile_score_breakdown();
967 }
968 }
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
972pub struct ContextPack {
973 pub task: String,
974 pub intent: String,
975 pub primary_files: Vec<SearchResult>,
976 pub primary_symbols: Vec<Symbol>,
977 pub supporting_files: Vec<SearchResult>,
978 pub dependency_edges: Vec<GraphEdge>,
979 pub runtime_signals: Vec<RuntimeSignal>,
980 pub test_candidates: Vec<TestTarget>,
981 pub risk_report: RiskReport,
982 pub recommended_change_boundary: ChangeBoundary,
983 pub validation_plan: ValidationPlan,
984 pub evidence: Vec<Evidence>,
985 #[serde(default)]
986 pub negative_evidence: Vec<NegativeEvidence>,
987 pub confidence_summary: String,
988 #[serde(default)]
989 pub confidence_breakdown: ConfidenceBreakdown,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
993pub struct ToolCallRecommendation {
994 pub tool: String,
995 pub purpose: String,
996 pub arguments: serde_json::Value,
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1000pub struct PlanReport {
1001 pub task: String,
1002 pub summary: String,
1003 pub primary_context: Vec<SearchResult>,
1004 pub relevant_symbols: Vec<Symbol>,
1005 pub impact: ImpactReport,
1006 pub validation: Vec<TestTarget>,
1007 pub risk: RiskReport,
1008 pub recommended_change_boundary: ChangeBoundary,
1009 pub recommended_next_steps: Vec<String>,
1010 pub tool_calls: Vec<ToolCallRecommendation>,
1011 pub memory_facts: Vec<MemorySearchResult>,
1012 #[serde(default)]
1013 pub runtime_signals: Vec<RuntimeSignal>,
1014 pub evidence: Vec<Evidence>,
1015 #[serde(default)]
1016 pub evidence_by_section: BTreeMap<String, Vec<String>>,
1017 #[serde(default)]
1018 pub negative_evidence: Vec<NegativeEvidence>,
1019 pub confidence_summary: String,
1020 #[serde(default)]
1021 pub confidence_breakdown: ConfidenceBreakdown,
1022 #[serde(default)]
1023 pub score_breakdown: Vec<ScoreComponent>,
1024}
1025
1026impl PlanReport {
1027 pub fn reconcile_score_breakdown(&mut self) {
1028 reconcile_score_breakdown(
1029 self.risk.score,
1030 &mut self.score_breakdown,
1031 "plan_risk",
1032 self.evidence
1033 .iter()
1034 .map(|evidence| evidence.id.0.clone())
1035 .collect(),
1036 "plan risk score",
1037 );
1038 for result in &mut self.primary_context {
1039 result.reconcile_score_breakdown();
1040 }
1041 self.impact.reconcile_score_breakdown();
1042 for test in &mut self.validation {
1043 test.reconcile_score_breakdown();
1044 }
1045 }
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1049pub struct PatchPlan {
1050 pub id: PatchId,
1051 pub task: String,
1052 pub allowed_files: Vec<PathBuf>,
1053 pub caution_files: Vec<PathBuf>,
1054 pub forbidden_files: Vec<PathBuf>,
1055 pub change_steps: Vec<String>,
1056 pub risks: Vec<String>,
1057 pub assumptions: Vec<String>,
1058 pub tests: Vec<TestTarget>,
1059 pub rollback_notes: Vec<String>,
1060 pub unified_diff: Option<String>,
1061 pub requires_approval: bool,
1062 pub evidence: Vec<Evidence>,
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::{
1068 reconcile_score_breakdown, score_component_total, Confidence, ConfidenceBreakdown,
1069 ConfidenceSignalInput, ScoreComponent,
1070 };
1071
1072 #[test]
1073 fn reconciliation_adds_delta_to_match_surfaced_score() {
1074 let mut components = vec![ScoreComponent::single(
1075 "base",
1076 0.4,
1077 vec!["ev:base".into()],
1078 "base signal",
1079 )];
1080
1081 reconcile_score_breakdown(
1082 0.65,
1083 &mut components,
1084 "fallback",
1085 vec!["ev:adjust".into()],
1086 "test score",
1087 );
1088
1089 assert_eq!(components.len(), 2);
1090 assert!((score_component_total(&components) - 0.65).abs() < 0.001);
1091 assert_eq!(components[1].signal, "score_reconciliation");
1092 }
1093
1094 #[test]
1095 fn reconciliation_creates_fallback_for_empty_components() {
1096 let mut components = Vec::new();
1097
1098 reconcile_score_breakdown(
1099 0.85,
1100 &mut components,
1101 "confidence",
1102 vec!["test:id".into()],
1103 "test confidence",
1104 );
1105
1106 assert_eq!(components.len(), 1);
1107 assert_eq!(components[0].signal, "confidence");
1108 assert!((score_component_total(&components) - 0.85).abs() < 0.001);
1109 }
1110
1111 #[test]
1112 fn confidence_breakdown_is_stable_for_same_signals() {
1113 let input = ConfidenceSignalInput {
1114 primary_file_count: 2,
1115 evidence_count: 8,
1116 exact_reference_count: 2,
1117 validation_count: 2,
1118 validation_with_command_count: 1,
1119 negative_evidence_count: 0,
1120 allowed_file_count: 2,
1121 runtime_signal_count: 1,
1122 };
1123
1124 let first = ConfidenceBreakdown::from_signals(input);
1125 let second = ConfidenceBreakdown::from_signals(input);
1126
1127 assert_eq!(first.overall_enum, second.overall_enum);
1128 assert_eq!(first.overall_score, second.overall_score);
1129 assert_eq!(first.components, second.components);
1130 assert!(first.caveats.is_empty());
1131 assert!(first.blockers.is_empty());
1132 }
1133
1134 #[test]
1135 fn confidence_drops_without_exact_tests_or_runtime() {
1136 let grounded = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1137 primary_file_count: 1,
1138 evidence_count: 6,
1139 exact_reference_count: 1,
1140 validation_count: 1,
1141 validation_with_command_count: 1,
1142 negative_evidence_count: 0,
1143 allowed_file_count: 1,
1144 runtime_signal_count: 1,
1145 });
1146 let thin = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1147 primary_file_count: 1,
1148 evidence_count: 6,
1149 exact_reference_count: 0,
1150 validation_count: 0,
1151 validation_with_command_count: 0,
1152 negative_evidence_count: 0,
1153 allowed_file_count: 1,
1154 runtime_signal_count: 0,
1155 });
1156
1157 assert!(thin.overall_score < grounded.overall_score);
1158 assert_eq!(thin.overall_enum, Confidence::Medium);
1159 assert!(thin
1160 .caveats
1161 .iter()
1162 .any(|caveat| caveat.contains("exact symbol/reference")));
1163 assert!(thin
1164 .caveats
1165 .iter()
1166 .any(|caveat| caveat.contains("no validation")));
1167 assert!(thin
1168 .caveats
1169 .iter()
1170 .any(|caveat| caveat.contains("runtime corroboration")));
1171 }
1172
1173 #[test]
1174 fn negative_evidence_prevents_false_high_confidence() {
1175 let breakdown = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1176 primary_file_count: 3,
1177 evidence_count: 12,
1178 exact_reference_count: 3,
1179 validation_count: 3,
1180 validation_with_command_count: 3,
1181 negative_evidence_count: 1,
1182 allowed_file_count: 3,
1183 runtime_signal_count: 1,
1184 });
1185
1186 assert!(breakdown.overall_score <= 0.60);
1187 assert_ne!(breakdown.overall_enum, Confidence::High);
1188 assert!(!breakdown.blockers.is_empty());
1189 }
1190}