Skip to main content

str_format/
meta.rs

1//! `._meta` 的解析、类型化提取与**归一化**(TOML → 规范 JSON)。
2//!
3//! 保注释写回与新建模板见 [`crate::meta_edit`]。
4
5use std::path::Path;
6
7use serde_json::{Map as JMap, Value as JValue};
8use toml_edit::{DocumentMut, Item, Table};
9
10use crate::error::{Error, Issue, Result, code};
11use crate::util;
12
13// ─────────────────────────── 规范 4.9:键序与表序 ───────────────────────────
14
15/// 顶层裸键的规范顺序(必须写在任何表头之前)。
16pub const TOP_BARE_KEYS: &[&str] = &[
17    "str",
18    "spec",
19    "kind",
20    "id",
21    "name",
22    "type",
23    "title",
24    "summary",
25    "tags",
26    "revision",
27    "created_at",
28    "updated_at",
29    "schema",
30];
31
32/// 表 / 数组表的规范顺序。
33pub const TABLE_ORDER: &[&str] = &["policies", "authors", "refs", "entries", "ext"];
34
35/// `[policies]` 内键序。
36pub const POLICIES_KEYS: &[&str] = &[
37    "id_version",
38    "max_depth",
39    "manifest",
40    "sha256",
41    "large_asset_bytes",
42    "deep_tree_warn",
43];
44
45/// `[[authors]]` 内键序。
46pub const AUTHOR_KEYS: &[&str] = &["id", "name", "role", "at"];
47
48/// `[[refs]]` 内键序。
49pub const REF_KEYS: &[&str] = &["id", "target", "rel", "title", "order", "note"];
50
51/// `[[entries]]` 内键序。
52pub const ENTRY_KEYS: &[&str] = &[
53    "path",
54    "role",
55    "id",
56    "type",
57    "title",
58    "summary",
59    "order",
60    "media_type",
61    "size",
62    "sha256",
63    "count",
64    "schema",
65    "optional",
66    "note",
67];
68
69/// root 允许的顶层键。
70pub const ROOT_ALLOWED: &[&str] = &[
71    "str", "spec", "kind", "id", "name", "type", "title", "summary", "tags", "revision",
72    "created_at", "updated_at", "schema", "policies", "authors", "refs", "entries", "ext",
73];
74
75/// node / branch 允许的顶层键(无 `policies`)。
76pub const BRANCH_ALLOWED: &[&str] = &[
77    "str", "spec", "kind", "id", "name", "type", "title", "summary", "tags", "revision",
78    "created_at", "updated_at", "schema", "authors", "refs", "entries", "ext",
79];
80
81// ─────────────────────────── 模型 ───────────────────────────
82
83/// `kind` 档位。
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Kind {
86    /// ROOT(深度 0)。
87    Root,
88    /// 独立节点(深度 1)。
89    Node,
90    /// 关联分支(深度 ≥2)。
91    Branch,
92}
93
94impl Kind {
95    /// 字面量。
96    pub fn as_str(self) -> &'static str {
97        match self {
98            Kind::Root => "root",
99            Kind::Node => "node",
100            Kind::Branch => "branch",
101        }
102    }
103
104    /// 解析字面量。
105    pub fn parse(s: &str) -> Option<Kind> {
106        match s {
107            "root" => Some(Kind::Root),
108            "node" => Some(Kind::Node),
109            "branch" => Some(Kind::Branch),
110            _ => None,
111        }
112    }
113
114    /// root 与 node/branch 的允许键集。
115    pub fn allowed_top_keys(self) -> &'static [&'static str] {
116        match self {
117            Kind::Root => ROOT_ALLOWED,
118            _ => BRANCH_ALLOWED,
119        }
120    }
121}
122
123/// 清单策略(`policies.manifest`)。
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ManifestPolicy {
126    /// 清单不一致为 error。
127    Strict,
128    /// 清单不一致仅告警。
129    Advisory,
130}
131
132/// 指纹策略(`policies.sha256`)。
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ShaPolicy {
135    /// 文件类条目必须有 `size` + `sha256`。
136    Required,
137    /// 可选。
138    Optional,
139    /// 完全不校验。
140    Off,
141}
142
143/// 校验策略(仅 root)。
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct Policies {
146    /// UUID 版本要求。
147    pub id_version: usize,
148    /// 分支树最大深度。
149    pub max_depth: usize,
150    /// 清单策略。
151    pub manifest: ManifestPolicy,
152    /// 指纹策略。
153    pub sha256: ShaPolicy,
154    /// 大文件告警阈值。
155    pub large_asset_bytes: u64,
156    /// 深树告警阈值。
157    pub deep_tree_warn: usize,
158}
159
160impl Default for Policies {
161    fn default() -> Self {
162        Self {
163            id_version: 7,
164            max_depth: 32,
165            manifest: ManifestPolicy::Strict,
166            sha256: ShaPolicy::Required,
167            large_asset_bytes: 10 * 1024 * 1024,
168            deep_tree_warn: 16,
169        }
170    }
171}
172
173/// `[[authors]]` 元素。
174#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct Author {
176    /// 稳定标识符。
177    pub id: String,
178    /// 展示名。
179    pub name: Option<String>,
180    /// 角色。
181    pub role: String,
182    /// 参与时间。
183    pub at: Option<String>,
184}
185
186/// `[[refs]]` 元素(跨枝关联线)。
187#[derive(Debug, Clone, Default, PartialEq, Eq)]
188pub struct RefItem {
189    /// 关联线自身 id。
190    pub id: String,
191    /// 目标分支 id。
192    pub target: String,
193    /// 关联语义。
194    pub rel: String,
195    /// 标签。
196    pub title: Option<String>,
197    /// 排序键。
198    pub order: Option<i64>,
199    /// 备注。
200    pub note: Option<String>,
201}
202
203/// `[[entries]]` 元素(本目录内容清单)。
204#[derive(Debug, Clone, Default, PartialEq, Eq)]
205pub struct Entry {
206    /// 单段路径。
207    pub path: String,
208    /// 角色。
209    pub role: String,
210    /// 子分支 id(`node` / `branch`)。
211    pub id: Option<String>,
212    /// 子分支类型。
213    pub r#type: Option<String>,
214    /// 展示名。
215    pub title: Option<String>,
216    /// 摘要。
217    pub summary: Option<String>,
218    /// 排序键。
219    pub order: Option<i64>,
220    /// IANA 媒体类型。
221    pub media_type: Option<String>,
222    /// 字节数。
223    pub size: Option<i64>,
224    /// 内容指纹。
225    pub sha256: Option<String>,
226    /// 直接子项数(`dir`)。
227    pub count: Option<i64>,
228    /// 该文件遵循的 Schema。
229    pub schema: Option<String>,
230    /// 是否允许缺失。
231    pub optional: bool,
232    /// 备注。
233    pub note: Option<String>,
234}
235
236impl Entry {
237    /// 是否为分支条目(`node` / `branch`)。
238    pub fn is_branch(&self) -> bool {
239        self.role == "node" || self.role == "branch"
240    }
241
242    /// 是否为文件类条目(需要指纹)。
243    pub fn is_file_like(&self) -> bool {
244        self.role == "payload" || self.role == "asset"
245    }
246}
247
248/// 一份 `._meta` 的完整内容:保序文档 + 类型化视图。
249#[derive(Debug)]
250pub struct Meta {
251    /// 保注释、保顺序的 TOML 文档(写回用)。
252    pub doc: DocumentMut,
253    /// `str` 主版本。
254    pub str_version: Option<i64>,
255    /// 规范版本。
256    pub spec: Option<String>,
257    /// 档位。
258    pub kind_raw: Option<String>,
259    /// 解析出的档位。
260    pub kind: Option<Kind>,
261    /// 自身 id。
262    pub id: Option<String>,
263    /// bundle 短名(root)。
264    pub name: Option<String>,
265    /// 类型。
266    pub r#type: Option<String>,
267    /// 标题。
268    pub title: Option<String>,
269    /// 摘要。
270    pub summary: Option<String>,
271    /// 标签。
272    pub tags: Vec<String>,
273    /// 修订号。
274    pub revision: Option<i64>,
275    /// 创建时间。
276    pub created_at: Option<String>,
277    /// 更新时间。
278    pub updated_at: Option<String>,
279    /// payload schema 引用。
280    pub schema: Option<String>,
281    /// 策略。
282    pub policies: Policies,
283    /// 贡献者。
284    pub authors: Vec<Author>,
285    /// 跨枝关联。
286    pub refs: Vec<RefItem>,
287    /// 内容清单。
288    pub entries: Vec<Entry>,
289}
290
291/// 加载结果。
292pub enum MetaLoad {
293    /// 解析成功(可能带有字段级问题)。
294    Ok(Box<Meta>, Vec<Issue>),
295    /// 解析失败(`E_PARSE`),无法构建模型。
296    Failed(Vec<Issue>),
297}
298
299/// 从磁盘读取并解析一份 `._meta`。
300pub fn load(path: &Path, rel: &str) -> Result<MetaLoad> {
301    let bytes = std::fs::read(path).map_err(|e| Error::io(path, e))?;
302    let mut issues = Vec::new();
303
304    // 编码:UTF-8 无 BOM
305    if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
306        issues.push(Issue::error(
307            code::PARSE,
308            rel,
309            "文件含 UTF-8 BOM,规范要求 UTF-8 无 BOM",
310        ));
311        return Ok(MetaLoad::Failed(issues));
312    }
313    let text = match std::str::from_utf8(&bytes) {
314        Ok(t) => t,
315        Err(e) => {
316            issues.push(Issue::error(
317                code::PARSE,
318                rel,
319                format!("编码非 UTF-8:{e}"),
320            ));
321            return Ok(MetaLoad::Failed(issues));
322        }
323    };
324
325    let doc: DocumentMut = match text.parse() {
326        Ok(d) => d,
327        Err(e) => {
328            issues.push(Issue::error(code::PARSE, rel, format!("TOML 解析失败:{e}")));
329            return Ok(MetaLoad::Failed(issues));
330        }
331    };
332
333    let (meta, mut field_issues) = extract(doc, rel);
334    issues.append(&mut field_issues);
335    Ok(MetaLoad::Ok(Box::new(meta), issues))
336}
337
338/// 从已解析的文档构建类型化视图。
339pub fn extract(doc: DocumentMut, rel: &str) -> (Meta, Vec<Issue>) {
340    let mut cx = Ctx {
341        rel: rel.to_string(),
342        issues: Vec::new(),
343    };
344    let table = doc.as_table().clone();
345
346    let str_version = cx.opt_int(&table, "str");
347    let spec = cx.opt_str(&table, "spec");
348    let kind_raw = cx.opt_str(&table, "kind");
349    let kind = match kind_raw.as_deref() {
350        Some(s) => match Kind::parse(s) {
351            Some(k) => Some(k),
352            None => {
353                cx.err(
354                    code::KIND_INVALID,
355                    format!("`kind` = {s:?} 非法(应为 root / node / branch)"),
356                );
357                None
358            }
359        },
360        None => {
361            cx.err(code::SCHEMA_FIELD, "缺少必填字段 `kind`");
362            None
363        }
364    };
365
366    let allowed = kind.map(|k| k.allowed_top_keys()).unwrap_or(BRANCH_ALLOWED);
367    cx.check_unknown(&table, allowed);
368
369    // 注意:TOML 无法表达空的数组表,故 `entries` / `refs` 为空时整表省略,
370    // 归一化时补 `[]`(规范 4.1 / 4.9)。
371    for key in ["id", "revision", "created_at", "updated_at"] {
372        if table.get(key).is_none() {
373            cx.err(code::SCHEMA_FIELD, format!("缺少必填字段 `{key}`"));
374        }
375    }
376
377    let id = cx.opt_str(&table, "id");
378    let name = cx.opt_str(&table, "name");
379    let r#type = cx.opt_str(&table, "type");
380    let title = cx.opt_str(&table, "title");
381    let summary = cx.opt_str(&table, "summary");
382    let tags = cx.opt_str_array(&table, "tags").unwrap_or_default();
383    let revision = cx.opt_int(&table, "revision");
384    let created_at = cx.opt_dt(&table, "created_at");
385    let updated_at = cx.opt_dt(&table, "updated_at");
386    let schema = cx.opt_str(&table, "schema");
387
388    // `[policies]`(仅 root)
389    let mut policies = Policies::default();
390    if let Some(item) = table.get("policies") {
391        match item.as_table() {
392            Some(pt) => {
393                if kind != Some(Kind::Root) {
394                    cx.err(
395                        code::SCHEMA_FIELD,
396                        "`[policies]` 只能出现在 root 的 `._meta` 中",
397                    );
398                }
399                cx.check_unknown(pt, POLICIES_KEYS);
400                if let Some(v) = cx.opt_int(pt, "id_version") {
401                    policies.id_version = v.max(0) as usize;
402                }
403                if let Some(v) = cx.opt_int(pt, "max_depth") {
404                    policies.max_depth = v.max(1) as usize;
405                }
406                if let Some(v) = cx.opt_str(pt, "manifest") {
407                    policies.manifest = match v.as_str() {
408                        "strict" => ManifestPolicy::Strict,
409                        "advisory" => ManifestPolicy::Advisory,
410                        _ => {
411                            cx.err(
412                                code::SCHEMA_FIELD,
413                                format!("`policies.manifest` = {v:?} 非法(strict / advisory)"),
414                            );
415                            policies.manifest
416                        }
417                    };
418                }
419                if let Some(v) = cx.opt_str(pt, "sha256") {
420                    policies.sha256 = match v.as_str() {
421                        "required" => ShaPolicy::Required,
422                        "optional" => ShaPolicy::Optional,
423                        "off" => ShaPolicy::Off,
424                        _ => {
425                            cx.err(
426                                code::SCHEMA_FIELD,
427                                format!("`policies.sha256` = {v:?} 非法(required / optional / off)"),
428                            );
429                            policies.sha256
430                        }
431                    };
432                }
433                if let Some(v) = cx.opt_int(pt, "large_asset_bytes") {
434                    policies.large_asset_bytes = v.max(0) as u64;
435                }
436                if let Some(v) = cx.opt_int(pt, "deep_tree_warn") {
437                    policies.deep_tree_warn = v.max(1) as usize;
438                }
439            }
440            None => cx.err(code::SCHEMA_FIELD, "`policies` 必须是表 `[policies]`"),
441        }
442    }
443
444    // `[[authors]]`
445    let mut authors = Vec::new();
446    if let Some(aot) = table.get("authors").and_then(|i| i.as_array_of_tables()) {
447        for (i, t) in aot.iter().enumerate() {
448            cx.set_rel(&format!("{rel} #authors[{i}]"));
449            cx.check_unknown(t, AUTHOR_KEYS);
450            let aid = cx.req_str(t, "id").unwrap_or_default();
451            let role = cx.req_str(t, "role").unwrap_or_default();
452            let aname = cx.opt_str(t, "name");
453            let at = cx.opt_dt(t, "at");
454            authors.push(Author {
455                id: aid,
456                name: aname,
457                role,
458                at,
459            });
460        }
461        cx.set_rel(rel);
462    } else if table.get("authors").is_some() {
463        cx.err(code::SCHEMA_FIELD, "`authors` 必须是数组表 `[[authors]]`");
464    }
465
466    // `[[refs]]`
467    let mut refs = Vec::new();
468    if let Some(aot) = table.get("refs").and_then(|i| i.as_array_of_tables()) {
469        for (i, t) in aot.iter().enumerate() {
470            cx.set_rel(&format!("{rel} #refs[{i}]"));
471            cx.check_unknown(t, REF_KEYS);
472            refs.push(RefItem {
473                id: cx.req_str(t, "id").unwrap_or_default(),
474                target: cx.req_str(t, "target").unwrap_or_default(),
475                rel: cx.req_str(t, "rel").unwrap_or_default(),
476                title: cx.opt_str(t, "title"),
477                order: cx.opt_int(t, "order"),
478                note: cx.opt_str(t, "note"),
479            });
480        }
481        cx.set_rel(rel);
482    } else if table.get("refs").is_some() {
483        cx.err(code::SCHEMA_FIELD, "`refs` 必须是数组表 `[[refs]]`");
484    }
485
486    // `[[entries]]`
487    let mut entries = Vec::new();
488    if let Some(aot) = table.get("entries").and_then(|i| i.as_array_of_tables()) {
489        for (i, t) in aot.iter().enumerate() {
490            cx.set_rel(&format!("{rel} #entries[{i}]"));
491            cx.check_unknown(t, ENTRY_KEYS);
492            let path = cx.req_str(t, "path").unwrap_or_default();
493            let role = cx.req_str(t, "role").unwrap_or_default();
494            if path.contains('/') {
495                cx.err(
496                    code::SCHEMA_FIELD,
497                    format!("`entries[].path` = {path:?} 必须是单段路径(不含 `/`)"),
498                );
499            }
500            let e = Entry {
501                path,
502                role,
503                id: cx.opt_str(t, "id"),
504                r#type: cx.opt_str(t, "type"),
505                title: cx.opt_str(t, "title"),
506                summary: cx.opt_str(t, "summary"),
507                order: cx.opt_int(t, "order"),
508                media_type: cx.opt_str(t, "media_type"),
509                size: cx.opt_int(t, "size"),
510                sha256: cx.opt_str(t, "sha256"),
511                count: cx.opt_int(t, "count"),
512                schema: cx.opt_str(t, "schema"),
513                optional: cx.opt_bool(t, "optional").unwrap_or(false),
514                note: cx.opt_str(t, "note"),
515            };
516            entries.push(e);
517        }
518        cx.set_rel(rel);
519    } else if table.get("entries").is_some() {
520        cx.err(code::SCHEMA_FIELD, "`entries` 必须是数组表 `[[entries]]`");
521    }
522
523    let meta = Meta {
524        doc,
525        str_version,
526        spec,
527        kind_raw,
528        kind,
529        id,
530        name,
531        r#type,
532        title,
533        summary,
534        tags,
535        revision,
536        created_at,
537        updated_at,
538        schema,
539        policies,
540        authors,
541        refs,
542        entries,
543    };
544    (meta, cx.issues)
545}
546
547// ─────────────────────────── 规范 4.9:确定性序列化 ───────────────────────────
548
549/// 规范 §4.9 的**全量规范化**:顶层裸键序 → 表序 → 各表内键序 → `entries` / `refs` 集合排序,
550/// 最后重设文档位置使顺序真正落到磁盘字节上。
551///
552/// 只调整**书写顺序**,不改动任何字段值:`toml_edit` 的键/项装饰(注释、空行)随项一起搬运,
553/// 因此注释保真(规范 4.1 / DoD 16)。
554pub fn canonicalize_doc(doc: &mut DocumentMut) {
555    // 1) 顶层:值项(裸键)按 `TOP_BARE_KEYS`,表项按 `TABLE_ORDER`。
556    //    未知的值项排在已知裸键之后、表项之前 —— 否则未知裸键会被排到表头之后,直接产出非法 TOML。
557    let top_rank = |k: &str, it: &Item| -> usize {
558        match it {
559            Item::Table(_) | Item::ArrayOfTables(_) => {
560                TOP_BARE_KEYS.len()
561                    + 1
562                    + TABLE_ORDER
563                        .iter()
564                        .position(|x| *x == k)
565                        .unwrap_or(TABLE_ORDER.len())
566            }
567            _ => TOP_BARE_KEYS
568                .iter()
569                .position(|x| *x == k)
570                .unwrap_or(TOP_BARE_KEYS.len()),
571        }
572    };
573    doc.as_table_mut()
574        .sort_values_by(|k1, i1, k2, i2| top_rank(k1.get(), i1).cmp(&top_rank(k2.get(), i2)));
575
576    // 2) 各表内的键序。
577    for (key, order) in [
578        ("policies", POLICIES_KEYS),
579        ("authors", AUTHOR_KEYS),
580        ("refs", REF_KEYS),
581        ("entries", ENTRY_KEYS),
582    ] {
583        let rank = |k: &str| order.iter().position(|x| *x == k).unwrap_or(usize::MAX);
584        match doc.as_table_mut().get_mut(key) {
585            Some(Item::Table(t)) => {
586                t.sort_values_by(|k1, _, k2, _| rank(k1.get()).cmp(&rank(k2.get())));
587            }
588            Some(Item::ArrayOfTables(a)) => {
589                for t in a.iter_mut() {
590                    t.sort_values_by(|k1, _, k2, _| rank(k1.get()).cmp(&rank(k2.get())));
591                }
592            }
593            _ => {}
594        }
595    }
596
597    // 3) 集合排序(§4.9「各自按 `order` 稳定排序」)。
598    sort_collections_in(doc);
599
600    // 4) 位置重设,见 `renumber_positions_in`。
601    renumber_positions_in(doc);
602}
603
604/// `entries` / `refs` 的排序键:`(order, path|id)`,缺 `order` 视为最大。
605fn collection_sort_key(t: &Table) -> (i64, String) {
606    let order = t
607        .get("order")
608        .and_then(Item::as_integer)
609        .unwrap_or(i64::MAX);
610    let id = t
611        .get("path")
612        .or_else(|| t.get("id"))
613        .and_then(Item::as_str)
614        .unwrap_or("")
615        .to_string();
616    (order, id)
617}
618
619/// 就地重排 `entries` / `refs` 的元素(不改变元素自身的 `doc_position`)。
620fn sort_collections_in(doc: &mut DocumentMut) {
621    for key in ["entries", "refs"] {
622        let Some(aot) = doc
623            .as_table_mut()
624            .get_mut(key)
625            .and_then(Item::as_array_of_tables_mut)
626        else {
627            continue;
628        };
629        let mut items: Vec<Table> = aot.iter().cloned().collect();
630        items.sort_by_key(collection_sort_key);
631        for (slot, table) in aot.iter_mut().zip(items) {
632            *slot = table;
633        }
634    }
635}
636
637/// 依**前序遍历顺序**重设整篇文档每个表的 `doc_position`。
638///
639/// `toml_edit` 的 `DocumentMut::fmt` 会用 `Table::position()` 把表「搬回原始位置」:
640/// 只重排内容而不重设位置,序列化结果仍保持解析时的旧顺序 —— 这正是 `sort_collections()`
641/// 此前「改了等于没改」的根因。前序赋值保证位置序 == 遍历序 ==(规范化后的)书写顺序。
642fn renumber_positions_in(doc: &mut DocumentMut) {
643    fn walk(t: &mut Table, pos: &mut isize) {
644        *pos += 1;
645        t.set_position(Some(*pos));
646        for (_, item) in t.iter_mut() {
647            match item {
648                Item::Table(child) => walk(child, pos),
649                Item::ArrayOfTables(a) => {
650                    for child in a.iter_mut() {
651                        walk(child, pos);
652                    }
653                }
654                _ => {}
655            }
656        }
657    }
658    let mut pos = 0isize;
659    walk(doc.as_table_mut(), &mut pos);
660}
661
662// ─────────────────────────── 归一化(TOML → 规范 JSON)───────────────────────────
663
664impl Meta {
665    /// 归一化为**规范 JSON**:顶层裸键与各表按 4.9 的键序/表序输出,
666    /// 未知键原样保留,使其可被 JSON Schema 校验器与 AI 稳定消费。
667    pub fn to_json(&self) -> JValue {
668        let table = self.doc.as_table();
669        let mut map = JMap::new();
670
671        for key in TOP_BARE_KEYS {
672            if let Some(item) = table.get(key) {
673                map.insert((*key).to_string(), item_to_json(item));
674            }
675        }
676        if let Some(item) = table.get("policies") {
677            match item.as_table() {
678                Some(t) => {
679                    map.insert("policies".into(), table_to_json(t, POLICIES_KEYS));
680                }
681                None => {
682                    map.insert("policies".into(), item_to_json(item));
683                }
684            }
685        }
686        if let Some(aot) = table.get("authors").and_then(|i| i.as_array_of_tables()) {
687            map.insert(
688                "authors".into(),
689                JValue::Array(
690                    aot.iter()
691                        .map(|t| table_to_json(t, AUTHOR_KEYS))
692                        .collect(),
693                ),
694            );
695        }
696        // `refs` 为空(TOML 无法表达空数组表)时补 `[]`
697        map.insert(
698            "refs".into(),
699            match table.get("refs").and_then(|i| i.as_array_of_tables()) {
700                Some(aot) => JValue::Array(
701                    aot.iter()
702                        .map(|t| table_to_json(t, REF_KEYS))
703                        .collect(),
704                ),
705                None => JValue::Array(Vec::new()),
706            },
707        );
708        map.insert(
709            "entries".into(),
710            match table.get("entries").and_then(|i| i.as_array_of_tables()) {
711                Some(aot) => JValue::Array(
712                    aot.iter()
713                        .map(|t| table_to_json(t, ENTRY_KEYS))
714                        .collect(),
715                ),
716                None => JValue::Array(Vec::new()),
717            },
718        );
719        if let Some(item) = table.get("ext") {
720            map.insert("ext".into(), item_to_json(item));
721        }
722        // 未知键原样保留(前向兼容)
723        for (k, v) in table.iter() {
724            if !map.contains_key(k) {
725                map.insert(k.to_string(), item_to_json(v));
726            }
727        }
728        JValue::Object(map)
729    }
730
731    /// 按 `(order, path|id)` 重排 `entries` / `refs`,并重设文档位置使新顺序真正落盘。
732    pub fn sort_collections(&mut self) {
733        sort_collections_in(&mut self.doc);
734        renumber_positions_in(&mut self.doc);
735    }
736
737    /// 规范 §4.9 全量规范化(顶层键序 + 表序 + 表内键序 + 集合排序)。
738    pub fn canonicalize(&mut self) {
739        canonicalize_doc(&mut self.doc);
740    }
741
742    /// 校验序号等基础合法性(`E_REVISION_STALE` 的可判定部分)。
743    pub fn revision_issues(&self, rel: &str) -> Vec<Issue> {
744        let mut out = Vec::new();
745        match self.revision {
746            None => {}
747            Some(r) if r < 1 => out.push(Issue::error(
748                code::REVISION_STALE,
749                rel,
750                format!("`revision` = {r},必须为 ≥ 1 的整数"),
751            )),
752            Some(_) => {}
753        }
754        if let (Some(c), Some(u)) = (self.created_at.as_deref(), self.updated_at.as_deref()) {
755            match (util::parse_rfc3339(c), util::parse_rfc3339(u)) {
756                (Some(cd), Some(ud)) if ud < cd => out.push(Issue::error(
757                    code::REVISION_STALE,
758                    rel,
759                    format!("`updated_at`({u})早于 `created_at`({c})"),
760                )),
761                _ => {}
762            }
763        }
764        out
765    }
766}
767
768/// TOML `Item` → JSON。
769fn item_to_json(item: &Item) -> JValue {
770    match item {
771        Item::None => JValue::Null,
772        Item::Value(v) => value_to_json(v),
773        Item::Table(t) => {
774            let mut m = JMap::new();
775            for (k, v) in t.iter() {
776                m.insert(k.to_string(), item_to_json(v));
777            }
778            JValue::Object(m)
779        }
780        Item::ArrayOfTables(aot) => JValue::Array(
781            aot.iter()
782                .map(|t| {
783                    let mut m = JMap::new();
784                    for (k, v) in t.iter() {
785                        m.insert(k.to_string(), item_to_json(v));
786                    }
787                    JValue::Object(m)
788                })
789                .collect(),
790        ),
791    }
792}
793
794/// TOML `Value` → JSON。
795fn value_to_json(v: &toml_edit::Value) -> JValue {
796    if let Some(s) = v.as_str() {
797        return JValue::String(s.to_string());
798    }
799    if let Some(i) = v.as_integer() {
800        return JValue::from(i);
801    }
802    if let Some(f) = v.as_float() {
803        return serde_json::Number::from_f64(f)
804            .map(JValue::Number)
805            .unwrap_or(JValue::Null);
806    }
807    if let Some(b) = v.as_bool() {
808        return JValue::Bool(b);
809    }
810    if let Some(d) = v.as_datetime() {
811        return JValue::String(d.to_string());
812    }
813    if let Some(a) = v.as_array() {
814        return JValue::Array(a.iter().map(value_to_json).collect());
815    }
816    if let Some(t) = v.as_inline_table() {
817        let mut m = JMap::new();
818        for (k, val) in t.iter() {
819            m.insert(k.to_string(), value_to_json(val));
820        }
821        return JValue::Object(m);
822    }
823    JValue::Null
824}
825
826/// 表 → JSON(先按 `order` 输出已知键,再补未知键)。
827fn table_to_json(table: &Table, order: &[&str]) -> JValue {
828    let mut m = JMap::new();
829    for k in order {
830        if let Some(v) = table.get(k) {
831            m.insert((*k).to_string(), item_to_json(v));
832        }
833    }
834    for (k, v) in table.iter() {
835        if !m.contains_key(k) {
836            m.insert(k.to_string(), item_to_json(v));
837        }
838    }
839    JValue::Object(m)
840}
841
842// ─────────────────────────── 提取辅助 ───────────────────────────
843
844/// 提取上下文:累积字段级问题。
845struct Ctx {
846    rel: String,
847    issues: Vec<Issue>,
848}
849
850impl Ctx {
851    fn set_rel(&mut self, rel: &str) {
852        self.rel = rel.to_string();
853    }
854
855    fn err(&mut self, code: &'static str, message: impl Into<String>) {
856        self.issues
857            .push(Issue::error(code, self.rel.clone(), message));
858    }
859
860    fn check_unknown(&mut self, t: &Table, allowed: &[&str]) {
861        for (k, _) in t.iter() {
862            if !allowed.contains(&k) {
863                self.err(
864                    code::SCHEMA_FIELD,
865                    format!("未知字段 `{k}`(扩展请放入 `[ext]`)"),
866                );
867            }
868        }
869    }
870
871    fn req_str(&mut self, t: &Table, key: &str) -> Option<String> {
872        match t.get(key) {
873            None => {
874                self.err(code::SCHEMA_FIELD, format!("缺少必填字段 `{key}`"));
875                None
876            }
877            Some(item) => match item.as_str() {
878                Some(s) => Some(s.to_string()),
879                None => {
880                    self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串"));
881                    None
882                }
883            },
884        }
885    }
886
887    fn opt_str(&mut self, t: &Table, key: &str) -> Option<String> {
888        match t.get(key) {
889            None => None,
890            Some(item) => match item.as_str() {
891                Some(s) => Some(s.to_string()),
892                None => {
893                    self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串"));
894                    None
895                }
896            },
897        }
898    }
899
900    fn opt_int(&mut self, t: &Table, key: &str) -> Option<i64> {
901        match t.get(key) {
902            None => None,
903            Some(item) => match item.as_integer() {
904                Some(i) => Some(i),
905                None => {
906                    self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是整数"));
907                    None
908                }
909            },
910        }
911    }
912
913    fn opt_bool(&mut self, t: &Table, key: &str) -> Option<bool> {
914        match t.get(key) {
915            None => None,
916            Some(item) => match item.as_bool() {
917                Some(b) => Some(b),
918                None => {
919                    self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是布尔值"));
920                    None
921                }
922            },
923        }
924    }
925
926    /// 读取 TOML 原生 offset date-time;写成字符串或缺少时区偏移均报 `E_PARSE`。
927    fn opt_dt(&mut self, t: &Table, key: &str) -> Option<String> {
928        let Some(item) = t.get(key) else {
929            return None;
930        };
931        if item.as_str().is_some() {
932            self.err(
933                code::PARSE,
934                format!("`{key}` 必须使用 TOML 原生 offset date-time,不得写成字符串"),
935            );
936            return None;
937        }
938        match item.as_datetime() {
939            Some(d) => {
940                if d.date.is_none() || d.time.is_none() || d.offset.is_none() {
941                    self.err(
942                        code::PARSE,
943                        format!("`{key}` 必须是带时区偏移的 offset date-time(如 2026-09-14T10:03:11+08:00)"),
944                    );
945                    return None;
946                }
947                Some(d.to_string())
948            }
949            None => {
950                self.err(
951                    code::PARSE,
952                    format!("`{key}` 必须是 offset date-time"),
953                );
954                None
955            }
956        }
957    }
958
959    fn opt_str_array(&mut self, t: &Table, key: &str) -> Option<Vec<String>> {
960        let Some(item) = t.get(key) else {
961            return None;
962        };
963        let Some(arr) = item.as_array() else {
964            self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串数组"));
965            return None;
966        };
967        let mut out = Vec::new();
968        for v in arr.iter() {
969            match v.as_str() {
970                Some(s) => out.push(s.to_string()),
971                None => {
972                    self.err(code::SCHEMA_FIELD, format!("`{key}` 的元素必须是字符串"));
973                }
974            }
975        }
976        Some(out)
977    }
978}