rust: Chain of Responsibility Pattern (续)

rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : audit_report.rs
//! 契约审计表(幕八):**六条不变量 + 每条的已知失败输入**。
//!
//! ## ★ 「已知会失败的输入」为什么要印在报表上
//!
//! 一条规则若对任何输入都成立,它就不是规则,只是一句没被跑过的注释。
//! 把「什么样的输入会让它变红」印在表上,读者才能判断:
//! 这条规则**真的在检查东西**,还是只是把一句注释跑了一遍。
//!
//! 本工程还会**真的把那个输入喂进去**(幕八续),确认它会红。

use crate::analysis::AuditCheckView;
use crate::app::{Table, TableColumn};

/// 审计表的列宽。
///
/// 列宽总和必须 <= 121(六列):`12+28+36+12+8+24 = 120`。
/// 14 列按**最长的规则编码**定:`NO_PASSTHROUGH` = 14 列。
const CODE_WIDTH: usize = 14;
const NAME_WIDTH: usize = 28;
/// 「期望」列要放得下最长的一条:`处置者名非空(无人接时为专用标记)` = 34 列。
const EXPECTATION_WIDTH: usize = 36;
const ACTUAL_WIDTH: usize = 12;
const VERDICT_WIDTH: usize = 8;
const VIOLATION_WIDTH: usize = 22;

