1use chrono::{DateTime, Utc};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
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
97impl Default for ConfidenceBreakdown {
98 fn default() -> Self {
99 Self {
100 overall_enum: Confidence::Low,
101 overall_score: 0.0,
102 components: Vec::new(),
103 blockers: Vec::new(),
104 caveats: Vec::new(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, Default)]
110pub struct ConfidenceSignalInput {
111 pub primary_file_count: usize,
112 pub evidence_count: usize,
113 pub exact_reference_count: usize,
114 pub validation_count: usize,
115 pub validation_with_command_count: usize,
116 pub negative_evidence_count: usize,
117 pub allowed_file_count: usize,
118 pub runtime_signal_count: usize,
119}
120
121impl ConfidenceBreakdown {
122 pub fn from_signals(input: ConfidenceSignalInput) -> Self {
123 let mut blockers = Vec::new();
124 let mut caveats = Vec::new();
125
126 if input.primary_file_count == 0 {
127 blockers.push("no primary context matched the task".into());
128 }
129 if input.negative_evidence_count > 0 {
130 blockers.push(format!(
131 "{} negative evidence signal(s) lowered confidence",
132 input.negative_evidence_count
133 ));
134 }
135 if input.exact_reference_count == 0 {
136 caveats.push("exact symbol/reference evidence is absent".into());
137 }
138 if input.validation_count == 0 {
139 caveats.push("no validation target was selected".into());
140 } else if input.validation_with_command_count == 0 {
141 caveats.push("validation targets require manual commands".into());
142 }
143 if input.runtime_signal_count == 0 {
144 caveats.push("runtime corroboration is absent".into());
145 }
146 if input.allowed_file_count == 0 {
147 caveats.push("change boundary has no allowed files".into());
148 } else if input.allowed_file_count > 8 {
149 caveats.push("change boundary is broad".into());
150 }
151
152 let evidence_target = input.primary_file_count.max(1) * 2;
153 let evidence_density = if input.primary_file_count == 0 {
154 0.0
155 } else {
156 (input.evidence_count as f32 / evidence_target.max(4) as f32).min(1.0)
157 };
158 if evidence_density < 0.5 {
159 caveats.push("evidence density is thin".into());
160 }
161
162 let exact_reference = if input.exact_reference_count > 0 {
163 1.0
164 } else {
165 0.25
166 };
167 let validation_availability = if input.validation_count > 0 { 1.0 } else { 0.2 };
168 let negative_evidence = if input.negative_evidence_count == 0 {
169 1.0
170 } else if input.negative_evidence_count <= 2 {
171 0.3
172 } else {
173 0.1
174 };
175 let boundary_tightness = if input.primary_file_count == 0 {
176 0.0
177 } else if input.allowed_file_count == 0 {
178 0.3
179 } else if input.allowed_file_count <= 3 {
180 1.0
181 } else if input.allowed_file_count <= 8
182 && input.allowed_file_count <= input.primary_file_count.max(1) * 2
183 {
184 0.85
185 } else {
186 0.45
187 };
188 let runtime_corroboration = if input.runtime_signal_count > 0 {
189 1.0
190 } else {
191 0.25
192 };
193 let test_coverage = if input.validation_count == 0 {
194 0.2
195 } else if input.validation_with_command_count > 0 {
196 1.0
197 } else {
198 0.6
199 };
200
201 let mut components = vec![
202 confidence_component(
203 "evidence_density",
204 evidence_density,
205 0.20,
206 "amount of independent indexed evidence near the selected context",
207 ),
208 confidence_component(
209 "exact_references",
210 exact_reference,
211 0.20,
212 "explicit exact symbol references or SCIP signals",
213 ),
214 confidence_component(
215 "validation_availability",
216 validation_availability,
217 0.15,
218 "presence of validation targets for the likely change",
219 ),
220 confidence_component(
221 "negative_evidence",
222 negative_evidence,
223 0.15,
224 "absence of low-confidence, missing-anchor, or no-match evidence",
225 ),
226 confidence_component(
227 "boundary_tightness",
228 boundary_tightness,
229 0.15,
230 "how narrowly allowed edit files bound the proposed change",
231 ),
232 confidence_component(
233 "runtime_corroboration",
234 runtime_corroboration,
235 0.05,
236 "runtime traces, incidents, or error signals that support the context",
237 ),
238 confidence_component(
239 "test_coverage",
240 test_coverage,
241 0.10,
242 "selected tests with runnable commands",
243 ),
244 ];
245 components.sort_by(|a, b| a.signal.cmp(&b.signal));
246 let mut overall_score = score_component_total(&components).clamp(0.0, 1.0);
247 if input.primary_file_count == 0 {
248 overall_score = overall_score.min(0.35);
249 }
250 if input.exact_reference_count == 0
251 && input.validation_count == 0
252 && input.runtime_signal_count == 0
253 {
254 overall_score = overall_score.min(0.55);
255 }
256 if input.negative_evidence_count > 0 {
257 overall_score = overall_score.min(0.60);
258 }
259
260 blockers.sort();
261 blockers.dedup();
262 caveats.sort();
263 caveats.dedup();
264 if !caveats.is_empty() {
265 overall_score = overall_score.min(0.94);
266 }
267
268 Self {
269 overall_enum: Confidence::from_score(overall_score),
270 overall_score,
271 components,
272 blockers,
273 caveats,
274 }
275 }
276}
277
278fn confidence_component(
279 signal: &'static str,
280 value: f32,
281 weight: f32,
282 rationale: &'static str,
283) -> ScoreComponent {
284 ScoreComponent::new(
285 signal,
286 value,
287 value,
288 weight,
289 value * weight,
290 Vec::new(),
291 rationale,
292 )
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
296pub struct LineRange {
297 pub start: u32,
298 pub end: u32,
299}
300
301impl LineRange {
302 pub fn single(line: u32) -> Self {
303 Self {
304 start: line,
305 end: line,
306 }
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
311pub struct FileRange {
312 pub path: PathBuf,
313 pub line_range: Option<LineRange>,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
317#[serde(rename_all = "snake_case")]
318pub enum EvidenceSourceType {
319 TreeSitter,
320 Scip,
321 Lsp,
322 Regex,
323 Lexical,
324 Semantic,
325 Runtime,
326 GitHistory,
327 StaticAnalysis,
328 ExternalIntegration,
329 Heuristic,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
333pub struct Evidence {
334 pub id: EvidenceId,
335 pub source: String,
336 pub source_type: EvidenceSourceType,
337 pub file_range: Option<FileRange>,
338 pub symbol_id: Option<SymbolId>,
339 pub confidence: Confidence,
340 pub message: String,
341 pub indexed_at: DateTime<Utc>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub confidence_score: Option<f32>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub confidence_reason: Option<String>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub freshness: Option<String>,
348}
349
350impl Default for Evidence {
351 fn default() -> Self {
352 Self {
353 id: EvidenceId::new(""),
354 source: String::new(),
355 source_type: EvidenceSourceType::Lexical,
356 file_range: None,
357 symbol_id: None,
358 confidence: Confidence::Low,
359 message: String::new(),
360 indexed_at: Utc::now(),
361 confidence_score: None,
362 confidence_reason: None,
363 freshness: None,
364 }
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
369pub struct ScoreComponent {
370 pub signal: String,
371 pub raw_value: f32,
372 pub normalized_value: f32,
373 pub weight: f32,
374 pub contribution: f32,
375 pub evidence_ids: Vec<String>,
376 pub rationale: String,
377}
378
379impl ScoreComponent {
380 pub fn new(
381 signal: impl Into<String>,
382 raw_value: f32,
383 normalized_value: f32,
384 weight: f32,
385 contribution: f32,
386 evidence_ids: Vec<String>,
387 rationale: impl Into<String>,
388 ) -> Self {
389 Self {
390 signal: signal.into(),
391 raw_value,
392 normalized_value,
393 weight,
394 contribution,
395 evidence_ids,
396 rationale: rationale.into(),
397 }
398 }
399
400 pub fn single(
401 signal: impl Into<String>,
402 score: f32,
403 evidence_ids: Vec<String>,
404 rationale: impl Into<String>,
405 ) -> Self {
406 Self::new(
407 signal,
408 score,
409 score.clamp(0.0, 1.0),
410 1.0,
411 score,
412 evidence_ids,
413 rationale,
414 )
415 }
416
417 pub fn adjustment(
418 signal: impl Into<String>,
419 contribution: f32,
420 evidence_ids: Vec<String>,
421 rationale: impl Into<String>,
422 ) -> Self {
423 Self::new(
424 signal,
425 contribution,
426 contribution.clamp(-1.0, 1.0),
427 1.0,
428 contribution,
429 evidence_ids,
430 rationale,
431 )
432 }
433}
434
435pub fn score_component_total(components: &[ScoreComponent]) -> f32 {
436 components
437 .iter()
438 .map(|component| component.contribution)
439 .sum()
440}
441
442pub fn reconcile_score_breakdown(
443 score: f32,
444 components: &mut Vec<ScoreComponent>,
445 fallback_signal: &str,
446 evidence_ids: Vec<String>,
447 rationale: &str,
448) {
449 if components.is_empty() {
450 components.push(ScoreComponent::single(
451 fallback_signal,
452 score,
453 evidence_ids,
454 rationale,
455 ));
456 return;
457 }
458
459 let delta = score - score_component_total(components);
460 if delta.abs() > 0.001 {
461 components.push(ScoreComponent::adjustment(
462 "score_reconciliation",
463 delta,
464 evidence_ids,
465 format!("adjusted component total to match surfaced score: {rationale}"),
466 ));
467 }
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
471pub struct Repository {
472 pub id: RepositoryId,
473 pub name: String,
474 pub root: PathBuf,
475 pub branch: Option<String>,
476 pub commit: Option<String>,
477 pub indexed_at: Option<DateTime<Utc>>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
481pub struct Commit {
482 pub sha: String,
483 pub message: Option<String>,
484 pub authored_at: Option<DateTime<Utc>>,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
488pub struct Branch {
489 pub name: String,
490 pub head: Option<String>,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
494#[serde(rename_all = "snake_case")]
495pub enum Language {
496 Rust,
497 Java,
498 TypeScript,
499 JavaScript,
500 Python,
501 Go,
502 Yaml,
503 Json,
504 Toml,
505 Sql,
506 Markdown,
507 Text,
508 Unknown,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
512pub struct File {
513 pub id: FileId,
514 pub repository_id: RepositoryId,
515 pub path: PathBuf,
516 pub language: Language,
517 pub size_bytes: u64,
518 pub content_hash: String,
519 pub is_generated: bool,
520 pub is_vendor: bool,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
524pub struct FileVersion {
525 pub id: FileVersionId,
526 pub file_id: FileId,
527 pub commit: Option<String>,
528 pub content_hash: String,
529 pub indexed_at: DateTime<Utc>,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
533#[serde(rename_all = "snake_case")]
534pub enum SymbolKind {
535 Module,
536 Package,
537 Class,
538 Trait,
539 Interface,
540 Function,
541 Method,
542 Field,
543 Variable,
544 Constant,
545 Endpoint,
546 DatabaseTable,
547 Test,
548 Unknown,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
552pub struct Symbol {
553 pub id: SymbolId,
554 pub name: String,
555 pub qualified_name: String,
556 pub kind: SymbolKind,
557 pub file_id: FileId,
558 pub range: Option<LineRange>,
559 pub language: Language,
560 pub confidence: Confidence,
561 pub provenance: EvidenceSourceType,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
565pub struct SymbolOccurrence {
566 pub symbol_id: SymbolId,
567 pub file_id: FileId,
568 pub range: Option<LineRange>,
569 pub is_definition: bool,
570 pub confidence: Confidence,
571 pub provenance: EvidenceSourceType,
572}
573
574pub type Reference = SymbolOccurrence;
575pub type Definition = SymbolOccurrence;
576
577#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
578pub struct Import {
579 pub file_id: FileId,
580 pub imported: String,
581 pub range: Option<LineRange>,
582 pub confidence: Confidence,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
586#[serde(rename_all = "snake_case")]
587pub enum ResolutionStatus {
588 Resolved,
589 Ambiguous { candidates: usize },
590 ExternalPackage,
591 Builtin,
592 Unresolved,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
596pub struct ImportResolution {
597 pub import: Import,
598 pub status: ResolutionStatus,
599 pub target_file: Option<FileId>,
600 pub target_symbol: Option<SymbolId>,
601 pub confidence: Confidence,
602 pub strategy: String,
603 pub caveats: Vec<String>,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
607pub struct AnalysisFact {
608 pub id: String,
609 pub file_id: FileId,
610 pub symbol_id: Option<SymbolId>,
611 pub target: String,
612 pub target_kind: GraphNodeType,
613 pub edge_type: GraphEdgeType,
614 pub range: Option<LineRange>,
615 pub confidence: Confidence,
616 pub source: String,
617 pub source_type: EvidenceSourceType,
618 pub message: String,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
622pub struct CodeChunk {
623 pub id: String,
624 pub file_id: FileId,
625 pub range: LineRange,
626 pub language: Language,
627 pub text: String,
628 pub symbol_id: Option<SymbolId>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
632pub struct Diagnostic {
633 pub severity: String,
634 pub message: String,
635 pub file_range: Option<FileRange>,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
639pub struct TestTarget {
640 pub id: String,
641 pub name: String,
642 pub file_id: FileId,
643 pub range: Option<LineRange>,
644 pub command: Option<String>,
645 pub confidence: Confidence,
646 pub reason: String,
647 #[serde(default)]
648 pub evidence_refs: Vec<String>,
649 #[serde(default)]
650 pub score_breakdown: Vec<ScoreComponent>,
651}
652
653impl TestTarget {
654 pub fn reconcile_score_breakdown(&mut self) {
655 if self.evidence_refs.is_empty() {
656 self.evidence_refs.push(format!("test:{}", self.id));
657 }
658 reconcile_score_breakdown(
659 self.confidence.score(),
660 &mut self.score_breakdown,
661 "test_confidence",
662 self.evidence_refs.clone(),
663 &self.reason,
664 );
665 }
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
669pub struct BuildTarget {
670 pub id: String,
671 pub name: String,
672 pub command: String,
673 pub files: Vec<FileId>,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
677pub struct RuntimeSignal {
678 pub id: String,
679 pub kind: String,
680 pub message: String,
681 pub file_range: Option<FileRange>,
682 pub occurred_at: Option<DateTime<Utc>>,
683 pub confidence: Confidence,
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
687pub struct Owner {
688 pub name: String,
689 pub email: Option<String>,
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
693#[serde(rename_all = "snake_case")]
694pub enum GitChangeKind {
695 Added,
696 Modified,
697 Deleted,
698 Renamed,
699 Copied,
700 TypeChanged,
701 Unknown,
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
705#[serde(rename_all = "snake_case")]
706pub enum ReviewerRole {
707 Reviewer,
708 Approver,
709 Author,
710 Committer,
711 Owner,
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
715pub struct GitCommitRecord {
716 pub id: GitCommitId,
717 #[serde(default)]
718 pub parent_ids: Vec<GitCommitId>,
719 pub author: Owner,
720 pub committer: Option<Owner>,
721 pub authored_at: DateTime<Utc>,
722 pub committed_at: DateTime<Utc>,
723 pub summary: String,
724 pub message: String,
725 pub file_count: usize,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
729pub struct GitFileTouch {
730 pub id: HistoryRecordId,
731 pub commit_id: GitCommitId,
732 pub path: PathBuf,
733 pub previous_path: Option<PathBuf>,
734 pub change_kind: GitChangeKind,
735 pub additions: Option<u32>,
736 pub deletions: Option<u32>,
737 pub touched_at: DateTime<Utc>,
738}
739
740#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
741pub struct GitSymbolTouch {
742 pub id: HistoryRecordId,
743 pub commit_id: GitCommitId,
744 pub symbol_id: Option<SymbolId>,
745 pub qualified_name: String,
746 pub file_path: PathBuf,
747 pub change_kind: GitChangeKind,
748 #[serde(default)]
749 pub line_ranges: Vec<LineRange>,
750 #[serde(default = "default_history_confidence")]
751 pub confidence: Confidence,
752 #[serde(default)]
753 pub uncertainty: Vec<String>,
754 pub touched_at: DateTime<Utc>,
755}
756
757fn default_history_confidence() -> Confidence {
758 Confidence::Low
759}
760
761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
762pub struct ProvenanceTouch {
763 pub commit: GitCommitRecord,
764 pub path: PathBuf,
765 pub previous_path: Option<PathBuf>,
766 pub symbol_id: Option<SymbolId>,
767 pub qualified_name: Option<String>,
768 pub change_kind: GitChangeKind,
769 pub line_ranges: Vec<LineRange>,
770 pub confidence: Confidence,
771 pub uncertainty: Vec<String>,
772}
773
774#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
775pub struct FileProvenance {
776 pub path: PathBuf,
777 pub first_seen: Option<ProvenanceTouch>,
778 pub last_touched: Option<ProvenanceTouch>,
779 pub recent_touches: Vec<ProvenanceTouch>,
780 pub confidence: Confidence,
781 pub truncated: bool,
782 pub uncertainty: Vec<String>,
783}
784
785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
786pub struct SymbolProvenance {
787 pub symbol_id: SymbolId,
788 pub qualified_name: String,
789 pub file_path: PathBuf,
790 pub range: Option<LineRange>,
791 pub first_seen: Option<ProvenanceTouch>,
792 pub last_touched: Option<ProvenanceTouch>,
793 pub recent_touches: Vec<ProvenanceTouch>,
794 pub confidence: Confidence,
795 pub truncated: bool,
796 pub uncertainty: Vec<String>,
797}
798
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
800pub struct GitCochangeEdge {
801 pub id: HistoryRecordId,
802 pub path: PathBuf,
803 pub cochanged_path: PathBuf,
804 pub commit_count: usize,
805 pub recency_weight: f32,
806 pub last_changed_at: Option<DateTime<Utc>>,
807 #[serde(default)]
808 pub sample_commits: Vec<GitCommitId>,
809 pub test_corun: bool,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
813pub struct ReviewerEvidence {
814 pub id: HistoryRecordId,
815 pub commit_id: Option<GitCommitId>,
816 pub path: Option<PathBuf>,
817 pub reviewer: Owner,
818 pub role: ReviewerRole,
819 pub observed_at: DateTime<Utc>,
820 pub source: String,
821 pub confidence: Confidence,
822}
823
824#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
825pub struct HistorySnapshot {
826 pub schema_version: u32,
827 #[serde(default)]
828 pub commits: Vec<GitCommitRecord>,
829 #[serde(default)]
830 pub file_touches: Vec<GitFileTouch>,
831 #[serde(default)]
832 pub symbol_touches: Vec<GitSymbolTouch>,
833 #[serde(default)]
834 pub cochange_edges: Vec<GitCochangeEdge>,
835 #[serde(default)]
836 pub reviewer_evidence: Vec<ReviewerEvidence>,
837}
838
839impl HistorySnapshot {
840 pub fn empty() -> Self {
841 Self {
842 schema_version: HISTORY_SCHEMA_VERSION,
843 commits: Vec::new(),
844 file_touches: Vec::new(),
845 symbol_touches: Vec::new(),
846 cochange_edges: Vec::new(),
847 reviewer_evidence: Vec::new(),
848 }
849 }
850}
851
852#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
853pub struct HistorySummary {
854 pub path: PathBuf,
855 pub recent_commits: Vec<GitCommitRecord>,
856 pub file_touches: Vec<GitFileTouch>,
857 pub symbol_touches: Vec<GitSymbolTouch>,
858 pub cochange_neighbors: Vec<GitCochangeEdge>,
859 pub reviewer_evidence: Vec<ReviewerEvidence>,
860 pub truncated: bool,
861 #[serde(default)]
862 pub uncertainty: Vec<String>,
863}
864
865impl HistorySummary {
866 pub fn empty(path: impl Into<PathBuf>) -> Self {
867 Self {
868 path: path.into(),
869 recent_commits: Vec::new(),
870 file_touches: Vec::new(),
871 symbol_touches: Vec::new(),
872 cochange_neighbors: Vec::new(),
873 reviewer_evidence: Vec::new(),
874 truncated: false,
875 uncertainty: vec!["no persisted history evidence is available for this path".into()],
876 }
877 }
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
881pub struct ArchitectureComponent {
882 pub id: String,
883 pub name: String,
884 pub paths: Vec<String>,
885 pub evidence: Vec<Evidence>,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
889pub struct PolicyComponentMatch {
890 pub component_id: String,
891 pub matched_glob: String,
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
895pub struct ResolvedArchitectureNode {
896 pub file_path: PathBuf,
897 pub symbol_id: Option<SymbolId>,
898 pub components: Vec<PolicyComponentMatch>,
899}
900
901#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
902pub struct UnmappedPolicyTarget {
903 pub file_path: PathBuf,
904 pub symbol_id: Option<SymbolId>,
905}
906
907#[derive(
908 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
909)]
910#[serde(rename_all = "snake_case")]
911pub enum EnforcedEdgeType {
912 Imports,
913 References,
914 Calls,
915}
916
917impl EnforcedEdgeType {
918 pub fn graph_edge_type(self) -> GraphEdgeType {
919 match self {
920 Self::Imports => GraphEdgeType::Imports,
921 Self::References => GraphEdgeType::References,
922 Self::Calls => GraphEdgeType::Calls,
923 }
924 }
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
928pub struct PolicyMatchEvidence {
929 pub edge_id: String,
930 pub edge_type: EnforcedEdgeType,
931 pub source_node: String,
932 pub target_node: String,
933 pub source_path: PathBuf,
934 pub target_path: PathBuf,
935 pub confidence: Confidence,
936 pub message: String,
937}
938
939#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
940pub struct PolicyViolation {
941 pub rule_id: String,
942 pub severity: String,
943 pub source_component: String,
944 pub target_component: String,
945 pub source_path: PathBuf,
946 pub target_path: PathBuf,
947 pub edge_type: EnforcedEdgeType,
948 pub evidence: PolicyMatchEvidence,
949 pub message: String,
950}
951
952#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
953pub struct UnknownPolicyEdge {
954 pub reason: String,
955 pub evidence: PolicyMatchEvidence,
956}
957
958#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
959pub struct PolicyCheckReport {
960 pub configured: bool,
961 pub evaluated_edge_count: usize,
962 pub allowed_edges: usize,
963 pub violation_count: usize,
964 pub unknown_edge_count: usize,
965 pub unknown_sample_count: usize,
966 pub unknown_edges_truncated: bool,
967 pub violations: Vec<PolicyViolation>,
968 pub unknown_edges: Vec<UnknownPolicyEdge>,
969 pub uncertainty: Vec<String>,
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
973pub struct IndexManifest {
974 pub repository: Repository,
975 pub file_count: usize,
976 pub symbol_count: usize,
977 pub chunk_count: usize,
978 pub indexed_at: DateTime<Utc>,
979 pub schema_version: u32,
980 #[serde(default)]
981 pub index_mode: IndexMode,
982 #[serde(default)]
983 pub phase_reports: Vec<IndexPhaseReport>,
984 #[serde(default)]
985 pub quality: IndexQuality,
986}
987
988#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
989#[serde(rename_all = "snake_case")]
990pub enum IndexMode {
991 #[default]
992 Full,
993 Balanced,
994 Fast,
995 CrossProject,
996}
997
998impl fmt::Display for IndexMode {
999 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000 let value = match self {
1001 Self::Full => "full",
1002 Self::Balanced => "balanced",
1003 Self::Fast => "fast",
1004 Self::CrossProject => "cross_project",
1005 };
1006 f.write_str(value)
1007 }
1008}
1009
1010#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1011pub struct IndexPhaseReport {
1012 pub phase: String,
1013 pub elapsed_ms: u64,
1014 pub scanned_files: usize,
1015 pub indexed_files: usize,
1016 pub nodes_added: usize,
1017 pub edges_added: usize,
1018 pub skipped: usize,
1019 pub warnings: Vec<String>,
1020}
1021
1022#[derive(
1023 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
1024)]
1025#[serde(rename_all = "snake_case")]
1026pub enum SkipReason {
1027 Ignored,
1028 Denied,
1029 Hidden,
1030 UnsupportedLanguage,
1031 Binary,
1032 TooLarge,
1033 Generated,
1034 Vendor,
1035 FastMode,
1036 SecretPolicy,
1037 SymlinkPolicy,
1038 Error,
1039}
1040
1041#[derive(
1042 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
1043)]
1044#[serde(rename_all = "snake_case")]
1045pub enum SkipSource {
1046 SecurityPolicy,
1047 HiddenPolicy,
1048 ConfigExclude,
1049 GitIgnore,
1050 OkIgnore,
1051 Detector,
1052 FastMode,
1053 SizeLimit,
1054 SymlinkPolicy,
1055 LanguageSupport,
1056 Filesystem,
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1060pub struct SkippedPath {
1061 pub path: PathBuf,
1062 pub reason: SkipReason,
1063 pub source: SkipSource,
1064 pub safe_to_show: bool,
1065}
1066
1067#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1068pub struct IndexQuality {
1069 #[serde(default)]
1070 pub index_mode: IndexMode,
1071 #[serde(default)]
1072 pub phase_reports: Vec<IndexPhaseReport>,
1073 pub scip_enabled: bool,
1074 pub scip_mode: String,
1075 pub scip_indexes_imported: usize,
1076 pub scip_symbols: usize,
1077 pub scip_occurrences: usize,
1078 pub scip_exact_references: usize,
1079 pub test_count: usize,
1080 pub import_count: usize,
1081 #[serde(default)]
1082 pub build_systems: Vec<String>,
1083 #[serde(default)]
1084 pub codeql_databases: usize,
1085 #[serde(default)]
1086 pub coverage_reports: usize,
1087 #[serde(default)]
1088 pub junit_reports: usize,
1089 #[serde(default)]
1090 pub static_analysis_facts: usize,
1091 #[serde(default)]
1092 pub runtime_analysis_facts: usize,
1093 #[serde(default)]
1094 pub git_history_facts: usize,
1095 #[serde(default)]
1096 pub architecture_facts: usize,
1097 #[serde(default)]
1098 pub semantic_provider_notes: Vec<String>,
1099 #[serde(default)]
1100 pub skip_counts: BTreeMap<SkipReason, usize>,
1101 #[serde(default)]
1102 pub skipped_paths: Vec<SkippedPath>,
1103 pub quality_notes: Vec<String>,
1104}
1105
1106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1107pub struct EvidenceGraphSchema {
1108 pub version: String,
1109 pub node_types: Vec<NodeTypeSpec>,
1110 pub edge_types: Vec<EdgeTypeSpec>,
1111 pub property_specs: Vec<PropertySpec>,
1112 pub feature_flags: Vec<String>,
1113 #[serde(default)]
1114 pub evidence_source_types: Vec<String>,
1115 #[serde(default)]
1116 pub query_features: Vec<String>,
1117 #[serde(default)]
1118 pub optional_evidence: Vec<OptionalEvidenceSpec>,
1119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1120 pub caveats: Vec<String>,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1122 pub indexed_at: Option<String>,
1123}
1124
1125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1126pub struct NodeTypeSpec {
1127 pub name: String,
1128 pub stable: bool,
1129 pub description: String,
1130 pub required_fields: Vec<String>,
1131 pub optional_fields: Vec<String>,
1132 #[serde(skip_serializing_if = "Option::is_none")]
1133 pub count: Option<usize>,
1134 #[serde(skip_serializing_if = "Option::is_none")]
1135 pub evidence_available: Option<bool>,
1136 #[serde(skip_serializing_if = "Option::is_none")]
1137 pub freshness: Option<String>,
1138}
1139
1140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1141pub struct EdgeTypeSpec {
1142 pub name: String,
1143 pub stable: bool,
1144 pub description: String,
1145 pub source_types: Vec<String>,
1146 pub target_types: Vec<String>,
1147 pub required_evidence: Vec<String>,
1148 #[serde(skip_serializing_if = "Option::is_none")]
1149 pub count: Option<usize>,
1150 #[serde(skip_serializing_if = "Option::is_none")]
1151 pub evidence_available: Option<bool>,
1152 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub freshness: Option<String>,
1154}
1155
1156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1157pub struct PropertySpec {
1158 pub name: String,
1159 pub type_name: String,
1160 pub description: String,
1161}
1162
1163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1164pub struct OptionalEvidenceSpec {
1165 pub name: String,
1166 pub available: bool,
1167 pub status: String,
1168 pub evidence_count: usize,
1169 pub description: String,
1170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1171 pub caveats: Vec<String>,
1172}
1173
1174#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1175#[serde(rename_all = "snake_case")]
1176pub enum GraphNodeType {
1177 File,
1178 Directory,
1179 Module,
1180 Package,
1181 Class,
1182 Trait,
1183 Interface,
1184 Function,
1185 Method,
1186 Field,
1187 Endpoint,
1188 DatabaseTable,
1189 Collection,
1190 Queue,
1191 Topic,
1192 ConfigKey,
1193 Test,
1194 BuildTarget,
1195 RuntimeError,
1196 Ticket,
1197 PullRequest,
1198 Resource,
1199 ArchitectureComponent,
1200}
1201
1202#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1203#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1204pub enum GraphEdgeType {
1205 Contains,
1206 Defines,
1207 References,
1208 Calls,
1209 Implements,
1210 Extends,
1211 Imports,
1212 DependsOn,
1213 ExposesEndpoint,
1214 CallsEndpoint,
1215 ReadsConfig,
1216 WritesConfig,
1217 ReadsTable,
1218 WritesTable,
1219 PublishesEvent,
1220 ConsumesEvent,
1221 Tests,
1222 TestCovers,
1223 Validates,
1224 OwnedBy,
1225 ChangedBy,
1226 FailedIn,
1227 BelongsTo,
1228 MentionedIn,
1229 RelatedToTicket,
1230 SimilarTo,
1231 SemanticallyRelated,
1232}
1233
1234#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1235pub struct GraphNode {
1236 pub id: NodeId,
1237 pub node_type: GraphNodeType,
1238 pub label: String,
1239 pub file_id: Option<FileId>,
1240 pub symbol_id: Option<SymbolId>,
1241 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1242 pub properties: BTreeMap<String, serde_json::Value>,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1244 pub schema_version: Option<String>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub source_pass: Option<String>,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub index_mode: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1250 pub extractor_version: Option<String>,
1251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1252 pub ambiguity: Vec<String>,
1253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1254 pub quality_notes: Vec<String>,
1255}
1256
1257impl Default for GraphNode {
1258 fn default() -> Self {
1259 Self {
1260 id: NodeId::new(""),
1261 node_type: GraphNodeType::File,
1262 label: String::new(),
1263 file_id: None,
1264 symbol_id: None,
1265 properties: BTreeMap::new(),
1266 schema_version: None,
1267 source_pass: None,
1268 index_mode: None,
1269 extractor_version: None,
1270 ambiguity: vec![],
1271 quality_notes: vec![],
1272 }
1273 }
1274}
1275
1276#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1277pub struct GraphEdge {
1278 pub id: EdgeId,
1279 pub from: NodeId,
1280 pub to: NodeId,
1281 pub edge_type: GraphEdgeType,
1282 pub evidence: Evidence,
1283 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1284 pub properties: BTreeMap<String, serde_json::Value>,
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1286 pub schema_version: Option<String>,
1287 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub source_pass: Option<String>,
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub index_mode: Option<String>,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub extractor_version: Option<String>,
1293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1294 pub ambiguity: Vec<String>,
1295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296 pub quality_notes: Vec<String>,
1297}
1298
1299impl Default for GraphEdge {
1300 fn default() -> Self {
1301 Self {
1302 id: EdgeId::new(""),
1303 from: NodeId::new(""),
1304 to: NodeId::new(""),
1305 edge_type: GraphEdgeType::References,
1306 evidence: Evidence::default(),
1307 properties: BTreeMap::new(),
1308 schema_version: None,
1309 source_pass: None,
1310 index_mode: None,
1311 extractor_version: None,
1312 ambiguity: vec![],
1313 quality_notes: vec![],
1314 }
1315 }
1316}
1317
1318#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1319pub struct SearchResult {
1320 pub path: PathBuf,
1321 pub line_range: Option<LineRange>,
1322 pub snippet: String,
1323 pub symbol: Option<Symbol>,
1324 pub score: f32,
1325 pub match_reason: String,
1326 pub evidence: Vec<String>,
1327 #[serde(default)]
1328 pub evidence_refs: Vec<String>,
1329 pub confidence: f32,
1330 #[serde(default)]
1331 pub score_breakdown: Vec<ScoreComponent>,
1332}
1333
1334impl SearchResult {
1335 pub fn derived_evidence_ids(&self) -> Vec<String> {
1336 if !self.evidence_refs.is_empty() {
1337 return self.evidence_refs.clone();
1338 }
1339 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len())
1340 }
1341
1342 pub fn reconcile_score_breakdown(&mut self) {
1343 if self.evidence_refs.is_empty() {
1344 self.evidence_refs =
1345 search_result_evidence_ids(&self.path, &self.line_range, self.evidence.len());
1346 }
1347 reconcile_score_breakdown(
1348 self.score,
1349 &mut self.score_breakdown,
1350 "search_score",
1351 self.evidence_refs.clone(),
1352 &self.match_reason,
1353 );
1354 }
1355
1356 pub fn add_score_component(&mut self, component: ScoreComponent) {
1357 self.score_breakdown.push(component);
1358 }
1359}
1360
1361pub fn search_result_evidence_ids(
1362 path: &Path,
1363 line_range: &Option<LineRange>,
1364 evidence_len: usize,
1365) -> Vec<String> {
1366 let range = line_range
1367 .as_ref()
1368 .map(|range| format!("{}-{}", range.start, range.end))
1369 .unwrap_or_else(|| "unknown".into());
1370 let count = evidence_len.max(1);
1371 (0..count)
1372 .map(|index| format!("search:{}:{range}:{index}", path.display()))
1373 .collect()
1374}
1375
1376#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1377pub struct EntityLink {
1378 pub kind: String,
1379 pub value: String,
1380 pub file_range: Option<FileRange>,
1381 pub confidence: Confidence,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1385pub struct MemoryFact {
1386 pub id: MemoryFactId,
1387 pub text: String,
1388 pub source: String,
1389 pub confidence: Confidence,
1390 pub entities: Vec<EntityLink>,
1391 pub created_at: DateTime<Utc>,
1392}
1393
1394#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1395pub struct MemorySearchResult {
1396 pub fact: MemoryFact,
1397 pub score: f32,
1398 pub match_reason: String,
1399 pub evidence: Vec<String>,
1400}
1401
1402#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1403pub struct ContextHandle {
1404 pub id: ContextHandleId,
1405 pub kind: String,
1406 pub summary: String,
1407 pub file_range: Option<FileRange>,
1408 pub entities: Vec<EntityLink>,
1409 pub original_tokens_estimate: usize,
1410 pub compressed_tokens_estimate: usize,
1411}
1412
1413#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1414pub struct CompressedContextPack {
1415 pub task: String,
1416 pub summary: String,
1417 pub handles: Vec<ContextHandle>,
1418 pub original_tokens_estimate: usize,
1419 pub compressed_tokens_estimate: usize,
1420 pub compression_ratio: f32,
1421 pub evidence: Vec<Evidence>,
1422}
1423
1424#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1425pub struct RiskReport {
1426 pub level: String,
1427 pub score: f32,
1428 pub reasons: Vec<String>,
1429}
1430
1431#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1432pub struct BoundaryFileRule {
1433 pub path: PathBuf,
1434 pub reason: String,
1435 #[serde(default)]
1436 pub evidence_refs: Vec<String>,
1437 #[serde(default)]
1438 pub symbols: Vec<String>,
1439}
1440
1441#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1442pub struct BoundaryForbiddenRule {
1443 pub pattern: String,
1444 pub reason: String,
1445 #[serde(default)]
1446 pub evidence_refs: Vec<String>,
1447}
1448
1449#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1450pub struct BoundaryExpansionRequirement {
1451 pub reason: String,
1452 #[serde(default)]
1453 pub required_evidence_refs: Vec<String>,
1454}
1455
1456#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1457pub struct BoundarySignalHooks {
1458 #[serde(default)]
1459 pub architecture_components: Vec<String>,
1460 #[serde(default)]
1461 pub ownership_sources: Vec<String>,
1462 #[serde(default)]
1463 pub cochange_sources: Vec<String>,
1464}
1465
1466#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1467pub struct ChangeBoundary {
1468 pub allowed_files: Vec<PathBuf>,
1469 pub caution_files: Vec<PathBuf>,
1470 pub forbidden_files: Vec<PathBuf>,
1471 #[serde(default)]
1472 pub evidence_refs: Vec<String>,
1473 #[serde(default)]
1474 pub allowed_symbols: Vec<String>,
1475 #[serde(default)]
1476 pub allowed_rules: Vec<BoundaryFileRule>,
1477 #[serde(default)]
1478 pub caution_rules: Vec<BoundaryFileRule>,
1479 #[serde(default)]
1480 pub forbidden_rules: Vec<BoundaryForbiddenRule>,
1481 #[serde(default)]
1482 pub expansion_requirements: Vec<BoundaryExpansionRequirement>,
1483 #[serde(default)]
1484 pub signal_hooks: BoundarySignalHooks,
1485}
1486
1487#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1488pub struct ValidationPlan {
1489 pub commands: Vec<String>,
1490 pub tests: Vec<TestTarget>,
1491 pub requires_approval: bool,
1492 pub evidence: Vec<Evidence>,
1493}
1494
1495#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1496pub struct ImpactReport {
1497 pub target: String,
1498 pub direct_impacts: Vec<SearchResult>,
1499 pub indirect_impacts: Vec<SearchResult>,
1500 pub risk_report: RiskReport,
1501 pub evidence: Vec<Evidence>,
1502 #[serde(default)]
1503 pub score_breakdown: Vec<ScoreComponent>,
1504}
1505
1506impl ImpactReport {
1507 pub fn reconcile_score_breakdown(&mut self) {
1508 reconcile_score_breakdown(
1509 self.risk_report.score,
1510 &mut self.score_breakdown,
1511 "impact_risk",
1512 self.evidence
1513 .iter()
1514 .map(|evidence| evidence.id.0.clone())
1515 .collect(),
1516 "impact risk score",
1517 );
1518 for result in &mut self.direct_impacts {
1519 result.reconcile_score_breakdown();
1520 }
1521 for result in &mut self.indirect_impacts {
1522 result.reconcile_score_breakdown();
1523 }
1524 }
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1528pub struct ContextPack {
1529 pub task: String,
1530 pub intent: String,
1531 pub primary_files: Vec<SearchResult>,
1532 pub primary_symbols: Vec<Symbol>,
1533 pub supporting_files: Vec<SearchResult>,
1534 pub dependency_edges: Vec<GraphEdge>,
1535 pub runtime_signals: Vec<RuntimeSignal>,
1536 pub test_candidates: Vec<TestTarget>,
1537 pub risk_report: RiskReport,
1538 pub recommended_change_boundary: ChangeBoundary,
1539 pub validation_plan: ValidationPlan,
1540 pub evidence: Vec<Evidence>,
1541 #[serde(default)]
1542 pub negative_evidence: Vec<NegativeEvidence>,
1543 pub confidence_summary: String,
1544 #[serde(default)]
1545 pub confidence_breakdown: ConfidenceBreakdown,
1546}
1547
1548#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1549pub struct ToolCallRecommendation {
1550 pub tool: String,
1551 pub purpose: String,
1552 pub arguments: serde_json::Value,
1553}
1554
1555#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1556pub struct PlanReport {
1557 pub task: String,
1558 pub summary: String,
1559 pub primary_context: Vec<SearchResult>,
1560 pub relevant_symbols: Vec<Symbol>,
1561 pub impact: ImpactReport,
1562 pub validation: Vec<TestTarget>,
1563 pub risk: RiskReport,
1564 pub recommended_change_boundary: ChangeBoundary,
1565 pub recommended_next_steps: Vec<String>,
1566 pub tool_calls: Vec<ToolCallRecommendation>,
1567 pub memory_facts: Vec<MemorySearchResult>,
1568 #[serde(default)]
1569 pub runtime_signals: Vec<RuntimeSignal>,
1570 pub evidence: Vec<Evidence>,
1571 #[serde(default)]
1572 pub evidence_by_section: BTreeMap<String, Vec<String>>,
1573 #[serde(default)]
1574 pub negative_evidence: Vec<NegativeEvidence>,
1575 pub confidence_summary: String,
1576 #[serde(default)]
1577 pub confidence_breakdown: ConfidenceBreakdown,
1578 #[serde(default)]
1579 pub score_breakdown: Vec<ScoreComponent>,
1580}
1581
1582impl PlanReport {
1583 pub fn reconcile_score_breakdown(&mut self) {
1584 reconcile_score_breakdown(
1585 self.risk.score,
1586 &mut self.score_breakdown,
1587 "plan_risk",
1588 self.evidence
1589 .iter()
1590 .map(|evidence| evidence.id.0.clone())
1591 .collect(),
1592 "plan risk score",
1593 );
1594 for result in &mut self.primary_context {
1595 result.reconcile_score_breakdown();
1596 }
1597 self.impact.reconcile_score_breakdown();
1598 for test in &mut self.validation {
1599 test.reconcile_score_breakdown();
1600 }
1601 }
1602}
1603
1604#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1605pub struct PatchPlan {
1606 pub id: PatchId,
1607 pub task: String,
1608 pub allowed_files: Vec<PathBuf>,
1609 pub caution_files: Vec<PathBuf>,
1610 pub forbidden_files: Vec<PathBuf>,
1611 pub change_steps: Vec<String>,
1612 pub risks: Vec<String>,
1613 pub assumptions: Vec<String>,
1614 pub tests: Vec<TestTarget>,
1615 pub rollback_notes: Vec<String>,
1616 pub unified_diff: Option<String>,
1617 pub requires_approval: bool,
1618 pub evidence: Vec<Evidence>,
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623 use super::{
1624 reconcile_score_breakdown, score_component_total, Confidence, ConfidenceBreakdown,
1625 ConfidenceSignalInput, EdgeId, Evidence, EvidenceSourceType, FileRange, GitChangeKind,
1626 GitCommitId, GitCommitRecord, GitFileTouch, GitSymbolTouch, GraphEdge, GraphEdgeType,
1627 GraphNode, GraphNodeType, HistoryRecordId, HistorySnapshot, HistorySummary, LineRange,
1628 NodeId, Owner, ScoreComponent, SymbolId, HISTORY_SCHEMA_VERSION,
1629 };
1630 use chrono::{TimeZone, Utc};
1631 use std::collections::BTreeMap;
1632
1633 #[test]
1634 fn reconciliation_adds_delta_to_match_surfaced_score() {
1635 let mut components = vec![ScoreComponent::single(
1636 "base",
1637 0.4,
1638 vec!["ev:base".into()],
1639 "base signal",
1640 )];
1641
1642 reconcile_score_breakdown(
1643 0.65,
1644 &mut components,
1645 "fallback",
1646 vec!["ev:adjust".into()],
1647 "test score",
1648 );
1649
1650 assert_eq!(components.len(), 2);
1651 assert!((score_component_total(&components) - 0.65).abs() < 0.001);
1652 assert_eq!(components[1].signal, "score_reconciliation");
1653 }
1654
1655 #[test]
1656 fn reconciliation_creates_fallback_for_empty_components() {
1657 let mut components = Vec::new();
1658
1659 reconcile_score_breakdown(
1660 0.85,
1661 &mut components,
1662 "confidence",
1663 vec!["test:id".into()],
1664 "test confidence",
1665 );
1666
1667 assert_eq!(components.len(), 1);
1668 assert_eq!(components[0].signal, "confidence");
1669 assert!((score_component_total(&components) - 0.85).abs() < 0.001);
1670 }
1671
1672 #[test]
1673 fn confidence_breakdown_is_stable_for_same_signals() {
1674 let input = ConfidenceSignalInput {
1675 primary_file_count: 2,
1676 evidence_count: 8,
1677 exact_reference_count: 2,
1678 validation_count: 2,
1679 validation_with_command_count: 1,
1680 negative_evidence_count: 0,
1681 allowed_file_count: 2,
1682 runtime_signal_count: 1,
1683 };
1684
1685 let first = ConfidenceBreakdown::from_signals(input);
1686 let second = ConfidenceBreakdown::from_signals(input);
1687
1688 assert_eq!(first.overall_enum, second.overall_enum);
1689 assert_eq!(first.overall_score, second.overall_score);
1690 assert_eq!(first.components, second.components);
1691 assert!(first.caveats.is_empty());
1692 assert!(first.blockers.is_empty());
1693 }
1694
1695 #[test]
1696 fn confidence_drops_without_exact_tests_or_runtime() {
1697 let grounded = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1698 primary_file_count: 1,
1699 evidence_count: 6,
1700 exact_reference_count: 1,
1701 validation_count: 1,
1702 validation_with_command_count: 1,
1703 negative_evidence_count: 0,
1704 allowed_file_count: 1,
1705 runtime_signal_count: 1,
1706 });
1707 let thin = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1708 primary_file_count: 1,
1709 evidence_count: 6,
1710 exact_reference_count: 0,
1711 validation_count: 0,
1712 validation_with_command_count: 0,
1713 negative_evidence_count: 0,
1714 allowed_file_count: 1,
1715 runtime_signal_count: 0,
1716 });
1717
1718 assert!(thin.overall_score < grounded.overall_score);
1719 assert_eq!(thin.overall_enum, Confidence::Medium);
1720 assert!(thin
1721 .caveats
1722 .iter()
1723 .any(|caveat| caveat.contains("exact symbol/reference")));
1724 assert!(thin
1725 .caveats
1726 .iter()
1727 .any(|caveat| caveat.contains("no validation")));
1728 assert!(thin
1729 .caveats
1730 .iter()
1731 .any(|caveat| caveat.contains("runtime corroboration")));
1732 }
1733
1734 #[test]
1735 fn negative_evidence_prevents_false_high_confidence() {
1736 let breakdown = ConfidenceBreakdown::from_signals(ConfidenceSignalInput {
1737 primary_file_count: 3,
1738 evidence_count: 12,
1739 exact_reference_count: 3,
1740 validation_count: 3,
1741 validation_with_command_count: 3,
1742 negative_evidence_count: 1,
1743 allowed_file_count: 3,
1744 runtime_signal_count: 1,
1745 });
1746
1747 assert!(breakdown.overall_score <= 0.60);
1748 assert_ne!(breakdown.overall_enum, Confidence::High);
1749 assert!(!breakdown.blockers.is_empty());
1750 }
1751
1752 #[test]
1753 fn history_snapshot_round_trips_with_versioned_records() {
1754 let committed_at = Utc.with_ymd_and_hms(2026, 6, 1, 12, 0, 0).unwrap();
1755 let commit = GitCommitRecord {
1756 id: GitCommitId::new("abc123"),
1757 parent_ids: vec![GitCommitId::new("parent123")],
1758 author: Owner {
1759 name: "Ada".into(),
1760 email: Some("ada@example.com".into()),
1761 },
1762 committer: None,
1763 authored_at: committed_at,
1764 committed_at,
1765 summary: "Add typed history".into(),
1766 message: "Add typed history\n\nPersist first-class records.".into(),
1767 file_count: 1,
1768 };
1769 let touch = GitFileTouch {
1770 id: HistoryRecordId::new("touch-1"),
1771 commit_id: commit.id.clone(),
1772 path: "src/history.rs".into(),
1773 previous_path: None,
1774 change_kind: GitChangeKind::Added,
1775 additions: Some(42),
1776 deletions: Some(0),
1777 touched_at: committed_at,
1778 };
1779 let snapshot = HistorySnapshot {
1780 schema_version: HISTORY_SCHEMA_VERSION,
1781 commits: vec![commit],
1782 file_touches: vec![touch],
1783 symbol_touches: Vec::new(),
1784 cochange_edges: Vec::new(),
1785 reviewer_evidence: Vec::new(),
1786 };
1787
1788 let json = serde_json::to_string(&snapshot).unwrap();
1789 let decoded: HistorySnapshot = serde_json::from_str(&json).unwrap();
1790
1791 assert_eq!(decoded, snapshot);
1792 assert_eq!(
1793 HistorySnapshot::empty().schema_version,
1794 HISTORY_SCHEMA_VERSION
1795 );
1796 }
1797
1798 #[test]
1799 fn empty_history_summary_exposes_uncertainty() {
1800 let summary = HistorySummary::empty("src/missing.rs");
1801
1802 assert!(summary.recent_commits.is_empty());
1803 assert!(!summary.uncertainty.is_empty());
1804 assert!(summary.uncertainty[0].contains("no persisted history evidence"));
1805 }
1806
1807 #[test]
1808 fn legacy_symbol_touch_json_remains_compatible() {
1809 let decoded: GitSymbolTouch = serde_json::from_value(serde_json::json!({
1810 "id": "touch",
1811 "commit_id": "abc123",
1812 "symbol_id": "symbol",
1813 "qualified_name": "crate::symbol",
1814 "file_path": "src/lib.rs",
1815 "change_kind": "modified",
1816 "touched_at": "2026-06-01T12:00:00Z"
1817 }))
1818 .unwrap();
1819
1820 assert_eq!(decoded.symbol_id, Some(SymbolId::new("symbol")));
1821 assert!(decoded.line_ranges.is_empty());
1822 assert_eq!(decoded.confidence, Confidence::Low);
1823 assert!(decoded.uncertainty.is_empty());
1824 }
1825
1826 #[test]
1827 fn legacy_graph_json_deserializes_with_default_metadata() {
1828 let decoded_node: GraphNode = serde_json::from_value(serde_json::json!({
1829 "id": "node:file",
1830 "node_type": "file",
1831 "label": "src/lib.rs",
1832 "file_id": "file:src/lib.rs",
1833 "symbol_id": null
1834 }))
1835 .unwrap();
1836 assert!(decoded_node.properties.is_empty());
1837 assert!(decoded_node.schema_version.is_none());
1838 assert!(decoded_node.ambiguity.is_empty());
1839 assert!(decoded_node.quality_notes.is_empty());
1840
1841 let decoded_edge: GraphEdge = serde_json::from_value(serde_json::json!({
1842 "id": "edge:defines",
1843 "from": "node:file",
1844 "to": "node:symbol",
1845 "edge_type": "DEFINES",
1846 "evidence": {
1847 "id": "evidence:legacy",
1848 "source": "tree-sitter",
1849 "source_type": "tree_sitter",
1850 "file_range": {
1851 "path": "src/lib.rs",
1852 "line_range": { "start": 1, "end": 3 }
1853 },
1854 "symbol_id": "symbol:main",
1855 "confidence": "high",
1856 "message": "legacy graph evidence",
1857 "indexed_at": "2026-06-01T12:00:00Z"
1858 }
1859 }))
1860 .unwrap();
1861 assert!(decoded_edge.properties.is_empty());
1862 assert!(decoded_edge.schema_version.is_none());
1863 assert!(decoded_edge.quality_notes.is_empty());
1864 assert!(decoded_edge.evidence.confidence_score.is_none());
1865 assert!(decoded_edge.evidence.confidence_reason.is_none());
1866 assert!(decoded_edge.evidence.freshness.is_none());
1867 }
1868
1869 #[test]
1870 fn enriched_graph_json_round_trips_metadata() {
1871 let indexed_at = Utc.with_ymd_and_hms(2026, 6, 1, 12, 0, 0).unwrap();
1872 let node = GraphNode {
1873 id: NodeId::new("node:file"),
1874 node_type: GraphNodeType::File,
1875 label: "src/lib.rs".into(),
1876 file_id: None,
1877 symbol_id: Some(SymbolId::new("symbol:main")),
1878 properties: BTreeMap::from([(
1879 "qualified_name".into(),
1880 serde_json::Value::String("crate::main".into()),
1881 )]),
1882 schema_version: Some("graph-v1".into()),
1883 source_pass: Some("tree_sitter".into()),
1884 index_mode: Some("scip".into()),
1885 extractor_version: Some("open-kioku-test".into()),
1886 ambiguity: vec!["overloaded symbol name".into()],
1887 quality_notes: vec!["exact definition".into()],
1888 };
1889 let edge = GraphEdge {
1890 id: EdgeId::new("edge:defines"),
1891 from: NodeId::new("node:file"),
1892 to: NodeId::new("node:symbol"),
1893 edge_type: GraphEdgeType::Defines,
1894 evidence: Evidence {
1895 id: super::EvidenceId::new("evidence:rich"),
1896 source: "scip".into(),
1897 source_type: EvidenceSourceType::Scip,
1898 file_range: Some(FileRange {
1899 path: "src/lib.rs".into(),
1900 line_range: Some(LineRange { start: 1, end: 1 }),
1901 }),
1902 symbol_id: Some(SymbolId::new("symbol:main")),
1903 confidence: Confidence::Exact,
1904 message: "exact reference".into(),
1905 indexed_at,
1906 confidence_score: Some(0.99),
1907 confidence_reason: Some("SCIP exact occurrence".into()),
1908 freshness: Some("fresh".into()),
1909 },
1910 properties: BTreeMap::from([("call_kind".into(), serde_json::json!("direct"))]),
1911 schema_version: Some("graph-v1".into()),
1912 source_pass: Some("scip".into()),
1913 index_mode: Some("full".into()),
1914 extractor_version: Some("scip-cli".into()),
1915 ambiguity: vec!["dynamic dispatch not expanded".into()],
1916 quality_notes: vec!["exact edge".into()],
1917 };
1918
1919 let decoded_node: GraphNode =
1920 serde_json::from_str(&serde_json::to_string(&node).unwrap()).unwrap();
1921 let decoded_edge: GraphEdge =
1922 serde_json::from_str(&serde_json::to_string(&edge).unwrap()).unwrap();
1923
1924 assert_eq!(decoded_node.properties, node.properties);
1925 assert_eq!(decoded_node.schema_version, Some("graph-v1".into()));
1926 assert_eq!(decoded_node.quality_notes, vec!["exact definition"]);
1927 assert_eq!(decoded_edge.properties, edge.properties);
1928 assert_eq!(decoded_edge.evidence.confidence_score, Some(0.99));
1929 assert_eq!(
1930 decoded_edge.evidence.confidence_reason.as_deref(),
1931 Some("SCIP exact occurrence")
1932 );
1933 assert_eq!(decoded_edge.evidence.freshness.as_deref(), Some("fresh"));
1934 }
1935}