Skip to main content

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}