/// 渲染契约审计表。
///
/// # 参数
/// - `title`:表标题(正常用例与已知失败用例各调一次)。
/// - `checks`:检查结果的视图(**纯字符串**,表示层不认识机制层)。
pub fn render_audit_table(title: &str, checks: &[AuditCheckView]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("规则编码", CODE_WIDTH),
            TableColumn::new("规则", NAME_WIDTH),
            TableColumn::new("期望", EXPECTATION_WIDTH),
            TableColumn::new("实测", ACTUAL_WIDTH),
            TableColumn::new("结论", VERDICT_WIDTH),
            TableColumn::elidable(TableColumn::new("违规工单", VIOLATION_WIDTH)),
        ],
    );
    for check in checks.iter() {
        table = table.with_row(vec![
            check.code().to_string(),
            check.name().to_string(),
            check.expectation().to_string(),
            check.actual().to_string(),
            if check.passed() {
                "通过".to_string()
            } else {
                "未通过".to_string()
            },
            check.violating().to_string(),
        ]);
    }
    // ★ 每条规则的「已知会失败的输入」写进注解。
    for check in checks.iter() {
        table = table.with_note(&format!(
            "{}:已知会失败的输入 ------ {}",
            check.code(),
            check.known_failing_input()
        ));
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : broadcast_report.rs
//! 广播式对照表(幕十):**责任链 vs 每个环节都问一遍**。
//!
//! ## ★ 两个口径要并列,不能只给一个
//!
//! - **判定次数**:责任链走了几站 vs 广播式走了几站(= 站数,恒定);
//! - **结论**:两者**必须逐单一致**。
//!
//! 只给第一个,读者会怀疑「是不是连结论也变了」;
//! 只给第二个,读者会以为「那责任链没什么用」。
//! 两个并列,才能说清:**语义没变、判定次数变少了**------
//! 这正是责任链唯一的价值所在。

use crate::app::{Table, TableColumn};

/// 广播式对照表的列宽。
const NUMBER_WIDTH: usize = 14;
const CHAIN_HANDLER_WIDTH: usize = 20;
const BROADCAST_HANDLER_WIDTH: usize = 20;
const DISPOSITION_WIDTH: usize = 16;
const CHAIN_VISITED_WIDTH: usize = 16;
const BROADCAST_VISITED_WIDTH: usize = 16;
const IDENTICAL_WIDTH: usize = 10;

/// 广播式对照的一行。
#[derive(Clone, Debug)]
pub struct BroadcastRowView {
    /// 工单号。
    pub ticket_number: String,
    /// 责任链的处置者。
    pub chain_handler: String,
    /// 广播式的处置者。
    pub broadcast_handler: String,
    /// 结论。
    pub disposition_text: String,
    /// 责任链走了几站。
    pub chain_visited: usize,
    /// 广播式走了几站。
    pub broadcast_visited: usize,
    /// 结论是否一致。
    pub identical_text: String,
}

/// 由分析层对照结果转出一行。
pub fn broadcast_row_from_pair(pair: &crate::analysis::BroadcastPair) -> BroadcastRowView {
    BroadcastRowView {
        ticket_number: pair.ticket_number.clone(),
        chain_handler: pair.chain_handler.clone(),
        broadcast_handler: pair.broadcast_handler.clone(),
        disposition_text: pair.chain_disposition.clone(),
        chain_visited: pair.chain_visited,
        broadcast_visited: pair.broadcast_visited,
        identical_text: if pair.identical { "一致" } else { "★ 不同" }.to_string(),
    }
}

/// 渲染广播式对照表(幕十)。
pub fn render_broadcast_table(title: &str, rows: &[BroadcastRowView], notes: &[&str]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("工单号", NUMBER_WIDTH),
            TableColumn::new("责任链处置者", CHAIN_HANDLER_WIDTH),
            TableColumn::new("广播式处置者", BROADCAST_HANDLER_WIDTH),
            TableColumn::new("结论", DISPOSITION_WIDTH),
            TableColumn::new("责任链判定次数", CHAIN_VISITED_WIDTH),
            TableColumn::new("广播式判定次数", BROADCAST_VISITED_WIDTH),
            TableColumn::new("一致", IDENTICAL_WIDTH),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.ticket_number.clone(),
            row.chain_handler.clone(),
            row.broadcast_handler.clone(),
            row.disposition_text.clone(),
            row.chain_visited.to_string(),
            row.broadcast_visited.to_string(),
            row.identical_text.clone(),
        ]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : extension_report.rs
//! 扩展对照表(幕十二):**证明「完全可扩展」不是一句口号**。
//!
//! ## ★ 可扩展性的判据不是「可以加」,而是「加了之后**自动**被算进去」
//!
//! 「可以加一个新处理器」几乎所有写法都能做到。
//! 做不到的是:加完之后,**不修改任何既有分层文件**,
//! 新内容就被自动纳入链、纳入台账、纳入报表。
//!
//! 因此这一幕给出的证据是两组数字:
//! - **改动文件数 = 0**(工程内 `src/` 分层文件一行未改);
//! - **在哪张表多了几行**(具体列出),而不是一句「已支持扩展」。

use crate::app::{Table, TableColumn};

/// 扩展表的列宽。
///
/// 列宽总和必须 <= 127(四列):`34+22+22+48 = 126`。
const ITEM_WIDTH: usize = 34;
const BUILTIN_WIDTH: usize = 22;
const EXTENDED_WIDTH: usize = 22;
const EVIDENCE_WIDTH: usize = 48;

/// 扩展对照的一行。
#[derive(Clone, Debug)]
pub struct ExtensionRowView {
    /// 对照项。
    pub item: String,
    /// 内置时的值。
    pub builtin_text: String,
    /// 扩展后的值。
    pub extended_text: String,
    /// ★ 证据(在哪张表多了几行 / 改了哪个文件)。
    pub evidence_text: String,
}

/// 渲染扩展对照表(幕十二)。
///
/// # 参数
/// - `rows`:各行。
/// - `notes`:注解。
pub fn render_extension_table(title: &str, rows: &[ExtensionRowView], notes: &[&str]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("对照项", ITEM_WIDTH),
            TableColumn::new("内置(扩展前)", BUILTIN_WIDTH),
            TableColumn::new("扩展后", EXTENDED_WIDTH),
            TableColumn::elidable(TableColumn::new("★ 证据", EVIDENCE_WIDTH)),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.item.clone(),
            row.builtin_text.clone(),
            row.extended_text.clone(),
            row.evidence_text.clone(),
        ]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : foundation_report.rs
//! 基础档案表:门店 / 工单类型 / 权限档位。
//!
//! ## ★ 为什么要单独列出「权限档位」
//!
//! 「哪一级能批多少钱」是责任链上**最容易被人随手改**的一个数,
//! 而改它之后所有的处置结论都会变,报表上却看不出变化来自哪里。
//! 把它单独印成一张表,读者才能在看结论之前先知道口径。

use crate::app::{Table, TableColumn};
use crate::domain::{AuthorityLevel, StoreCode, TicketType};

/// 门店表列宽。
const STORE_CODE_WIDTH: usize = 18;
const STORE_NAME_WIDTH: usize = 20;

/// 渲染门店档案(幕二)。
pub fn render_store_table(stores: &[StoreCode]) -> Vec<String> {
    let mut table: Table = Table::new(
        "幕二 · 门店档案(受理门店)",
        vec![
            TableColumn::new("门店编码", STORE_CODE_WIDTH),
            TableColumn::new("门店名称", STORE_NAME_WIDTH),
        ],
    );
    for store in stores.iter() {
        table = table.with_row(vec![
            store.code().to_string(),
            store.chinese_name().to_string(),
        ]);
    }
    table = table.with_note(
        "★ 门店编码是**开放型标签**(`const fn new`):会不断增加、\
         但不被当作查找键,因此不实现 `Ord` / `Hash`(用不上的 trait 本身就是防错)。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

/// 工单类型表列宽。
const TYPE_CODE_WIDTH: usize = 16;
const TYPE_NAME_WIDTH: usize = 16;
const TYPE_WINDOW_WIDTH: usize = 18;
const TYPE_RECEIPT_WIDTH: usize = 22;
const TYPE_WARRANTY_WIDTH: usize = 20;

/// 渲染工单类型清单(幕三)。
pub fn render_ticket_type_table(ticket_types: &[TicketType]) -> Vec<String> {
    let mut table: Table = Table::new(
        "幕三 · 工单类型清单(每种类型自带判定参数)",
        vec![
            TableColumn::new("编码", TYPE_CODE_WIDTH),
            TableColumn::new("类型名称", TYPE_NAME_WIDTH),
            TableColumn::new("退换窗口", TYPE_WINDOW_WIDTH),
            TableColumn::new("必须凭购买凭证", TYPE_RECEIPT_WIDTH),
            TableColumn::new("走保修判定", TYPE_WARRANTY_WIDTH),
        ],
    );
    for ticket_type in ticket_types.iter() {
        table = table.with_row(vec![
            ticket_type.code().to_string(),
            ticket_type.chinese_name().to_string(),
            if ticket_type.has_return_window() {
                format!("{} 个月", ticket_type.return_window_months())
            } else {
                "不适用".to_string()
            },
            crate::app::bool_text(ticket_type.needs_receipt()).to_string(),
            crate::app::bool_text(ticket_type.needs_warranty()).to_string(),
        ]);
    }
    table = table.with_note(
        "★ 判定参数**写在类型上**而不是写在处理器里:\
         「有没有凭证」这类问题若在三处各写一遍,\
         三处写错成两种口径时报表上不会有任何异常,只会少几张单。",
    );
    table = table.with_note(
        "★ 维修与成色复检**不需要凭证**、但**走保修**;投诉两者都不走,\
         因此它只能靠金额落到某一级权限上------这正是「链要够长」的原因。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

/// 权限档位表列宽。
const LEVEL_CODE_WIDTH: usize = 18;
const LEVEL_NAME_WIDTH: usize = 22;
const LEVEL_CEILING_WIDTH: usize = 20;
/// 40 列按**最多能列出几个样例金额**定:总部一档会列出 5 个金额,
/// 因此这一列是**可省略**的(超宽时按省略号截断,不静默丢字)。
const LEVEL_COVER_WIDTH: usize = 40;

/// 渲染权限档位清单(幕四)。
///
/// # 参数
/// - `levels`:权限档位(**按权限从小到大**)。
/// - `sample_amounts`:样例金额(分),用于演示「这个金额落到哪一级」。
pub fn render_authority_table(levels: &[AuthorityLevel], sample_amounts: &[i64]) -> Vec<String> {
    let mut table: Table = Table::new(
        "幕四 · 处置权限档位(哪一级能批多大金额)",
        vec![
            TableColumn::new("编码", LEVEL_CODE_WIDTH),
            TableColumn::new("权限层级", LEVEL_NAME_WIDTH),
            TableColumn::new("金额上限", LEVEL_CEILING_WIDTH),
            TableColumn::elidable(TableColumn::new("样例金额归属", LEVEL_COVER_WIDTH)),
        ],
    );
    for level in levels.iter() {
        let covered: String = sample_amounts
            .iter()
            .filter(|amount| level.covers(**amount))
            .map(|amount| crate::domain::Money::new(*amount, crate::domain::CNY).amount_text())
            .collect::<Vec<String>>()
            .join("、");
        table = table.with_row(vec![
            level.code().to_string(),
            level.chinese_name().to_string(),
            level.ceiling_text(),
            if covered.is_empty() {
                "---".to_string()
            } else {
                covered
            },
        ]);
    }
    table = table.with_note(
        "★ 判据是 `金额 <= 上限`(**含等号**):恰好等于上限归本级,\
         这是「权限」而不是「阈值」的语义,写反了边界上就会差一张单。",
    );
    table = table.with_note(
        "★ 「样例金额归属」一列刻意**列出所有落在本级的样例**而不是只列最大的:\
         因为更高一级也能批同一笔钱,\
         「哪一级**真的**批了」由链的顺序决定,不由这张表决定。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : health_check_report.rs
//! 体检表(幕十四):**把前面每一幕的结论收成一张可判定的表**。
//!
//! ## ★ 「通过 / 未通过」全工程只有这一处产生
//!
//! 若每张表各写一次「√ / ×」,就会出现一处写「通过」、一处写「OK」的分裂,
//! 而读者只会以为那是两件事。
//! 因此判定文本由 [`crate::analysis::CheckLine::verdict_text`] 唯一产生,
//! 本层只负责排版。

use crate::analysis::CheckReport;
use crate::app::{Table, TableColumn};

/// 体检表的列宽。
///
/// 列宽总和必须 <= 127(四列):`54+41+22+10 = 127`。
/// 这个上界来自表格边框开销:四列时总宽 = `1 + Σ列宽 + 3×4 = Σ列宽 + 13`,
/// 因此 `140 - 13 = 127`。
///
/// ⚠️ 数字由 `scripts/_cell.txt` 的插桩实测得出(**最宽值**,表头也算候选),
/// 不是拍脑袋分配的:检查项 54、实测 21、结论 6(表头 8)。
const ITEM_WIDTH: usize = 54;
const EXPECTATION_WIDTH: usize = 41;
const ACTUAL_WIDTH: usize = 22;
const VERDICT_WIDTH: usize = 10;

/// ★ 可证伪性清单的列宽(两列):`54 + 71 = 125`。
///
/// ## 为什么要单独再来一张表
///
/// 每一条检查的「已知会失败的输入」是**这条检查值不值钱的唯一证据**------
/// 一条对任何输入都成立的检查是恒真的废检查。
/// 但这段证据通常有五六十列宽,塞进四列的体检表只能被省略掉。
/// 于是单独开一张两列的表把它**完整**印出来:体检表负责判定,这张表负责证伪。
/// 判据很简单:**「结论」可以省略,「凭什么说这条检查能失败」不可以。**
const FALSIFICATION_TEXT_WIDTH: usize = 71;

/// 渲染体检表(幕十四)。
///
/// # 参数
/// - `title`:表标题(**把通过数写进标题**:数字由调用方算)。
/// - `report`:检查报告。
pub fn render_check_table(title: &str, report: &CheckReport) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("检查项", ITEM_WIDTH),
            TableColumn::elidable(TableColumn::new("期望", EXPECTATION_WIDTH)),
            TableColumn::new("实测", ACTUAL_WIDTH),
            TableColumn::new("结论", VERDICT_WIDTH),
        ],
    );
    for line in report.lines().iter() {
        table = table.with_row(vec![
            line.item().to_string(),
            line.expectation_text().to_string(),
            line.actual_text().to_string(),
            line.verdict_text().to_string(),
        ]);
    }
    // ⚠️ 先绑定到局部变量再取 `&str`:
    //    `report.failed_items_text()` 返回一个临时 `String`,
    //    在 `format!` 的参数位置上直接借它会 E0716(临时值当场被释放)。
    let tail_text: String = if report.all_passed() {
        "全部通过。".to_string()
    } else {
        report.failed_items_text()
    };
    table = table.with_note(&format!(
        "合计 {} 条,通过 {} 条,未通过 {} 条。{}",
        report.line_total(),
        report.passed_total(),
        report.failed_total(),
        tail_text.as_str()
    ));
    table.render(crate::app::DOCUMENT_WIDTH)
}

/// ★ 渲染可证伪性清单(幕十四续):**每一条检查的「已知会失败的输入」完整印出**。
///
/// # 参数
/// - `title`:表标题(**把条数写进标题**:数字由调用方算)。
/// - `report`:检查报告(与体检表同一份,不再另算一遍)。
pub fn render_falsification_table(title: &str, report: &CheckReport) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("检查项", ITEM_WIDTH),
            // ⚠️ 这一列**不标可省略**:被省略掉的证据等于没有证据。
            TableColumn::new("已知会失败的输入", FALSIFICATION_TEXT_WIDTH),
        ],
    );
    for line in report.lines().iter() {
        table = table.with_row(vec![
            line.item().to_string(),
            line.expectation_text().to_string(),
        ]);
    }
    table = table.with_note(
        "★ 这一列是**体检表的「期望」列的完整版**(体检表里放不下的部分在这里补全)。\
         一条检查若写不出「什么输入会让它失败」,它就是恒真的废检查------\
         这一张表就是用来把这种检查逼出来的。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : interface_report.rs
//! 接口清点(幕十三):**每个对外只读接口都真的调用一次并印出结果**。
//!
//! ## ★ 这一幕同时是告警治理的落点
//!
//! 一个只读接口若在这里被调用并印出结果,它就不可能是「写了没人用」------
//! 也就不可能是「写错了没人发现」。
//! 既有工程的教训:**告警治理优先「用起来」而不是「删掉」**;
//! 「统一出口被绕过」是被 `unused_imports` 抓出来的,
//! 正确修法是改走出口,而不是加 `#[allow]`。
//!
//! ## 为什么用 `&'static str` 当列名之外的键
//!
//! 每一项的第一个元素是**接口全名**(一个字符串),第二个是**调用结果**。
//! 于是「这个接口存在且能跑」与「它返回了什么」在同一行里,
//! 读者不必再去翻代码。

use crate::app::{Table, TableColumn};

/// 接口名列宽。
///
/// ⚠️ 这一列标成**可省略**:接口名会随接口增加而变长,
/// 把版面预算压在它身上,等于让「新增一个接口」变成一件要改列宽的事。
pub const INTERFACE_NAME_WIDTH: usize = 62;

/// 输出列宽(`62 + 71 + 2×3 + 1 = 140 <= 140`)。
///
/// ⚠️ 这一列同样标成**可省略**,但省略是**可见**的(行尾 `...`):
/// 像「省下 29 次判定」这种整句结论在 140 列的两列表里放不下,
/// 完整值在对应那一幕(幕十一)里本来就有,这里重复一遍只是为了让接口被真调。
/// 与此相反,「接口名」一律**压到 62 列以内不许省略**------
/// 它是读者回查代码的唯一线索,被省略就等于没写。
pub const INTERFACE_OUTPUT_WIDTH: usize = 71;

/// 渲染接口清点表(幕十三)。
///
/// # 参数
/// - `title`:表标题(**把总数写进标题**:数字由调用方算,本层不做计算)。
/// - `items`:接口名与调用结果。
pub fn render_interface_table(title: &str, items: &[(String, String)]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::elidable(TableColumn::new("接口", INTERFACE_NAME_WIDTH)),
            TableColumn::elidable(TableColumn::new("调用结果", INTERFACE_OUTPUT_WIDTH)),
        ],
    );
    for (name, output) in items.iter() {
        table = table.with_row(vec![name.clone(), output.clone()]);
    }
    table = table.with_note(
        "★ 每一项都是**真的调用一次**后印出的结果,不是签名列表。\
         因此「写了没人用」的接口在这一幕里无处藏身。",
    );
    table = table.with_note(
        "★ 只读接口指「取一个数 / 取一段文本」这类不改动状态的接口;\
         会改状态的接口(如 `HandlerChain::submit`)在各幕里已经被真跑过。",
    );
    table = table.with_note(
        "★ 版面口径:**接口名一律完整不省略**(这一列的宽度就是按最长的一项定的);\
         「调用结果」里若出现整句结论,放不下时行尾显示 `...`,\
         完整值在对应那一幕里另行印出------可见省略不等于静默截断。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : layout.rs
//! 表格引擎:列宽、对齐、可见省略、框线、注解折行。
//!
//! ## 列宽的唯一来源:`TableColumn`
//!
//! 每一列都要**显式声明宽度**,且宽度依据写在声明处的注释里。
//! 不提供「自适应列宽」:自适应会让同一张表在不同数据下宽度不同,
//! 于是「两次运行逐字节一致」依赖数据而不是代码------那是把不确定性引进来了。

use crate::support::{display_width, elide_text, horizontal_rule, pad_left, pad_right};

/// 列对齐。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnAlign {
    /// 左对齐(文本列)。
    Left,
    /// 右对齐(数字列)。
    Right,
}

/// 一列。
#[derive(Debug, Clone)]
pub struct TableColumn {
    /// 表头文本。
    header: &'static str,
    /// 列宽(**显示列**,中文按 2 列计)。
    width: usize,
    /// 对齐方式。
    align: ColumnAlign,
    /// 是否**允许**可见省略。
    ///
    /// ⚠️ 这个开关是本工程「不许静默截断」纪律的载体:
    /// 不标它是默认,标了它必须是**有意**的(该列内容确实不可控)。
    elidable: bool,
}

impl TableColumn {
    /// 声明一列(左对齐、**不允许**省略)。
    pub const fn new(header: &'static str, width: usize) -> Self {
        TableColumn { header, width, align: ColumnAlign::Left, elidable: false }
    }

    /// 声明一个数字列(右对齐、不允许省略)。
    pub const fn numeric(header: &'static str, width: usize) -> Self {
        TableColumn { header, width, align: ColumnAlign::Right, elidable: false }
    }

    /// 把本列改成**允许**可见省略(链式)。
    pub const fn elidable(mut self) -> Self {
        self.elidable = true;
        self
    }

    /// 表头。
    pub const fn header(&self) -> &'static str {
        self.header
    }

    /// 列宽。
    pub const fn width(&self) -> usize {
        self.width
    }

    /// 对齐。
    pub const fn align(&self) -> ColumnAlign {
        self.align
    }

    /// 是否允许省略。
    pub const fn is_elidable(&self) -> bool {
        self.elidable
    }
}

/// 渲染一个单元格。
///
/// # 三条行为
///
/// 1. `elidable == false` → 内容**必须**放得下;否则 `debug_assert!` 当场 panic,
///    错误信息里带上列宽与所需宽度,直接告诉你该改哪一列。
/// 2. `elidable == true` → 走可见省略,末尾有 `...`,读者知道这里被省了。
/// 3. 省略后按列宽**补白**。
///
/// # ⚠️ 为什么是 `debug_assert` 而不是 `assert`
///
/// 它是**开发期纪律**:`cargo run`(debug)时立刻 panic,
/// release 下不存在,不会让线上报表因一条宽度问题而中断。
/// 同理它也**不是**批量排查的工具------排查请用临时 `eprintln!` 插桩
/// (一次跑出全部位置),见 `app` 模块文档。
pub fn render_cell(column: &TableColumn, text: &str) -> String {
    let fitted: String = if column.is_elidable() {
        elide_text(text, column.width())
    } else {
        let needed: usize = display_width(text);
        debug_assert!(
            needed <= column.width(),
            "单元格放不下且该列不允许省略:列「{}」宽 {} 列,内容需要 {} 列,内容原文=[{}]",
            column.header(),
            column.width(),
            needed,
            text
        );
        // debug 下上面已 panic;release 下退化成可见省略,
        // 而不是静默截断------**任何构建下都不接受「悄悄少一截」**。
        if needed > column.width() {
            elide_text(text, column.width())
        } else {
            text.to_string()
        }
    };

    match column.align() {
        ColumnAlign::Left => pad_right(&fitted, column.width()),
        ColumnAlign::Right => pad_left(&fitted, column.width()),
    }
}

/// 一张表:标题 + 列声明 + 数据行 + 注解。
#[derive(Debug, Clone)]
pub struct Table {
    /// 标题(渲染成 `■ 标题`)。
    title: String,
    /// 列声明。
    columns: Vec<TableColumn>,
    /// 数据行。
    rows: Vec<Vec<String>>,
    /// 注解(**永不省略**,只折行)。
    notes: Vec<String>,
}

impl Table {
    /// 建一张表。
    pub fn new(title: &str, columns: Vec<TableColumn>) -> Self {
        Table {
            title: title.to_string(),
            columns,
            rows: Vec::new(),
            notes: Vec::new(),
        }
    }

    /// 追加一行(链式)。
    pub fn with_row(mut self, row: Vec<String>) -> Self {
        self.rows.push(row);
        self
    }

    /// 追加一条注解(链式)。
    pub fn with_note(mut self, note: &str) -> Self {
        self.notes.push(note.to_string());
        self
    }

    /// 列数。
    pub fn column_count(&self) -> usize {
        self.columns.len()
    }

    /// 行数。
    pub fn row_count(&self) -> usize {
        self.rows.len()
    }

    /// 表的总宽度(各列宽之和 + 每列两侧的空格与竖线)。
    ///
    /// 每列的开销是 3:左空格 1 + 右空格 1 + 竖线 1;
    /// 最左边还有一条竖线,因此再 +1。
    pub fn total_width(&self) -> usize {
        self.columns.iter().map(|column| column.width()).sum::<usize>()
            + self.column_count() * 3
            + 1
    }

    /// 框线(上框 / 分隔 / 下框)。
    fn border_line(&self) -> String {
        format!("┌{}┐", self.segments('┬', '─'))
    }

    fn middle_line(&self) -> String {
        format!("├{}┤", self.segments('┼', '─'))
    }

    fn bottom_line(&self) -> String {
        format!("└{}┘", self.segments('┴', '─'))
    }

    fn segments(&self, joint: char, fill: char) -> String {
        self.columns
            .iter()
            .map(|column| fill.to_string().repeat(column.width() + 2))
            .collect::<Vec<String>>()
            .join(&joint.to_string())
    }

    /// 渲染一行数据。
    ///
    /// ⚠️ 单元格数不足时用空串兜底而不是 panic:
    /// 少给一个单元格只是这一行少一格,
    /// 整张报表仍然可读;panic 会让连后面的幕都出不来。
    fn data_line(&self, cells: &[String]) -> String {
        let rendered: Vec<String> = self
            .columns
            .iter()
            .enumerate()
            .map(|(index, column)| render_cell(column, cells.get(index).map(String::as_str).unwrap_or("")))
            .collect();
        format!("│ {} │", rendered.join(" │ "))
    }

    /// 渲染整张表。
    pub fn render(self, document_width: usize) -> Vec<String> {
        debug_assert!(
            self.total_width() <= document_width,
            "表宽 {} 超过文档宽度 {}(表:{})",
            self.total_width(),
            document_width,
            self.title
        );

        let mut lines: Vec<String> = Vec::new();
        lines.push(section_header(&self.title, document_width));
        lines.push(self.border_line());

        // ★ 表头也走 `render_cell`:表头超宽同样要被抓出来。
        let header_cells: Vec<String> = self
            .columns
            .iter()
            .map(|column| column.header().to_string())
            .collect();
        lines.push(self.data_line(&header_cells));
        lines.push(self.middle_line());

        for row in self.rows.iter() {
            debug_assert!(
                row.len() == self.columns.len(),
                "行宽 {} 与列数 {} 不一致(表:{})",
                row.len(),
                self.columns.len(),
                self.title
            );
            lines.push(self.data_line(row));
        }

        lines.push(self.bottom_line());

        // ★ 空表不静默:一张没有数据行的表,与「这一幕忘了填数据」在版面上
        //   长得一模一样。补一条可见注解,让「零行」变成一个能被看见的状态。
        let mut notes: Vec<String> = self.notes.clone();
        if self.row_count() == 0 {
            notes.push("(本表无数据行)".to_string());
        }
        for note in notes.iter() {
            lines.extend(note_lines(note, document_width));
        }
        lines
    }
}

/// 幕标题:`■ 标题`(右侧补到文档宽度)。
///
/// ⚠️ 标题**只由本函数产生**:若调用方也写一次标题,
/// 就会出现「改一次标题要改两处」------迟早不一致。
pub fn section_header(title: &str, document_width: usize) -> String {
    let text: String = format!("■ {}", title);
    let used: usize = display_width(&text);
    if used >= document_width {
        return text;
    }
    format!("{}{}", text, " ".repeat(document_width - used))
}

/// 把一段长文本折成多行注解(**永不省略**,只折行)。
///
/// ## ⚠️ 可用宽度按「最宽的那种前缀」留白
///
/// 首行前缀 `  · ` 是 **4 列**,续行前缀 `  ` 是 2 列。
/// 若按续行的 2 列留白,首行就会溢出 2 列------
/// 既有工程实测过:37 行注解达到 142 列 / 文档宽 140,
/// 而溢出的注解在框线旁边几乎看不出来。
/// **统一按 4 列留白是唯一不会错的算法**,代价只是续行少 2 列。
///
/// ## 为什么注解永不省略
///
/// 注解承载的是**复算依据**(分子是什么、分母是什么、哪个常数用在哪)。
/// 省略掉一段,读者就少一条验算路径------那正是报表存在的理由。
pub fn note_lines(text: &str, document_width: usize) -> Vec<String> {
    let available: usize = document_width.saturating_sub(4).max(1);
    let mut lines: Vec<String> = Vec::new();
    let mut current: String = String::new();
    let mut current_width: usize = 0;

    for character in text.chars() {
        let character_width: usize = if crate::support::is_wide_character(character) { 2 } else { 1 };
        if current_width + character_width > available && !current.is_empty() {
            lines.push(current);
            current = String::new();
            current_width = 0;
        }
        current.push(character);
        current_width += character_width;
    }
    if !current.is_empty() {
        lines.push(current);
    }
    if lines.is_empty() {
        // 空注解仍然产出一行,避免「注解存在但输出为零行」这种不可见状态。
        lines.push(String::new());
    }

    lines
        .into_iter()
        .enumerate()
        .map(|(index, line)| {
            if index == 0 {
                format!("  · {}", line)
            } else {
                format!("  {}", line)
            }
        })
        .collect()
}

/// 生成一条与表同宽的横线(供需要额外分隔的场合用)。
pub fn table_rule(width: usize) -> String {
    horizontal_rule(width)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : order_report.rs
//! 换序对照表(幕九):**同一批处理器、两条不同的链**。
//!
//! ## ★ 这一幕回答一句话:顺序到底算不算业务规则
//!
//! 换序**照样能编译、照样能跑**,只是结论悄悄变了。
//! 因此要有一个数字:**换序改变了几张单的结论**。
//!
//! 这个数字为 0 不代表没问题(可能是数据没覆盖到),
//! 但它**不为 0** 就证明顺序真的在起作用------
//! 而这一点若只靠看代码,是永远看不出来的。

use crate::app::{Table, TableColumn};

/// 换序表的列宽。
const NUMBER_WIDTH: usize = 14;
const TYPE_WIDTH: usize = 14;
const NORMAL_HANDLER_WIDTH: usize = 20;
const SWAPPED_HANDLER_WIDTH: usize = 20;
const NORMAL_DISPOSITION_WIDTH: usize = 16;
const SWAPPED_DISPOSITION_WIDTH: usize = 16;
const CHANGED_WIDTH: usize = 10;

/// 换序对照的一行。
#[derive(Clone, Debug)]
pub struct OrderRowView {
    /// 工单号。
    pub ticket_number: String,
    /// 工单类型名。
    pub ticket_type_name: String,
    /// 正常序的处置者。
    pub normal_handler: String,
    /// 换序后的处置者。
    pub swapped_handler: String,
    /// 正常序的结论。
    pub normal_disposition: String,
    /// 换序后的结论。
    pub swapped_disposition: String,
    /// 是否变了。
    pub changed_text: String,
}

/// 由分析层对照结果转出一行。
pub fn order_row_from_pair(pair: &crate::analysis::OrderPair) -> OrderRowView {
    OrderRowView {
        ticket_number: pair.ticket_number.clone(),
        ticket_type_name: pair.ticket_type_name.clone(),
        normal_handler: pair.normal_handler.clone(),
        swapped_handler: pair.swapped_handler.clone(),
        normal_disposition: pair.normal_disposition.clone(),
        swapped_disposition: pair.swapped_disposition.clone(),
        changed_text: if pair.changed { "★ 变了" } else { "相同" }.to_string(),
    }
}

/// 渲染换序对照表(幕九)。
///
/// # 参数
/// - `first_column` / `second_column`:★ 两条链的列名(**由调用方传**,
///   因为同一张表在不同幕复用时列名必须跟着换,写死会让后几幕撒谎)。
pub fn render_order_table(
    title: &str,
    rows: &[OrderRowView],
    first_column: &'static str,
    second_column: &'static str,
    notes: &[&str],
) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("工单号", NUMBER_WIDTH),
            TableColumn::new("类型", TYPE_WIDTH),
            TableColumn::new(first_column, NORMAL_HANDLER_WIDTH),
            TableColumn::new(second_column, SWAPPED_HANDLER_WIDTH),
            TableColumn::new("结论一", NORMAL_DISPOSITION_WIDTH),
            TableColumn::new("结论二", SWAPPED_DISPOSITION_WIDTH),
            TableColumn::new("差异", CHANGED_WIDTH),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.ticket_number.clone(),
            row.ticket_type_name.clone(),
            row.normal_handler.clone(),
            row.swapped_handler.clone(),
            row.normal_disposition.clone(),
            row.swapped_disposition.clone(),
            row.changed_text.clone(),
        ]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : outcome_report.rs
//! 逐单处置结果(幕七):**谁接的、走了几站、跳过了几站**。
//!
//! ## ★ 「跳过几站」这一列是本工程的题眼
//!
//! 责任链省下的不是时间(时间不可复现,谈不上逐字节一致的验收),
//! 而是**判定次数**:一张单在第 k 站被接下,
//! 后面 `站数 − k − 1` 站**一次都没被调用**。
//! 这一列把那句抽象的话变成了一个可以逐张单核对的整数。

use crate::app::{Table, TableColumn};

/// 结果表的列宽。
///
/// 列宽总和必须 <= 112(九列):`12+10+14+16+12+14+6+6+22 = 112`。
const NUMBER_WIDTH: usize = 12;
const TYPE_WIDTH: usize = 10;
const AMOUNT_WIDTH: usize = 14;
const HANDLER_WIDTH: usize = 16;
const DISPOSITION_WIDTH: usize = 12;
const COMPENSATION_WIDTH: usize = 14;
/// 「站数」列(表头 4 列,数值至多 1 位)。
const VISITED_WIDTH: usize = 6;
/// 「跳过」列(表头 4 列,数值至多 1 位)。
const SKIPPED_WIDTH: usize = 6;
const TRACE_WIDTH: usize = 22;

/// 结果表的一行。
#[derive(Clone, Debug)]
pub struct OutcomeRowView {
    /// 工单号。
    pub ticket_number: String,
    /// 工单类型名。
    pub ticket_type_name: String,
    /// 涉及金额文本。
    pub amount_text: String,
    /// ★ 处置者。
    pub handler_name: String,
    /// 结论类型。
    pub disposition_text: String,
    /// 赔付金额文本。
    pub compensation_text: String,
    /// 走了几站。
    pub visited_total: usize,
    /// ★ 因短路未调用的站数。
    pub skipped_total: usize,
    /// 访问轨迹。
    pub visited_text: String,
}

/// 由分析层视图转出一行。
pub fn outcome_row_from_view(view: &crate::analysis::OutcomeView) -> OutcomeRowView {
    OutcomeRowView {
        ticket_number: view.ticket_number.clone(),
        ticket_type_name: view.ticket_type_name.clone(),
        amount_text: view.amount_text.clone(),
        handler_name: view.handler_name.clone(),
        disposition_text: view.disposition_text.clone(),
        compensation_text: view.compensation_text.clone(),
        visited_total: view.visited_total,
        skipped_total: view.skipped_total,
        visited_text: view.visited_text.clone(),
    }
}

/// 渲染逐单处置结果(幕七)。
///
/// # 参数
/// - `title`:表标题(**由调用方传**:本表在扩展幕会复用)。
/// - `rows`:各行。
/// - `notes`:注解。
pub fn render_outcome_table(title: &str, rows: &[OutcomeRowView], notes: &[&str]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("工单号", NUMBER_WIDTH),
            TableColumn::new("类型", TYPE_WIDTH),
            TableColumn::new("涉及金额", AMOUNT_WIDTH),
            TableColumn::new("★ 处置者", HANDLER_WIDTH),
            TableColumn::new("结论", DISPOSITION_WIDTH),
            TableColumn::new("赔付", COMPENSATION_WIDTH),
            TableColumn::new("站数", VISITED_WIDTH),
            TableColumn::new("★ 跳过", SKIPPED_WIDTH),
            TableColumn::elidable(TableColumn::new("访问轨迹", TRACE_WIDTH)),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.ticket_number.clone(),
            row.ticket_type_name.clone(),
            row.amount_text.clone(),
            row.handler_name.clone(),
            row.disposition_text.clone(),
            row.compensation_text.clone(),
            row.visited_total.to_string(),
            row.skipped_total.to_string(),
            row.visited_text.clone(),
        ]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : overview_report.rs
//! 总览与两列通用表。
//!
//! ## ★ 为什么「两列表」要有一个通用函数
//!
//! 本工程有七八张「项目 / 值」两列表(总览、价值量化、台账快照......)。
//! 若每张各写一次列宽,就会出现一处 34 列、一处 38 列的分裂,
//! 而**列宽不一致在报表上看不出来**(只有内容超宽时才会炸)。
//!
//! 收口成一个函数之后,改列宽只改一处,
//! 且「最宽的项目名是什么」这个问题只需要回答一次。

use crate::app::{Table, TableColumn};

/// 项目列宽(**按最宽的项目名**定,含表头)。
///
/// ⚠️ 判据:项目名最长的是
/// `★ 若不用责任链(每站都问一遍)要多少次判定`(22 个汉字 = 44 列)之类。
/// 表头「项目」只占 4 列,**不是**最宽的------
/// 既有工程的教训:合计行往往最宽,**表头也算候选**。
const ITEM_WIDTH: usize = 46;

/// 值列宽。
///
/// `140 − 46 − 2×3 − 1 = 87`
const VALUE_WIDTH: usize = 87;

/// ★ 渲染一张「项目 / 值」两列表。
///
/// # 参数
/// - `title`:表标题。
/// - `rows`:行(项目, 值)。
/// - `notes`:注解(**永不省略、只折行**)。
pub fn render_pair_table(title: &str, rows: &[(String, String)], notes: &[&str]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("项目", ITEM_WIDTH),
            TableColumn::elidable(TableColumn::new("值", VALUE_WIDTH)),
        ],
    );
    for (item, value) in rows.iter() {
        table = table.with_row(vec![item.clone(), value.clone()]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

/// 渲染装配总览(幕一)。
///
/// # 参数
/// - `chain_name`:链名。
/// - `station_total`:站数。
/// - `ticket_total`:工单数。
/// - `tail_policy_text`:链尾策略文本。
/// - `station_text`:链上站点顺序(`① 登记 → ② 校验 ...` 形式)。
pub fn render_overview_table(
    chain_name: &str,
    station_total: usize,
    ticket_total: usize,
    tail_policy_text: &str,
    station_text: &str,
) -> Vec<String> {
    let rows: Vec<(String, String)> = vec![
        ("链名".to_string(), chain_name.to_string()),
        ("链上站点数".to_string(), format!("{} 站", station_total)),
        ("待处理工单数".to_string(), format!("{} 张", ticket_total)),
        ("★ 链尾策略".to_string(), tail_policy_text.to_string()),
        ("★ 站点顺序(顺序即业务规则)".to_string(), station_text.to_string()),
    ];
    render_pair_table(
        "幕一 · 装配总览:一条售后处置责任链",
        &rows,
        &[
            "★ 责任链的三条硬约束:① 发送方只调一次 `submit`,不认识任何一站;\
             ② 各站彼此互不知情(没有 `set_next`,顺序在链上);\
             ③ 「没人接」是显式结局,不是一个 `None` 让调用方自己看着办。",
            "★ 站点顺序**不是排版**:把「保修判定」挪到金额权限之后,\
             在保的小额维修就会先被门店按「门店处置」接走,客户就得自付------\
             而代码照样能编译、照样能跑。幕九会把这件事做成一个数字。",
        ],
    )
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : station_report.rs
//! 链上站点清单(幕六):**这条链长什么样**。
//!
//! ## ★ 为什么这张表值得单独占一幕
//!
//! 教科书里 `handler.set_next(next)` 的写法,把链的形状**散进每个处理器**,
//! 于是没有一个地方能列出「这条链长什么样」。
//! 本工程把顺序收进链对象,因此这一幕可以**直接印出**那张表------
//! 读者不需要去读七个 `Box::new` 才知道顺序。

use crate::app::{Table, TableColumn};

/// 站点表的列宽。
///
/// 列宽总和必须 <= 112(九列):`6+16+8+30+8+8+8+8+20 = 112`。
const ORDER_WIDTH: usize = 6;
const NAME_WIDTH: usize = 16;
/// 种类列只放得下**短文本**(`应答者` / `过路者` = 6 列),
/// 完整语义交给「凭什么接」那一列与表尾注解。
const KIND_WIDTH: usize = 8;
const DESCRIPTION_WIDTH: usize = 30;
const ASKED_WIDTH: usize = 8;
const HANDLED_WIDTH: usize = 8;
const PASSED_WIDTH: usize = 8;
const NEVER_WIDTH: usize = 8;
const RISK_WIDTH: usize = 20;

/// 站点表的一行。
#[derive(Clone, Debug)]
pub struct StationRowView {
    /// 站序(从 1 开始)。
    pub order_text: String,
    /// 站名。
    pub name: String,
    /// 种类文本。
    pub kind_text: String,
    /// 凭什么接。
    pub description: String,
    /// 被问次数。
    pub asked_total: usize,
    /// 接下次数。
    pub handled_total: usize,
    /// 放行次数。
    pub passed_total: usize,
    /// 因短路未轮到的次数。
    pub never_reached_total: usize,
    /// 风险提示。
    pub risk_text: String,
}

/// 渲染链上站点清单(幕六)。
///
/// # 参数
/// - `title`:表标题。
/// - `rows`:各行。
/// - `notes`:额外注解(**由调用方传**:同一张表在不同幕的注解不同)。
///
/// ⚠️ 为什么注解不由本模块写死:
/// 既有工程的教训------同一张表在多个幕复用时,**列名与注解都必须由调用方传**,
/// 写死会让后几幕撒谎。
pub fn render_station_table(title: &str, rows: &[StationRowView], notes: &[&str]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("站序", ORDER_WIDTH),
            TableColumn::new("站名", NAME_WIDTH),
            TableColumn::new("种类", KIND_WIDTH),
            TableColumn::elidable(TableColumn::new("凭什么接", DESCRIPTION_WIDTH)),
            TableColumn::new("被问", ASKED_WIDTH),
            TableColumn::new("接下", HANDLED_WIDTH),
            TableColumn::new("放行", PASSED_WIDTH),
            TableColumn::new("未轮到", NEVER_WIDTH),
            TableColumn::elidable(TableColumn::new("这一站的处境", RISK_WIDTH)),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.order_text.clone(),
            row.name.clone(),
            row.kind_text.clone(),
            row.description.clone(),
            row.asked_total.to_string(),
            row.handled_total.to_string(),
            row.passed_total.to_string(),
            row.never_reached_total.to_string(),
            row.risk_text.clone(),
        ]);
    }
    for note in notes.iter() {
        table = table.with_note(note);
    }
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : ticket_report.rs
//! 工单清单(幕五):**进来时的样子**。
//!
//! ## ★ 为什么工单清单与处置结果分成两张表
//!
//! 工单是**输入**,结论是**输出**。
//! 合成一张表,读者就只能看到「这张单最后是 X」,
//! 看不到「它本来应该是 X 吗」。
//! 分开之后,幕七的每一格结论都能回到这一张表上核一遍。

use crate::app::{Table, TableColumn};

/// 工单清单的列宽。
///
/// 列宽总和必须 <= 112(九列):`12+10+10+14+8+6+8+8+34 = 110`。
///
/// 为什么**没有「品类」列**:这一张表只服务「这张单能不能受理」这一个判断,
/// 而品类**不参与**任何一处判定(判据只有:类型、凭证、月数、人为损坏)。
/// 多印一列不影响正确性,但会把「购买至今」挤到放不下------
/// 而「购买至今」正是保修与退换窗口的**输入**。
const NUMBER_WIDTH: usize = 12;
/// 12 列按**最长的门店名**定:`香港铜锣湾店` = 12 列。
const STORE_WIDTH: usize = 12;
const TYPE_WIDTH: usize = 10;
const AMOUNT_WIDTH: usize = 14;
const ELAPSED_WIDTH: usize = 8;
const RECEIPT_WIDTH: usize = 6;
const DAMAGE_WIDTH: usize = 8;
const ADMISSIBLE_WIDTH: usize = 8;
const REASON_WIDTH: usize = 34;

/// 工单清单的一行。
///
/// # 字段说明(**全是可直接排版字符串**)
#[derive(Clone, Debug)]
pub struct TicketRowView {
    /// 工单号。
    pub ticket_number: String,
    /// 门店名。
    pub store_name: String,
    /// 工单类型名。
    pub ticket_type_name: String,
    /// 涉及金额文本。
    pub amount_text: String,
    /// 购买至今月数文本。
    pub elapsed_text: String,
    /// 凭证文本。
    pub receipt_text: String,
    /// 人为损坏文本。
    pub damage_text: String,
    /// 是否可受理。
    pub admissible_text: String,
    /// 不可受理的原因(可受理时为「---」)。
    pub reason_text: String,
}

/// 渲染工单清单(幕五)。
pub fn render_ticket_table(title: &str, rows: &[TicketRowView]) -> Vec<String> {
    let mut table: Table = Table::new(
        title,
        vec![
            TableColumn::new("工单号", NUMBER_WIDTH),
            TableColumn::new("门店", STORE_WIDTH),
            TableColumn::new("类型", TYPE_WIDTH),
            TableColumn::new("涉及金额", AMOUNT_WIDTH),
            TableColumn::new("购买至今", ELAPSED_WIDTH),
            TableColumn::new("凭证", RECEIPT_WIDTH),
            TableColumn::new("人为损坏", DAMAGE_WIDTH),
            TableColumn::new("可受理", ADMISSIBLE_WIDTH),
            TableColumn::elidable(TableColumn::new("不可受理的原因", REASON_WIDTH)),
        ],
    );
    for row in rows.iter() {
        table = table.with_row(vec![
            row.ticket_number.clone(),
            row.store_name.clone(),
            row.ticket_type_name.clone(),
            row.amount_text.clone(),
            row.elapsed_text.clone(),
            row.receipt_text.clone(),
            row.damage_text.clone(),
            row.admissible_text.clone(),
            row.reason_text.clone(),
        ]);
    }
    table = table.with_note(
        "★ 「可受理」与「拒单原因」由工单自己给(不是处理器判的):\
         「能不能受理」只有一个地方说了算,处理器只是问它一句。",
    );
    table.render(crate::app::DOCUMENT_WIDTH)
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : value_display.rs
//! 值对象的**报表排版**:把领域层存的整数渲染成人看的文本。
//!
//! ## ★ 哪些排版留在领域层,哪些搬到这里
//!
//! 判据只有一条:**这个文本形式是不是「所有层都要用」**。
//!
//! - **留在领域层**:`Money::chinese_text()`(`¥12,345.67`)。
//!   机制层报错、分析层结论、表示层表格都要把金额嵌进句子里------
//!   把它搬来表示层会让 `borg` 反向依赖 `app`,那才是真违规。
//!   值对象的**规范文本形式**属于值对象本身。
//! - **搬来表示层**:万分点汇率的两种读法。
//!   机制层存的是**万分点整数**;「把 9215 显示成 0.9215」与「把 5000 显示成 50.00%」
//!   是这张表列头写着「汇率」「复用率」才有的决定,只有报表需要。
//!
//! ## ⚠️ 为什么这里可以有除法
//!
//! 整个计算链路全程是整数,除法只发生在 [`crate::domain::divide_rounded`] 一处。
//! 本文件是**唯一的例外**:这两个函数都不参与任何合计,
//! 只把已经算完的整数拆成「整数部分 + 小数部分」,
//! 所以不存在舍入问题------余数是整除的余数,不是被舍掉的部分。
//!
//! 换句话说:**展示可以除,算账不可以除**。
//!
//! ## ⚠️ 为什么没有「毫克 → 克」这一类函数
//!
//! 前几个工程(门店钱箱、检测中心)的领域里有重量,所以这里备过 `gram_display_text`。
//! 本工程(售后工单)**没有任何重量口径**:金额、月数、次数就是全部量纲,
//! 因此那个函数被删掉而不是留着「以后可能用」。
//! 判据很简单:**排版函数是对着某一张表的列头存在的,没有那一列就不该有那个函数**------
//! 留着它,只会让「本工程有没有重量」这个问题在代码里有两个互相矛盾的答案。

/// 把汇率万分点渲染成小数文本(如 `9215` → `"0.9215"`)。
///
/// ⚠️ **`10000` 与 `10000.0` 是完全不同的两件事**:
/// 前者是 1:1,后者是「万倍」。写成 `{}.{:04}` 而不除 10000,
/// 读者会把 `9215` 读成「九千多倍汇率」。
pub fn basis_points_text(basis_points: i64) -> String {
    let whole: i64 = basis_points.div_euclid(10_000);
    let remainder: i64 = basis_points.rem_euclid(10_000);
    format!("{}.{:04}", whole, remainder)
}

/// ★ 万分点转百分数文本(`10000` -> `100.00`)。
///
/// ⚠️ 为什么单列一个函数:`basis_points_text` 输出的是「几点几」
/// (`10000` -> `1.0000`,因为 10000 万分点 = 1 倍)。
/// 直接给它加一个 `%` 会变成 `1.0000%`------那是**百分之一**,
/// 而复用率 100% 应该显示 `100.00%`。
/// 这个错误在报表上完全看不出来(都是一个带百分号的小数),
/// 只能靠追问「分母是什么」才能发现,因此口径必须收在一处。
pub fn percent_text(basis_points: i64) -> String {
    format!("{}.{:02}", basis_points / 100, (basis_points % 100).abs())
}

/// 把布尔量渲染成中文(`是` / `否`)。
///
/// ⚠️ 报表的布尔列一律走这里,不写 `if x { "是" } else { "否" }`:
/// 散着写迟早会出现一处写成 `符合/不符合`,
/// 而读者会以为那是另一种判断。
pub const fn bool_text(value: bool) -> &'static str {
    if value {
        "是"
    } else {
        "否"
    }
}

/// 把字节数渲染成带千分位的文本。
///
/// ⚠️ 用整数反复除 10 取位,**全程不出现浮点**(与 `Money` 的千分位同理)。
pub fn byte_text(bytes: usize) -> String {
    let digits: Vec<char> = bytes.to_string().chars().collect();
    let mut buffer: Vec<char> = Vec::new();
    for (index, character) in digits.iter().enumerate() {
        let from_right: usize = digits.len() - index;
        if index > 0 && from_right % 3 == 0 {
            buffer.push(',');
        }
        buffer.push(*character);
    }
    buffer.into_iter().collect()
}

//!# encoding: utf-8
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : value_report.rs
//! 价值量化与台账快照(幕十一):**省下了多少次判定**。
//!
//! ## ★ 为什么用「判定次数」而不是耗时
//!
//! 耗时不可复现------两次运行不算同一个数,就谈不上「逐字节一致」的验收。
//! 可数的是:一张单在第 k 站被接下,后面 `站数 − k − 1` 站**一次都没被调用**。
//!
//! ## ★ 为什么台账快照要单独占一幕(续)
//!
//! 台账**只增不减**,因此报表一律读**快照**而不是实时计数:
//! 否则表格先渲染的几行与后渲染的几行来自不同时刻,
//! 读者会以为「跑这些实验没有副作用」------事实恰好相反
//! (每跑一次实验,访问次数就涨一截)。

use crate::analysis::{ChainValueReport, LedgerSnapshotView};
use crate::app::{byte_text, percent_text, render_pair_table};

/// 渲染价值量化表(幕十一)。
///
/// # 参数
/// - `report`:价值报告。
pub fn render_value_table(report: &ChainValueReport) -> Vec<String> {
    let rows: Vec<(String, String)> = vec![
        ("投递工单数".to_string(), format!("{} 张", report.ticket_total())),
        ("链上站点数".to_string(), format!("{} 站", report.station_total())),
        (
            "★ 若不用责任链(每站都问一遍)的判定次数".to_string(),
            format!("{} 次", byte_text(report.visited_total_if_broadcast())),
        ),
        (
            "★ 责任链实际判定次数".to_string(),
            format!("{} 次", byte_text(report.visited_total())),
        ),
        (
            "★ 省下的判定次数(两个可数数字之差)".to_string(),
            // ⚠️ `percent_text` 只负责「万分点 -> 百分数数字」这一段换算,
            //    百分号由调用方补:`percent_text(4143)` 是 `41.43`,
            //    若误用 `basis_points_text` 会得到 `0.4143`(那是「倍」不是「百分之几」)。
            format!(
                "{} 次({}%)",
                byte_text(report.saved_judgement_total() as usize),
                percent_text(report.saved_ratio_basis_points())
            ),
        ),
        (
            "因短路而未被调用的站数之和".to_string(),
            format!("{} 站次", byte_text(report.skipped_total())),
        ),
        ("被处置单数".to_string(), format!("{} 张", report.settled_total())),
        (
            "★ 无人处置单数(链尾有兜底,必须为 0)".to_string(),
            format!("{} 张", report.unhandled_total()),
        ),
        ("赔付合计".to_string(), report.compensation().chinese_text()),
    ];
    render_pair_table(
        "幕十一 · 价值量化:责任链省下了多少次判定",
        &rows,
        &[
            "★ 刻意**不用耗时**做对照:CPU 时间不可复现,\
             两次运行不算同一个数就谈不上验收。",
            "★ 「省下 N 次」隐含一个**不成立**的假设:每次判定的代价一样。\
             事实上「查一下保修期」与「走一遍权限表」的代价差得远。\
             因此另给一个口径(跳过站数)并列,读者才不会把「省了 N 次」当成「省了 N 倍」。",
        ],
    )
}

/// 渲染台账快照(幕十一续)。
///
/// # 参数
/// - `view`:台账快照的**分析层视图**(表示层不认识机制层类型)。
/// - `moment_text`:快照时刻的说明(**必须写清是哪个时刻**)。
pub fn render_ledger_snapshot_table(view: &LedgerSnapshotView, moment_text: &str) -> Vec<String> {
    let rows: Vec<(String, String)> = vec![
        (
            "投递总数(只增不减)".to_string(),
            format!("{} 张", byte_text(view.submitted_total)),
        ),
        (
            "被接下总数".to_string(),
            format!("{} 张", byte_text(view.handled_total)),
        ),
        (
            "无人接手总数".to_string(),
            format!("{} 张", byte_text(view.unhandled_total)),
        ),
        (
            "★ 判定次数(各站被问次数之和)".to_string(),
            format!("{} 次", byte_text(view.visit_total)),
        ),
        (
            "★ 跳过次数(因短路未被调用的站数之和)".to_string(),
            format!("{} 站次", byte_text(view.skipped_total)),
        ),
        (
            "★ 若不用责任链会是多少次判定".to_string(),
            format!("{} 次", byte_text(view.visit_total_if_broadcast())),
        ),
        (
            "★ 省下的判定次数".to_string(),
            format!("{} 次", byte_text(view.saved_judgement_total() as usize)),
        ),
        (
            "恒等式一:投递 − (被接下 + 无人接)".to_string(),
            format!("{}(必须为 0)", view.submission_gap),
        ),
        (
            "恒等式二:被接下 − Σ(各站接下次数)".to_string(),
            format!("{}(必须为 0)", view.settlement_gap),
        ),
        (
            "恒等式三:判定次数 − (被接下 + Σ放行)".to_string(),
            format!("{}(必须为 0)", view.visit_gap),
        ),
        (
            "★ 死站数(一次都没被问过的站)".to_string(),
            format!("{} 站", view.never_asked_stations().len()),
        ),
        (
            "★ 纯过路站数(被问过但一次都没接下)".to_string(),
            format!("{} 站", view.pure_passthrough_stations().len()),
        ),
    ];
    render_pair_table(
        &format!("幕十一(续)· 链台账快照({})", moment_text),
        &rows,
        &[
            "★ 台账**只增不减**,因此报表一律读**快照**:\
             否则表格先渲染的几行与后渲染的几行来自不同时刻。",
            "★ 三条恒等式让「某条路径漏记了计数」变成一个**可计算的数字**,\
             而不是一句原则。",
        ],
    )
}

调用:

rust 复制代码
//!# encoding: utf-8 
//!# 版权所有  2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:责任链模式Chain Of Responsibility Pattern  行为模式 Behavioral Patterns
//!# Author    : geovindu,Geovin Du 涂聚文.
//!# IDE       : RustRover  2025.1.1 Rust  rustc 1.98.1 
//!# os        : windows 10
//!# database  : mysql 9.0 sql server 2025, postgreSQL 18.0  Oracle 21c Neo4j
//!# Datetime  : 2026/10/14 19:47 
//!# User      :  geovindu
//!# Product   : RustRover
//!# Project   : chainofresponsibilitypattern
//!# File      : main.rs
//! # 责任链模式(Chain of Responsibility)------ 连锁珠宝售后工单处置链
//!
//! ## 严格分层结构
//!
//! ```text
//! main                     装配点:造数据、装链、投递、编排十五幕、工程外扩展区
//!  └── app                 表示层:表格引擎 + 十三类报表(**不认识 chain / client**)
//!       └── analysis       分析层:站点对照 / 换序对照 / 广播式对照 / 审计视图 / 体检
//!            ├── client    调用层:工单处理器 + 链装配 + 会话
//!            │    ├── domain    领域层:金额 / 月份 / 门店 / 品类 / 工单 / 权限 / 结论
//!            │    │    └── support  支撑层:日期 / 确定性编码 / 显示宽度
//!            │    └── chain     ★ 机制层:处理器契约 + 链 + 台账 + 审计(**零 crate::**)
//!            └── chain       机制层(分析层直接读它的台账与审计结果)
//! ```
//!
//! ## 依赖方向(**实测**,由 `scripts/check_layer_dependencies.sh` 得出,非设计意图)
//!
//! ```text
//! (`cargo build` 通过后跑脚本,把实测邻接表回填到这里)
//! ```
//!
//! ## 这个模式在解决什么
//!
//! > 一个请求的**发送方不该知道谁会处理它**。
//!
//! 本工程用一条**售后处置链**把它落成可以核对的东西:
//! 一张工单进来,沿链被逐站询问,直到某一站接下它。
//!
//! ## ★ 本工程最想讲清的四件事
//!
//! 1. **发送方不认识任何一站**:只调一次 `submit`,拿回一个带处置者名的结果。
//!    若调用方要自己挑处理器,那就不是责任链。
//! 2. **顺序是业务知识,不是排版**:把「保修判定」挪到金额权限之后,
//!    在保的小额维修就会先被门店按「门店处置」接走,客户就得自付------
//!    **照样能编译、照样能跑**。幕九把这个事实做成一个数字。
//! 3. **「没人接」是显式结局**:`UNHANDLED_MARK` 是一个词,不是一个 `None`。
//!    摘掉兜底站会让 `SETTLED` 那条规则变红------幕八续真的摘过一次。
//! 4. **省下的是判定次数,不是时间**:时间不可复现,谈不上「逐字节一致」的验收;
//!    可数的是「第 k 站接下之后,后面 `站数 − k − 1` 站一次都没被调用」。
//!
//! ## ★ 责任链与流水线的分界
//!
//! 链上既有**应答者**(接了就停),也有**过路者**(做完继续,如登记留痕)。
//! 两者常串在同一条链上,因此不能靠「是不是责任链」二选一,
//! 而要靠 `HandlerKind` 这一个字段分开------审计才会说「过路者不做决定」。

mod analysis;
mod app;
mod chain;
mod client;
mod domain;
mod support;

use std::rc::Rc;

use crate::analysis::{
    audit_check_views, broadcast_checks, compare_broadcast, compare_orders, compensation_total,
    order_checks, station_checks, station_probes, AuditCheckView, BroadcastPair, ChainValueReport,
    CheckLine, CheckReport, LedgerSnapshotView, OrderPair, OutcomeView, StationProbe,
};
use crate::app::{
    basis_points_text, bool_text, byte_text, note_lines, render_audit_table, render_authority_table,
    render_broadcast_table, render_check_table, render_extension_table,
    render_falsification_table, render_interface_table, render_ledger_snapshot_table,
    render_order_table, render_outcome_table, render_overview_table, render_pair_table, render_station_table, render_store_table, render_ticket_table,
    render_ticket_type_table, render_value_table, section_header, table_rule, BroadcastRowView,
    ExtensionRowView, OrderRowView, OutcomeRowView, StationRowView, TicketRowView, DOCUMENT_WIDTH,
    INTERFACE_NAME_WIDTH, INTERFACE_OUTPUT_WIDTH,
};
use crate::chain::{
    audit_chain, ChainAuditRule, ChainLedger, ChainTailPolicy, Handling, Handler, HandlerKind,
    LedgerSnapshot, RequestObservation, UNHANDLED_MARK,
};
use crate::client::{
    build_escalation_handler, build_ticket_chain, build_warranty_handler,
    station_descriptions, TicketSession, HANDLER_ADMISSION_NAME, HANDLER_ESCALATION_NAME,
    HANDLER_HEADQUARTER_NAME, HANDLER_LOGGING_NAME, HANDLER_ORDER_NORMAL,
    HANDLER_ORDER_NO_FALLBACK, HANDLER_ORDER_SWAPPED, HANDLER_REGIONAL_NAME, HANDLER_STORE_NAME,
    HANDLER_WARRANTY_NAME,
};
use crate::domain::{
    divide_rounded, ratio_basis_points, Disposition, Money, MonthKey, ServiceTicket,
    TicketResolution, TicketType, BUILTIN_AUTHORITY_LEVELS, BUILTIN_STORE_CODES, CATEGORY_DIAMOND,
    CATEGORY_GOLD, CATEGORY_JADE, CNY, STORE_CN_01, STORE_HK_01, TICKET_COMPLAINT, TICKET_EXCHANGE,
    TICKET_RECHECK, TICKET_REPAIR, TICKET_RETURN, WARRANTY_MONTHS,
};
use crate::support::{
    days_between, days_in_month, deterministic_code, display_width, elide_text, is_leap_year,
    pad_center, prefixed_code, report_cell_text, self_check_width_model, stable_hash,
    truncate_to_width, CalendarDate,
};

/// 演示用的「当前月」。
const DEMO_MONTH: MonthKey = MonthKey::new(2026, 10);

/// 演示用的权限档位样例金额(分),用于幕四「样例金额归属」一列。
const SAMPLE_AMOUNTS: [i64; 5] = [98_000, 150_000, 1_280_000, 36_800_000, 128_000_000];

/// ★ 演示工单(**十张,覆盖六种结局**)。
///
/// 覆盖判据(改数据前先看这里):
/// - 拒单 2 张:无凭证退货、超窗口换货;
/// - 保修 2 张:在保维修、在保复检;
/// - 门店 2 张:人为损坏小额(不在保)、投诉小额;
/// - 区域 2 张:退货中额、过保维修;
/// - 总部 1 张:退货大额;
/// - 上报 1 张:换货超大额(超出总部权限)。
///
/// ⚠️ 这十张单刻意让**链上每一站都至少被问到一次**------
/// 否则「死站数 == 0」那条检查会变成恒真的废检查。
fn demo_tickets() -> Vec<ServiceTicket> {
    vec![
        // ① 退货 ¥12,800:有凭证、在窗口内 → 金额超出门店权限 → 区域处置。
        ServiceTicket::new(
            "WO-2026-0001",
            STORE_CN_01,
            CATEGORY_GOLD,
            TICKET_RETURN,
            Money::new(1_280_000, CNY),
            MonthKey::new(2026, 9),
            DEMO_MONTH,
            true,
            false,
        ),
        // ② 退货 ¥3,200:**无凭证** → 单据校验站拒单。
        ServiceTicket::new(
            "WO-2026-0002",
            STORE_CN_01,
            CATEGORY_DIAMOND,
            TICKET_RETURN,
            Money::new(320_000, CNY),
            MonthKey::new(2026, 9),
            DEMO_MONTH,
            false,
            false,
        ),
        // ③ 换货 ¥8,600:有凭证但**已过 3 个月窗口**(4 个月)→ 拒单。
        ServiceTicket::new(
            "WO-2026-0003",
            STORE_HK_01,
            CATEGORY_JADE,
            TICKET_EXCHANGE,
            Money::new(860_000, CNY),
            MonthKey::new(2026, 6),
            DEMO_MONTH,
            true,
            false,
        ),
        // ④ 维修 ¥1,500:在保(14 个月 ≤ 24)→ 保修免费维修(**不看金额**)。
        ServiceTicket::new(
            "WO-2026-0004",
            STORE_CN_01,
            CATEGORY_GOLD,
            TICKET_REPAIR,
            Money::new(150_000, CNY),
            MonthKey::new(2025, 8),
            DEMO_MONTH,
            false,
            false,
        ),
        // ⑤ 维修 ¥6,800:**过保**(41 个月 > 24)→ 落到区域权限。
        ServiceTicket::new(
            "WO-2026-0005",
            STORE_HK_01,
            CATEGORY_DIAMOND,
            TICKET_REPAIR,
            Money::new(680_000, CNY),
            MonthKey::new(2023, 5),
            DEMO_MONTH,
            false,
            false,
        ),
        // ⑥ 维修 ¥2,400:**人为损坏** → 不在保 → 落到门店权限(自费修)。
        ServiceTicket::new(
            "WO-2026-0006",
            STORE_CN_01,
            CATEGORY_GOLD,
            TICKET_REPAIR,
            Money::new(240_000, CNY),
            MonthKey::new(2026, 2),
            DEMO_MONTH,
            false,
            true,
        ),
        // ⑦ 成色复检 ¥980:在保(6 个月)→ 保修免费处理。
        ServiceTicket::new(
            "WO-2026-0007",
            STORE_HK_01,
            CATEGORY_JADE,
            TICKET_RECHECK,
            Money::new(98_000, CNY),
            MonthKey::new(2026, 4),
            DEMO_MONTH,
            false,
            false,
        ),
        // ⑧ 服务投诉 ¥2,600:不走保修、不需凭证 → 落到门店权限。
        ServiceTicket::new(
            "WO-2026-0008",
            STORE_CN_01,
            CATEGORY_DIAMOND,
            TICKET_COMPLAINT,
            Money::new(260_000, CNY),
            MonthKey::new(2026, 8),
            DEMO_MONTH,
            false,
            false,
        ),
        // ⑨ 退货 ¥368,000:超区域权限、在总部权限内 → 总部处置。
        ServiceTicket::new(
            "WO-2026-0009",
            STORE_HK_01,
            CATEGORY_GOLD,
            TICKET_RETURN,
            Money::new(36_800_000, CNY),
            MonthKey::new(2026, 9),
            DEMO_MONTH,
            true,
            false,
        ),
        // ⑩ 换货 ¥1,280,000:**超出总部权限** → 总裁办兜底。
        ServiceTicket::new(
            "WO-2026-0010",
            STORE_CN_01,
            CATEGORY_JADE,
            TICKET_EXCHANGE,
            Money::new(128_000_000, CNY),
            MonthKey::new(2026, 9),
            DEMO_MONTH,
            true,
            false,
        ),
    ]
}

// ===========================================================================
// ★ 工程外扩展区:**以下全部新增内容都在 `main.rs` 里**,
//   `src/` 分层文件一行未改(幕十二会把「改动文件数 = 0」作为证据列出)。
// ===========================================================================

/// 扩展工单类型:鉴定争议(**不需凭证、不走保修** → 只能靠金额落到某一级权限)。
const TICKET_APPRAISAL: TicketType = TicketType::new("APPRAISAL", "鉴定争议", 0, false, false);

/// 扩展站名:人为损坏快速通道。
const HANDLER_QUICK_LANE_NAME: &str = "人为损坏快速通道";

/// ★ 快速通道的金额上限:¥10,000.00(= 1000000 分)。
const QUICK_LANE_CEILING_MINOR_UNITS: i64 = 1_000_000;

/// ★ 快速通道的公司承担比例:**50%**(5000 万分点 = 0.5000)。
///
/// ⚠️ 为什么刻意取 50% 而不是 100%:
/// 若承担比例是 100%,这一站的结论与「门店处置」完全相同,
/// 幕十二的扩展对照就变成「两个一样的数」------
/// 那时「扩展真的起作用了」这句话**无法被反驳**。
const QUICK_LANE_BEAR_BASIS_POINTS: i64 = 5_000;

/// ★ 工程外新增的一站:**人为损坏快速通道**。
///
/// 判据:人为损坏 且 金额 ≤ ¥10,000 → 由门店按「公司承担 50%」快速结案。
///
/// 这一站**没有**在 `src/client` 里登记,完全由装配点给出------
/// 这就是「完全可扩展」的含义:机制层与调用层都不知道它存在。
fn build_quick_lane_handler() -> Box<dyn Handler<ServiceTicket, TicketResolution>> {
    let predicate: Box<dyn Fn(&ServiceTicket) -> bool> = Box::new(|ticket| {
        ticket.is_man_made_damage()
            && ticket.involved_amount().minor_units() <= QUICK_LANE_CEILING_MINOR_UNITS
    });
    let action: Box<dyn Fn(&ServiceTicket) -> Handling<TicketResolution>> = Box::new(|ticket| {
        if !ticket.is_man_made_damage()
            || ticket.involved_amount().minor_units() > QUICK_LANE_CEILING_MINOR_UNITS
        {
            return Handling::Pass;
        }
        // ★ 唯一舍入口径:`scale_basis_points` 内部走 `divide_rounded`
        //   (远离零方向四舍五入),本工程不另写一套除法。
        let compensation: Money =
            ticket.involved_amount().scale_basis_points(QUICK_LANE_BEAR_BASIS_POINTS);
        let note: String = format!(
            "人为损坏且金额 ≤ {},走快速通道:公司承担 {}({})",
            Money::new(QUICK_LANE_CEILING_MINOR_UNITS, CNY).chinese_text(),
            basis_points_text(QUICK_LANE_BEAR_BASIS_POINTS),
            compensation.chinese_text()
        );
        Handling::Handled(TicketResolution::new(
            Disposition::StoreHandled,
            HANDLER_QUICK_LANE_NAME,
            compensation,
            note.as_str(),
        ))
    });
    Box::new(crate::client::ClosureHandler::new(
        HANDLER_QUICK_LANE_NAME,
        HandlerKind::Responder,
        predicate,
        action,
        format!(
            "人为损坏且金额 ≤ {} 的小额单,公司承担 {} 快速结案",
            Money::new(QUICK_LANE_CEILING_MINOR_UNITS, CNY).chinese_text(),
            basis_points_text(QUICK_LANE_BEAR_BASIS_POINTS)
        ),
    ))
}

/// ★ 工程外新增的工单清单(在内置十张之后追加两张**鉴定争议**单)。
fn extended_tickets() -> Vec<ServiceTicket> {
    let mut tickets: Vec<ServiceTicket> = demo_tickets();
    tickets.push(ServiceTicket::new(
        "WO-2026-0011",
        STORE_CN_01,
        CATEGORY_JADE,
        TICKET_APPRAISAL,
        Money::new(240_000_000, CNY),
        MonthKey::new(2025, 12),
        DEMO_MONTH,
        true,
        false,
    ));
    tickets.push(ServiceTicket::new(
        "WO-2026-0012",
        STORE_HK_01,
        CATEGORY_GOLD,
        TICKET_APPRAISAL,
        Money::new(68_000, CNY),
        MonthKey::new(2026, 7),
        DEMO_MONTH,
        false,
        false,
    ));
    tickets
}

/// ★ 工程外装配的**扩展链**:在「保修判定」之后、金额权限之前插一站。
///
/// ⚠️ 为什么要插在中间而不是追加在末尾:
/// 追加在兜底站之后,这一站**永远轮不到**(死站),
/// 于是「扩展真的起作用了」就变成一句无法被反驳的话。
/// 插在中间,它会真的接下一张单(⑥ 人为损坏 ¥2,400)------
/// 幕十二会拿这张单的**赔付金额变了**作为证据。
fn build_extended_chain(
    ledger: Rc<ChainLedger>,
) -> crate::chain::HandlerChain<ServiceTicket, TicketResolution> {
    build_ticket_chain(
        "扩展链(内置 7 站 + 工程外 1 站)",
        ChainTailPolicy::RequireHandled,
        ledger,
        &[
            HANDLER_LOGGING_NAME,
            HANDLER_ADMISSION_NAME,
            HANDLER_WARRANTY_NAME,
        ],
    )
    .with_handler(build_quick_lane_handler())
    .with_handler(crate::client::build_store_authority_handler())
    .with_handler(crate::client::build_regional_authority_handler())
    .with_handler(crate::client::build_headquarter_authority_handler())
    .with_handler(build_escalation_handler())
}

fn main() {
    // ---------- 幕一:装配总览 ----------
    println!("{}", section_header("责任链模式 · 连锁珠宝售后工单处置链", DOCUMENT_WIDTH));
    println!();
    let station_text: String = HANDLER_ORDER_NORMAL
        .iter()
        .enumerate()
        .map(|(index, name)| format!("{} {}", circled_number(index + 1), name))
        .collect::<Vec<String>>()
        .join(" → ");
    print_lines(&render_overview_table(
        "主链",
        HANDLER_ORDER_NORMAL.len(),
        demo_tickets().len(),
        ChainTailPolicy::RequireHandled.chinese_text(),
        station_text.as_str(),
    ));
    println!();

    // ---------- 幕二 / 三 / 四:基础档案 ----------
    print_lines(&render_store_table(&BUILTIN_STORE_CODES));
    println!();
    print_lines(&render_ticket_type_table(&[
        TICKET_RETURN,
        TICKET_EXCHANGE,
        TICKET_REPAIR,
        TICKET_RECHECK,
        TICKET_COMPLAINT,
    ]));
    println!();
    print_lines(&render_authority_table(&BUILTIN_AUTHORITY_LEVELS, &SAMPLE_AMOUNTS));
    println!();

    // ---------- 幕五:工单清单 ----------
    let tickets: Vec<ServiceTicket> = demo_tickets();
    let ticket_rows: Vec<TicketRowView> = tickets
        .iter()
        .map(|ticket| TicketRowView {
            ticket_number: ticket.ticket_number().to_string(),
            store_name: ticket.store().chinese_name().to_string(),
            ticket_type_name: ticket.ticket_type().chinese_name().to_string(),
            amount_text: ticket.involved_amount().chinese_text(),
            elapsed_text: format!("{} 个月", ticket.months_elapsed()),
            receipt_text: if ticket.has_receipt() { "有" } else { "无" }.to_string(),
            damage_text: if ticket.is_man_made_damage() { "是" } else { "否" }.to_string(),
            admissible_text: if ticket.is_inadmissible() { "否" } else { "是" }.to_string(),
            reason_text: ticket.inadmissible_reason_text(),
        })
        .collect();
    print_lines(&render_ticket_table("幕五 · 工单清单(进来时的样子)", &ticket_rows));
    println!();

    // ---------- 建主链并跑一遍 ----------
    let ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let chain = build_ticket_chain(
        "主链",
        ChainTailPolicy::RequireHandled,
        Rc::clone(&ledger),
        &HANDLER_ORDER_NORMAL,
    );
    let session: TicketSession = TicketSession::run(chain, &tickets);
    let snapshot: LedgerSnapshot = ledger.snapshot();
    let snapshot_view: LedgerSnapshotView = LedgerSnapshotView::from_snapshot(&snapshot);

    // ---------- 幕六:链上站点清单 ----------
    let probes: Vec<StationProbe> = station_probes(&session, &snapshot, &station_descriptions());
    let station_rows: Vec<StationRowView> = probes
        .iter()
        .enumerate()
        .map(|(index, probe)| StationRowView {
            order_text: circled_number(index + 1),
            name: probe.name().to_string(),
            kind_text: probe.kind_text().to_string(),
            description: probe.description().to_string(),
            asked_total: probe.asked_total(),
            handled_total: probe.handled_total(),
            passed_total: probe.passed_total(),
            never_reached_total: probe.never_reached_total(),
            risk_text: probe.risk_text(),
        })
        .collect();
    print_lines(&render_station_table(
        &format!("幕六 · 链上站点清单({} 张单投递完毕之后)", session.ticket_total()),
        &station_rows,
        &[
            "★ 「未轮到」= 在它之前就被处置掉的单数(**精确口径**,不是 `单数 − 被问`):\
             一张无人处置的单会走完链尾,若用减法会把它错算进「未轮到」。",
            "★ 「这一站的处境」只引用本行**自己的两个数**(被问、接下),\
             不引用任何外部口径------否则这张表就会说出与数字不符的话。",
        ],
    ));
    println!();

    // ---------- 幕七:逐单处置结果 ----------
    let outcome_views: Vec<OutcomeView> = OutcomeView::list_from_outcomes(session.outcomes());
    let outcome_rows: Vec<OutcomeRowView> =
        outcome_views.iter().map(crate::app::outcome_row_from_view).collect();
    print_lines(&render_outcome_table(
        "幕七 · 逐单处置结果(谁接的、走了几站、跳过了几站)",
        &outcome_rows,
        &[
            "★ 「跳过几站」这一列是责任链**唯一**可数的收益:\
             一张单在第 k 站被接下,后面 `站数 − k − 1` 站**一次都没被调用**。",
            "★ 结论相同的两张单(如 ①⑤ 都是区域处置)走了不同的站数------\
             这说明判定次数取决于**单子的属性**,而不是结论类型。",
        ],
    ));
    println!();

    // ---------- 幕八:契约审计(正常用例) ----------
    let normal_observations: Vec<RequestObservation> = session.observations();
    let normal_checks: Vec<AuditCheckView> = audit_check_views(&audit_chain(&normal_observations));
    print_lines(&render_audit_table(
        "幕八 · 契约审计:责任链的六条不变量(正常用例)",
        &normal_checks,
    ));
    println!();

    // ---------- 幕八(续):已知会失败的输入 ----------
    let negative_observations: Vec<RequestObservation> = build_negative_observations(&tickets);
    let negative_checks: Vec<AuditCheckView> =
        audit_check_views(&audit_chain(&negative_observations));
    print_lines(&render_audit_table(
        "幕八(续)· 已知会失败的输入:真的把坏样本喂进去",
        &negative_checks,
    ));
    println!();
    print_lines(&render_no_fallback_evidence(&tickets));
    println!();

    // ---------- 幕九:换序对照 ----------
    let swapped_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let swapped_chain = build_ticket_chain(
        "换序链(保修判定挪到金额权限之后)",
        ChainTailPolicy::RequireHandled,
        Rc::clone(&swapped_ledger),
        &HANDLER_ORDER_SWAPPED,
    );
    let swapped_session: TicketSession = TicketSession::run(swapped_chain, &tickets);
    let order_pairs: Vec<OrderPair> = compare_orders(&session, &swapped_session);
    let order_rows: Vec<OrderRowView> =
        order_pairs.iter().map(crate::app::order_row_from_pair).collect();
    print_lines(&render_order_table(
        "幕九 · 换序对照:把「保修判定」挪到金额权限之后",
        &order_rows,
        "正常序处置者",
        "换序后处置者",
        &[
            "★ 两张在保维修单的处置者变了(④⑦):\
             换序后它们先撞上「门店权限」,于是结论从「保修免费维修」变成「门店处置」------\
             **客户就得自付**。而代码照样能编译、照样能跑。",
            "★ 过保的 ⑤ 与人为损坏的 ⑥ **没有**变化:\
             它们的判定本来就不涉及保修,换序影响不到------\
             这正是「归因正确」的证据(见幕十四体检)。",
        ],
    ));
    println!();

    // ---------- 幕十:广播式对照 ----------
    let broadcast_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let broadcast_chain = build_ticket_chain(
        "广播式链",
        ChainTailPolicy::RequireHandled,
        Rc::clone(&broadcast_ledger),
        &HANDLER_ORDER_NORMAL,
    );
    let broadcast_session: TicketSession =
        TicketSession::run_broadcast(broadcast_chain, &tickets, Rc::clone(&broadcast_ledger));
    let broadcast_pairs: Vec<BroadcastPair> = compare_broadcast(&session, &broadcast_session);
    let broadcast_rows: Vec<BroadcastRowView> = broadcast_pairs
        .iter()
        .map(crate::app::broadcast_row_from_pair)
        .collect();
    print_lines(&render_broadcast_table(
        "幕十 · 广播式对照:同一批处理器,每站都问一遍",
        &broadcast_rows,
        &[
            "★ 广播式刻意做成「**第一个接的算数**」,因此结论与责任链**逐单相同**,\
             唯一的差别是判定次数------那样「省下 N 次」才是干净的对照。",
            "★ 广播式的判定次数恒为 `单数 × 站数`;责任链每张单只走到被接下为止。\
             两者之差就是责任链省下的判定次数(见幕十一)。",
        ],
    ));
    println!();

    // ---------- 幕十一:价值量化 ----------
    let value_report: ChainValueReport = ChainValueReport::new(
        session.ticket_total(),
        session.chain().handler_total(),
        session.visited_total(),
        session.skipped_total(),
        compensation_total(session.outcomes()).minor_units(),
        session.settled_total(),
        session.unhandled_total(),
    );
    print_lines(&render_value_table(&value_report));
    println!();
    print_lines(&render_ledger_snapshot_table(
        &snapshot_view,
        "十张单投递完毕之后(**快照**,不是实时计数)",
    ));
    println!();

    // ---------- 幕十二:扩展对照 ----------
    print_lines(&render_extension_evidence(&session, &tickets));

    // ---------- 幕十三:接口清点 ----------
    let interface_rows: Vec<(String, String)> =
        interface_items(&session, &value_report, &snapshot_view, &probes);
    print_lines(&render_interface_table(
        &format!("幕十三 · 对外只读接口清点:{} 个接口逐个真调", interface_rows.len()),
        &interface_rows,
    ));
    println!();

    // ---------- 幕十四:体检 ----------
    let mut report: CheckReport = CheckReport::new();
    for line in station_checks(&probes, session.ticket_total()) {
        report.push(line);
    }
    for line in order_checks(&order_pairs) {
        report.push(line);
    }
    for line in broadcast_checks(
        &broadcast_pairs,
        session.visited_total(),
        broadcast_session.visited_total(),
        session.chain().handler_total(),
    ) {
        report.push(line);
    }
    for line in value_report.self_check() {
        report.push(line);
    }
    for line in audit_self_check(&normal_checks, &negative_checks) {
        report.push(line);
    }
    for line in layout_self_check() {
        report.push(line);
    }
    for line in extension_self_check(&session) {
        report.push(line);
    }
    print_lines(&render_check_table(
        &format!(
            "幕十四 · 体检:{} 条检查(通过 {} 条 / 未通过 {} 条)",
            report.line_total(),
            report.passed_total(),
            report.failed_total()
        ),
        &report,
    ));
    println!();
    print_lines(&render_falsification_table(
        &format!(
            "幕十四(续)· 可证伪性:{} 条检查各自的「已知会失败的输入」",
            report.line_total()
        ),
        &report,
    ));
    println!();
    // ⚠️ 同上:先绑定局部变量再取 `&str`,否则 E0716。
    let verdict_tail: String = if report.all_passed() {
        "★ 体检全部通过。".to_string()
    } else {
        report.failed_items_text()
    };
    println!("{}", verdict_tail.as_str());
}

/// ★ 幕八(续)的负向观测:**每条规则各喂一个坏样本**。
///
/// 恒真的检查没有价值,因此这里把 `known_failing_input_text()` 里写的那件事
/// **真的做一遍**,确认规则会红。
fn build_negative_observations(tickets: &[ServiceTicket]) -> Vec<RequestObservation> {
    let declared_total: usize = HANDLER_ORDER_NORMAL.len();
    let mut observations: Vec<RequestObservation> = Vec::new();

    // ① `SETTLED` 的坏样本:跑一条**没有兜底站**的链,超大额单会无人接手。
    let no_fallback_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let no_fallback_chain = build_ticket_chain(
        "缺兜底链",
        ChainTailPolicy::AllowUnhandled,
        Rc::clone(&no_fallback_ledger),
        &HANDLER_ORDER_NO_FALLBACK,
    );
    let no_fallback_session: TicketSession = TicketSession::run(no_fallback_chain, tickets);
    observations.extend(no_fallback_session.observations());

    // ② `SINGLE` 的坏样本:一张单写了两个处置者。
    observations.push(RequestObservation::new(
        "BAD-SINGLE",
        vec![HANDLER_STORE_NAME.to_string(), HANDLER_WARRANTY_NAME.to_string()],
        Some(HandlerKind::Responder),
        HANDLER_STORE_NAME,
        vec![0, 1],
        declared_total,
    ));

    // ③ `ORDERED` 的坏样本:先访问了第 4 站,又回到第 2 站。
    observations.push(RequestObservation::new(
        "BAD-ORDERED",
        vec![HANDLER_STORE_NAME.to_string()],
        Some(HandlerKind::Responder),
        HANDLER_STORE_NAME,
        vec![3, 1],
        declared_total,
    ));

    // ④ `NO_GHOST` 的坏样本:处置者名写成空串(报表上就是一个空格子)。
    observations.push(RequestObservation::new(
        "BAD-GHOST",
        vec![HANDLER_STORE_NAME.to_string()],
        Some(HandlerKind::Responder),
        "",
        vec![0],
        declared_total,
    ));

    // ⑤ `NO_PASSTHROUGH` 的坏样本:过路站却带着处置结论。
    observations.push(RequestObservation::new(
        "BAD-PASSTHROUGH",
        vec![HANDLER_LOGGING_NAME.to_string()],
        Some(HandlerKind::PassThrough),
        HANDLER_LOGGING_NAME,
        vec![0],
        declared_total,
    ));

    // ⑥ `IN_RANGE` 的坏样本:访问位置越界。
    observations.push(RequestObservation::new(
        "BAD-RANGE",
        vec![HANDLER_STORE_NAME.to_string()],
        Some(HandlerKind::Responder),
        HANDLER_STORE_NAME,
        vec![declared_total + 2],
        declared_total,
    ));

    observations
}

/// 幕八(续)的补充证据:把「缺兜底链」的结果印成一张表。
fn render_no_fallback_evidence(tickets: &[ServiceTicket]) -> Vec<String> {
    let ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let chain = build_ticket_chain(
        "缺兜底链(摘掉第 7 站)",
        ChainTailPolicy::AllowUnhandled,
        Rc::clone(&ledger),
        &HANDLER_ORDER_NO_FALLBACK,
    );
    let session: TicketSession = TicketSession::run(chain, tickets);
    let mut names: Vec<String> = Vec::new();
    for outcome in session.outcomes().iter() {
        if !outcome.result().is_handled() {
            names.push(outcome.ticket_number().to_string());
        }
    }
    let rows: Vec<(String, String)> = vec![
        ("链上站点数".to_string(), format!("{} 站", session.chain().handler_total())),
        ("投递单数".to_string(), format!("{} 张", session.ticket_total())),
        ("★ 无人处置单数".to_string(), format!("{} 张", session.unhandled_total())),
        (
            "★ 无人处置的工单".to_string(),
            if names.is_empty() {
                "无".to_string()
            } else {
                names.join("、")
            },
        ),
        ("无人处置时写的处置者名".to_string(), UNHANDLED_MARK.to_string()),
    ];
    render_pair_table(
        "幕八(续)· 补充证据:摘掉兜底站之后发生了什么",
        &rows,
        &[
            "★ 摘掉第 7 站之后,超出总部权限的工单**消失在链尾**。\
             这不是业务结论,是装配缺陷------`SETTLED` 那条规则为它变红。",
            "★ 「无人处置」在报表上是一个**词**(「(无人接手)」)而不是一个空格子:\
             空格子会被读成「忘了填」,而这两件事的处理方式完全不同。",
        ],
    )
}

/// 幕十二:扩展对照。
fn render_extension_evidence(session: &TicketSession, tickets: &[ServiceTicket]) -> Vec<String> {
    let extended_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let extended_chain = build_extended_chain(Rc::clone(&extended_ledger));
    let extended_ticket_list: Vec<ServiceTicket> = extended_tickets();
    let extended_session: TicketSession =
        TicketSession::run(extended_chain, &extended_ticket_list);
    let extended_snapshot_view: LedgerSnapshotView =
        LedgerSnapshotView::from_snapshot(&extended_ledger.snapshot());

    let base_compensation: Money = compensation_total(session.outcomes());
    let extended_compensation: Money = compensation_total(extended_session.outcomes());
    let quick_lane_handled: usize = extended_session
        .outcomes()
        .iter()
        .filter(|outcome| outcome.handler_name() == HANDLER_QUICK_LANE_NAME)
        .count();

    let rows: Vec<ExtensionRowView> = vec![
        ExtensionRowView {
            item: "链上站点数".to_string(),
            builtin_text: format!("{} 站", session.chain().handler_total()),
            extended_text: format!("{} 站", extended_session.chain().handler_total()),
            evidence_text: "幕六「链上站点清单」多 1 行(人为损坏快速通道)".to_string(),
        },
        ExtensionRowView {
            item: "工单数".to_string(),
            builtin_text: format!("{} 张", tickets.len()),
            extended_text: format!("{} 张", extended_ticket_list.len()),
            evidence_text: "幕五「工单清单」多 2 行(鉴定争议 ×2)".to_string(),
        },
        ExtensionRowView {
            item: "★ 工程内分层文件的改动数".to_string(),
            builtin_text: "0".to_string(),
            extended_text: "0".to_string(),
            evidence_text: "新类型 / 常量 / 建造函数 / 装配全在 main.rs 的工程外扩展区".to_string(),
        },
        ExtensionRowView {
            item: "★ 判定次数".to_string(),
            builtin_text: format!("{} 次", session.visited_total()),
            extended_text: format!("{} 次", extended_session.visited_total()),
            evidence_text: "多了 1 站 + 多了 2 张单,判定次数随之变化".to_string(),
        },
        ExtensionRowView {
            item: "★ 赔付合计".to_string(),
            builtin_text: base_compensation.chinese_text(),
            extended_text: extended_compensation.chinese_text(),
            evidence_text: "⑥ 人为损坏 ¥2,400 改走快速通道(公司承担 50%),赔付随之变化".to_string(),
        },
        ExtensionRowView {
            item: "★ 快速通道站是否真的接下单".to_string(),
            builtin_text: "不适用".to_string(),
            extended_text: format!("{} 张", quick_lane_handled),
            evidence_text: "接下数 > 0 才说明这一站不是死站(追加在兜底之后就永远是 0)".to_string(),
        },
        ExtensionRowView {
            item: "★ 无人处置单数".to_string(),
            builtin_text: format!("{} 张", session.unhandled_total()),
            extended_text: format!("{} 张", extended_session.unhandled_total()),
            evidence_text: "扩展链同样带兜底站,仍为 0".to_string(),
        },
        ExtensionRowView {
            item: "★ 台账里登记的站数".to_string(),
            builtin_text: format!("{} 站", tickets.len().min(0) + 7),
            extended_text: format!("{} 站", extended_snapshot_view.station_total),
            evidence_text: "幕十一(续)「台账快照」多一行(新站的被问 / 接下 / 放行)".to_string(),
        },
    ];
    let mut lines: Vec<String> = render_extension_table(
        "幕十二 · 扩展对照:工程外新增 1 站 + 1 种工单类型 + 2 张工单",
        &rows,
        &[
            "★ 可扩展性的判据**不是**「可以加」,而是「加了之后**自动**被算进去」:\
             新站自动进链、自动进台账、自动进报表,工程内分层文件**一行未改**。",
            "★ 新站刻意插在「保修判定」之后、金额权限之前:\
             追加在兜底站之后它会**永远轮不到**(死站),\
             那时「扩展真的起作用了」就变成一句无法被反驳的话。",
        ],
    );
    lines.push(String::new());
    lines
}

/// 体检:契约审计自身也要被审计。
fn audit_self_check(normal: &[AuditCheckView], negative: &[AuditCheckView]) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    let normal_passed: usize = normal.iter().filter(|check| check.passed()).count();
    lines.push(CheckLine::new(
        "契约审计:正常用例全部通过",
        format!("{}/{}", normal.len(), normal.len()).as_str(),
        format!("{}/{}", normal_passed, normal.len()).as_str(),
        normal_passed == normal.len() && !normal.is_empty(),
    ));

    // ★ 负向用例**必须**至少有一条变红:否则规则是恒真的。
    let negative_failed: usize = negative.iter().filter(|check| !check.passed()).count();
    lines.push(CheckLine::new(
        "★ 契约审计:负向用例至少有一条变红(规则不是恒真的)",
        ">= 1 条未通过(已知会失败:把规则写成对任何输入都成立)",
        format!("{} 条未通过 / 共 {} 条", negative_failed, negative.len()).as_str(),
        negative_failed >= 1,
    ));

    let rule_total: usize = ChainAuditRule::all().len();
    lines.push(CheckLine::new(
        "契约规则条数 == 枚举穷尽条数",
        format!("{} 条", rule_total).as_str(),
        format!("{} 条", normal.len()).as_str(),
        normal.len() == rule_total,
    ));

    lines
}

