Skip to main content

open_kioku_core/
lib.rs

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}