seshat_core/detector_result.rs
1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4use crate::knowledge::KnowledgeNature;
5
6/// Structural classification of a [`ConventionFinding`].
7///
8/// Replaces ad-hoc string matching on `description.contains("(heuristic)")`
9/// scattered across the pipeline. Each emit site sets the kind explicitly
10/// at construction time; downstream consumers (filters, aggregators, the
11/// review TUI) match on this enum instead of parsing free-form text.
12///
13/// `Other` is the [`Default`] fallback for legacy data deserialised from
14/// older DBs that predate this field. New code MUST set the kind
15/// explicitly.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum FindingKind {
19 /// Canonical library for a domain — emitted by `dependency_usage`.
20 /// Description shape: `"Canonical {domain} library: {pkg}"`.
21 Canonical,
22 /// Heuristic name-based observation. Description shape:
23 /// `"Likely {domain} library (heuristic): {pkg}"` /
24 /// `"Possible logging library (name heuristic): {module}"`.
25 Heuristic,
26 /// Logging style observation: `"Logging style: {structured|unstructured} logging"`.
27 Style,
28 /// Multiple competing libraries in the same file:
29 /// `"Conflicting {domain} libraries in same file: A, B"`.
30 Conflict,
31 /// Naming-convention findings — function / parameter / type / file naming.
32 Naming,
33 /// File-level structural conventions: by-feature dirs, src-layout, etc.
34 FileStructure,
35 /// Import organization: ordering, grouping, blank-line separation.
36 ImportOrganization,
37 /// Test-related conventions: framework, placement, fixture style.
38 Testing,
39 /// Error handling conventions: Result types, custom enums, etc.
40 ErrorHandling,
41 /// Export / re-export conventions.
42 Export,
43 /// Cross-file wrapper / facade detection in `dependency_usage`.
44 DependencyWrapper,
45 /// Backward-compat fallback for findings deserialised from older DBs
46 /// or from external callers that have not been migrated yet.
47 #[default]
48 Other,
49}
50
51/// How a single [`CodeEvidence`] row is anchored in source.
52///
53/// Each anchor kind has a different downstream policy:
54/// - `CallSite` / `Declaration` get source-extracted snippets via
55/// `detect_with_source`.
56/// - `ImportLine` is the dependency_usage import-line fallback —
57/// evidence at the line that brings the lib into scope when no
58/// real call sites exist.
59/// - `FileLevel` are synthetic file-level signals (line == 0) with a
60/// pre-populated descriptive snippet that must NOT be overwritten.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
62#[serde(rename_all = "snake_case")]
63pub enum AnchorKind {
64 /// Real call site — function/method call, macro invocation, or
65 /// derive macro. Extracted snippet shows the actual usage.
66 #[default]
67 CallSite,
68 /// Declaration site — `fn foo()`, `struct Bar`, parameter line, etc.
69 Declaration,
70 /// `use foo::*` import line, used as fallback when no call sites
71 /// exist for a canonical lib (rayon prelude, transitive deps).
72 ImportLine,
73 /// Synthetic file-level signal: line == 0, snippet is a
74 /// human-readable description set by the detector.
75 FileLevel,
76}
77
78/// Output of a single convention detector for a single file.
79///
80/// Lives in `seshat-core` because it flows: detectors -> storage -> graph.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub struct ConventionFinding {
84 pub file_path: PathBuf,
85 pub detector_name: String,
86 pub nature: KnowledgeNature,
87 /// Structural classification — see [`FindingKind`]. Defaults to
88 /// [`FindingKind::Other`] for backward-compat deserialisation of
89 /// older DB rows.
90 #[serde(default)]
91 pub kind: FindingKind,
92 pub description: String,
93 pub evidence: Vec<CodeEvidence>,
94 /// Whether this file follows the detected convention pattern.
95 pub follows_convention: bool,
96}
97
98/// A snippet of code serving as evidence for a finding.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub struct CodeEvidence {
102 /// Path to the source file this evidence comes from.
103 pub file: PathBuf,
104 pub line: usize,
105 pub end_line: usize,
106 /// Real source code lines extracted from the file.
107 /// Empty string when only IR-based detection was run (unchanged files).
108 pub snippet: String,
109 /// Line number where the snippet text starts.
110 /// May be less than `line` when leading context lines are included.
111 /// Defaults to 0 (meaning: use `line` as the start).
112 #[serde(default)]
113 pub snippet_start_line: usize,
114 /// How this row is anchored — see [`AnchorKind`]. Defaults to
115 /// [`AnchorKind::CallSite`] for backward-compat deserialisation.
116 #[serde(default)]
117 pub anchor: AnchorKind,
118}
119
120/// Aggregate output of all detectors for a single file.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub struct DetectorResults {
124 pub file_path: PathBuf,
125 pub findings: Vec<ConventionFinding>,
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn snippet_start_line_backward_compat_deserialization() {
134 let json = r#"{
135 "file": "src/main.rs",
136 "line": 10,
137 "end_line": 12,
138 "snippet": "fn main() {}"
139 }"#;
140 let evidence: CodeEvidence = serde_json::from_str(json).unwrap();
141 assert_eq!(evidence.snippet_start_line, 0);
142 assert_eq!(evidence.line, 10);
143 }
144}