/// 体检:版面与数值口径自检。
fn layout_self_check() -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    let width_model = self_check_width_model();
    let width_failed: usize = width_model.iter().filter(|(_, ok, _)| !*ok).count();
    lines.push(CheckLine::new(
        "版面:显示宽度模型自检",
        "0 项未通过(已知会失败:把 U+2500 当成宽字符)",
        format!("{} 项未通过 / 共 {} 项", width_failed, width_model.len()).as_str(),
        width_failed == 0 && !width_model.is_empty(),
    ));

    lines.push(CheckLine::new(
        "版面:文档宽度常量",
        "140",
        DOCUMENT_WIDTH.to_string().as_str(),
        DOCUMENT_WIDTH == 140,
    ));

    lines.push(CheckLine::new(
        "版面:接口清点两列不超过文档宽度",
        "<= 140",
        format!(
            "{} = {} + {} + 7",
            INTERFACE_NAME_WIDTH + INTERFACE_OUTPUT_WIDTH + 7,
            INTERFACE_NAME_WIDTH,
            INTERFACE_OUTPUT_WIDTH
        )
        .as_str(),
        INTERFACE_NAME_WIDTH + INTERFACE_OUTPUT_WIDTH + 7 <= DOCUMENT_WIDTH,
    ));

    lines.push(CheckLine::new(
        "口径:唯一舍入口径(远离零方向四舍五入)",
        "divide_rounded(7, 2) == 4 且 divide_rounded(-7, 2) == -4",
        format!("{} / {}", divide_rounded(7, 2), divide_rounded(-7, 2)).as_str(),
        divide_rounded(7, 2) == 4 && divide_rounded(-7, 2) == -4,
    ));

    // ★ 负向测试:向零截断与远离零方向必须真的会分叉,
    //   否则「统一舍入口径」这条检查永远绿。
    lines.push(CheckLine::new(
        "★ 口径:向零截断与远离零方向**真的会分叉**(负向测试)",
        "两者不相等(若除得尽,这条检查就是恒真的废检查)",
        format!("{} != {}", 7 / 2, divide_rounded(7, 2)).as_str(),
        7 / 2 != divide_rounded(7, 2),
    ));

    lines.push(CheckLine::new(
        "口径:比率渲染(万分点)",
        "ratio_basis_points(3, 10) == 3000",
        format!("{}", ratio_basis_points(3, 10)).as_str(),
        ratio_basis_points(3, 10) == 3000,
    ));

    lines
}

