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