1use chrono::{DateTime, Utc};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6use std::path::{Path, PathBuf};
7
8pub mod identity;
9
10macro_rules! id_type {
11 ($name:ident) => {
12 #[derive(
13 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
14 )]
15 pub struct $name(pub String);
16
17 impl $name {
18 pub fn new(value: impl Into<String>) -> Self {
19 Self(value.into())
20 }
21 }
22
23 impl fmt::Display for $name {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(&self.0)
26 }
27 }
28 };
29}
30
31id_type!(RepositoryId);
32id_type!(FileId);
33id_type!(FileVersionId);
34id_type!(SymbolId);
35id_type!(NodeId);
36id_type!(EdgeId);
37id_type!(PatchId);
38id_type!(EvidenceId);
39id_type!(MemoryFactId);
40id_type!(ContextHandleId);
41id_type!(GitCommitId);
42id_type!(HistoryRecordId);
43
44pub const HISTORY_SCHEMA_VERSION: u32 = 1;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47#[serde(rename_all = "snake_case")]
48pub enum Confidence {
49 Low,
50 Medium,
51 High,
52 Exact,
53}
54
55impl Confidence {
56 pub fn score(self) -> f32 {
57 match self {
58 Self::Low => 0.35,
59 Self::Medium => 0.6,
60 Self::High => 0.85,
61 Self::Exact => 1.0,
62 }
63 }
64
65 pub fn from_score(score: f32) -> Self {
66 if score >= 0.95 {
67 Self::Exact
68 } else if score >= 0.75 {
69 Self::High
70 } else if score >= 0.55 {
71 Self::Medium
72 } else {
73 Self::Low
74 }
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
79pub struct ConfidenceBreakdown {
80 pub overall_enum: Confidence,
81 pub overall_score: f32,
82 pub components: Vec<ScoreComponent>,
83 pub blockers: Vec<String>,
84 pub caveats: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
88pub struct NegativeEvidence {
89 pub query: String,
90 pub scope: String,
91 pub inspected_sources: Vec<String>,
92 pub reason: String,
93 pub confidence: f32,
94 pub suggested_next_probe: Option<String>,
95}
96
97const DEFAULT_EVIDENCE_FRESHNESS_MAX_AGE_DAYS: i64 = 7;
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
100pub struct EvidenceQuality {
101 pub index_mode: String,
102 pub freshness: String,
103 pub exact_reference_available: bool,
104 pub runtime_available: bool,
105 pub history_available: bool,
106 pub test_coverage_available: bool,
107 pub skipped_path_count: usize,
108 pub unresolved_import_count: usize,
109 pub ambiguous_edge_count: usize,
110 pub failed_optional_passes: Vec<String>,
111 pub caveats: Vec<String>,
112}
113
114impl Default for EvidenceQuality {
115 fn default() -> Self {
116 Self {
117 index_mode: "unknown".into(),
118 freshness: "missing".into(),
119 exact_reference_available: false,
120 runtime_available: false,
121 history_available: false,
122 test_coverage_available: false,
123 skipped_path_count: 0,
124 unresolved_import_count: 0,
125 ambiguous_edge_count: 0,
126 failed_optional_passes: Vec::new(),
127 caveats: vec![
128 "no index manifest was available; evidence quality could not be verified".into(),
129 ],
130 }
131 }
132}
133
134impl EvidenceQuality {
135 pub fn from_manifest(manifest: Option<&IndexManifest>) -> Self {
136 Self::from_manifest_with_counts(manifest, 0, 0)
137 }
138
139 pub fn from_manifest_with_counts(
140 manifest: Option<&IndexManifest>,
141 unresolved_import_count: usize,
142 ambiguous_edge_count: usize,
143 ) -> Self {
144 let Some(manifest) = manifest else {
145 return Self::default();
146 };
147 let quality = &manifest.quality;
148 let unresolved_import_count = unresolved_import_count.max(count_resolution_notes(
149 quality,
150 "import resolver caveat",
151 "unresolved import",
152 ));
153 let ambiguous_edge_count = ambiguous_edge_count.max(count_resolution_notes(
154 quality,
155 "import resolver caveat",
156 "ambiguous import",
157 ));
158 let failed_optional_passes = failed_optional_passes(quality);
159 let mut value = Self {
160 index_mode: manifest.index_mode.to_string(),
161 freshness: evidence_freshness(manifest.indexed_at),
162 exact_reference_available: quality.scip_exact_references > 0,
163 runtime_available: quality.runtime_analysis_facts > 0,
164 history_available: quality.git_history_facts > 0,
165 test_coverage_available: quality.coverage_reports > 0 || quality.junit_reports > 0,
166 skipped_path_count: quality.skipped_paths.len(),
167 unresolved_import_count,
168 ambiguous_edge_count,
169 failed_optional_passes,
170 caveats: Vec::new(),
171 };
172 value.refresh_caveats();
173 value
174 }
175
176 pub fn is_fresh(&self) -> bool {
177 self.freshness == "fresh"
178 }
179
180 pub fn is_stale(&self) -> bool {
181 self.freshness == "stale"
182 }
183
184 pub fn is_missing(&self) -> bool {
185 self.freshness == "missing" || self.index_mode == "unknown"
186 }
187
188 pub fn refresh_caveats(&mut self) {
189 let mut caveats = Vec::new();
190 match self.freshness.as_str() {
191 "stale" => caveats.push(
192 "index is stale; re-index before relying on exact impact or verification gates"
193 .into(),
194 ),
195 "missing" => caveats.push(
196 "no index manifest was available; evidence quality could not be verified".into(),
197 ),
198 _ => {}
199 }
200 match self.index_mode.as_str() {
201 "fast" => caveats.push(
202 "fast index mode may skip docs, examples, testdata, generated, vendor, unsupported, and oversized paths".into(),
203 ),
204 "balanced" => caveats.push(
205 "balanced index mode may skip expensive optional evidence passes".into(),
206 ),
207 "cross_project" => caveats.push(
208 "cross-project index mode links already-indexed projects without full source parsing".into(),
209 ),
210 _ => {}
211 }
212 if !self.exact_reference_available {
213 caveats.push("exact symbol/reference evidence is unavailable".into());
214 }
215 if !self.runtime_available {
216 caveats.push("runtime evidence is unavailable".into());
217 }
218 if !self.history_available {
219 caveats.push("history evidence is unavailable".into());
220 }
221 if !self.test_coverage_available {
222 caveats.push("coverage or JUnit evidence is unavailable".into());
223 }
224 if self.skipped_path_count > 0 {
225 caveats.push(format!(
226 "index skipped {} path(s); evidence may be incomplete for skipped areas",
227 self.skipped_path_count
228 ));
229 }
230 if self.unresolved_import_count > 0 {
231 caveats.push(format!(
232 "{} unresolved import(s) reduce dependency evidence confidence",
233 self.unresolved_import_count
234 ));
235 }
236 if self.ambiguous_edge_count > 0 {
237 caveats.push(format!(
238 "{} ambiguous edge(s) reduce impact and policy confidence",
239 self.ambiguous_edge_count
240 ));
241 }
242 for pass in &self.failed_optional_passes {
243 caveats.push(format!("optional evidence pass did not complete: {pass}"));
244 }
245 self.caveats = dedup_strings(caveats);
246 }
247}
248
249fn evidence_freshness(indexed_at: DateTime<Utc>) -> String {
250 let max_age = chrono::Duration::days(DEFAULT_EVIDENCE_FRESHNESS_MAX_AGE_DAYS);
251 if Utc::now().signed_duration_since(indexed_at) > max_age {
252 "stale".into()
253 } else {
254 "fresh".into()
255 }
256}
257
258fn count_resolution_notes(quality: &IndexQuality, source: &str, needle: &str) -> usize {
259 let source = source.to_ascii_lowercase();
260 let needle = needle.to_ascii_lowercase();
261 quality
262 .quality_notes
263 .iter()
264 .filter(|note| {
265 let note = note.to_ascii_lowercase();
266 note.contains(&source) && note.contains(&needle)
267 })
268 .count()
269}
270
271fn failed_optional_passes(quality: &IndexQuality) -> Vec<String> {
272 let mut passes = Vec::new();
273 for note in quality.quality_notes.iter().chain(
274 quality
275 .phase_reports
276 .iter()
277 .flat_map(|report| report.warnings.iter()),
278 ) {
279 let lowered = note.to_ascii_lowercase();
280 if lowered.contains("failed")
281 || lowered.contains("timed out")
282 || lowered.contains("timedout")
283 || lowered.contains("was enabled but no scip index was imported")
284 {
285 passes.push(note.clone());
286 }
287 }
288 dedup_strings(passes)
289}
290
291fn dedup_strings(values: Vec<String>) -> Vec<String> {
292 let mut seen = BTreeSet::new();
293 values
294 .into_iter()
295 .filter(|value| seen.insert(value.clone()))
296 .collect()
297}
298
299impl Default for ConfidenceBreakdown {
300 fn default() -> Self {
301 Self {
302 overall_enum: Confidence::Low,
303 overall_score: 0.0,
304 components: Vec::new(),
305 blockers: Vec::new(),
306 caveats: Vec::new(),
307 }
308 }
309}
310
311#[derive(Debug, Clone, Copy, Default)]
312pub struct ConfidenceSignalInput {
313 pub primary_file_count: usize,
314 pub evidence_count: usize,
315 pub exact_reference_count: usize,
316 pub validation_count: usize,
317 pub validation_with_command_count: usize,
318 pub negative_evidence_count: usize,
319 pub allowed_file_count: usize,
320 pub runtime_signal_count: usize,
321}
322
323impl ConfidenceBreakdown {
324 pub fn from_signals(input: ConfidenceSignalInput) -> Self {
325 let mut blockers = Vec::new();
326 let mut caveats = Vec::new();
327
328 if input.primary_file_count == 0 {
329 blockers.push("no primary context matched the task".into());
330 }
331 if input.negative_evidence_count > 0 {
332 blockers.push(format!(
333 "{} negative evidence signal(s) lowered confidence",
334 input.negative_evidence_count
335 ));
336 }
337 if input.exact_reference_count == 0 {
338 caveats.push("exact symbol/reference evidence is absent".into());
339 }
340 if input.validation_count == 0 {
341 caveats.push("no validation target was selected".into());
342 } else if input.validation_with_command_count == 0 {
343 caveats.push("validation targets require manual commands".into());
344 }
345 if input.runtime_signal_count == 0 {
346 caveats.push("runtime corroboration is absent".into());
347 }
348 if input.allowed_file_count == 0 {
349 caveats.push("change boundary has no allowed files".into());
350 } else if input.allowed_file_count > 8 {
351 caveats.push("change boundary is broad".into());
352 }
353
354 let evidence_target = input.primary_file_count.max(1) * 2;
355 let evidence_density = if input.primary_file_count == 0 {
356 0.0
357 } else {
358 (input.evidence_count as f32 / evidence_target.max(4) as f32).min(1.0)
359 };
360 if evidence_density < 0.5 {
361 caveats.push("evidence density is thin".into());
362 }
363
364 let exact_reference = if input.exact_reference_count > 0 {
365 1.0
366 } else {
367 0.25
368 };
369 let validation_availability = if input.validation_count > 0 { 1.0 } else { 0.2 };
370 let negative_evidence = if input.negative_evidence_count == 0 {
371 1.0
372 } else if input.negative_evidence_count <= 2 {
373 0.3
374 } else {
375 0.1
376 };
377 let boundary_tightness = if input.primary_file_count == 0 {
378 0.0
379 } else if input.allowed_file_count == 0 {
380 0.3
381 } else if input.allowed_file_count <= 3 {
382 1.0
383 } else if input.allowed_file_count <= 8
384 && input.allowed_file_count <= input.primary_file_count.max(1) * 2
385 {
386 0.85
387 } else {
388 0.45
389 };
390 let runtime_corroboration = if input.runtime_signal_count > 0 {
391 1.0
392 } else {
393 0.25
394 };
395 let test_coverage = if input.validation_count == 0 {
396 0.2
397 } else if input.validation_with_command_count > 0 {
398 1.0
399 } else {
400 0.6
401 };
402
403 let mut components = vec![
404 confidence_component(
405 "evidence_density",
406 evidence_density,
407 0.20,
408 "amount of independent indexed evidence near the selected context",
409 ),
410 confidence_component(
411 "exact_references",
412 exact_reference,
413 0.20,
414 "explicit exact symbol references or SCIP signals",
415 ),
416 confidence_component(
417 "validation_availability",
418 validation_availability,
419 0.15,
420 "presence of validation targets for the likely change",
421 ),
422 confidence_component(
423 "negative_evidence",
424 negative_evidence,
425 0.15,
426 "absence of low-confidence, missing-anchor, or no-match evidence",
427 ),
428 confidence_component(
429 "boundary_tightness",
430 boundary_tightness,
431 0.15,
432 "how narrowly allowed edit files bound the proposed change",
433 ),
434 confidence_component(
435 "runtime_corroboration",
436 runtime_corroboration,
437 0.05,
438 "runtime traces, incidents, or error signals that support the context",
439 ),
440 confidence_component(
441 "test_coverage",
442 test_coverage,
443 0.10,
444 "selected tests with runnable commands",
445 ),
446 ];
447 components.sort_by(|a, b| a.signal.cmp(&b.signal));
448 let mut overall_score = score_component_total(&components).clamp(0.0, 1.0);
449 if input.primary_file_count == 0 {
450 overall_score = overall_score.min(0.35);
451 }
452 if input.exact_reference_count == 0
453 && input.validation_count == 0
454 && input.runtime_signal_count == 0
455 {
456 overall_score = overall_score.min(0.55);
457 }
458 if input.negative_evidence_count > 0 {
459 overall_score = overall_score.min(0.60);
460 }
461
462 blockers.sort();
463 blockers.dedup();
464 caveats.sort();
465 caveats.dedup();
466 if !caveats.is_empty() {
467 overall_score = overall_score.min(0.94);
468 }
469
470 Self {
471 overall_enum: Confidence::from_score(overall_score),
472 overall_score,
473 components,
474 blockers,
475 caveats,
476 }
477 }
478}
479
480fn confidence_component(
481 signal: &'static str,
482 value: f32,
483 weight: f32,
484 rationale: &'static str,
485) -> ScoreComponent {
486 ScoreComponent::new(
487 signal,
488 value,
489 value,
490 weight,
491 value * weight,
492 Vec::new(),
493 rationale,
494 )
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
498pub struct LineRange {
499 pub start: u32,
500 pub end: u32,
501}
502
503impl LineRange {
504 pub fn single(line: u32) -> Self {
505 Self {
506 start: line,
507 end: line,
508 }
509 }
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
513pub struct FileRange {
514 pub path: PathBuf,
515 pub line_range: Option<LineRange>,
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
519#[serde(rename_all = "snake_case")]
520pub enum EvidenceSourceType {
521 TreeSitter,
522 Scip,
523 Lsp,
524 Regex,
525 Lexical,
526 Semantic,
527 Runtime,
528 GitHistory,
529 StaticAnalysis,
530 ExternalIntegration,
531 Heuristic,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
535pub struct Evidence {
536 pub id: EvidenceId,
537 pub source: String,
538 pub source_type: EvidenceSourceType,
539 pub file_range: Option<FileRange>,
540 pub symbol_id: Option<SymbolId>,
541 pub confidence: Confidence,
542 pub message: String,
543 pub indexed_at: DateTime<Utc>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub confidence_score: Option<f32>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub confidence_reason: Option<String>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub freshness: Option<String>,
550}
551
552impl Default for Evidence {
553 fn default() -> Self {
554 Self {
555 id: EvidenceId::new(""),
556 source: String::new(),
557 source_type: EvidenceSourceType::Lexical,
558 file_range: None,
559 symbol_id: None,
560 confidence: Confidence::Low,
561 message: String::new(),
562 indexed_at: Utc::now(),
563 confidence_score: None,
564 confidence_reason: None,
565 freshness: None,
566 }
567 }
568}
569
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
571pub struct ScoreComponent {
572 pub signal: String,
573 pub raw_value: f32,
574 pub normalized_value: f32,
575 pub weight: f32,
576 pub contribution: f32,
577 pub evidence_ids: Vec<String>,
578 pub rationale: String,
579}
580
581impl ScoreComponent {
582 pub fn new(
583 signal: impl Into<String>,
584 raw_value: f32,
585 normalized_value: f32,
586 weight: f32,
587 contribution: f32,
588 evidence_ids: Vec<String>,
589 rationale: impl Into<String>,
590 ) -> Self {
591 Self {
592 signal: signal.into(),
593 raw_value,
594 normalized_value,
595 weight,
596 contribution,
597 evidence_ids,
598 rationale: rationale.into(),
599 }
600 }
601
602 pub fn single(
603 signal: impl Into<String>,
604 score: f32,
605 evidence_ids: Vec<String>,
606 rationale: impl Into<String>,
607 ) -> Self {
608 Self::new(
609 signal,
610 score,
611 score.clamp(0.0, 1.0),
612 1.0,
613 score,
614 evidence_ids,
615 rationale,
616 )
617 }
618
619 pub fn adjustment(
620 signal: impl Into<String>,
621 contribution: f32,
622 evidence_ids: Vec<String>,
623 rationale: impl Into<String>,
624 ) -> Self {
625 Self::new(
626 signal,
627 contribution,
628 contribution.clamp(-1.0, 1.0),
629 1.0,
630 contribution,
631 evidence_ids,
632 rationale,
633 )
634 }
635}
636
637pub fn score_component_total(components: &[ScoreComponent]) -> f32 {
638 components
639 .iter()
640 .map(|component| component.contribution)
641 .sum()
642}
643
644pub fn reconcile_score_breakdown(
645 score: f32,
646 components: &mut Vec<ScoreComponent>,
647 fallback_signal: &str,
648 evidence_ids: Vec<String>,
649 rationale: &str,
650) {
651 if components.is_empty() {
652 components.push(ScoreComponent::single(
653 fallback_signal,
654 score,
655 evidence_ids,
656 rationale,
657 ));
658 return;
659 }
660
661 let delta = score - score_component_total(components);
662 if delta.abs() > 0.001 {
663 components.push(ScoreComponent::adjustment(
664 "score_reconciliation",
665 delta,
666 evidence_ids,
667 format!("adjusted component total to match surfaced score: {rationale}"),
668 ));
669 }
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
673pub struct Repository {
674 pub id: RepositoryId,
675 pub name: String,
676 pub root: PathBuf,
677 pub branch: Option<String>,
678 pub commit: Option<String>,
679 pub indexed_at: Option<DateTime<Utc>>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
683pub struct Commit {
684 pub sha: String,
685 pub message: Option<String>,
686 pub authored_at: Option<DateTime<Utc>>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
690pub struct Branch {
691 pub name: String,
692 pub head: Option<String>,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
696#[serde(rename_all = "snake_case")]
697pub enum Language {
698 Rust,
699 Java,
700 TypeScript,
701 JavaScript,
702 Python,
703 Go,
704 Yaml,
705 Json,
706 Toml,
707 Sql,
708 Markdown,
709 Text,
710 Unknown,
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
714pub struct File {
715 pub id: FileId,
716 pub repository_id: RepositoryId,
717 pub path: PathBuf,
718 pub language: Language,
719 pub size_bytes: u64,
720 pub content_hash: String,
721 pub is_generated: bool,
722 pub is_vendor: bool,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
726pub struct FileVersion {
727 pub id: FileVersionId,
728 pub file_id: FileId,
729 pub commit: Option<String>,
730 pub content_hash: String,
731 pub indexed_at: DateTime<Utc>,
732}
733
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
735#[serde(rename_all = "snake_case")]
736pub enum SymbolKind {
737 Module,
738 Package,
739 Class,
740 Trait,
741 Interface,
742 Function,
743 Method,
744 Field,
745 Variable,
746 Constant,
747 Endpoint,
748 DatabaseTable,
749 Test,
750 Unknown,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
754pub struct Symbol {
755 pub id: SymbolId,
756 pub name: String,
757 pub qualified_name: String,
758 pub kind: SymbolKind,
759 pub file_id: FileId,
760 pub range: Option<LineRange>,
761 pub language: Language,
762 pub confidence: Confidence,
763 pub provenance: EvidenceSourceType,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
767pub struct SymbolOccurrence {
768 pub symbol_id: SymbolId,
769 pub file_id: FileId,
770 pub range: Option<LineRange>,
771 pub is_definition: bool,
772 pub confidence: Confidence,
773 pub provenance: EvidenceSourceType,
774}
775
776pub type Reference = SymbolOccurrence;
777pub type Definition = SymbolOccurrence;
778
779#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
780pub struct Import {
781 pub file_id: FileId,
782 pub imported: String,
783 pub range: Option<LineRange>,
784 pub confidence: Confidence,
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
788#[serde(rename_all = "snake_case")]
789pub enum ResolutionStatus {
790 Resolved,
791 Ambiguous { candidates: usize },
792 ExternalPackage,
793 Builtin,
794 Unresolved,
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
798pub struct ImportResolution {
799 pub import: Import,
800 pub status: ResolutionStatus,
801 pub target_file: Option<FileId>,
802 pub target_symbol: Option<SymbolId>,
803 pub confidence: Confidence,
804 pub strategy: String,
805 pub caveats: Vec<String>,
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
809pub struct AnalysisFact {
810 pub id: String,
811 pub file_id: FileId,
812 pub symbol_id: Option<SymbolId>,
813 pub target: String,
814 pub target_kind: GraphNodeType,
815 pub edge_type: GraphEdgeType,
816 pub range: Option<LineRange>,
817 pub confidence: Confidence,
818 pub source: String,
819 pub source_type: EvidenceSourceType,
820 pub message: String,
821}
822
823#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
824pub struct CodeChunk {
825 pub id: String,
826 pub file_id: FileId,
827 pub range: LineRange,
828 pub language: Language,
829 pub text: String,
830 pub symbol_id: Option<SymbolId>,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
834pub struct Diagnostic {
835 pub severity: String,
836 pub message: String,
837 pub file_range: Option<FileRange>,
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
841pub struct TestTarget {
842 pub id: String,
843 pub name: String,
844 pub file_id: FileId,
845 pub range: Option<LineRange>,
846 pub command: Option<String>,
847 pub confidence: Confidence,
848 pub reason: String,
849 #[serde(default)]
850 pub evidence_refs: Vec<String>,
851 #[serde(default)]
852 pub score_breakdown: Vec<ScoreComponent>,
853}
854
855impl TestTarget {
856 pub fn reconcile_score_breakdown(&mut self) {
857 if self.evidence_refs.is_empty() {
858 self.evidence_refs.push(format!("test:{}", self.id));
859 }
860 reconcile_score_breakdown(
861 self.confidence.score(),
862 &mut self.score_breakdown,
863 "test_confidence",
864 self.evidence_refs.clone(),
865 &self.reason,
866 );
867 }
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
871pub struct BuildTarget {
872 pub id: String,
873 pub name: String,
874 pub command: String,
875 pub files: Vec<FileId>,
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
879pub struct RuntimeSignal {
880 pub id: String,
881 pub kind: String,
882 pub message: String,
883 pub file_range: Option<FileRange>,
884 pub occurred_at: Option<DateTime<Utc>>,
885 pub confidence: Confidence,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
889pub struct Owner {
890 pub name: String,
891 pub email: Option<String>,
892}
893
894#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
895#[serde(rename_all = "snake_case")]
896pub enum GitChangeKind {
897 Added,
898 Modified,
899 Deleted,
900 Renamed,
901 Copied,
902 TypeChanged,
903 Unknown,
904}
905
906#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
907#[serde(rename_all = "snake_case")]
908pub enum ReviewerRole {
909 Reviewer,
910 Approver,
911 Author,
912 Committer,
913 Owner,
914}
915
916#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
917pub struct GitCommitRecord {
918 pub id: GitCommitId,
919 #[serde(default)]
920 pub parent_ids: Vec<GitCommitId>,
921 pub author: Owner,
922 pub committer: Option<Owner>,
923 pub authored_at: DateTime<Utc>,
924 pub committed_at: DateTime<Utc>,
925 pub summary: String,
926 pub message: String,
927 pub file_count: usize,
928}
929
930#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
931pub struct GitFileTouch {
932 pub id: HistoryRecordId,
933 pub commit_id: GitCommitId,
934 pub path: PathBuf,
935 pub previous_path: Option<PathBuf>,
936 pub change_kind: GitChangeKind,
937 pub additions: Option<u32>,
938 pub deletions: Option<u32>,
939 pub touched_at: DateTime<Utc>,
940}
941
942#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
943pub struct GitSymbolTouch {
944 pub id: HistoryRecordId,
945 pub commit_id: GitCommitId,
946 pub symbol_id: Option<SymbolId>,
947 pub qualified_name: String,
948 pub file_path: PathBuf,
949 pub change_kind: GitChangeKind,
950 #[serde(default)]
951 pub line_ranges: Vec<LineRange>,
952 #[serde(default = "default_history_confidence")]
953 pub confidence: Confidence,
954 #[serde(default)]
955 pub uncertainty: Vec<String>,
956 pub touched_at: DateTime<Utc>,
957}
958
959fn default_history_confidence() -> Confidence {
960 Confidence::Low
961}
962
963#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
964pub struct ProvenanceTouch {
965 pub commit: GitCommitRecord,
966 pub path: PathBuf,
967 pub previous_path: Option<PathBuf>,
968 pub symbol_id: Option<SymbolId>,
969 pub qualified_name: Option<String>,
970 pub change_kind: GitChangeKind,
971 pub line_ranges: Vec<LineRange>,
972 pub confidence: Confidence,
973 pub uncertainty: Vec<String>,
974}
975
976#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
977pub struct FileProvenance {
978 pub path: PathBuf,
979 pub first_seen: Option<ProvenanceTouch>,
980 pub last_touched: Option<ProvenanceTouch>,
981 pub recent_touches: Vec<ProvenanceTouch>,
982 pub confidence: Confidence,
983 pub truncated: bool,
984 pub uncertainty: Vec<String>,
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
988#[serde(rename_all = "snake_case")]
989pub enum OwnershipSourceType {
990 Codeowners,
991 GitHistory,
992 RepoMemory,
993}
994
995#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
996pub struct OwnershipEvidence {
997 pub source_type: OwnershipSourceType,
998 pub owner: Owner,
999 pub source: String,
1000 pub message: String,
1001 pub confidence: Confidence,
1002 pub observed_at: Option<DateTime<Utc>>,
1003 pub stale: bool,
1004}
1005
1006#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1007pub struct OwnershipConfidenceBreakdown {
1008 pub codeowners: f32,
1009 pub git_history: f32,
1010 pub memory: f32,
1011 pub freshness: f32,
1012 pub ambiguity_penalty: f32,
1013 pub final_score: f32,
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1017pub struct OwnerSuggestion {
1018 pub owner: Owner,
1019 pub rationale: String,
1020 pub confidence: Confidence,
1021 pub score: f32,
1022 pub source_types: Vec<OwnershipSourceType>,
1023 pub stale: bool,
1024 pub evidence: Vec<OwnershipEvidence>,
1025 pub confidence_breakdown: OwnershipConfidenceBreakdown,
1026}
1027
1028#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1029pub struct OwnershipReport {
1030 pub path: PathBuf,
1031 #[serde(default)]
1032 pub components: Vec<PolicyComponentMatch>,
1033 pub generated_at: DateTime<Utc>,
1034 pub owners: Vec<OwnerSuggestion>,
1035 #[serde(default)]
1036 pub uncertainty: Vec<String>,
1037}
1038
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1040#[serde(rename_all = "snake_case")]
1041pub enum ReviewerSignalSourceType {
1042 ReviewEvidence,
1043 Ownership,
1044 GitAuthor,
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1048#[serde(rename_all = "snake_case")]
1049pub enum ReviewerAvailability {
1050 ActualReviewEvidence,
1051 InferredFromOwnershipAndAuthors,
1052 InferredFromOwnership,
1053 InferredFromAuthors,
1054 Unavailable,
1055}
1056
1057#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1058pub struct ReviewerSignal {
1059 pub source_type: ReviewerSignalSourceType,
1060 pub reviewer: Owner,
1061 pub source: String,
1062 pub role: Option<ReviewerRole>,
1063 pub message: String,
1064 pub confidence: Confidence,
1065 pub observed_at: Option<DateTime<Utc>>,
1066 pub stale: bool,
1067 pub actual_review_evidence: bool,
1068}
1069
1070#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1071pub struct ReviewerConfidenceBreakdown {
1072 pub review_evidence: f32,
1073 pub ownership: f32,
1074 pub author_history: f32,
1075 pub freshness: f32,
1076 pub ambiguity_penalty: f32,
1077 pub final_score: f32,
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1081pub struct ReviewerSuggestion {
1082 pub reviewer: Owner,
1083 pub rationale: String,
1084 pub confidence: Confidence,
1085 pub score: f32,
1086 pub availability: ReviewerAvailability,
1087 pub source_types: Vec<ReviewerSignalSourceType>,
1088 pub inferred_from_authors: bool,
1089 pub actual_review_evidence: bool,
1090 pub stale: bool,
1091 pub signals: Vec<ReviewerSignal>,
1092 pub confidence_breakdown: ReviewerConfidenceBreakdown,
1093}
1094
1095#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1096pub struct ReviewerSuggestionReport {
1097 pub path: PathBuf,
1098 pub generated_at: DateTime<Utc>,
1099 pub availability: ReviewerAvailability,
1100 pub suggestions: Vec<ReviewerSuggestion>,
1101 #[serde(default)]
1102 pub uncertainty: Vec<String>,
1103}
1104
1105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1106pub struct SymbolProvenance {
1107 pub symbol_id: SymbolId,
1108 pub qualified_name: String,
1109 pub file_path: PathBuf,
1110 pub range: Option<LineRange>,
1111 pub first_seen: Option<ProvenanceTouch>,
1112 pub last_touched: Option<ProvenanceTouch>,
1113 pub recent_touches: Vec<ProvenanceTouch>,
1114 pub confidence: Confidence,
1115 pub truncated: bool,
1116 pub uncertainty: Vec<String>,
1117}
1118
1119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1120pub struct GitCochangeEdge {
1121 pub id: HistoryRecordId,
1122 pub path: PathBuf,
1123 pub cochanged_path: PathBuf,
1124 pub commit_count: usize,
1125 pub recency_weight: f32,
1126 pub last_changed_at: Option<DateTime<Utc>>,
1127 #[serde(default)]
1128 pub sample_commits: Vec<GitCommitId>,
1129 pub test_corun: bool,
1130}
1131
1132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1133pub struct ReviewerEvidence {
1134 pub id: HistoryRecordId,
1135 pub commit_id: Option<GitCommitId>,
1136 pub path: Option<PathBuf>,
1137 pub reviewer: Owner,
1138 pub role: ReviewerRole,
1139 pub observed_at: DateTime<Utc>,
1140 pub source: String,
1141 pub confidence: Confidence,
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1145pub struct HistorySnapshot {
1146 pub schema_version: u32,
1147 #[serde(default)]
1148 pub commits: Vec<GitCommitRecord>,
1149 #[serde(default)]
1150 pub file_touches: Vec<GitFileTouch>,
1151 #[serde(default)]
1152 pub symbol_touches: Vec<GitSymbolTouch>,
1153 #[serde(default)]
1154 pub cochange_edges: Vec<GitCochangeEdge>,
1155 #[serde(default)]
1156 pub reviewer_evidence: Vec<ReviewerEvidence>,
1157}
1158
1159impl HistorySnapshot {
1160 pub fn empty() -> Self {
1161 Self {
1162 schema_version: HISTORY_SCHEMA_VERSION,
1163 commits: Vec::new(),
1164 file_touches: Vec::new(),
1165 symbol_touches: Vec::new(),
1166 cochange_edges: Vec::new(),
1167 reviewer_evidence: Vec::new(),
1168 }
1169 }
1170}
1171
1172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1173pub struct HistorySummary {
1174 pub path: PathBuf,
1175 pub recent_commits: Vec<GitCommitRecord>,
1176 pub file_touches: Vec<GitFileTouch>,
1177 pub symbol_touches: Vec<GitSymbolTouch>,
1178 pub cochange_neighbors: Vec<GitCochangeEdge>,
1179 pub reviewer_evidence: Vec<ReviewerEvidence>,
1180 pub truncated: bool,
1181 #[serde(default)]
1182 pub uncertainty: Vec<String>,
1183}
1184
1185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1186pub struct HistorySignalQuery {
1187 pub path: PathBuf,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub task: Option<String>,
1190 #[serde(default)]
1191 pub symbols: Vec<String>,
1192}
1193
1194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1195pub struct HistorySignalSummary {
1196 pub path: PathBuf,
1197 pub generated_at: DateTime<Utc>,
1198 #[serde(default)]
1199 pub components: Vec<ScoreComponent>,
1200 #[serde(default)]
1201 pub evidence_refs: Vec<String>,
1202 #[serde(default)]
1203 pub reasons: Vec<String>,
1204 pub similar_change_count: usize,
1205 pub distinct_author_count: usize,
1206 pub reviewer_count: usize,
1207 #[serde(default)]
1208 pub uncertainty: Vec<String>,
1209}
1210
1211impl HistorySignalSummary {
1212 pub fn empty(path: impl Into<PathBuf>) -> Self {
1213 Self {
1214 path: path.into(),
1215 generated_at: Utc::now(),
1216 components: Vec::new(),
1217 evidence_refs: Vec::new(),
1218 reasons: Vec::new(),
1219 similar_change_count: 0,
1220 distinct_author_count: 0,
1221 reviewer_count: 0,
1222 uncertainty: vec!["no bounded history signals were available for this path".into()],
1223 }
1224 }
1225}
1226
1227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1228pub struct SimilarChangeQuery {
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub task: Option<String>,
1231 #[serde(default)]
1232 pub paths: Vec<PathBuf>,
1233 #[serde(default)]
1234 pub symbols: Vec<String>,
1235}
1236
1237#[derive(
1238 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
1239)]
1240#[serde(rename_all = "snake_case")]
1241pub enum SimilarityEvidenceSource {
1242 TaskText,
1243 Path,
1244 Symbol,
1245 Churn,
1246 Cochange,
1247 CommitMetadata,
1248}
1249
1250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1251pub struct SimilarityEvidence {
1252 pub source_type: SimilarityEvidenceSource,
1253 pub score: f32,
1254 pub message: String,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1256 pub query: Option<String>,
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub path: Option<PathBuf>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub symbol: Option<String>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub commit_id: Option<GitCommitId>,
1263}
1264
1265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1266pub struct HistoricalChangeSummary {
1267 pub commit: GitCommitRecord,
1268 #[serde(default)]
1269 pub touched_paths: Vec<PathBuf>,
1270 #[serde(default)]
1271 pub touched_symbols: Vec<String>,
1272 #[serde(default)]
1273 pub cochange_paths: Vec<PathBuf>,
1274 pub churn_hotspot_score: f32,
1275}
1276
1277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1278pub struct SimilarChangeHit {
1279 pub change: HistoricalChangeSummary,
1280 pub score: f32,
1281 pub confidence: Confidence,
1282 pub evidence: Vec<SimilarityEvidence>,
1283 #[serde(default)]
1284 pub uncertainty: Vec<String>,
1285}
1286
1287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1288pub struct SimilarChangeReport {
1289 pub query: SimilarChangeQuery,
1290 pub generated_at: DateTime<Utc>,
1291 pub hits: Vec<SimilarChangeHit>,
1292 pub truncated: bool,
1293 #[serde(default)]
1294 pub uncertainty: Vec<String>,
1295}
1296
1297#[derive(
1298 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
1299)]
1300#[serde(rename_all = "snake_case")]
1301pub enum ChurnEntityKind {
1302 File,
1303 Module,
1304 Symbol,
1305}
1306
1307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1308pub struct ChurnStats {
1309 pub all_time: usize,
1310 pub last_30d: usize,
1311 pub last_90d: usize,
1312 pub recency_weighted: f32,
1313 pub touch_count: usize,
1314 pub hotspot_score: f32,
1315}
1316
1317impl ChurnStats {
1318 pub fn empty() -> Self {
1319 Self {
1320 all_time: 0,
1321 last_30d: 0,
1322 last_90d: 0,
1323 recency_weighted: 0.0,
1324 touch_count: 0,
1325 hotspot_score: 0.0,
1326 }
1327 }
1328}
1329
1330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1331pub struct ChurnSummary {
1332 pub entity_kind: ChurnEntityKind,
1333 pub key: String,
1334 pub path: Option<PathBuf>,
1335 pub symbol_id: Option<SymbolId>,
1336 pub qualified_name: Option<String>,
1337 pub generated_at: DateTime<Utc>,
1338 pub stats: ChurnStats,
1339 pub confidence: Confidence,
1340 #[serde(default)]
1341 pub uncertainty: Vec<String>,
1342}
1343
1344impl ChurnSummary {
1345 pub fn missing(entity_kind: ChurnEntityKind, key: impl Into<String>) -> Self {
1346 let key = key.into();
1347 Self {
1348 entity_kind,
1349 key: key.clone(),
1350 path: None,
1351 symbol_id: None,
1352 qualified_name: None,
1353 generated_at: Utc::now(),
1354 stats: ChurnStats::empty(),
1355 confidence: Confidence::Low,
1356 uncertainty: vec![format!(
1357 "no persisted churn summary is available for `{key}`"
1358 )],
1359 }
1360 }
1361}
1362
1363impl HistorySummary {
1364 pub fn empty(path: impl Into<PathBuf>) -> Self {
1365 Self {
1366 path: path.into(),
1367 recent_commits: Vec::new(),
1368 file_touches: Vec::new(),
1369 symbol_touches: Vec::new(),
1370 cochange_neighbors: Vec::new(),
1371 reviewer_evidence: Vec::new(),
1372 truncated: false,
1373 uncertainty: vec!["no persisted history evidence is available for this path".into()],
1374 }
1375 }
1376}
1377
1378#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1379pub struct ArchitectureComponent {
1380 pub id: String,
1381 pub name: String,
1382 pub paths: Vec<String>,
1383 pub evidence: Vec<Evidence>,
1384}
1385
1386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1387pub struct PolicyComponentMatch {
1388 pub component_id: String,
1389 pub matched_glob: String,
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1393pub struct ResolvedArchitectureNode {
1394 pub file_path: PathBuf,
1395 pub symbol_id: Option<SymbolId>,
1396 pub components: Vec<PolicyComponentMatch>,
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1400pub struct UnmappedPolicyTarget {
1401 pub file_path: PathBuf,
1402 pub symbol_id: Option<SymbolId>,
1403}
1404
1405#[derive(
1406 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
1407)]
1408#[serde(rename_all = "snake_case")]
1409pub enum EnforcedEdgeType {
1410 Imports,
1411 References,
1412 Calls,
1413}
1414
1415impl EnforcedEdgeType {
1416 pub fn graph_edge_type(self) -> GraphEdgeType {
1417 match self {
1418 Self::Imports => GraphEdgeType::Imports,
1419 Self::References => GraphEdgeType::References,
1420 Self::Calls => GraphEdgeType::Calls,
1421 }
1422 }
1423}
1424
1425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1426pub struct PolicyMatchEvidence {
1427 pub edge_id: String,
1428 pub edge_type: EnforcedEdgeType,
1429 pub source_node: String,
1430 pub target_node: String,
1431 pub source_path: PathBuf,
1432 pub target_path: PathBuf,
1433 pub confidence: Confidence,
1434 pub message: String,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1438pub struct PolicyViolation {
1439 pub rule_id: String,
1440 pub severity: String,
1441 pub source_component: String,
1442 pub target_component: String,
1443 pub source_path: PathBuf,
1444 pub target_path: PathBuf,
1445 pub edge_type: EnforcedEdgeType,
1446 pub evidence: PolicyMatchEvidence,
1447 pub message: String,
1448}
1449
1450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1451pub struct UnknownPolicyEdge {
1452 pub reason: String,
1453 pub evidence: PolicyMatchEvidence,
1454}
1455
1456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1457pub struct PolicyExemptionEvidence {
1458 pub exemption_id: String,
1459 pub rule_id: String,
1460 pub scope: String,
1461 pub source_path: PathBuf,
1462 pub target_path: PathBuf,
1463 pub evidence: PolicyMatchEvidence,
1464 pub reason: String,
1465}
1466
1467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1468pub struct PolicyViolationEvidenceRef {
1469 pub id: String,
1470 pub rule_id: String,
1471 pub severity: String,
1472 pub source_path: PathBuf,
1473 pub target_path: PathBuf,
1474 pub edge_type: EnforcedEdgeType,
1475}
1476
1477#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1478pub struct PolicySignalSummary {
1479 pub configured: bool,
1480 pub evaluated_edge_count: usize,
1481 pub allowed_edges: usize,
1482 pub violation_count: usize,
1483 pub public_api_violation_count: usize,
1484 pub exempted_violation_count: usize,
1485 pub unknown_edge_count: usize,
1486 pub evidence_refs: Vec<String>,
1487 pub violation_refs: Vec<PolicyViolationEvidenceRef>,
1488 pub uncertainty: Vec<String>,
1489}
1490
1491#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1492pub struct PublicApiBoundaryReport {
1493 pub configured: bool,
1494 pub evaluated_edge_count: usize,
1495 pub violation_count: usize,
1496 pub exempted_violation_count: usize,
1497 pub violations: Vec<PolicyViolation>,
1498 pub exemptions: Vec<PolicyExemptionEvidence>,
1499 pub uncertainty: Vec<String>,
1500}
1501
1502#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1503pub struct PolicyCheckReport {
1504 pub configured: bool,
1505 pub evaluated_edge_count: usize,
1506 pub allowed_edges: usize,
1507 pub violation_count: usize,
1508 #[serde(default)]
1509 pub public_api_violation_count: usize,
1510 #[serde(default)]
1511 pub exempted_violation_count: usize,
1512 pub unknown_edge_count: usize,
1513 pub unknown_sample_count: usize,
1514 pub unknown_edges_truncated: bool,
1515 pub violations: Vec<PolicyViolation>,
1516 #[serde(default)]
1517 pub exemptions: Vec<PolicyExemptionEvidence>,
1518 pub unknown_edges: Vec<UnknownPolicyEdge>,
1519 pub uncertainty: Vec<String>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1523pub struct IndexManifest {
1524 pub repository: Repository,
1525 pub file_count: usize,
1526 pub symbol_count: usize,
1527 pub chunk_count: usize,
1528 pub indexed_at: DateTime<Utc>,
1529 pub schema_version: u32,
1530 #[serde(default)]
1531 pub index_mode: IndexMode,
1532 #[serde(default)]
1533 pub phase_reports: Vec<IndexPhaseReport>,
1534 #[serde(default)]
1535 pub quality: IndexQuality,
1536}
1537
1538#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1539#[serde(rename_all = "snake_case")]
1540pub enum IndexMode {
1541 #[default]
1542 Full,
1543 Balanced,
1544 Fast,
1545 CrossProject,
1546}
1547
1548impl fmt::Display for IndexMode {
1549 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1550 let value = match self {
1551 Self::Full => "full",
1552 Self::Balanced => "balanced",
1553 Self::Fast => "fast",
1554 Self::CrossProject => "cross_project",
1555 };
1556 f.write_str(value)
1557 }
1558}
1559
1560#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1561pub struct IndexPhaseReport {
1562 pub phase: String,
1563 pub elapsed_ms: u64,
1564 pub scanned_files: usize,
1565 pub indexed_files: usize,
1566 pub nodes_added: usize,
1567 pub edges_added: usize,
1568 pub skipped: usize,
1569 pub warnings: Vec<String>,
1570}
1571
1572#[derive(
1573 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
1574)]
1575#[serde(rename_all = "snake_case")]
1576pub enum SkipReason {
1577 Ignored,
1578 Denied,
1579 Hidden,
1580 UnsupportedLanguage,
1581 Binary,
1582 TooLarge,
1583 Generated,
1584 Vendor,
1585 FastMode,
1586 SecretPolicy,
1587 SymlinkPolicy,
1588 Error,
1589}
1590
1591#[derive(
1592 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
1593)]
1594#[serde(rename_all = "snake_case")]
1595pub enum SkipSource {
1596 SecurityPolicy,
1597 HiddenPolicy,
1598 ConfigExclude,
1599 GitIgnore,
1600 OkIgnore,
1601 Detector,
1602 FastMode,
1603 SizeLimit,
1604 SymlinkPolicy,
1605 LanguageSupport,
1606 Filesystem,
1607}
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1610pub struct SkippedPath {
1611 pub path: PathBuf,
1612 pub reason: SkipReason,
1613 pub source: SkipSource,
1614 pub safe_to_show: bool,
1615}
1616
1617#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1618pub struct IndexQuality {
1619 #[serde(default)]
1620 pub index_mode: IndexMode,
1621 #[serde(default)]
1622 pub phase_reports: Vec<IndexPhaseReport>,
1623 pub scip_enabled: bool,
1624 pub scip_mode: String,
1625 pub scip_indexes_imported: usize,
1626 pub scip_symbols: usize,
1627 pub scip_occurrences: usize,
1628 pub scip_exact_references: usize,
1629 pub test_count: usize,
1630 pub import_count: usize,
1631 #[serde(default)]
1632 pub build_systems: Vec<String>,
1633 #[serde(default)]
1634 pub codeql_databases: usize,
1635 #[serde(default)]
1636 pub coverage_reports: usize,
1637 #[serde(default)]
1638 pub junit_reports: usize,
1639 #[serde(default)]
1640 pub static_analysis_facts: usize,
1641 #[serde(default)]
1642 pub runtime_analysis_facts: usize,
1643 #[serde(default)]
1644 pub git_history_facts: usize,
1645 #[serde(default)]
1646 pub architecture_facts: usize,
1647 #[serde(default)]
1648 pub semantic_provider_notes: Vec<String>,
1649 #[serde(default)]
1650 pub skip_counts: BTreeMap<SkipReason, usize>,
1651 #[serde(default)]
1652 pub skipped_paths: Vec<SkippedPath>,
1653 pub quality_notes: Vec<String>,
1654}
1655
1656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1657pub struct EvidenceGraphSchema {
1658 pub version: String,
1659 pub node_types: Vec<NodeTypeSpec>,
1660 pub edge_types: Vec<EdgeTypeSpec>,
1661 pub property_specs: Vec<PropertySpec>,
1662 pub feature_flags: Vec<String>,
1663 #[serde(default)]
1664 pub evidence_source_types: Vec<String>,
1665 #[serde(default)]
1666 pub query_features: Vec<String>,
1667 #[serde(default)]
1668 pub optional_evidence: Vec<OptionalEvidenceSpec>,
1669 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1670 pub caveats: Vec<String>,
1671 #[serde(default, skip_serializing_if = "Option::is_none")]
1672 pub indexed_at: Option<String>,
1673}
1674
1675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1676pub struct NodeTypeSpec {
1677 pub name: String,
1678 pub stable: bool,
1679 pub description: String,
1680 pub required_fields: Vec<String>,
1681 pub optional_fields: Vec<String>,
1682 #[serde(skip_serializing_if = "Option::is_none")]
1683 pub count: Option<usize>,
1684 #[serde(skip_serializing_if = "Option::is_none")]
1685 pub evidence_available: Option<bool>,
1686 #[serde(skip_serializing_if = "Option::is_none")]
1687 pub freshness: Option<String>,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1691pub struct EdgeTypeSpec {
1692 pub name: String,
1693 pub stable: bool,
1694 pub description: String,
1695 pub source_types: Vec<String>,
1696 pub target_types: Vec<String>,
1697 pub required_evidence: Vec<String>,
1698 #[serde(skip_serializing_if = "Option::is_none")]
1699 pub count: Option<usize>,
1700 #[serde(skip_serializing_if = "Option::is_none")]
1701 pub evidence_available: Option<bool>,
1702 #[serde(skip_serializing_if = "Option::is_none")]
1703 pub freshness: Option<String>,
1704}
1705
1706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1707pub struct PropertySpec {
1708 pub name: String,
1709 pub type_name: String,
1710 pub description: String,
1711}
1712
1713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1714pub struct OptionalEvidenceSpec {
1715 pub name: String,
1716 pub available: bool,
1717 pub status: String,
1718 pub evidence_count: usize,
1719 pub description: String,
1720 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1721 pub caveats: Vec<String>,
1722}
1723
1724#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1725#[serde(rename_all = "snake_case")]
1726pub enum GraphNodeType {
1727 File,
1728 Directory,
1729 Module,
1730 Package,
1731 Class,
1732 Trait,
1733 Interface,
1734 Function,
1735 Method,
1736 Field,
1737 Endpoint,
1738 DatabaseTable,
1739 Collection,
1740 Queue,
1741 Topic,
1742 ConfigKey,
1743 Test,
1744 BuildTarget,
1745 RuntimeError,
1746 Ticket,
1747 PullRequest,
1748 Resource,
1749 ArchitectureComponent,
1750}
1751
1752#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1753#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1754pub enum GraphEdgeType {
1755 Contains,
1756 Defines,
1757 References,
1758 Calls,
1759 Implements,
1760 Extends,
1761 Imports,
1762 DependsOn,
1763 ExposesEndpoint,
1764 CallsEndpoint,
1765 ReadsConfig,
1766 WritesConfig,
1767 ReadsTable,
1768 WritesTable,
1769 PublishesEvent,
1770 ConsumesEvent,
1771 Tests,
1772 TestCovers,
1773 Validates,
1774 OwnedBy,
1775 ChangedBy,
1776 FailedIn,
1777 BelongsTo,
1778 MentionedIn,
1779 RelatedToTicket,
1780 SimilarTo,
1781 SemanticallyRelated,
1782}
1783
1784#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1785pub struct GraphNode {
1786 pub id: NodeId,
1787 pub node_type: GraphNodeType,
1788 pub label: String,
1789 pub file_id: Option<FileId>,
1790 pub symbol_id: Option<SymbolId>,
1791 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1792 pub properties: BTreeMap<String, serde_json::Value>,
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1794 pub schema_version: Option<String>,
1795 #[serde(default, skip_serializing_if = "Option::is_none")]
1796 pub source_pass: Option<String>,
1797 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub index_mode: Option<String>,
1799 #[serde(default, skip_serializing_if = "Option::is_none")]
1800 pub extractor_version: Option<String>,
1801 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1802 pub ambiguity: Vec<String>,
1803 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1804 pub quality_notes: Vec<String>,
1805}
1806
1807impl Default for GraphNode {
1808 fn default() -> Self {
1809 Self {
1810 id: NodeId::new(""),
1811 node_type: GraphNodeType::File,
1812 label: String::new(),
1813 file_id: None,
1814 symbol_id: None,
1815 properties: BTreeMap::new(),
1816 schema_version: None,
1817 source_pass: None,
1818 index_mode: None,
1819 extractor_version: None,
1820 ambiguity: vec![],
1821 quality_notes: vec![],
1822 }
1823 }
1824}
1825
1826#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1827pub struct GraphEdge {
1828 pub id: EdgeId,
1829 pub from: NodeId,
1830 pub to: NodeId,
1831 pub edge_type: GraphEdgeType,
1832 pub evidence: Evidence,
1833 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1834 pub properties: BTreeMap<String, serde_json::Value>,
1835 #[serde(default, skip_serializing_if = "Option::is_none")]
1836 pub schema_version: Option<String>,
1837 #[serde(default, skip_serializing_if = "Option::is_none")]
1838 pub source_pass: Option<String>,
1839 #[serde(default, skip_serializing_if = "Option::is_none")]
1840 pub index_mode: Option<String>,
1841 #[serde(default, skip_serializing_if = "Option::is_none")]
1842 pub extractor_version: Option<String>,
1843 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1844 pub ambiguity: Vec<String>,
1845 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1846 pub quality_notes: Vec<String>,
1847}
1848
1849impl Default for GraphEdge {
1850 fn default() -> Self {
1851 Self {
1852 id: EdgeId::new(""),
1853 from: NodeId::new(""),
1854 to: NodeId::new(""),
1855 edge_type: GraphEdgeType::References,
1856 evidence: Evidence::default(),
1857 properties: BTreeMap::new(),
1858 schema_version: None,
1859 source_pass: None,
1860 index_mode: None,
1861 extractor_version: None,
1862 ambiguity: vec![],
1863 quality_notes: vec![],
1864 }
1865 }
1866}
1867
1868#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1869pub struct SearchResult {
1870 pub path: PathBuf,
1871 pub line_range: Option<LineRange>,
1872 pub snippet: String,
1873 pub symbol: Option<Symbol>,
1874 pub score: f32,
1875 pub match_reason: String,
1876 pub evidence: Vec<String>,
1877 #[serde(default)]
1878 pub evidence_refs: Vec<String>,
1879 pub confidence: f32,
1880 #[serde(default)]
1881 pub score_breakdown: Vec<ScoreComponent>,
1882}
1883
1884impl SearchResult {
1885 pub fn derived_evidence_ids(&self) -> Vec<String> {
1886 if !self.evidence_refs.is_empty() {
1887 return self.evidence_refs.clone();
1888 }
1889 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len())
1890 }
1891
1892 pub fn reconcile_score_breakdown(&mut self) {
1893 if self.evidence_refs.is_empty() {
1894 self.evidence_refs =
1895 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len());
1896 }
1897 reconcile_score_breakdown(
1898 self.score,
1899 &mut self.score_breakdown,
1900 "search_score",
1901 self.evidence_refs.clone(),
1902 &self.match_reason,
1903 );
1904 }
1905
1906 pub fn add_score_component(&mut self, component: ScoreComponent) {
1907 self.score_breakdown.push(component);
1908 }
1909}
1910
1911pub fn search_result_evidence_ids(
1912 path: &Path,
1913 line_range: &Option<LineRange>,
1914 evidence_len: usize,
1915) -> Vec<String> {
1916 let range = line_range
1917 .as_ref()
1918 .map(|range| format!("{}-{}", range.start, range.end))
1919 .unwrap_or_else(|| "unknown".into());
1920 let count = evidence_len.max(1);
1921 (0..count)
1922 .map(|index| format!("search:{}:{range}:{index}", path.display()))
1923 .collect()
1924}
1925
1926#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1927pub struct EntityLink {
1928 pub kind: String,
1929 pub value: String,
1930 pub file_range: Option<FileRange>,
1931 pub confidence: Confidence,
1932}
1933
1934#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1935pub struct MemoryFact {
1936 pub id: MemoryFactId,
1937 pub text: String,
1938 pub source: String,
1939 pub confidence: Confidence,
1940 pub entities: Vec<EntityLink>,
1941 pub created_at: DateTime<Utc>,
1942}
1943
1944#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1945pub struct MemorySearchResult {
1946 pub fact: MemoryFact,
1947 pub score: f32,
1948 pub match_reason: String,
1949 pub evidence: Vec<String>,
1950}
1951
1952#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1953pub struct ContextHandle {
1954 pub id: ContextHandleId,
1955 pub kind: String,
1956 pub summary: String,
1957 pub file_range: Option<FileRange>,
1958 pub entities: Vec<EntityLink>,
1959 pub original_tokens_estimate: usize,
1960 pub compressed_tokens_estimate: usize,
1961}
1962
1963#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1964pub struct CompressedContextPack {
1965 pub task: String,
1966 pub summary: String,
1967 pub handles: Vec<ContextHandle>,
1968 pub original_tokens_estimate: usize,
1969 pub compressed_tokens_estimate: usize,
1970 pub compression_ratio: f32,
1971 pub evidence: Vec<Evidence>,
1972}
1973
1974#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1975pub struct RiskReport {
1976 pub level: String,
1977 pub score: f32,
1978 pub reasons: Vec<String>,
1979}
1980
1981#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1982pub struct BoundaryFileRule {
1983 pub path: PathBuf,
1984 pub reason: String,
1985 #[serde(default)]
1986 pub evidence_refs: Vec<String>,
1987 #[serde(default)]
1988 pub symbols: Vec<String>,
1989}
1990
1991#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1992pub struct BoundaryForbiddenRule {
1993 pub pattern: String,
1994 pub reason: String,
1995 #[serde(default)]
1996 pub evidence_refs: Vec<String>,
1997}
1998
1999#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
2000pub struct BoundaryExpansionRequirement {
2001 pub reason: String,
2002 #[serde(default)]
2003 pub required_evidence_refs: Vec<String>,
2004}
2005
2006#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
2007pub struct BoundarySignalHooks {
2008 #[serde(default)]
2009 pub architecture_components: Vec<String>,
2010 #[serde(default)]
2011 pub ownership_sources: Vec<String>,
2012 #[serde(default)]
2013 pub cochange_sources: Vec<String>,
2014}
2015
2016#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
2017pub struct ChangeBoundary {
2018 pub allowed_files: Vec<PathBuf>,
2019 pub caution_files: Vec<PathBuf>,
2020 pub forbidden_files: Vec<PathBuf>,
2021 #[serde(default)]
2022 pub evidence_refs: Vec<String>,
2023 #[serde(default)]
2024 pub allowed_symbols: Vec<String>,
2025 #[serde(default)]
2026 pub allowed_rules: Vec<BoundaryFileRule>,
2027 #[serde(default)]
2028 pub caution_rules: Vec<BoundaryFileRule>,
2029 #[serde(default)]
2030 pub forbidden_rules: Vec<BoundaryForbiddenRule>,
2031 #[serde(default)]
2032 pub expansion_requirements: Vec<BoundaryExpansionRequirement>,
2033 #[serde(default)]
2034 pub signal_hooks: BoundarySignalHooks,
2035}
2036
2037#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2038pub struct ValidationPlan {
2039 pub commands: Vec<String>,
2040 pub tests: Vec<TestTarget>,
2041 pub requires_approval: bool,
2042 pub evidence: Vec<Evidence>,
2043}
2044
2045#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2046pub struct ImpactReport {
2047 pub target: String,
2048 pub direct_impacts: Vec<SearchResult>,
2049 pub indirect_impacts: Vec<SearchResult>,
2050 pub risk_report: RiskReport,
2051 pub evidence: Vec<Evidence>,
2052 #[serde(default, skip_serializing_if = "Option::is_none")]
2053 pub architecture_policy: Option<PolicyCheckReport>,
2054 #[serde(default)]
2055 pub score_breakdown: Vec<ScoreComponent>,
2056}
2057
2058impl ImpactReport {
2059 pub fn reconcile_score_breakdown(&mut self) {
2060 reconcile_score_breakdown(
2061 self.risk_report.score,
2062 &mut self.score_breakdown,
2063 "impact_risk",
2064 self.evidence
2065 .iter()
2066 .map(|evidence| evidence.id.0.clone())
2067 .collect(),
2068 "impact risk score",
2069 );
2070 for result in &mut self.direct_impacts {
2071 result.reconcile_score_breakdown();
2072 }
2073 for result in &mut self.indirect_impacts {
2074 result.reconcile_score_breakdown();
2075 }
2076 }
2077}
2078
2079#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2080pub struct ContextPack {
2081 pub task: String,
2082 pub intent: String,
2083 pub primary_files: Vec<SearchResult>,
2084 pub primary_symbols: Vec<Symbol>,
2085 pub supporting_files: Vec<SearchResult>,
2086 pub dependency_edges: Vec<GraphEdge>,
2087 pub runtime_signals: Vec<RuntimeSignal>,
2088 pub test_candidates: Vec<TestTarget>,
2089 pub risk_report: RiskReport,
2090 pub recommended_change_boundary: ChangeBoundary,
2091 pub validation_plan: ValidationPlan,
2092 pub evidence: Vec<Evidence>,
2093 #[serde(default)]
2094 pub negative_evidence: Vec<NegativeEvidence>,
2095 #[serde(default, skip_serializing_if = "Option::is_none")]
2096 pub architecture_policy: Option<PolicyCheckReport>,
2097 pub confidence_summary: String,
2098 #[serde(default)]
2099 pub confidence_breakdown: ConfidenceBreakdown,
2100}
2101
2102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2103pub struct ToolCallRecommendation {
2104 pub tool: String,
2105 pub purpose: String,
2106 pub arguments: serde_json::Value,
2107}
2108
2109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2110pub struct PlanReport {
2111 pub task: String,
2112 pub summary: String,
2113 pub primary_context: Vec<SearchResult>,
2114 pub relevant_symbols: Vec<Symbol>,
2115 pub impact: ImpactReport,
2116 pub validation: Vec<TestTarget>,
2117 pub risk: RiskReport,
2118 pub recommended_change_boundary: ChangeBoundary,
2119 pub recommended_next_steps: Vec<String>,
2120 pub tool_calls: Vec<ToolCallRecommendation>,
2121 pub memory_facts: Vec<MemorySearchResult>,
2122 #[serde(default)]
2123 pub runtime_signals: Vec<RuntimeSignal>,
2124 #[serde(default, skip_serializing_if = "Option::is_none")]
2125 pub architecture_policy: Option<PolicyCheckReport>,
2126 pub evidence: Vec<Evidence>,
2127 #[serde(default)]
2128 pub evidence_by_section: BTreeMap<String, Vec<String>>,
2129 #[serde(default)]
2130 pub negative_evidence: Vec<NegativeEvidence>,
2131 pub confidence_summary: String,
2132 #[serde(default)]
2133 pub confidence_breakdown: ConfidenceBreakdown,
2134 #[serde(default)]
2135 pub score_breakdown: Vec<ScoreComponent>,
2136 #[serde(default)]
2137 pub evidence_quality: EvidenceQuality,
2138}
2139
2140impl PlanReport {
2141 pub fn reconcile_score_breakdown(&mut self) {
2142 reconcile_score_breakdown(
2143 self.risk.score,
2144 &mut self.score_breakdown,
2145 "plan_risk",
2146 self.evidence
2147 .iter()
2148 .map(|evidence| evidence.id.0.clone())
2149 .collect(),
2150 "plan risk score",
2151 );
2152 for result in &mut self.primary_context {
2153 result.reconcile_score_breakdown();
2154 }
2155 self.impact.reconcile_score_breakdown();
2156 for test in &mut self.validation {
2157 test.reconcile_score_breakdown();
2158 }
2159 }
2160}
2161
2162#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
2163pub struct PatchPlan {
2164 pub id: PatchId,
2165 pub task: String,
2166 pub allowed_files: Vec<PathBuf>,
2167 pub caution_files: Vec<PathBuf>,
2168 pub forbidden_files: Vec<PathBuf>,
2169 pub change_steps: Vec<String>,
2170 pub risks: Vec<String>,
2171 pub assumptions: Vec<String>,
2172 pub tests: Vec<TestTarget>,
2173 pub rollback_notes: Vec<String>,
2174 pub unified_diff: Option<String>,
2175 pub requires_approval: bool,
2176 pub evidence: Vec<Evidence>,
2177}
2178
2179#[cfg(test)]
2180mod tests {
2181 use super::{
2182 count_resolution_notes, reconcile_score_breakdown, score_component_total, Confidence,
2183 ConfidenceBreakdown, ConfidenceSignalInput, EdgeId, Evidence, EvidenceSourceType,
2184 FileRange, GitChangeKind, GitCommitId, GitCommitRecord, GitFileTouch, GitSymbolTouch,
2185 GraphEdge, GraphEdgeType, GraphNode, GraphNodeType, HistoryRecordId, HistorySnapshot,
2186 HistorySummary, IndexQuality, LineRange, NodeId, Owner, ScoreComponent, SymbolId,
2187 HISTORY_SCHEMA_VERSION,
2188 };
2189 use chrono::{TimeZone, Utc};
2190 use std::collections::BTreeMap;
2191
2192 #[test]
2193 fn quality_counts_only_import_resolver_notes() {
2194 let quality = IndexQuality {
2195 quality_notes: vec![
2196 "import resolver caveat in src/lib.rs for `crate::missing`: unresolved import"
2197 .into(),
2198 "symbol registry unresolved `documentation_word` in chunk abc".into(),
2199 "ambiguous wording in a non-resolver diagnostic".into(),
2200 ],
2201 ..Default::default()
2202 };
2203
2204 assert_eq!(
2205 count_resolution_notes(&quality, "import resolver caveat", "unresolved import"),
2206 1
2207 );
2208 assert_eq!(
2209 count_resolution_notes(&quality, "import resolver caveat", "ambiguous import"),
2210 0
2211 );
2212 }
2213
2214 #[test]
2215 fn reconciliation_adds_delta_to_match_surfaced_score() {
2216 let mut components = vec![ScoreComponent::single(
2217 "base",
2218 0.4,
2219 vec!["ev:base".into()],
2220 "base signal",
2221 )];
2222
2223 reconcile_score_breakdown(
2224 0.65,
2225 &mut components,
2226 "fallback",
2227 vec!["ev:adjust".into()],
2228 "test score",
2229 );
2230
2231 assert_eq!(components.len(), 2);
2232 assert!((score_component_total(&components) - 0.65).abs() < 0.001);
2233 assert_eq!(components[1].signal, "score_reconciliation");
2234 }
2235
2236 #[test]
2237 fn reconciliation_creates_fallback_for_empty_components() {
2238 let mut components = Vec::new();
2239
2240 reconcile_score_breakdown(
2241 0.85,
2242 &mut components,
2243 "confidence",
2244 vec!["test:id".into()],
2245 "test confidence",
2246 );
2247
2248 assert_eq!(components.len(), 1);
2249 assert_eq!(components[0].signal, "confidence");
2250 assert!((score_component_total(&components) - 0.85).abs() < 0.001);
2251 }
2252
2253 #[test]
2254 fn confidence_breakdown_is_stable_for_same_signals() {
2255 let input = ConfidenceSignalInput {
2256 primary_file_count: 2,
2257 evidence_count: 8,
2258 exact_reference_count: 2,
2259 validation_count: 2,
2260 validation_with_command_count: 1,
2261 negative_evidence_count: 0,
2262 allowed_file_count: 2,
2263 runtime_signal_count: 1,
2264 };
2265
2266 let first = ConfidenceBreakdown::from_signals(input);
2267 let second = ConfidenceBreakdown::from_signals(input);
2268
2269 assert_eq!(first.overall_enum, second.overall_enum);
2270 assert_eq!(first.overall_score, second.overall_score);
2271 assert_eq!(first.components, second.components);
2272 assert!(first.caveats.is_empty());
2273 assert!(first.blockers.is_empty());
2274 }
2275
2276 #[test]
2277 fn confidence_drops_without_exact_tests_or_runtime() {
2278 let grounded = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
2279 primary_file_count: 1,
2280 evidence_count: 6,
2281 exact_reference_count: 1,
2282 validation_count: 1,
2283 validation_with_command_count: 1,
2284 negative_evidence_count: 0,
2285 allowed_file_count: 1,
2286 runtime_signal_count: 1,
2287 });
2288 let thin = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
2289 primary_file_count: 1,
2290 evidence_count: 6,
2291 exact_reference_count: 0,
2292 validation_count: 0,
2293 validation_with_command_count: 0,
2294 negative_evidence_count: 0,
2295 allowed_file_count: 1,
2296 runtime_signal_count: 0,
2297 });
2298
2299 assert!(thin.overall_score < grounded.overall_score);
2300 assert_eq!(thin.overall_enum, Confidence::Medium);
2301 assert!(thin
2302 .caveats
2303 .iter()
2304 .any(|caveat| caveat.contains("exact symbol/reference")));
2305 assert!(thin
2306 .caveats
2307 .iter()
2308 .any(|caveat| caveat.contains("no validation")));
2309 assert!(thin
2310 .caveats
2311 .iter()
2312 .any(|caveat| caveat.contains("runtime corroboration")));
2313 }
2314
2315 #[test]
2316 fn negative_evidence_prevents_false_high_confidence() {
2317 let breakdown = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
2318 primary_file_count: 3,
2319 evidence_count: 12,
2320 exact_reference_count: 3,
2321 validation_count: 3,
2322 validation_with_command_count: 3,
2323 negative_evidence_count: 1,
2324 allowed_file_count: 3,
2325 runtime_signal_count: 1,
2326 });
2327
2328 assert!(breakdown.overall_score <= 0.60);
2329 assert_ne!(breakdown.overall_enum, Confidence::High);
2330 assert!(!breakdown.blockers.is_empty());
2331 }
2332
2333 #[test]
2334 fn history_snapshot_round_trips_with_versioned_records() {
2335 let committed_at = Utc.with_ymd_and_hms(2026, 6, 1, 12, 0, 0).unwrap();
2336 let commit = GitCommitRecord {
2337 id: GitCommitId::new("abc123"),
2338 parent_ids: vec![GitCommitId::new("parent123")],
2339 author: Owner {
2340 name: "Ada".into(),
2341 email: Some("ada@example.com".into()),
2342 },
2343 committer: None,
2344 authored_at: committed_at,
2345 committed_at,
2346 summary: "Add typed history".into(),
2347 message: "Add typed history\n\nPersist first-class records.".into(),
2348 file_count: 1,
2349 };
2350 let touch = GitFileTouch {
2351 id: HistoryRecordId::new("touch-1"),
2352 commit_id: commit.id.clone(),
2353 path: "src/history.rs".into(),
2354 previous_path: None,
2355 change_kind: GitChangeKind::Added,
2356 additions: Some(42),
2357 deletions: Some(0),
2358 touched_at: committed_at,
2359 };
2360 let snapshot = HistorySnapshot {
2361 schema_version: HISTORY_SCHEMA_VERSION,
2362 commits: vec![commit],
2363 file_touches: vec![touch],
2364 symbol_touches: Vec::new(),
2365 cochange_edges: Vec::new(),
2366 reviewer_evidence: Vec::new(),
2367 };
2368
2369 let json = serde_json::to_string(&snapshot).unwrap();
2370 let decoded: HistorySnapshot = serde_json::from_str(&json).unwrap();
2371
2372 assert_eq!(decoded, snapshot);
2373 assert_eq!(
2374 HistorySnapshot::empty().schema_version,
2375 HISTORY_SCHEMA_VERSION
2376 );
2377 }
2378
2379 #[test]
2380 fn empty_history_summary_exposes_uncertainty() {
2381 let summary = HistorySummary::empty("src/missing.rs");
2382
2383 assert!(summary.recent_commits.is_empty());
2384 assert!(!summary.uncertainty.is_empty());
2385 assert!(summary.uncertainty[0].contains("no persisted history evidence"));
2386 }
2387
2388 #[test]
2389 fn legacy_symbol_touch_json_remains_compatible() {
2390 let decoded: GitSymbolTouch = serde_json::from_value(serde_json::json!({
2391 "id": "touch",
2392 "commit_id": "abc123",
2393 "symbol_id": "symbol",
2394 "qualified_name": "crate::symbol",
2395 "file_path": "src/lib.rs",
2396 "change_kind": "modified",
2397 "touched_at": "2026-06-01T12:00:00Z"
2398 }))
2399 .unwrap();
2400
2401 assert_eq!(decoded.symbol_id, Some(SymbolId::new("symbol")));
2402 assert!(decoded.line_ranges.is_empty());
2403 assert_eq!(decoded.confidence, Confidence::Low);
2404 assert!(decoded.uncertainty.is_empty());
2405 }
2406
2407 #[test]
2408 fn legacy_graph_json_deserializes_with_default_metadata() {
2409 let decoded_node: GraphNode = serde_json::from_value(serde_json::json!({
2410 "id": "node:file",
2411 "node_type": "file",
2412 "label": "src/lib.rs",
2413 "file_id": "file:src/lib.rs",
2414 "symbol_id": null
2415 }))
2416 .unwrap();
2417 assert!(decoded_node.properties.is_empty());
2418 assert!(decoded_node.schema_version.is_none());
2419 assert!(decoded_node.ambiguity.is_empty());
2420 assert!(decoded_node.quality_notes.is_empty());
2421
2422 let decoded_edge: GraphEdge = serde_json::from_value(serde_json::json!({
2423 "id": "edge:defines",
2424 "from": "node:file",
2425 "to": "node:symbol",
2426 "edge_type": "DEFINES",
2427 "evidence": {
2428 "id": "evidence:legacy",
2429 "source": "tree-sitter",
2430 "source_type": "tree_sitter",
2431 "file_range": {
2432 "path": "src/lib.rs",
2433 "line_range": { "start": 1, "end": 3 }
2434 },
2435 "symbol_id": "symbol:main",
2436 "confidence": "high",
2437 "message": "legacy graph evidence",
2438 "indexed_at": "2026-06-01T12:00:00Z"
2439 }
2440 }))
2441 .unwrap();
2442 assert!(decoded_edge.properties.is_empty());
2443 assert!(decoded_edge.schema_version.is_none());
2444 assert!(decoded_edge.quality_notes.is_empty());
2445 assert!(decoded_edge.evidence.confidence_score.is_none());
2446 assert!(decoded_edge.evidence.confidence_reason.is_none());
2447 assert!(decoded_edge.evidence.freshness.is_none());
2448 }
2449
2450 #[test]
2451 fn enriched_graph_json_round_trips_metadata() {
2452 let indexed_at = Utc.with_ymd_and_hms(2026, 6, 1, 12, 0, 0).unwrap();
2453 let node = GraphNode {
2454 id: NodeId::new("node:file"),
2455 node_type: GraphNodeType::File,
2456 label: "src/lib.rs".into(),
2457 file_id: None,
2458 symbol_id: Some(SymbolId::new("symbol:main")),
2459 properties: BTreeMap::from([(
2460 "qualified_name".into(),
2461 serde_json::Value::String("crate::main".into()),
2462 )]),
2463 schema_version: Some("graph-v1".into()),
2464 source_pass: Some("tree_sitter".into()),
2465 index_mode: Some("scip".into()),
2466 extractor_version: Some("open-kioku-test".into()),
2467 ambiguity: vec!["overloaded symbol name".into()],
2468 quality_notes: vec!["exact definition".into()],
2469 };
2470 let edge = GraphEdge {
2471 id: EdgeId::new("edge:defines"),
2472 from: NodeId::new("node:file"),
2473 to: NodeId::new("node:symbol"),
2474 edge_type: GraphEdgeType::Defines,
2475 evidence: Evidence {
2476 id: super::EvidenceId::new("evidence:rich"),
2477 source: "scip".into(),
2478 source_type: EvidenceSourceType::Scip,
2479 file_range: Some(FileRange {
2480 path: "src/lib.rs".into(),
2481 line_range: Some(LineRange { start: 1, end: 1 }),
2482 }),
2483 symbol_id: Some(SymbolId::new("symbol:main")),
2484 confidence: Confidence::Exact,
2485 message: "exact reference".into(),
2486 indexed_at,
2487 confidence_score: Some(0.99),
2488 confidence_reason: Some("SCIP exact occurrence".into()),
2489 freshness: Some("fresh".into()),
2490 },
2491 properties: BTreeMap::from([("call_kind".into(), serde_json::json!("direct"))]),
2492 schema_version: Some("graph-v1".into()),
2493 source_pass: Some("scip".into()),
2494 index_mode: Some("full".into()),
2495 extractor_version: Some("scip-cli".into()),
2496 ambiguity: vec!["dynamic dispatch not expanded".into()],
2497 quality_notes: vec!["exact edge".into()],
2498 };
2499
2500 let decoded_node: GraphNode =
2501 serde_json::from_str(&serde_json::to_string(&node).unwrap()).unwrap();
2502 let decoded_edge: GraphEdge =
2503 serde_json::from_str(&serde_json::to_string(&edge).unwrap()).unwrap();
2504
2505 assert_eq!(decoded_node.properties, node.properties);
2506 assert_eq!(decoded_node.schema_version, Some("graph-v1".into()));
2507 assert_eq!(decoded_node.quality_notes, vec!["exact definition"]);
2508 assert_eq!(decoded_edge.properties, edge.properties);
2509 assert_eq!(decoded_edge.evidence.confidence_score, Some(0.99));
2510 assert_eq!(
2511 decoded_edge.evidence.confidence_reason.as_deref(),
2512 Some("SCIP exact occurrence")
2513 );
2514 assert_eq!(decoded_edge.evidence.freshness.as_deref(), Some("fresh"));
2515 }
2516}