Skip to main content

str_format/
baseline.rs

1//! `E_REVISION_STALE` 的**历史基线**(`._cache/revisions.json`)。
2//!
3//! 规范 §6.1 的 `E_REVISION_STALE` 是「`updated_at` 变化但 `revision` 未前进」——这是
4//! **历史相关**判定,单看一份 `._meta` 无从下手(`created_at` / `updated_at` / `revision`
5//! 都是自描述的,文件本身不携带「上一版」)。
6//!
7//! 因此由**写入端**在每次成功写盘后登记当前快照,校验端只读比对:
8//!
9//! - 写入端:`str` 的全部写命令(经 `cmd::save_meta`)与 `str sync`(经 [`record_scan`]);
10//! - 校验端:[`crate::validate`] 读基线,`updated_at` 变了而 `revision` 未前进即报码。
11//!
12//! 基线文件是**派生数据**:位于 `._cache/`(格式保留名、不入 `entries` 清单、`.gitignore`
13//! 已排除),删掉即关闭这项检查;`str sync` 会按当前扫描结果整份重建,因此不会积累陈旧条目。
14
15use std::collections::BTreeMap;
16use std::path::{Path, PathBuf};
17
18use serde::{Deserialize, Serialize};
19
20use crate::bundle::{Bundle, Scan};
21use crate::meta::Meta;
22use crate::util::CACHE_DIR;
23
24/// 基线文件名(位于 `._cache/` 下)。
25const FILE: &str = "revisions.json";
26
27/// 一个分支的基线快照。
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct Snap {
30    /// 上次写盘时的 `revision`。
31    pub revision: i64,
32    /// 上次写盘时的 `updated_at`(TOML 原生 offset date-time 的字符串形式)。
33    pub updated_at: String,
34}
35
36/// 全 bundle 的基线:`._meta` 所在目录的相对路径 → 快照。
37///
38/// 键与 `Visit::rel` 一致:ROOT 为 `"."`,其余为 `"<uuid>/"` 形式。
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct Baseline {
41    /// 各分支的基线快照。
42    #[serde(default)]
43    pub branches: BTreeMap<String, Snap>,
44}
45
46/// 基线文件路径。
47fn file_path(bundle: &Bundle) -> PathBuf {
48    bundle.root.join(CACHE_DIR).join(FILE)
49}
50
51/// 从 `._meta` 取快照;`revision` / `updated_at` 缺任一者则视为不可登记。
52fn snap_of(meta: &Meta) -> Option<Snap> {
53    Some(Snap {
54        revision: meta.revision?,
55        updated_at: meta.updated_at.clone()?,
56    })
57}
58
59/// 读取基线。文件缺失 / 损坏一律退化为「无基线」:只关闭历史检查,不阻断主流程。
60pub fn load(bundle: &Bundle) -> Baseline {
61    std::fs::read_to_string(file_path(bundle))
62        .ok()
63        .and_then(|t| serde_json::from_str(&t).ok())
64        .unwrap_or_default()
65}
66
67/// 写回基线。**尽力而为**:写失败只影响历史检查,不影响调用方的主流程。
68fn store(bundle: &Bundle, baseline: &Baseline) {
69    let path = file_path(bundle);
70    if let Some(parent) = path.parent()
71        && std::fs::create_dir_all(parent).is_err()
72    {
73        return;
74    }
75    if let Ok(text) = serde_json::to_string_pretty(baseline) {
76        let _ = std::fs::write(path, format!("{text}\n"));
77    }
78}
79
80/// 登记单个分支的当前状态(单个写命令用)。
81pub fn record(bundle: &Bundle, dir: &Path, meta: &Meta) {
82    let Some(snap) = snap_of(meta) else {
83        return;
84    };
85    let mut baseline = load(bundle);
86    baseline.branches.insert(bundle.rel(dir), snap);
87    store(bundle, &baseline);
88}
89
90/// 以一次扫描的结果校准基线(`str sync` / `str branch rm` 用)。
91///
92/// 与 [`record`] 的关键差别:**只有 `revision` 确实前进才推进基线**。否则保留旧快照,
93/// 让 §6.1 的违规在后续 `validate` 中**持续可见**,直到有人真正修好 `revision` ——
94/// 若这里无条件覆盖,`str sync` 会把刚犯下的违规顺手洗白。
95///
96/// 同时丢弃已消失分支的陈旧条目(`str branch rm` 之后)。
97pub fn record_scan(bundle: &Bundle, scan: &Scan) {
98    let mut baseline = load(bundle);
99    let mut seen = std::collections::BTreeSet::new();
100    for v in &scan.visits {
101        let Some(meta) = v.meta.as_ref() else {
102            continue;
103        };
104        let Some(snap) = snap_of(meta) else {
105            continue;
106        };
107        seen.insert(v.rel.clone());
108        match baseline.branches.get(&v.rel) {
109            None => {
110                baseline.branches.insert(v.rel.clone(), snap);
111            }
112            Some(old) if snap.revision > old.revision => {
113                baseline.branches.insert(v.rel.clone(), snap);
114            }
115            Some(_) => {}
116        }
117    }
118    baseline.branches.retain(|rel, _| seen.contains(rel));
119    store(bundle, &baseline);
120}