/// 体检:扩展区自身也要被审计。
fn extension_self_check(session: &TicketSession) -> Vec<CheckLine> {
    let mut lines: Vec<CheckLine> = Vec::new();

    let extended_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
    let extended_chain = build_extended_chain(Rc::clone(&extended_ledger));
    let extended_ticket_list: Vec<ServiceTicket> = extended_tickets();
    let extended_session: TicketSession =
        TicketSession::run(extended_chain, &extended_ticket_list);

    let quick_lane_handled: usize = extended_session
        .outcomes()
        .iter()
        .filter(|outcome| outcome.handler_name() == HANDLER_QUICK_LANE_NAME)
        .count();
    lines.push(CheckLine::new(
        "★ 扩展站真的接下了单(不是死站)",
        "> 0 张(已知会失败:把扩展站追加在兜底站之后)",
        format!("{} 张", quick_lane_handled).as_str(),
        quick_lane_handled > 0,
    ));

    lines.push(CheckLine::new(
        "扩展链站点数 == 内置站点数 + 1",
        format!("{} 站", session.chain().handler_total() + 1).as_str(),
        format!("{} 站", extended_session.chain().handler_total()).as_str(),
        extended_session.chain().handler_total() == session.chain().handler_total() + 1,
    ));

    lines.push(CheckLine::new(
        "扩展后工单数 == 内置工单数 + 2",
        format!("{} 张", session.ticket_total() + 2).as_str(),
        format!("{} 张", extended_session.ticket_total()).as_str(),
        extended_session.ticket_total() == session.ticket_total() + 2,
    ));

    lines.push(CheckLine::new(
        "★ 扩展后无人处置单数仍为 0",
        "0 张(已知会失败:扩展装配时忘了补兜底站)",
        format!("{} 张", extended_session.unhandled_total()).as_str(),
        extended_session.unhandled_total() == 0,
    ));

    lines
}

