Skip to main content

verbs/
semantic_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure semantic hot-event kind tokens and labels (no store I/O, no CLI deps).
3//!
4//! Owns the stable snake_case labels used by `heddle semantic hot` human/JSON
5//! output and a pure mirror enum so callers can map clap / `semantic` kinds
6//! without pulling CLI types into core.
7//!
8//! [`HotEventKind`](semantic::analysis::HotEventKind) lives in the optional
9//! `semantic` crate. Bridge helpers under `cfg(feature = "semantic")` convert
10//! tokens ↔ that enum; label helpers stay always-on string/token pure code.
11
12/// Pure mirror of hot-event kinds used for planning and labels.
13///
14/// Variant names match `semantic::HotEventKind` and the CLI `HotEventKindArg`
15/// clap surface one-to-one.
16#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
17pub enum HotEventKindToken {
18    FileAdded,
19    FileDeleted,
20    FileModified,
21    FileRenamed,
22    FunctionExtracted,
23    FunctionDeleted,
24    FunctionRenamed,
25    FunctionModified,
26    FunctionMoved,
27    SignatureChanged,
28    DependencyChanged,
29}
30
31/// All tokens in a stable order (matches historical CLI match arm order).
32pub const HOT_EVENT_KIND_TOKENS: &[HotEventKindToken] = &[
33    HotEventKindToken::FileAdded,
34    HotEventKindToken::FileDeleted,
35    HotEventKindToken::FileModified,
36    HotEventKindToken::FileRenamed,
37    HotEventKindToken::FunctionExtracted,
38    HotEventKindToken::FunctionDeleted,
39    HotEventKindToken::FunctionRenamed,
40    HotEventKindToken::FunctionModified,
41    HotEventKindToken::FunctionMoved,
42    HotEventKindToken::SignatureChanged,
43    HotEventKindToken::DependencyChanged,
44];
45
46/// Human/JSON snake_case label for a hot-event kind token.
47///
48/// Mirrors historical CLI `human_event_kind` output (`file_added`, …).
49pub fn hot_event_kind_label(kind: HotEventKindToken) -> &'static str {
50    match kind {
51        HotEventKindToken::FileAdded => "file_added",
52        HotEventKindToken::FileDeleted => "file_deleted",
53        HotEventKindToken::FileModified => "file_modified",
54        HotEventKindToken::FileRenamed => "file_renamed",
55        HotEventKindToken::FunctionExtracted => "function_extracted",
56        HotEventKindToken::FunctionDeleted => "function_deleted",
57        HotEventKindToken::FunctionRenamed => "function_renamed",
58        HotEventKindToken::FunctionModified => "function_modified",
59        HotEventKindToken::FunctionMoved => "function_moved",
60        HotEventKindToken::SignatureChanged => "signature_changed",
61        HotEventKindToken::DependencyChanged => "dependency_changed",
62    }
63}
64
65/// Parse a hot-event kind from a string token.
66///
67/// Accepts snake_case labels (`file_added`), clap-style kebab-case
68/// (`file-added`), and PascalCase variant names (`FileAdded`).
69pub fn parse_hot_event_kind_token(raw: &str) -> Option<HotEventKindToken> {
70    match raw {
71        "file_added" | "file-added" | "FileAdded" => Some(HotEventKindToken::FileAdded),
72        "file_deleted" | "file-deleted" | "FileDeleted" => Some(HotEventKindToken::FileDeleted),
73        "file_modified" | "file-modified" | "FileModified" => Some(HotEventKindToken::FileModified),
74        "file_renamed" | "file-renamed" | "FileRenamed" => Some(HotEventKindToken::FileRenamed),
75        "function_extracted" | "function-extracted" | "FunctionExtracted" => {
76            Some(HotEventKindToken::FunctionExtracted)
77        }
78        "function_deleted" | "function-deleted" | "FunctionDeleted" => {
79            Some(HotEventKindToken::FunctionDeleted)
80        }
81        "function_renamed" | "function-renamed" | "FunctionRenamed" => {
82            Some(HotEventKindToken::FunctionRenamed)
83        }
84        "function_modified" | "function-modified" | "FunctionModified" => {
85            Some(HotEventKindToken::FunctionModified)
86        }
87        "function_moved" | "function-moved" | "FunctionMoved" => {
88            Some(HotEventKindToken::FunctionMoved)
89        }
90        "signature_changed" | "signature-changed" | "SignatureChanged" => {
91            Some(HotEventKindToken::SignatureChanged)
92        }
93        "dependency_changed" | "dependency-changed" | "DependencyChanged" => {
94            Some(HotEventKindToken::DependencyChanged)
95        }
96        _ => None,
97    }
98}
99
100/// Alias matching historical CLI name for the label helper.
101#[inline]
102pub fn human_event_kind(kind: HotEventKindToken) -> &'static str {
103    hot_event_kind_label(kind)
104}
105
106// ---------------------------------------------------------------------------
107// Optional bridge to `semantic::HotEventKind` (feature = "semantic")
108// ---------------------------------------------------------------------------
109
110#[cfg(feature = "semantic")]
111use semantic::analysis::HotEventKind;
112
113/// Map a pure token to [`HotEventKind`] (feature `semantic`).
114#[cfg(feature = "semantic")]
115pub fn map_hot_event_kind(token: HotEventKindToken) -> HotEventKind {
116    match token {
117        HotEventKindToken::FileAdded => HotEventKind::FileAdded,
118        HotEventKindToken::FileDeleted => HotEventKind::FileDeleted,
119        HotEventKindToken::FileModified => HotEventKind::FileModified,
120        HotEventKindToken::FileRenamed => HotEventKind::FileRenamed,
121        HotEventKindToken::FunctionExtracted => HotEventKind::FunctionExtracted,
122        HotEventKindToken::FunctionDeleted => HotEventKind::FunctionDeleted,
123        HotEventKindToken::FunctionRenamed => HotEventKind::FunctionRenamed,
124        HotEventKindToken::FunctionModified => HotEventKind::FunctionModified,
125        HotEventKindToken::FunctionMoved => HotEventKind::FunctionMoved,
126        HotEventKindToken::SignatureChanged => HotEventKind::SignatureChanged,
127        HotEventKindToken::DependencyChanged => HotEventKind::DependencyChanged,
128    }
129}
130
131/// Map [`HotEventKind`] back to a pure token (feature `semantic`).
132#[cfg(feature = "semantic")]
133pub fn hot_event_kind_token(kind: HotEventKind) -> HotEventKindToken {
134    match kind {
135        HotEventKind::FileAdded => HotEventKindToken::FileAdded,
136        HotEventKind::FileDeleted => HotEventKindToken::FileDeleted,
137        HotEventKind::FileModified => HotEventKindToken::FileModified,
138        HotEventKind::FileRenamed => HotEventKindToken::FileRenamed,
139        HotEventKind::FunctionExtracted => HotEventKindToken::FunctionExtracted,
140        HotEventKind::FunctionDeleted => HotEventKindToken::FunctionDeleted,
141        HotEventKind::FunctionRenamed => HotEventKindToken::FunctionRenamed,
142        HotEventKind::FunctionModified => HotEventKindToken::FunctionModified,
143        HotEventKind::FunctionMoved => HotEventKindToken::FunctionMoved,
144        HotEventKind::SignatureChanged => HotEventKindToken::SignatureChanged,
145        HotEventKind::DependencyChanged => HotEventKindToken::DependencyChanged,
146    }
147}
148
149/// Label a [`HotEventKind`] via the pure token table (feature `semantic`).
150#[cfg(feature = "semantic")]
151pub fn human_hot_event_kind(kind: HotEventKind) -> &'static str {
152    hot_event_kind_label(hot_event_kind_token(kind))
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn labels_are_snake_case_and_stable() {
161        assert_eq!(
162            hot_event_kind_label(HotEventKindToken::FileAdded),
163            "file_added"
164        );
165        assert_eq!(
166            hot_event_kind_label(HotEventKindToken::SignatureChanged),
167            "signature_changed"
168        );
169        assert_eq!(
170            human_event_kind(HotEventKindToken::DependencyChanged),
171            "dependency_changed"
172        );
173        assert_eq!(HOT_EVENT_KIND_TOKENS.len(), 11);
174        for token in HOT_EVENT_KIND_TOKENS {
175            let label = hot_event_kind_label(*token);
176            assert!(label.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
177            assert_eq!(parse_hot_event_kind_token(label), Some(*token));
178        }
179    }
180
181    #[test]
182    fn parse_accepts_snake_kebab_and_pascal() {
183        assert_eq!(
184            parse_hot_event_kind_token("file-modified"),
185            Some(HotEventKindToken::FileModified)
186        );
187        assert_eq!(
188            parse_hot_event_kind_token("FunctionMoved"),
189            Some(HotEventKindToken::FunctionMoved)
190        );
191        assert_eq!(parse_hot_event_kind_token("not-a-kind"), None);
192    }
193
194    #[cfg(feature = "semantic")]
195    #[test]
196    fn semantic_bridge_round_trips() {
197        for token in HOT_EVENT_KIND_TOKENS {
198            let kind = map_hot_event_kind(*token);
199            assert_eq!(hot_event_kind_token(kind), *token);
200            assert_eq!(human_hot_event_kind(kind), hot_event_kind_label(*token));
201        }
202    }
203}