1use clap::ValueEnum;
2use serde::Serialize;
3use wowlab_fs::path::Path;
4
5#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
6pub(super) enum LedgerFormat {
7 Json,
8 #[default]
9 Markdown,
10}
11
12#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub(super) enum OperationCategory {
15 Damage,
16 AuraApply,
17 AuraRemove,
18 AuraTick,
19 Resource,
20 Cooldown,
21 Proc,
22 Gate,
23 Replacement,
24 Targeting,
25 ActorOrPet,
26 Mastery,
27 TelemetryOnly,
28 OrderedContent,
29}
30
31impl OperationCategory {
32 #[expect(
33 clippy::trivially_copy_pass_by_ref,
34 reason = "conversion-style accessors conventionally borrow self"
35 )]
36 pub(super) const fn as_str(&self) -> &'static str {
37 match self {
38 Self::Damage => "damage",
39 Self::AuraApply => "aura_apply",
40 Self::AuraRemove => "aura_remove",
41 Self::AuraTick => "aura_tick",
42 Self::Resource => "resource",
43 Self::Cooldown => "cooldown",
44 Self::Proc => "proc",
45 Self::Gate => "gate",
46 Self::Replacement => "replacement",
47 Self::Targeting => "targeting",
48 Self::ActorOrPet => "actor_or_pet",
49 Self::Mastery => "mastery",
50 Self::TelemetryOnly => "telemetry_only",
51 Self::OrderedContent => "ordered_content",
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub(super) enum LedgerDisposition {
59 RedundantGeneric,
60 GenericGap,
61 DataAbsent,
62 ContentOrdering,
63 TelemetryBinding,
64 Unresolved,
65}
66
67impl LedgerDisposition {
68 #[expect(
69 clippy::trivially_copy_pass_by_ref,
70 reason = "conversion-style accessors conventionally borrow self"
71 )]
72 pub(super) const fn as_str(&self) -> &'static str {
73 match self {
74 Self::RedundantGeneric => "redundant_generic",
75 Self::GenericGap => "generic_gap",
76 Self::DataAbsent => "data_absent",
77 Self::ContentOrdering => "content_ordering",
78 Self::TelemetryBinding => "telemetry_binding",
79 Self::Unresolved => "unresolved",
80 }
81 }
82
83 pub(super) const fn sort_rank(self) -> u8 {
84 const GENERIC_GAP: u8 = 0;
85 const REDUNDANT_GENERIC: u8 = 1;
86 const DATA_ABSENT: u8 = 2;
87 const CONTENT_ORDERING: u8 = 3;
88 const TELEMETRY_BINDING: u8 = 4;
89 const UNRESOLVED: u8 = 5;
90
91 match self {
92 Self::GenericGap => GENERIC_GAP,
93 Self::RedundantGeneric => REDUNDANT_GENERIC,
94 Self::DataAbsent => DATA_ABSENT,
95 Self::ContentOrdering => CONTENT_ORDERING,
96 Self::TelemetryBinding => TELEMETRY_BINDING,
97 Self::Unresolved => UNRESOLVED,
98 }
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
103pub(super) struct LedgerRow {
104 pub(super) spec_or_item: String,
105 pub(super) manifest_path: String,
106 pub(super) manifest_key: String,
107 #[serde(skip_serializing_if = "Vec::is_empty")]
108 pub(super) owning_spell_ids: Vec<u32>,
109 #[serde(skip_serializing_if = "Vec::is_empty")]
110 pub(super) child_spell_ids: Vec<u32>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub(super) hook_file: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub(super) hook_function: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub(super) hook_operation: Option<usize>,
117 pub(super) category: OperationCategory,
118 #[serde(skip_serializing_if = "Vec::is_empty")]
119 pub(super) semantic_ids: Vec<Box<str>>,
120 pub(super) disposition: LedgerDisposition,
121 pub(super) evidence: String,
122 pub(super) spec_count: usize,
123 pub(super) operation_count: usize,
124 #[serde(skip)]
125 pub(super) effect_coordinates: Vec<(u32, u8)>,
126}
127
128pub(super) struct LedgerSource {
129 spec_or_item: String,
130 manifest_path: String,
131 manifest_key: String,
132}
133
134impl LedgerSource {
135 pub(super) fn new(
136 spec_or_item: impl Into<String>,
137 manifest_path: impl Into<String>,
138 manifest_key: impl Into<String>,
139 ) -> Self {
140 Self {
141 spec_or_item: spec_or_item.into(),
142 manifest_path: manifest_path.into(),
143 manifest_key: manifest_key.into(),
144 }
145 }
146}
147
148impl<Spec, Path, Key> From<(Spec, Path, Key)> for LedgerSource
149where
150 Spec: Into<String>,
151 Path: Into<String>,
152 Key: Into<String>,
153{
154 fn from((spec_or_item, manifest_path, manifest_key): (Spec, Path, Key)) -> Self {
155 Self::new(spec_or_item, manifest_path, manifest_key)
156 }
157}
158
159pub(super) struct LedgerFinding {
160 category: OperationCategory,
161 disposition: LedgerDisposition,
162 evidence: String,
163}
164
165impl LedgerFinding {
166 pub(super) fn new(
167 category: OperationCategory,
168 disposition: LedgerDisposition,
169 evidence: impl Into<String>,
170 ) -> Self {
171 Self {
172 category,
173 disposition,
174 evidence: evidence.into(),
175 }
176 }
177}
178
179impl<Evidence> From<(OperationCategory, LedgerDisposition, Evidence)> for LedgerFinding
180where
181 Evidence: Into<String>,
182{
183 fn from(
184 (category, disposition, evidence): (OperationCategory, LedgerDisposition, Evidence),
185 ) -> Self {
186 Self::new(category, disposition, evidence)
187 }
188}
189
190impl LedgerRow {
191 pub(super) fn new(source: impl Into<LedgerSource>, finding: impl Into<LedgerFinding>) -> Self {
192 let source = source.into();
193 let finding = finding.into();
194
195 Self {
196 spec_or_item: source.spec_or_item,
197 manifest_path: source.manifest_path,
198 manifest_key: source.manifest_key,
199 owning_spell_ids: Vec::new(),
200 child_spell_ids: Vec::new(),
201 hook_file: None,
202 hook_function: None,
203 hook_operation: None,
204 category: finding.category,
205 semantic_ids: Vec::new(),
206 disposition: finding.disposition,
207 evidence: finding.evidence,
208 spec_count: 0,
209 operation_count: 0,
210 effect_coordinates: Vec::new(),
211 }
212 }
213
214 pub(super) fn owner(mut self, spell_id: u32) -> Self {
215 self.owning_spell_ids.push(spell_id);
216
217 self
218 }
219
220 pub(super) fn child(mut self, spell_id: u32) -> Self {
221 self.child_spell_ids.push(spell_id);
222
223 self
224 }
225
226 pub(super) fn effect(mut self, spell_id: u32, effect: u8) -> Self {
227 self.effect_coordinates.push((spell_id, effect));
228
229 self
230 }
231
232 pub(super) fn hook(mut self, file: &Path, function: String) -> Self {
233 self.hook_file = Some(file.display().to_string());
234 self.hook_function = Some(function);
235
236 self
237 }
238}
239
240#[derive(Debug, Serialize)]
241pub(super) struct BehaviorShapeSummary {
242 pub(super) shape: String,
243 pub(super) observations: usize,
244 pub(super) specs: usize,
245}
246
247#[derive(Debug, Serialize)]
248pub(super) struct ManifestLedger {
249 pub(super) composed_specs: usize,
250 pub(super) items: usize,
251 pub(super) behavior_shapes: Vec<BehaviorShapeSummary>,
252 pub(super) category_totals: std::collections::BTreeMap<String, usize>,
253 pub(super) disposition_totals: std::collections::BTreeMap<String, usize>,
254 pub(super) rows: Vec<LedgerRow>,
255}