fallow-types 3.34.1

Shared types and serde paths for fallow codebase intelligence
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
//! Typed audit cache-key inputs.

use serde::Serialize;

use crate::source_fingerprint::SourceFingerprint;

/// Filesystem state of one bounded audit context input.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditContextPathState {
    /// The path does not exist.
    Missing,
    /// The path exists and was read successfully.
    Present,
    /// The path exists, but its metadata or contents could not be read.
    Unreadable(String),
}

/// Fingerprint of one file that can affect base-worktree resolution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditContextFileFingerprint {
    /// Project-root-relative, forward-slash path.
    pub path: String,
    /// Presence or read-error state.
    pub state: AuditContextPathState,
    /// Source metadata when readable.
    pub source: Option<SourceFingerprint>,
    /// Stable content hash when readable.
    pub content_hash: Option<String>,
}

/// Fingerprint of one host directory materialized into an audit base worktree.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditContextDirectoryFingerprint {
    /// Directory name relative to the analysis root.
    pub name: String,
    /// Presence or metadata-error state.
    pub state: AuditContextPathState,
    /// Canonical source identity when available.
    pub canonical_path: Option<String>,
    /// Root directory metadata when readable.
    pub source: Option<SourceFingerprint>,
    /// Bounded marker files whose contents affect resolution.
    pub markers: Vec<AuditContextFileFingerprint>,
}

/// Bounded host context shared with an audit base worktree.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditMaterializedContextFingerprint {
    /// Current package-manager lockfiles at the analysis root.
    pub lockfiles: Vec<AuditContextFileFingerprint>,
    /// Dependency and generated-context directories shared with the base view.
    pub directories: Vec<AuditContextDirectoryFingerprint>,
}

/// Fingerprint of the resolved config that can affect audit output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditConfigFingerprint {
    /// Path of the config file that was loaded, or `None` when no config exists.
    pub path: Option<String>,
    /// Stable hash of the resolved config object.
    pub resolved_hash: Option<String>,
}

/// Fingerprint of an optional coverage input that can affect health findings.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditCoverageFingerprint {
    /// User-provided coverage path.
    pub path: String,
    /// Actual file path hashed after directory resolution.
    pub resolved_path: String,
    /// Metadata freshness for the resolved coverage file, when it was readable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<SourceFingerprint>,
    /// Stable content hash for the resolved coverage file, when it was readable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_hash: Option<String>,
    /// File length in bytes, when it was readable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub len: Option<usize>,
    /// I/O error kind, when the resolved coverage file was not readable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// Typed payload hashed to address an audit base-snapshot cache entry.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct AuditCacheKeyPayload {
    /// Audit base snapshot cache schema version.
    pub cache_version: u8,
    /// Fallow CLI version that produced the key.
    pub cli_version: String,
    /// Resolved git SHA for the base ref.
    pub base_sha: String,
    /// Config fingerprint.
    pub config_file: AuditConfigFingerprint,
    /// Changed files normalized to git-root-relative, forward-slash paths.
    pub changed_files: Vec<String>,
    /// Host dependency and generated context materialized into the base view.
    pub materialized_context: AuditMaterializedContextFingerprint,
    /// Global production mode.
    pub production: bool,
    /// Dead-code-specific production override.
    pub production_dead_code: Option<bool>,
    /// Health-specific production override.
    pub production_health: Option<bool>,
    /// Duplication-specific production override.
    pub production_dupes: Option<bool>,
    /// Workspace filters.
    pub workspace: Option<Vec<String>>,
    /// Changed-workspaces base ref, when enabled.
    pub changed_workspaces: Option<String>,
    /// Grouping mode.
    pub group_by: Option<String>,
    /// Whether entry exports are analyzed.
    pub include_entry_exports: bool,
    /// Keep the export and member findings of unused files (`--show-cascade`).
    pub show_cascade: bool,
    /// Whether the base pass ran for the review brief (`--brief`). Only a
    /// brief base pass computes the public-export set of the snapshot.
    pub brief: bool,
    /// CRAP threshold override.
    pub max_crap: Option<f64>,
    /// Coverage input fingerprint.
    pub coverage: Option<AuditCoverageFingerprint>,
    /// Coverage root override.
    pub coverage_root: Option<String>,
    /// Whether audit ran the dead-code sub-analysis for the base snapshot.
    pub dead_code: bool,
    /// Whether audit ran the health sub-analysis for the base snapshot.
    pub health: bool,
    /// Whether audit ran the duplication sub-analysis for the base snapshot.
    pub dupes: bool,
    /// Which dead-code findings the selection keeps: `all`, `only` (the
    /// architecture findings) or `exclude` (all but the architecture findings).
    pub architecture: String,
    /// Whether audit health computed styling keys for the base snapshot.
    pub css: bool,
    /// Whether audit health used deep CSS analysis for the base snapshot.
    pub css_deep: bool,
    /// Dead-code baseline path.
    pub dead_code_baseline: Option<String>,
    /// Health baseline path.
    pub health_baseline: Option<String>,
    /// Duplication baseline path.
    pub dupes_baseline: Option<String>,
}

