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 =
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}