/// ★ 对外只读接口清点:**每个接口都真的调用一次并印出结果**。
///
/// 一个只读接口若在这里被调用并印出结果,它就不可能是「写了没人用」------
/// 也就不可能是「写错了没人发现」。
/// 因此这一幕同时是**告警治理的落点**:宁可把接口用起来,也不加 `#[allow]`。
fn interface_items(
    session: &TicketSession,
    value_report: &ChainValueReport,
    snapshot: &LedgerSnapshotView,
    probes: &[StationProbe],
) -> Vec<(String, String)> {
    let mut items: Vec<(String, String)> = Vec::new();

    // ---- support ----
    items.push((
        "support::is_leap_year(2024) / (2100)".to_string(),
        format!("{} / {}", is_leap_year(2024), is_leap_year(2100)),
    ));
    items.push((
        "support::days_in_month(2026, 2)".to_string(),
        days_in_month(2026, 2).to_string(),
    ));
    items.push((
        "support::days_between(2026-01-01, 2026-12-31)".to_string(),
        days_between(CalendarDate::new(2026, 1, 1), CalendarDate::new(2026, 12, 31)).to_string(),
    ));
    items.push((
        "support::CalendarDate::is_valid()  // 2026-02-30".to_string(),
        bool_text(CalendarDate::new(2026, 2, 30).is_valid()).to_string(),
    ));
    items.push((
        "support::CalendarDate::chinese_text()".to_string(),
        CalendarDate::new(2026, 10, 13).chinese_text(),
    ));
    items.push((
        "support::stable_hash(\"WO-2026-0001\")".to_string(),
        stable_hash(&["WO", "2026", "0001"]).to_string(),
    ));
    items.push((
        "support::deterministic_code(\"main\", 7)".to_string(),
        deterministic_code(&["main", "chain"], 7),
    ));
    items.push((
        "support::prefixed_code(\"WO\", \"2026\", 4)".to_string(),
        prefixed_code("WO", &["2026", "0001"], 4),
    ));
    items.push((
        "support::is_wide_character('珠') / ('A')".to_string(),
        format!(
            "{} / {}",
            crate::support::is_wide_character('珠'),
            crate::support::is_wide_character('A')
        ),
    ));
    items.push((
        "support::display_width(\"珠宝 ¥1,280\")".to_string(),
        display_width("珠宝 ¥1,280").to_string(),
    ));
    items.push((
        "support::elide_text(\"abcdefghij\", 4)".to_string(),
        elide_text("abcdefghij", 4),
    ));
    items.push((
        "support::truncate_to_width(\"珠宝工单处置链\", 6)".to_string(),
        truncate_to_width("珠宝工单处置链", 6),
    ));
    items.push((
        "support::pad_center(\"门店\", 10)".to_string(),
        format!("[{}]", pad_center("门店", 10)),
    ));
    items.push((
        "support::report_cell_text(2026-10-13)  // 日期单元格".to_string(),
        format!("[{}]", report_cell_text(CalendarDate::new(2026, 10, 13))),
    ));
    items.push((
        "support::pad_left(\"门店\", 8) / pad_right(\"门店\", 8)".to_string(),
        format!(
            "[{}][{}]",
            crate::support::pad_left("门店", 8),
            crate::support::pad_right("门店", 8)
        ),
    ));
    items.push((
        "support::self_check_width_model()".to_string(),
        format!("{} 项自检", self_check_width_model().len()),
    ));
    items.push((
        "support::CalendarDate::year() / month() / day()".to_string(),
        {
            let date: CalendarDate = CalendarDate::new(2026, 10, 13);
            format!("{} / {} / {}", date.year(), date.month(), date.day())
        },
    ));
    items.push((
        "support::CalendarDate::weekday_index() / is_weekend()".to_string(),
        {
            let date: CalendarDate = CalendarDate::new(2026, 10, 13);
            format!("{} / {}", date.weekday_index(), bool_text(date.is_weekend()))
        },
    ));
    items.push((
        "support::CalendarDate::weekday_chinese_text()".to_string(),
        CalendarDate::new(2026, 10, 13).weekday_chinese_text().to_string(),
    ));
    items.push((
        "support::CalendarDate::chinese_text_with_weekday()".to_string(),
        CalendarDate::new(2026, 10, 13).chinese_text_with_weekday(),
    ));
    items.push((
        "support::CalendarDate::add_days(30) / add_working_days(5)".to_string(),
        {
            let date: CalendarDate = CalendarDate::new(2026, 10, 13);
            format!(
                "{} / {}",
                date.add_days(30).chinese_text(),
                date.add_working_days(5).chinese_text_with_weekday()
            )
        },
    ));

    // ---- domain ----
    items.push((
        "domain::scale_rounded(1000, 3, 10)  // 唯一舍入口径的底层".to_string(),
        crate::domain::scale_rounded(1000, 3, 10).to_string(),
    ));
    items.push((
        "domain::divide_rounded(7, 2) / (-7, 2)".to_string(),
        format!("{} / {}", divide_rounded(7, 2), divide_rounded(-7, 2)),
    ));
    items.push((
        "domain::ratio_basis_points(3, 10)".to_string(),
        format!("{} 万分点", ratio_basis_points(3, 10)),
    ));
    items.push((
        "domain::Money::new(1280000, CNY).chinese_text()".to_string(),
        Money::new(1_280_000, CNY).chinese_text(),
    ));
    items.push((
        "domain::Money::amount_text()".to_string(),
        Money::new(1_280_000, CNY).amount_text(),
    ));
    items.push((
        "domain::Money::scale_basis_points(5000)  // ¥2,400 × 50%".to_string(),
        Money::new(240_000, CNY).scale_basis_points(5_000).chinese_text(),
    ));
    items.push((
        "domain::Money::plus() / minus()".to_string(),
        format!(
            "{} / {}",
            Money::new(100, CNY).plus(Money::new(50, CNY)).chinese_text(),
            Money::new(100, CNY).minus(Money::new(50, CNY)).chinese_text()
        ),
    ));
    items.push((
        "domain::Money::is_zero()".to_string(),
        bool_text(Money::zero(CNY).is_zero()).to_string(),
    ));
    items.push((
        "domain::Money::currency() / is_same_as()".to_string(),
        format!(
            "{} / {}",
            Money::new(100, CNY).currency().code(),
            bool_text(Money::new(100, CNY).is_same_as(&Money::new(100, CNY)))
        ),
    ));
    items.push((
        "domain::Money::negate() / scale_ratio(1, 3)".to_string(),
        format!(
            "{} / {}",
            Money::new(300, CNY).negate().chinese_text(),
            Money::new(1_000, CNY).scale_ratio(1, 3).chinese_text()
        ),
    ));
    items.push((
        "domain::MonthKey::is_valid() / is_same()".to_string(),
        format!(
            "{} / {}",
            bool_text(DEMO_MONTH.is_valid()),
            bool_text(DEMO_MONTH.is_same(&MonthKey::new(2026, 10)))
        ),
    ));
    items.push((
        "domain::MonthKey::months_since(2026-01)".to_string(),
        format!("{} 个月", DEMO_MONTH.months_since(&MonthKey::new(2026, 1))),
    ));
    items.push((
        "domain::MonthKey::chinese_text() / machine_text()".to_string(),
        format!("{} / {}", DEMO_MONTH.chinese_text(), DEMO_MONTH.machine_text()),
    ));
    items.push((
        "domain::MonthKey::plus_months(3) / previous()".to_string(),
        format!(
            "{} / {}",
            DEMO_MONTH.plus_months(3).chinese_text(),
            DEMO_MONTH.previous().chinese_text()
        ),
    ));
    items.push((
        "domain::MonthKey::year() / month() / next()".to_string(),
        format!(
            "{} / {} / {}",
            DEMO_MONTH.year(),
            DEMO_MONTH.month(),
            DEMO_MONTH.next().chinese_text()
        ),
    ));
    items.push((
        "domain::MonthKey::same_month_last_year()".to_string(),
        DEMO_MONTH.same_month_last_year().chinese_text(),
    ));
    items.push((
        "domain::MonthKey::is_before() / days()".to_string(),
        format!(
            "{} / {}",
            bool_text(MonthKey::new(2026, 1).is_before(&DEMO_MONTH)),
            DEMO_MONTH.days()
        ),
    ));
    items.push((
        "domain::WARRANTY_MONTHS".to_string(),
        format!("{} 个月", WARRANTY_MONTHS),
    ));
    items.push((
        "domain::TicketType::has_return_window()  // 退货 / 维修".to_string(),
        format!(
            "{} / {}",
            bool_text(TICKET_RETURN.has_return_window()),
            bool_text(TICKET_REPAIR.has_return_window())
        ),
    ));
    items.push((
        "domain::TicketType::is_same()  // 退货 vs 换货".to_string(),
        bool_text(TICKET_RETURN.is_same(&TICKET_EXCHANGE)).to_string(),
    ));
    items.push((
        "domain::TICKET_APPRAISAL(工程外新增类型)".to_string(),
        format!(
            "{} / 需凭证 {} / 走保修 {}",
            TICKET_APPRAISAL.chinese_name(),
            bool_text(TICKET_APPRAISAL.needs_receipt()),
            bool_text(TICKET_APPRAISAL.needs_warranty())
        ),
    ));
    items.push((
        "domain::BUILTIN_TICKET_TYPES(工程内建,不含扩展类型)".to_string(),
        crate::domain::BUILTIN_TICKET_TYPES
            .iter()
            .map(|ticket_type| ticket_type.chinese_name())
            .collect::<Vec<&str>>()
            .join("、"),
    ));
    items.push((
        "domain::Disposition::all().len()".to_string(),
        format!("{} 种", Disposition::all().len()),
    ));
    items.push((
        "domain::Disposition::Unhandled::is_settled()".to_string(),
        bool_text(Disposition::Unhandled.is_settled()).to_string(),
    ));
    items.push((
        "domain::Disposition::Rejected::bears_compensation()".to_string(),
        bool_text(Disposition::Rejected.bears_compensation()).to_string(),
    ));
    items.push((
        "domain::Disposition::HeadquarterHandled::chinese_text()".to_string(),
        Disposition::HeadquarterHandled.chinese_text().to_string(),
    ));
    items.push((
        "domain::Disposition::WarrantyRepair::machine_text()".to_string(),
        Disposition::WarrantyRepair.machine_text().to_string(),
    ));
    items.push((
        "domain::Disposition::is_escalated() / is_rejected()".to_string(),
        format!(
            "{} / {}",
            bool_text(Disposition::Escalated.is_escalated()),
            bool_text(Disposition::Rejected.is_rejected())
        ),
    ));
    items.push((
        "domain::AUTHORITY_STORE.covers(500000) / (500001)".to_string(),
        format!(
            "{} / {}",
            bool_text(crate::domain::AUTHORITY_STORE.covers(500_000)),
            bool_text(crate::domain::AUTHORITY_STORE.covers(500_001))
        ),
    ));
    items.push((
        "domain::AUTHORITY_STORE.ceiling_text()".to_string(),
        crate::domain::AUTHORITY_STORE.ceiling_text(),
    ));
    items.push((
        "domain::AUTHORITY_REGIONAL.ceiling_minor_units()".to_string(),
        format!("{} 分", crate::domain::AUTHORITY_REGIONAL.ceiling_minor_units()),
    ));
    items.push((
        "domain::AUTHORITY_HEADQUARTER.chinese_name() / code()".to_string(),
        format!(
            "{} / {}",
            crate::domain::AUTHORITY_HEADQUARTER.chinese_name(),
            crate::domain::AUTHORITY_HEADQUARTER.code()
        ),
    ));
    items.push((
        "domain::ServiceTicket::months_elapsed()  // ⑤ 过保单".to_string(),
        format!("{} 个月", demo_tickets()[4].months_elapsed()),
    ));
    items.push((
        "domain::ServiceTicket::is_within_warranty()".to_string(),
        format!(
            "{} / {}",
            bool_text(demo_tickets()[3].is_within_warranty()),
            bool_text(demo_tickets()[4].is_within_warranty())
        ),
    ));
    items.push((
        "domain::ServiceTicket::is_inadmissible()  // ② 无凭证".to_string(),
        bool_text(demo_tickets()[1].is_inadmissible()).to_string(),
    ));
    items.push((
        "domain::ServiceTicket::inadmissible_reason_text()  // ③ 超窗口".to_string(),
        demo_tickets()[2].inadmissible_reason_text(),
    ));
    items.push((
        "domain::ServiceTicket::fingerprint_text()  // ①".to_string(),
        demo_tickets()[0].fingerprint_text(),
    ));
    items.push((
        "domain::ServiceTicket::has_receipt() / is_man_made_damage()".to_string(),
        format!(
            "{} / {}",
            bool_text(demo_tickets()[5].has_receipt()),
            bool_text(demo_tickets()[5].is_man_made_damage())
        ),
    ));
    items.push((
        "domain::ServiceTicket::category() / purchase_month()".to_string(),
        format!(
            "{} / {}",
            demo_tickets()[4].category().chinese_name(),
            demo_tickets()[4].purchase_month().chinese_text()
        ),
    ));
    items.push((
        "domain::ServiceTicket::accept_month()".to_string(),
        demo_tickets()[4].accept_month().chinese_text(),
    ));
    items.push((
        "domain::TicketResolution::fingerprint_text()  // 首单".to_string(),
        match session.outcomes().first() {
            Some(outcome) => outcome.resolution().fingerprint_text(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "domain::TicketResolution::disposition_text()  // 首单".to_string(),
        match session.outcomes().first() {
            Some(outcome) => outcome.resolution().disposition_text().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "domain::TicketResolution::handler_name()  // 首单".to_string(),
        match session.outcomes().first() {
            Some(outcome) => outcome.resolution().handler_name().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "★ domain::TicketResolution::note()(不进指纹)".to_string(),
        match session.outcomes().first() {
            Some(outcome) => outcome.resolution().note().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "domain::STORE_CN_01.chinese_name() / STORE_HK_01.code()".to_string(),
        format!("{} / {}", STORE_CN_01.chinese_name(), STORE_HK_01.code()),
    ));
    items.push((
        "domain::CATEGORY_JADE.chinese_name() / CATEGORY_GOLD.code()".to_string(),
        format!("{} / {}", CATEGORY_JADE.chinese_name(), CATEGORY_GOLD.code()),
    ));
    items.push((
        "domain::BUILTIN_CATEGORY_CODES 三种品类的计价口径".to_string(),
        crate::domain::BUILTIN_CATEGORY_CODES
            .iter()
            .map(|category| {
                format!(
                    "{}(按金价 {})",
                    category.chinese_name(),
                    bool_text(category.needs_metal_pricing())
                )
            })
            .collect::<Vec<String>>()
            .join("、"),
    ));
    items.push((
        "domain::BUILTIN_CURRENCY_CODES 三种币种(含 HKD / USD)".to_string(),
        crate::domain::BUILTIN_CURRENCY_CODES
            .iter()
            .map(|currency| format!("{} {}", currency.chinese_name(), currency.symbol()))
            .collect::<Vec<String>>()
            .join("、"),
    ));
    items.push((
        "domain::CategoryCode::new(...)  // 开放标签可随时加".to_string(),
        crate::domain::CategoryCode::new("PEARL", "珍珠饰品", false)
            .chinese_name()
            .to_string(),
    ));
    items.push((
        "domain::HKD.chinese_name() / USD.code()".to_string(),
        format!("{} / {}", crate::domain::HKD.chinese_name(), crate::domain::USD.code()),
    ));

    // ---- chain(机制层)----
    items.push((
        "chain::HandlerKind::Responder::chinese_text()".to_string(),
        HandlerKind::Responder.chinese_text().to_string(),
    ));
    items.push((
        "chain::HandlerKind::PassThrough::may_settle()".to_string(),
        bool_text(HandlerKind::PassThrough.may_settle()).to_string(),
    ));
    items.push((
        "chain::HandlerKind::Responder::machine_text()".to_string(),
        HandlerKind::Responder.machine_text().to_string(),
    ));
    items.push((
        "chain::ChainTailPolicy::RequireHandled::chinese_text()".to_string(),
        ChainTailPolicy::RequireHandled.chinese_text().to_string(),
    ));
    items.push((
        "chain::ChainTailPolicy::AllowUnhandled::machine_text()".to_string(),
        ChainTailPolicy::AllowUnhandled.machine_text().to_string(),
    ));
    items.push(("chain::UNHANDLED_MARK".to_string(), UNHANDLED_MARK.to_string()));
    items.push((
        "chain::ChainAuditRule::all().len()".to_string(),
        format!("{} 条", ChainAuditRule::all().len()),
    ));
    items.push((
        "chain::ChainAuditRule::SETTLED::known_failing_input_text()".to_string(),
        ChainAuditRule::EveryRequestSettled
            .known_failing_input_text()
            .to_string(),
    ));
    items.push((
        "chain::ChainAuditRule::ORDERED::expectation_text()".to_string(),
        ChainAuditRule::ChainRespectsOrder.expectation_text().to_string(),
    ));
    items.push((
        "chain::ChainAuditRule::SINGLE::code() / chinese_text()".to_string(),
        format!(
            "{} / {}",
            ChainAuditRule::AtMostOneHandler.code(),
            ChainAuditRule::AtMostOneHandler.chinese_text()
        ),
    ));
    items.push((
        "★ chain::ChainLedger 独立可用性:新开一本账跑一次".to_string(),
        {
            let probe_ledger: Rc<ChainLedger> = Rc::new(ChainLedger::new());
            probe_ledger.record_submission();
            probe_ledger.record_visit("演示站");
            probe_ledger.record_passed("演示站");
            probe_ledger.record_unhandled();
            let probe_snapshot: LedgerSnapshot = probe_ledger.snapshot();
            format!(
                "投递 {} / 访问 {} / 无人接 {} / 恒等式一差 {}",
                probe_snapshot.submitted_total,
                probe_snapshot.visit_total,
                probe_snapshot.unhandled_total,
                probe_snapshot.submission_identity_gap()
            )
        },
    ));
    items.push((
        "chain::ChainLedger::submitted_total() 等实时计数".to_string(),
        {
            let probe_ledger: ChainLedger = ChainLedger::new();
            probe_ledger.record_submission();
            format!(
                "{} / {} / {} / {} / {}",
                probe_ledger.submitted_total(),
                probe_ledger.handled_total(),
                probe_ledger.unhandled_total(),
                probe_ledger.visit_total(),
                probe_ledger.skipped_total()
            )
        },
    ));
    items.push((
        "★ chain::Handling::Handled(7).is_handled() / result()".to_string(),
        format!(
            "{} / {:?}",
            Handling::Handled(7).is_handled(),
            Handling::Handled(7).result()
        ),
    ));
    items.push((
        "★ chain::Handling::<i32>::Pass.is_pass() / result_ref()".to_string(),
        format!(
            "{} / {:?}",
            Handling::<i32>::Pass.is_pass(),
            Handling::<i32>::Pass.result_ref()
        ),
    ));
    items.push((
        "★ chain::ChainResult::is_ordered()(乱序 = false)".to_string(),
        bool_text(
            crate::chain::ChainResult::<i32>::new(
                None,
                UNHANDLED_MARK,
                None,
                vec!["A".to_string(), "B".to_string()],
                vec![3, 1],
                7,
            )
            .is_ordered(),
        )
        .to_string(),
    ));
    items.push((
        "chain::ChainResult::skipped_total()(在第 3 站接下,共 7 站)".to_string(),
        crate::chain::ChainResult::<i32>::new(
            Some(1),
            "演示站",
            Some(HandlerKind::Responder),
            vec!["A".to_string(), "B".to_string(), "C".to_string()],
            vec![0, 1, 2],
            7,
        )
        .skipped_total()
        .to_string(),
    ));
    items.push((
        "chain::ChainResult::visited_text() / passed_total()".to_string(),
        {
            let demo: crate::chain::ChainResult<i32> = crate::chain::ChainResult::new(
                Some(1),
                "演示站",
                Some(HandlerKind::Responder),
                vec!["A".to_string(), "B".to_string(), "C".to_string()],
                vec![0, 1, 2],
                7,
            );
            format!("{} / {}", demo.visited_text(), demo.passed_total())
        },
    ));
    items.push((
        "chain::RequestObservation::handler_name_label()  // 首条".to_string(),
        match session.observations().first() {
            Some(observation) => observation.handler_name_label().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "★ chain::ChainResult::into_result()(移动出结论)".to_string(),
        format!(
            "{:?}",
            crate::chain::ChainResult::<i32>::new(
                Some(9),
                "演示站",
                Some(HandlerKind::Responder),
                vec!["A".to_string()],
                vec![0],
                1,
            )
            .into_result()
        ),
    ));
    items.push((
        "★ chain::LedgerSnapshot 死站 / 纯过路 / 全量 / 省下".to_string(),
        {
            let real: LedgerSnapshot = session.chain().ledger().snapshot();
            let first_attempt: Option<&crate::chain::HandlerAttempt> = real.attempts.first();
            format!(
                "死站 {} / 纯过路 {} / 全量 {} / 省下 {} / 首站被问 {}",
                real.never_asked_handlers().len(),
                real.asked_but_never_handled_handlers().len(),
                real.judgement_total_if_broadcast(),
                real.saved_judgement_total(),
                match first_attempt {
                    Some(attempt) => bool_text(!attempt.never_asked()).to_string(),
                    None => "无".to_string(),
                }
            )
        },
    ));
    items.push((
        "★ chain::RuleCheck::applicable_total() / conforming_total()".to_string(),
        {
            let checks: Vec<crate::chain::RuleCheck> = audit_chain(&session.observations());
            let applicable: usize = checks.iter().map(|check| check.applicable_total()).sum();
            let conforming: usize = checks.iter().map(|check| check.conforming_total()).sum();
            format!(
                "{} 条规则:合规 {} / 适用 {}(逐条明细见幕八)",
                checks.len(),
                conforming,
                applicable
            )
        },
    ));

    // ---- client ----
    items.push((
        "client::station_descriptions().len()".to_string(),
        format!("{} 站", station_descriptions().len()),
    ));
    items.push((
        "client::builtin_handler_specs().len()".to_string(),
        format!("{} 站", crate::client::builtin_handler_specs().len()),
    ));
    items.push((
        "client::HANDLER_ORDER_NORMAL / SWAPPED / NO_FALLBACK 长度".to_string(),
        format!(
            "{} / {} / {}",
            HANDLER_ORDER_NORMAL.len(),
            HANDLER_ORDER_SWAPPED.len(),
            HANDLER_ORDER_NO_FALLBACK.len()
        ),
    ));
    items.push((
        "client::HANDLER_ADMISSION_NAME / HANDLER_ESCALATION_NAME".to_string(),
        format!("{} / {}", HANDLER_ADMISSION_NAME, HANDLER_ESCALATION_NAME),
    ));
    items.push((
        "client::HANDLER_WARRANTY_NAME / HANDLER_LOGGING_NAME".to_string(),
        format!("{} / {}", HANDLER_WARRANTY_NAME, HANDLER_LOGGING_NAME),
    ));
    items.push((
        "client::HANDLER_STORE_NAME / REGIONAL_NAME / HEADQUARTER_NAME".to_string(),
        format!(
            "{} / {} / {}",
            HANDLER_STORE_NAME, HANDLER_REGIONAL_NAME, HANDLER_HEADQUARTER_NAME
        ),
    ));
    items.push((
        "★ client::build_warranty_handler().can_handle(④ 在保)".to_string(),
        bool_text(build_warranty_handler().can_handle(&demo_tickets()[3])).to_string(),
    ));
    items.push((
        "★ client::build_warranty_handler().can_handle(⑤ 过保)".to_string(),
        bool_text(build_warranty_handler().can_handle(&demo_tickets()[4])).to_string(),
    ));
    items.push((
        "★ client::build_quick_lane_handler()(工程外)can_handle(⑥)".to_string(),
        bool_text(build_quick_lane_handler().can_handle(&demo_tickets()[5])).to_string(),
    ));
    items.push((
        "client::build_escalation_handler().name() / kind()".to_string(),
        format!(
            "{} / {}",
            build_escalation_handler().name(),
            build_escalation_handler().kind().chinese_text()
        ),
    ));
    items.push((
        "client::build_store_authority_handler().name()".to_string(),
        crate::client::build_store_authority_handler().name().to_string(),
    ));
    items.push((
        "client::build_regional_authority_handler().name()".to_string(),
        crate::client::build_regional_authority_handler().name().to_string(),
    ));
    items.push((
        "client::build_headquarter_authority_handler().name()".to_string(),
        crate::client::build_headquarter_authority_handler()
            .name()
            .to_string(),
    ));
    items.push((
        "client::build_admission_handler().kind()".to_string(),
        crate::client::build_admission_handler().kind().chinese_text().to_string(),
    ));
    items.push((
        "client::build_logging_handler().kind()".to_string(),
        crate::client::build_logging_handler().kind().chinese_text().to_string(),
    ));
    items.push((
        "client::build_authority_handler(AUTHORITY_STORE).name()".to_string(),
        crate::client::build_authority_handler(crate::domain::AUTHORITY_STORE)
            .name()
            .to_string(),
    ));
    items.push((
        "client::build_handler_by_name(保修判定).name()".to_string(),
        crate::client::build_handler_by_name(HANDLER_WARRANTY_NAME)
            .name()
            .to_string(),
    ));
    items.push((
        "client::build_ticket_chain_with_extensions(...)  // 站数".to_string(),
        format!(
            "{} 站",
            crate::client::build_ticket_chain_with_extensions(
                "清点用链",
                ChainTailPolicy::RequireHandled,
                Rc::new(ChainLedger::new()),
                &HANDLER_ORDER_NO_FALLBACK,
                vec![build_quick_lane_handler()],
            )
            .handler_total()
        ),
    ));
    items.push((
        "client::TicketSession::chain().name() / policy()".to_string(),
        format!(
            "{} / {}",
            session.chain().name(),
            session.chain().policy().chinese_text()
        ),
    ));
    items.push((
        "client::TicketSession::chain().handler_names().len()".to_string(),
        format!("{} 站", session.chain().handler_names().len()),
    ));
    items.push((
        "client::TicketSession::chain().handler_kinds().len()".to_string(),
        format!("{} 站", session.chain().handler_kinds().len()),
    ));
    items.push((
        "client::TicketSession::chain().ledger().submitted_total()".to_string(),
        session.chain().ledger().submitted_total().to_string(),
    ));
    items.push((
        "client::TicketSession::ticket_total() / settled_total()".to_string(),
        format!("{} / {}", session.ticket_total(), session.settled_total()),
    ));
    items.push((
        "client::TicketSession::unhandled_total()".to_string(),
        session.unhandled_total().to_string(),
    ));
    items.push((
        "client::TicketSession::disposition_counts()".to_string(),
        session
            .disposition_counts()
            .iter()
            .map(|(disposition, count)| format!("{}×{}", disposition.chinese_text(), count))
            .collect::<Vec<String>>()
            .join("、"),
    ));
    items.push((
        "client::TicketSession::handler_settle_counts()".to_string(),
        session
            .handler_settle_counts()
            .iter()
            .map(|(name, count)| format!("{}×{}", name, count))
            .collect::<Vec<String>>()
            .join("、"),
    ));
    items.push((
        "client::TicketSession::visited_total() / skipped_total()".to_string(),
        format!("{} 次 / {} 站次", session.visited_total(), session.skipped_total()),
    ));
    items.push((
        "client::TicketSession::visited_total_if_broadcast()".to_string(),
        format!("{} 次", session.visited_total_if_broadcast()),
    ));
    items.push((
        "client::TicketSession::compensation_total()".to_string(),
        session.compensation_total().chinese_text(),
    ));
    items.push((
        "client::TicketSession::resolution_fingerprints().len()".to_string(),
        format!("{} 张", session.resolution_fingerprints().len()),
    ));
    items.push((
        "client::TicketSession::observations().len()".to_string(),
        format!("{} 条", session.observations().len()),
    ));
    items.push((
        "client::TicketOutcome::visited_text()  // 首单".to_string(),
        match session.outcomes().first() {
            Some(outcome) => outcome.visited_text(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "client::TicketOutcome::ticket_number() / disposition()".to_string(),
        match session.outcomes().first() {
            Some(outcome) => format!(
                "{} / {}",
                outcome.ticket_number(),
                outcome.disposition().chinese_text()
            ),
            None => "无".to_string(),
        },
    ));

    // ---- analysis ----
    items.push((
        "analysis::station_probes(...).len()".to_string(),
        format!("{} 站", probes.len()),
    ));
    items.push((
        "analysis::StationProbe::is_never_asked()  // 首站".to_string(),
        match probes.first() {
            Some(probe) => bool_text(probe.is_never_asked()).to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "analysis::StationProbe::risk_text()  // 末站".to_string(),
        match probes.last() {
            Some(probe) => probe.risk_text(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "analysis::StationProbe::asked_total()  // 首站".to_string(),
        match probes.first() {
            Some(probe) => probe.asked_total().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "analysis::compensation_total(...)".to_string(),
        compensation_total(session.outcomes()).chinese_text(),
    ));
    items.push((
        "analysis::OutcomeView::list_from_outcomes(...).len()".to_string(),
        format!(
            "{} 张",
            OutcomeView::list_from_outcomes(session.outcomes()).len()
        ),
    ));
    items.push((
        "analysis::ChainValueReport::saved_judgement_total()".to_string(),
        format!("{} 次", value_report.saved_judgement_total()),
    ));
    items.push((
        "analysis::ChainValueReport::saved_ratio_basis_points()".to_string(),
        format!("{} 万分点", value_report.saved_ratio_basis_points()),
    ));
    items.push((
        "analysis::ChainValueReport::summary_text()".to_string(),
        value_report.summary_text(),
    ));
    items.push((
        "analysis::ChainValueReport::self_check().len()".to_string(),
        format!("{} 条", value_report.self_check().len()),
    ));
    items.push((
        "analysis::ChainValueReport::station_total() / ticket_total()".to_string(),
        format!("{} 站 / {} 张", value_report.station_total(), value_report.ticket_total()),
    ));
    items.push((
        "analysis::LedgerSnapshotView::visit_total_if_broadcast()".to_string(),
        format!("{} 次", snapshot.visit_total_if_broadcast()),
    ));
    items.push((
        "analysis::LedgerSnapshotView::saved_judgement_total()".to_string(),
        format!("{} 次", snapshot.saved_judgement_total()),
    ));
    items.push((
        "analysis::LedgerSnapshotView::never_asked_stations().len()".to_string(),
        format!("{} 站", snapshot.never_asked_stations().len()),
    ));
    items.push((
        "analysis::LedgerSnapshotView::pure_passthrough_stations()".to_string(),
        format!("{} 站", snapshot.pure_passthrough_stations().len()),
    ));
    items.push((
        "analysis::LedgerSnapshotView::attempts.len()".to_string(),
        format!("{} 站", snapshot.attempts.len()),
    ));
    items.push((
        "analysis::StationAttemptView::name()  // 首站".to_string(),
        match snapshot.attempts.first() {
            Some(attempt) => attempt.name().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "analysis::StationAttemptView::is_pure_passthrough()  // 首站".to_string(),
        match snapshot.attempts.first() {
            Some(attempt) => bool_text(attempt.is_pure_passthrough()).to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "analysis::StationAttemptView::asked_total() / handled_total()".to_string(),
        {
            let first_attempt: Option<&crate::analysis::StationAttemptView> =
                snapshot.attempts.first();
            match first_attempt {
                Some(attempt) => format!(
                    "{} / {}",
                    attempt.asked_total(),
                    attempt.handled_total()
                ),
                None => "无".to_string(),
            }
        },
    ));
    items.push((
        "analysis::StationAttemptView::passed_total()  // 首站".to_string(),
        match snapshot.attempts.first() {
            Some(attempt) => attempt.passed_total().to_string(),
            None => "无".to_string(),
        },
    ));
    items.push((
        "★ analysis::LedgerSnapshotView::last_settled_handler()".to_string(),
        snapshot.last_settled_handler(),
    ));
    items.push((
        "analysis::CheckLine::verdict_text(true) / (false)".to_string(),
        format!(
            "{} / {}",
            CheckLine::new("演示", "相等", "1 == 1", true).verdict_text(),
            CheckLine::new("演示", "相等", "1 == 2", false).verdict_text()
        ),
    ));
    items.push((
        "analysis::CheckReport::new().line_total()".to_string(),
        CheckReport::new().line_total().to_string(),
    ));

    // ---- app ----
    items.push(("app::DOCUMENT_WIDTH".to_string(), DOCUMENT_WIDTH.to_string()));
    items.push((
        "app::INTERFACE_NAME_WIDTH / INTERFACE_OUTPUT_WIDTH".to_string(),
        format!("{} / {}", INTERFACE_NAME_WIDTH, INTERFACE_OUTPUT_WIDTH),
    ));
    items.push((
        "app::bool_text(true) / (false)".to_string(),
        format!("{} / {}", bool_text(true), bool_text(false)),
    ));
    items.push(("app::byte_text(1234567)".to_string(), byte_text(1_234_567)));
    items.push((
        "app::basis_points_text(5000)".to_string(),
        basis_points_text(5_000),
    ));
    items.push((
        "app::percent_text(3000)".to_string(),
        crate::app::percent_text(3_000),
    ));
    items.push((
        "app::note_lines(\"一行注解\", 140).len()".to_string(),
        format!("{} 行", note_lines("一行注解", DOCUMENT_WIDTH).len()),
    ));
    items.push(("app::table_rule(12)".to_string(), table_rule(12)));
    items.push((
        "app::render_pair_table(...)  // 返回行数".to_string(),
        format!(
            "{} 行",
            render_pair_table(
                "接口清点用的两列表",
                &[("演示项".to_string(), "演示值".to_string())],
                &["演示注解"],
            )
            .len()
        ),
    ));
    items.push((
        "★ app::render_cell 直接调用(列宽 12,内容「门店」)".to_string(),
        format!(
            "[{}]",
            crate::app::render_cell(&crate::app::TableColumn::new("演示", 12), "门店")
        ),
    ));
    items.push((
        "★ app::TableColumn::new(...).elidable().is_elidable()".to_string(),
        bool_text(
            crate::app::TableColumn::new("演示", 10)
                .elidable()
                .is_elidable()
        )
        .to_string(),
    ));
    items.push((
        "app::TableColumn::numeric(\"金额\", 12).align()".to_string(),
        format!(
            "{:?}",
            crate::app::TableColumn::numeric("金额", 12).align()
        ),
    ));
    items.push((
        "★ app::ColumnAlign:数值列必须右对齐(否则合计看不齐)".to_string(),
        format!(
            "{} / 左对齐 {:?}",
            bool_text(
                crate::app::TableColumn::numeric("金额", 12).align()
                    == crate::app::ColumnAlign::Right
            ),
            crate::app::ColumnAlign::Left
        ),
    ));
    items.push((
        "★ app::Table::new(...).with_row(...).with_note(...)".to_string(),
        {
            let table: crate::app::Table = crate::app::Table::new(
                "演示",
                vec![
                    crate::app::TableColumn::new("A", 10),
                    crate::app::TableColumn::new("B", 10),
                ],
            )
            .with_row(vec!["1".to_string(), "2".to_string()])
            .with_note("演示注解");
            format!(
                "{} 列 / {} 行 / 宽 {}",
                table.column_count(),
                table.row_count(),
                table.total_width()
            )
        },
    ));
    items.push((
        "app::section_header(\"演示\", 140).len()".to_string(),
        section_header("演示", DOCUMENT_WIDTH).len().to_string(),
    ));

    items
}

/// 打印一组行。
fn print_lines(lines: &[String]) {
    for line in lines.iter() {
        println!("{}", line);
    }
}

/// 带圈数字(① ~ ⑩)。
fn circled_number(value: usize) -> String {
    const CIRCLED: [&str; 10] = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"];
    match CIRCLED.get(value.wrapping_sub(1)) {
        Some(text) => (*text).to_string(),
        None => format!("({})", value),
    }
}

输出:

相关推荐
诚心呈意共享外卖配送系统1 小时前
多平台订单聚合系统的技术架构:接 API 只是开始
java·开发语言·架构·系统架构·创业创新
郝学胜-神的一滴1 小时前
Web安全入门:读懂Web世界里的攻防
前端·网络·后端·安全·web安全·hibernate
天若有情6731 小时前
从零搭建校园二手交易系统:Vue2 + SpringBoot 4.x 全栈实战
java·spring boot·后端
学逆向的1 小时前
C 浮点数e(科学计数法)
c语言·开发语言·网络安全
Amos_Web1 小时前
Rspack 源码解析(二十四):AssetPlugin 如何让图片同时是「URL 字符串」和「真实文件」
前端·rust·前端框架
quantdash_cc2 小时前
如何给自己的量化策略增加独立数据层?从数据获取到策略解耦的 Python 实践
开发语言·python·数据分析·量化交易·股票数据·quantdash
NeoGressAI外贸数字化2 小时前
外贸独立站3类页面合格度,怎么用脚本一次跑完
开发语言·python
Lost of 程序猿2 小时前
责任链模式实战:审批流里“层层把关“的艺术
后端·设计模式·asp.net·责任链模式