/// Builder for audit base-snapshot cache keys.
#[derive(Debug, Clone)]
pub struct AuditCacheKeyBuilder {
    payload: AuditCacheKeyPayload,
}

impl AuditCacheKeyBuilder {
    /// Start a cache-key payload with the invariant identity fields.
    #[must_use]
    pub fn new(
        cache_version: u8,
        cli_version: impl Into<String>,
        base_sha: impl Into<String>,
        config_file: AuditConfigFingerprint,
        changed_files: Vec<String>,
        materialized_context: AuditMaterializedContextFingerprint,
    ) -> Self {
        Self {
            payload: AuditCacheKeyPayload {
                cache_version,
                cli_version: cli_version.into(),
                base_sha: base_sha.into(),
                config_file,
                changed_files,
                materialized_context,
                production: false,
                production_dead_code: None,
                production_health: None,
                production_dupes: None,
                workspace: None,
                changed_workspaces: None,
                group_by: None,
                include_entry_exports: false,
                show_cascade: false,
                brief: false,
                max_crap: None,
                coverage: None,
                coverage_root: None,
                dead_code: true,
                health: true,
                dupes: true,
                architecture: "all".to_string(),
                css: false,
                css_deep: false,
                dead_code_baseline: None,
                health_baseline: None,
                dupes_baseline: None,
            },
        }
    }

    /// Set production-mode options.
    #[must_use]
    pub const fn production(
        mut self,
        production: bool,
        dead_code: Option<bool>,
        health: Option<bool>,
        dupes: Option<bool>,
    ) -> Self {
        self.payload.production = production;
        self.payload.production_dead_code = dead_code;
        self.payload.production_health = health;
        self.payload.production_dupes = dupes;
        self
    }

    /// Set scope and grouping options.
    #[must_use]
    pub fn scope(
        mut self,
        workspace: Option<Vec<String>>,
        changed_workspaces: Option<String>,
        group_by: Option<String>,
        include_entry_exports: bool,
    ) -> Self {
        self.payload.workspace = workspace;
        self.payload.changed_workspaces = changed_workspaces;
        self.payload.group_by = group_by;
        self.payload.include_entry_exports = include_entry_exports;
        self
    }

    /// Set whether the export and member findings of unused files stay in
    /// the report.
    #[must_use]
    pub const fn show_cascade(mut self, show_cascade: bool) -> Self {
        self.payload.show_cascade = show_cascade;
        self
    }

    /// Set whether the base pass ran for the review brief (`--brief`).
    #[must_use]
    pub const fn brief(mut self, brief: bool) -> Self {
        self.payload.brief = brief;
        self
    }

    /// Set health and coverage options.
    #[must_use]
    pub fn health(
        mut self,
        max_crap: Option<f64>,
        coverage: Option<AuditCoverageFingerprint>,
        coverage_root: Option<String>,
    ) -> Self {
        self.payload.max_crap = max_crap;
        self.payload.coverage = coverage;
        self.payload.coverage_root = coverage_root;
        self
    }

    /// Set which sub-analyses ran for the base snapshot, and which dead-code
    /// findings the architecture selection kept.
    #[must_use]
    pub fn analyses(
        mut self,
        dead_code: bool,
        health: bool,
        dupes: bool,
        architecture: impl Into<String>,
    ) -> Self {
        self.payload.dead_code = dead_code;
        self.payload.health = health;
        self.payload.dupes = dupes;
        self.payload.architecture = architecture.into();
        self
    }

    /// Set styling-analysis options that affect base health snapshot keys.
    #[must_use]
    pub const fn styling(mut self, css: bool, css_deep: bool) -> Self {
        self.payload.css = css;
        self.payload.css_deep = css_deep;
        self
    }

    /// Set baseline paths.
    #[must_use]
    pub fn baselines(
        mut self,
        dead_code: Option<String>,
        health: Option<String>,
        dupes: Option<String>,
    ) -> Self {
        self.payload.dead_code_baseline = dead_code;
        self.payload.health_baseline = health;
        self.payload.dupes_baseline = dupes;
        self
    }

