Skip to main content

nichlink_run_method/runtime/
graft_record.rs

1//! Graft records on disk: loading `.nichlink/external-grafts/` and applying it.
2//! 磁盘上的嫁接记录:读取 `.nichlink/external-grafts/` 并应用。
3//!
4//! The kernel owns reconciliation
5//! ([`Registry::overlay_recorded`](nichlink::Registry::overlay_recorded)); this
6//! page owns the filesystem boundary. It is deliberately **not**
7//! feature-gated: a runtime host must not need the `authoring` feature (and its
8//! `syn` dependency) merely to read a plan file, so the loader takes an explicit
9//! `package_root` instead of the authoring thread-local context.
10//! 内核拥有对账([`Registry::overlay_recorded`](nichlink::Registry::overlay_recorded));
11//! 本页拥有文件系统边界。它刻意**不**受特性门控:运行期宿主读取计划文件不该需要
12//! `authoring` 特性(及其 `syn` 依赖),因此加载器接收显式的 `package_root`,而不是
13//! 创作期的线程局部上下文。
14//!
15//! One parser and one layout serve Studio and hosts: this module reuses
16//! [`GraftPlanDocument::parse`] and the [`lexicon`] constants, and
17//! `authoring::external_graft` delegates here.
18//! 一个解析器、一套版式同时服务 Studio 与宿主:本模块复用
19//! [`GraftPlanDocument::parse`] 与 [`lexicon`] 常量,`authoring::external_graft`
20//! 委派到这里。
21
22use std::fs;
23use std::path::{Path, PathBuf};
24
25use nichlink::lexicon;
26
27use crate::{GraftPlanDocument, Registry, StaticGraftCut};
28
29pub use crate::registry_core::tree::graft_ops::{RecordReport, RecordedGraft, ResolvedRecord};
30
31/// The directory that owns every external graft plan under `package_root`.
32/// `package_root` 下拥有全部外部 graft 计划的目录。
33///
34/// `package_root` is a parameter, not a thread-local or an environment
35/// variable, so a test and a host can point at a throwaway tree without
36/// mutating process-global state.
37/// `package_root` 是参数,而不是线程局部变量或环境变量,因此测试与宿主都能指向一棵
38/// 一次性目录树,而不必改动进程级状态。
39pub fn graft_record_root(package_root: &Path) -> PathBuf {
40    package_root
41        .join(lexicon::NICHLINK_DIR)
42        .join(lexicon::EXTERNAL_GRAFT_DIR)
43}
44
45/// One directory under `.nichlink/external-grafts/`, read or broken.
46/// `.nichlink/external-grafts/` 下的一个目录,读得懂或坏了。
47///
48/// A broken plan is reported rather than hidden: the author has to see it, and
49/// the other records still apply. The build treats the same file as advisory
50/// (`build_method::graft_plan_check`), so the runtime does not let it abort.
51/// 坏计划会被报告而不是藏起来:作者必须看见它,其余记录照常应用。构建把同一文件当作
52/// 提示(`build_method::graft_plan_check`),因此运行期也不会因它中止。
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum LoadedGraft {
55    /// A plan directory that parsed into a usable recorded graft.
56    /// 解析成功、可用的已记录 graft 计划目录。
57    Record(RecordedGraft),
58    /// A plan directory that could not be read; the reason is reported.
59    /// 无法读取的计划目录;原因随条目一同上报。
60    Unreadable {
61        /// The directory name this entry came from.
62        /// 本条目来自的目录名。
63        selector: String,
64        /// Why the plan could not be read or parsed.
65        /// 计划无法读取或解析的原因。
66        reason: String,
67    },
68}
69
70impl LoadedGraft {
71    /// The directory name this entry came from.
72    /// 本条目来自的目录名。
73    pub fn selector(&self) -> &str {
74        match self {
75            Self::Record(record) => &record.selector,
76            Self::Unreadable { selector, .. } => selector,
77        }
78    }
79}
80
81/// Every plan directory under `package_root`, readable or not, sorted by
82/// selector.
83/// `package_root` 下的每个计划目录,无论是否可读,按选择器排序。
84///
85/// A missing `.nichlink/external-grafts/` is an empty list, not an error: most
86/// packages have no records at all.
87/// `.nichlink/external-grafts/` 不存在时返回空列表而不是错误:大多数包根本没有记录。
88pub fn load_graft_records(package_root: &Path) -> Result<Vec<LoadedGraft>, String> {
89    let root = graft_record_root(package_root);
90    let entries = match fs::read_dir(&root) {
91        Ok(entries) => entries,
92        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
93        Err(error) => return Err(format!("cannot scan {}: {error}", root.display())),
94    };
95    let mut loaded = Vec::new();
96    for entry in entries {
97        let entry = entry.map_err(|error| format!("cannot scan {}: {error}", root.display()))?;
98        if !entry.path().is_dir() {
99            continue;
100        }
101        let selector = entry.file_name().to_string_lossy().into_owned();
102        let path = entry.path().join(lexicon::GRAFT_PLAN_FILE);
103        let document = fs::read_to_string(&path)
104            .map_err(|error| format!("cannot read {}: {error}", path.display()))
105            .and_then(|text| GraftPlanDocument::parse(&text).map_err(|error| format!("{error}")));
106        match document {
107            Ok(document) => {
108                loaded.push(LoadedGraft::Record(RecordedGraft::new(selector, document)))
109            }
110            Err(reason) => loaded.push(LoadedGraft::Unreadable { selector, reason }),
111        }
112    }
113    loaded.sort_by(|left, right| left.selector().cmp(right.selector()));
114    Ok(loaded)
115}
116
117/// Read one plan back by its directory selector.
118/// 按目录选择器读回一条计划。
119///
120/// The selector is validated before it is joined, so a record cannot address a
121/// path outside `.nichlink/external-grafts/`.
122/// 选择器在拼接之前先校验,因此记录无法寻址 `.nichlink/external-grafts/` 之外的路径。
123pub fn load_graft_record(package_root: &Path, selector: &str) -> Result<GraftPlanDocument, String> {
124    let selector = selector.trim();
125    crate::registry_core::validate_graft_selector(selector)?;
126    let path = graft_record_root(package_root)
127        .join(selector)
128        .join(lexicon::GRAFT_PLAN_FILE);
129    let text = fs::read_to_string(&path)
130        .map_err(|error| format!("cannot read {}: {error}", path.display()))?;
131    GraftPlanDocument::parse(&text).map_err(|error| format!("{}: {error}", path.display()))
132}
133
134/// The effective tree record files produced, with their evidence.
135/// 记录文件产生的有效树及其证据。
136///
137/// `reports` is the kernel's reconciliation evidence for the records that were
138/// carried through. It is also printed by [`apply_recorded_grafts`], so a host
139/// that never looks at this struct still sees every problem. Records the kernel
140/// refuses, and plans this loader cannot parse, never reach here: they are an
141/// `Err` instead of a field.
142/// `reports` 是被带过来的那些记录在内核里的对账证据。它也会由
143/// [`apply_recorded_grafts`] 打印,因此从不查看本结构体的宿主依然能看到每一个问题。
144/// 内核拒绝的记录与本加载器解析不了的计划到不了这里:它们是 `Err`,而不是某个字段。
145#[derive(Debug)]
146pub struct GraftOverlay {
147    /// The base tree with every applied record overlaid.
148    /// 叠加全部已应用记录后的有效树。
149    pub effective: Registry,
150    /// The kernel's per-record reconciliation evidence, problems included.
151    /// 内核逐条记录的对账证据,含问题项。
152    pub reports: Vec<RecordReport>,
153}
154
155/// Whether one report describes a problem rather than a precedence decision.
156/// 一条报告描述的是问题,还是一次优先级裁决。
157///
158/// The split is what makes the printed lines scannable without reading the
159/// message text: these two mean a graft did **not** take effect, while the other
160/// four are the documented precedence order working as intended. The defects
161/// that have no legitimate reading at all — an unparseable plan, a directory
162/// selector that disagrees with the plan's `graft` — never get this far; they
163/// refuse the overlay outright.
164/// 这条分界让打印出来的行不必读消息文本就能扫出问题:这两个意味着某次嫁接**没有**生效,
165/// 另外四个则是记录在案的优先级顺序按设计生效。完全没有合法解读的缺陷——解析不了的
166/// 计划、目录选择器与计划里的 `graft` 不一致——根本到不了这里:它们直接拒绝整次 overlay。
167fn report_is_problem(report: &RecordReport) -> bool {
168    matches!(
169        report,
170        RecordReport::UnkeptSlot { .. } | RecordReport::RecordSelectorUnresolved { .. }
171    )
172}
173
174/// Render every report as one level-prefixed line.
175/// 把每条报告渲染为一行,并带严重程度前缀。
176///
177/// Why a host cannot be left to print these itself: the obvious design returns
178/// `reports` as data and documents "print them", but a host that only takes
179/// `.effective` then applies **no** graft at all and sees nothing — and a graft
180/// that silently never happens is worse than one that fails, because the source
181/// tree says one thing and the running binary does another. So the two
182/// "did not take effect" reports are `warning:`; the four precedence decisions
183/// are `note:`.
184/// 为什么不能把打印交给宿主自己:显而易见的做法是把 `reports` 当数据返回并注明
185/// "打印它们",但只取 `.effective` 的宿主会**完全没有**应用嫁接却什么都看不到——一次
186/// 悄无声息从未发生的嫁接比一次失败更糟,因为源码说的是一回事、运行中的二进制是另一
187/// 回事。因此两条"没有生效"的报告是 `warning:`,四条优先级裁决是 `note:`。
188///
189/// Boundary: this only renders; it does not decide whether to fail. A skipped
190/// record stays non-fatal on purpose (`Registry::resolve_record` — one stale
191/// record must not stop every other record), but it can no longer be invisible.
192/// 边界:这里只渲染,不决定是否失败。被跳过的记录刻意保持非致命
193/// (`Registry::resolve_record`——一条陈旧记录不该让其余记录全部失效),但它再也无法
194/// 不可见。
195///
196/// Pinned by `graft_report_lines_mark_problems_as_warnings`.
197/// 由 `graft_report_lines_mark_problems_as_warnings` 钉住。
198pub(crate) fn graft_report_lines(reports: &[RecordReport]) -> Vec<String> {
199    reports
200        .iter()
201        .map(|report| {
202            let level = if report_is_problem(report) {
203                "warning"
204            } else {
205                "note"
206            };
207            format!("{level}: {report}")
208        })
209        .collect()
210}
211
212/// Load `.nichlink/external-grafts/` under `package_root` and overlay it.
213/// 读取 `package_root` 下的 `.nichlink/external-grafts/` 并覆盖。
214///
215/// `declared` is the build-captured static plan (`builtin_static_plan().grafts()`);
216/// it is the arbiter of which slots stay alive. A record cannot resurrect a slot
217/// no declaration hands over.
218/// `declared` 是构建捕获的静态计划(`builtin_static_plan().grafts()`);它才是哪些
219/// 槽位存活的仲裁者。记录无法复活没有声明交出的槽位。
220///
221/// Three things fail this call instead of being carried as evidence, because none
222/// of them has a legitimate reading: a plan file that does not parse, a record
223/// directory that disagrees with the `graft` its plan names, and a record whose
224/// identity and path name different faces. Everything else is **printed to
225/// stderr** — one level-prefixed line per report — before this returns, and the
226/// same items stay in the returned value for a host that routes evidence
227/// elsewhere. The build refuses the unambiguous half of the same class even
228/// earlier (`build_method::graft_plan_check`: a plan no declaration could ever
229/// name); this covers a record added after the build, or one whose slot the base
230/// tree no longer has.
231/// 有三种情况会让本次调用失败而不是作为证据带出来,因为它们都没有合法解读:解析不了的
232/// 计划文件、目录与该计划里的 `graft` 不一致的记录,以及身份与路径指向不同面的记录。
233/// 其余情况都会在返回前**打印到 stderr**(每条报告一行,带严重程度前缀),同样的条目
234/// 仍留在返回值里,供把证据转往别处的宿主使用。同一类问题里无歧义的那一半,构建拒绝得
235/// 更早(`build_method::graft_plan_check`:没有任何声明可能命名的计划);这里覆盖构建
236/// 之后新增的记录,或槽位已不在原树里的记录。
237///
238/// `.nichlink/external-grafts/` is **runtime input**, not generated state. A
239/// record may re-route a string-form declaration, so a package that does not
240/// review this directory can have shipped behavior changed by a machine-local
241/// file; review it the way source is reviewed and keep it out of untrusted
242/// checkouts. Because of that same power, `apply_recorded_grafts` and
243/// `overlay_static` can produce different trees for identical inputs; the
244/// example test `a_record_moves_the_effective_tree_but_not_the_static_plan`
245/// pins that intended divergence rather than treating it as a bug.
246/// `.nichlink/external-grafts/` 是**运行期输入**而不是生成物。记录可以重新路由字符串
247/// 形式声明,因此不审查该目录的包可能被机器本地文件改变已发布行为;请像审查源码一样
248/// 审查它,并把它挡在不可信检出之外。也正因如此,`apply_recorded_grafts` 与
249/// `overlay_static` 对相同输入可能产出不同的树;示例测试
250/// `a_record_moves_the_effective_tree_but_not_the_static_plan` 把这处有意为之的偏离
251/// 钉住,而不是当作缺陷。
252pub fn apply_recorded_grafts(
253    base: &Registry,
254    external: &Registry,
255    declared: &[StaticGraftCut],
256    package_root: &Path,
257) -> Result<GraftOverlay, String> {
258    let loaded = load_graft_records(package_root)?;
259    let mut records = Vec::new();
260    let mut unreadable = Vec::new();
261    for entry in loaded {
262        match entry {
263            LoadedGraft::Record(record) => records.push(record),
264            LoadedGraft::Unreadable { selector, reason } => unreadable.push((selector, reason)),
265        }
266    }
267    // All the unreadable plans at once, not the first: an author fixing a hand-
268    // edited directory should see every broken file in one run, and a host that
269    // wants to tolerate them calls `load_graft_records` (which still reports them
270    // one by one) instead of this convenience entry point.
271    // 一次报出全部不可读计划,而不是第一条:手工整理目录的作者应当一次看到所有坏文件,
272    // 而想要容忍它们的宿主应改用 `load_graft_records`(它仍然逐条上报),而不是这个便利
273    // 入口。
274    if !unreadable.is_empty() {
275        let details = unreadable
276            .iter()
277            .map(|(selector, reason)| format!("`{selector}`: {reason}"))
278            .collect::<Vec<_>>()
279            .join("; ");
280        return Err(format!(
281            "{} external graft plan(s) could not be read, so no record was applied: {details}",
282            unreadable.len()
283        ));
284    }
285    let outcome = base
286        .overlay_recorded(&records, declared, external)
287        .map_err(|error| error.to_string())?;
288    // Printed here rather than left to the caller. The returned `reports` are
289    // data a host can ignore, and ignoring `UnkeptSlot` means a graft that
290    // silently never happens — the one failure mode the source tree cannot show.
291    // 在这里打印,而不是留给调用方。返回的 `reports` 是宿主可以忽略的数据,而忽略
292    // `UnkeptSlot` 意味着一次悄无声息从未发生的嫁接——这是源码树唯一无法体现的失败模式。
293    for line in graft_report_lines(&outcome.reports) {
294        eprintln!("{line}");
295    }
296    Ok(GraftOverlay {
297        effective: outcome.effective,
298        reports: outcome.reports,
299    })
300}
301
302#[cfg(test)]
303mod tests {
304    use super::{RecordReport, graft_report_lines};
305    use crate::registry_core::root_node_id;
306
307    /// Problems and precedence decisions must be distinguishable before the
308    /// message text is read, and a skipped record must say it was skipped.
309    /// 问题与优先级裁决必须在读消息文本之前就能区分,且被跳过的记录必须说明自己被跳过。
310    #[test]
311    fn graft_report_lines_mark_problems_as_warnings() {
312        let reports = vec![
313            RecordReport::UnkeptSlot {
314                selector: "canvas_graft".to_owned(),
315                target_path: "root/canvas".to_owned(),
316            },
317            RecordReport::DeclarationOverridden {
318                slot: root_node_id("report-lines"),
319                declared: "canvas_fast".to_owned(),
320                recorded: "canvas_graft".to_owned(),
321            },
322        ];
323        let lines = graft_report_lines(&reports);
324
325        assert_eq!(lines.len(), 2, "{lines:?}");
326        // A skipped record says so in words a reader cannot miss.
327        // 被跳过的记录用读者不可能漏掉的措辞说明这一点。
328        assert!(lines[0].starts_with("warning: "), "{}", lines[0]);
329        assert!(lines[0].contains("canvas_graft"), "{}", lines[0]);
330        assert!(lines[0].contains("record skipped"), "{}", lines[0]);
331        assert!(lines[1].starts_with("note: "), "{}", lines[1]);
332    }
333}