    /// Borrow the completed payload.
    #[must_use]
    pub const fn payload(&self) -> &AuditCacheKeyPayload {
        &self.payload
    }

    /// Serialize the completed payload into stable JSON bytes for hashing.
    ///
    /// # Errors
    ///
    /// Returns a serde error when a payload field cannot be serialized.
    pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
        serde_json::to_vec(&self.payload)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn config() -> AuditConfigFingerprint {
        AuditConfigFingerprint {
            path: Some("fallow.toml".to_string()),
            resolved_hash: Some("abc".to_string()),
        }
    }

    fn context() -> AuditMaterializedContextFingerprint {
        AuditMaterializedContextFingerprint {
            lockfiles: Vec::new(),
            directories: Vec::new(),
        }
    }

    #[test]
    fn audit_cache_key_builder_preserves_typed_fields() {
        let coverage = AuditCoverageFingerprint {
            path: "coverage".to_string(),
            resolved_path: "coverage/coverage-final.json".to_string(),
            source: Some(SourceFingerprint::new(12, 34)),
            content_hash: Some("hash".to_string()),
            len: Some(34),
            error: None,
        };

        let builder = AuditCacheKeyBuilder::new(
            3,
            "1.2.3",
            "abc123",
            config(),
            vec!["src/a.ts".to_string()],
            context(),
        )
        .production(true, Some(false), Some(true), None)
        .scope(
            Some(vec!["web".to_string()]),
            Some("main".to_string()),
            Some("Package".to_string()),
            true,
        )
        .health(Some(42.0), Some(coverage), Some("/workspace".to_string()))
        .styling(true, true)
        .baselines(
            Some("dead.json".to_string()),
            Some("health.json".to_string()),
            Some("dupes.json".to_string()),
        );

        let payload = builder.payload();
        assert_eq!(payload.cache_version, 3);
        assert_eq!(payload.base_sha, "abc123");
        assert_eq!(payload.workspace.as_deref(), Some(&["web".to_string()][..]));
        assert!(payload.include_entry_exports);
        assert!(payload.css);
        assert!(payload.css_deep);
        assert_eq!(
            payload.coverage.as_ref().and_then(|c| c.source),
            Some(SourceFingerprint::new(12, 34))
        );
    }

    #[test]
    fn audit_cache_key_bytes_reflect_changed_file_order() {
        let first = AuditCacheKeyBuilder::new(
            1,
            "1.0.0",
            "base",
            config(),
            vec!["src/a.ts".to_string(), "src/b.ts".to_string()],
            context(),
        )
        .to_json_bytes()
        .expect("payload should serialize");
        let second = AuditCacheKeyBuilder::new(
            1,
            "1.0.0",
            "base",
            config(),
            vec!["src/b.ts".to_string(), "src/a.ts".to_string()],
            context(),
        )
        .to_json_bytes()
        .expect("payload should serialize");

        assert_ne!(first, second);
    }

    #[test]
    fn audit_cache_key_bytes_include_the_analysis_selection() {
        let build = |dead_code, health, dupes, architecture: &str| {
            AuditCacheKeyBuilder::new(
                1,
                "1.0.0",
                "base",
                config(),
                vec!["src/a.ts".to_string()],
                context(),
            )
            .analyses(dead_code, health, dupes, architecture)
            .to_json_bytes()
            .expect("payload should serialize")
        };

        let all = build(true, true, true, "all");
        assert_ne!(all, build(false, true, true, "all"));
        assert_ne!(all, build(true, false, true, "all"));
        assert_ne!(all, build(true, true, false, "all"));
        assert_ne!(all, build(true, true, true, "exclude"));
    }

    #[test]
    fn audit_cache_key_bytes_include_materialized_context() {
        let missing = context();
        let present = AuditMaterializedContextFingerprint {
            lockfiles: vec![AuditContextFileFingerprint {
                path: "pnpm-lock.yaml".to_string(),
                state: AuditContextPathState::Present,
                source: Some(SourceFingerprint::new(1, 10)),
                content_hash: Some("lock-hash".to_string()),
            }],
            directories: Vec::new(),
        };
        let build = |materialized_context| {
            AuditCacheKeyBuilder::new(
                5,
                "1.0.0",
                "base",
                config(),
                vec!["src/a.ts".to_string()],
                materialized_context,
            )
            .to_json_bytes()
            .expect("payload should serialize")
        };

        let missing_json = build(missing);
        let present_json = build(present);

        assert_ne!(missing_json, present_json);
        assert!(
            String::from_utf8(present_json)
                .expect("utf8 json")
                .contains("pnpm-lock.yaml")
        );
    }
}