rust: Simple Factory Pattern(再续)
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:Simple Factory Pattern 简单工厂模式 Creational 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/8 21:48
//!# User : geovindu
//!# Product : RustRover
//!# Project : simplefactorypattern
//!# File : main.rs
//! # 简单工厂模式(Simple Factory Pattern)—— 珠宝检测实验室的委托单创建
//!
//! ## 一句话与三个角色
//!
//! > **一个工厂类,根据传入的参数决定创建哪一种产品类的实例。**
//!
//! 它**不是 GoF 二十三种模式之一**,而是一种编程习惯。三个角色在本工程的落点是:
//!
//! | 角色 | 本工程的落点 |
//! |---|---|
//! | 工厂(Factory) | `factory::TestingOrderFactory`(编译期白名单)/ `factory::TestingOrderRegistry`(运行期登记表) |
//! | 抽象产品(Abstract Product) | `product::TestingOrder` |
//! | 具体产品(Concrete Product) | `product` 层里五个 `pub(super)` 结构体 |
//!
//! ## 本工程不满足于「写一个 match」,也不满足于「写两版看着差不多」
//!
//! 教科书版本的分派长这样:
//!
//! ```text
//! match code {
//! "A" => Box::new(ProductA::new()),
//! "B" => Box::new(ProductB::new()),
//! _ => panic!("unknown"),
//! }
//! ```
//!
//! 它的结构问题是:**分派表与产品实现是两份东西**,必须手动保持同步。
//! 本工程要把这件事**量化**,因此同时挂着两版机制,用**同一段驱动代码**
//! 跑**同一批送检单**,把「扩展时要改几处」与「会不会失配」变成报表上的数字。
//!
//! ## ★ 本工程最重要的一句结论(初稿写错过,被实测纠正)
//!
//! 简单工厂的代价不是「改一行」,而是**「改两处并保持同步」**。
//! 两份清单会以两个方向失配,而**两个方向都不会「没有报错」**——
//! 准确的说法是:
//!
//! | 失配方向 | `supports` 的回答 | 创建的结果 | 判定 |
//! |---|---|---|---|
//! | 白名单有、构造器表没有 | `true`(承诺支持) | 失败 | **自相矛盾** |
//! | 构造器表有、白名单没有 | `false`(不承诺) | 失败 | **对内完全自洽** |
//!
//! 第二行才是真正难发现的:从对外行为看,它和「这个功能本来就没做」
//! **不可区分**。所以那句话的准确版本是——
//! **不是「没有报错」,而是「报的错看起来完全合理」**。
//!
//! 本工程把这句话变成第三幕、第九幕里的两列读数
//! (「声明与行为矛盾」与「跑完整批后未知编码」),
//! 因为**一个更顺口但不准确的表述,比一句笨拙的准确表述危险得多**。
//!
//! ## 严格分层结构
//!
//! ```text
//! ┌──────────────────────────────────────────────────────────────────────────┐
//! │ main.rs 编排:十幕的顺序、工程外扩展、输出自查 │
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ app 排版层 把已算好的值排成定宽文本表(零判断、零 IO) │
//! │ layout / dispatch_table_report / outcome_report / comparison_report │
//! │ profile_report / extension_report / health_check_report │
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │ 只读结构
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ analysis 分析层 只读地说出「这批数据意味着什么」 │
//! │ check_line(结论是一等值)/ creation_ledger_analysis(账本与两版对照) │
//! │ dispatch_coverage(覆盖率与失配)/ capacity_profile / cost_profile │
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │ 只读 WorkbenchRun
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ client 调用方层 「送检批次 → 工作台驱动 → 逐条快照」这条唯一的业务主干 │
//! │ consignment_batch(输入)/ laboratory_workbench(过程)/ product_snapshot(输出)│
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │ &dyn OrderCreator
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ factory 工厂层 两版分派机制:编译期白名单 / 运行期登记表 │
//! │ order_creator(trait)/ testing_order_factory / testing_order_registry │
//! │ creation_error(不知道造什么)/ creation_ledger(创建账本) │
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │ 按编码取构造器(ProductBuilder)
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ product 产品层 抽象产品 + 五个具体产品(类型名 pub(super),外界写不出来)│
//! │ testing_order(trait + ProductBuilder)/ builtin_product_builders │
//! │ sample_specification / specification_rejection │
//! │ specification_guard、item_fee_sum(模块私有,不对外暴露) │
//! └───────────────────────────────┬──────────────────────────────────────────┘
//! │
//! ┌───────────────────────────────▼──────────────────────────────────────────┐
//! │ domain 领域层 与「谁创建」无关的业务事实(五个开放型标签 + Money/Rate)│
//! │ support 支持层 零业务语义的纯工具(日历 / 文本宽度 / 确定性哈希) │
//! └──────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! ### 实测依赖邻接表(`bash scripts/check_layer_dependencies.sh src`)
//!
//! ```text
//! app -> analysis client domain support
//! analysis -> client domain factory support
//! client -> domain factory product support
//! factory -> domain product
//! product -> domain support
//! domain -> none
//! support -> none
//! ```
//!
//! ⚠️ **实测与设计意图有两处偏差,都留档在这里**:
//!
//! 1. `analysis -> factory`:本层初稿期望「连 `factory` 都不需要」,
//! 但 `audit_support_claims` 的入参里有一个 `&dyn OrderCreator`。
//! 这不是顺手引类型,而是**「声明与实际行为是否矛盾」这条审计必须
//! 站在创建者抽象之上才能问**。分叉点揭示了真实的职责边界。
//! 2. `app` **不**依赖 `factory` 与 `product`:报表要印产品目录与两版机制的清单,
//! 实际做法是把它们**以纯数据形态**从本文件传进去
//! (`app::DispatchEntry` 与 `analysis::CreatorCoverage`)。
//! 于是报表也不认识实现细节——**这既是依赖表更干净,
//! 也意味着这张报表能给工程外的第三方创建者排版**。
//!
//! 两处都不改图,因为**图必须以实测为准,不以设计意图为准**。
//!
//! ### 这七层各自「不许做什么」
//!
//! | 层 | 不许做什么 | 为什么 |
//! |---|---|---|
//! | `support` | 不许有业务语义 | 一旦它认识「检测类型」,它就必须跟着业务改,最底层会天天动 |
//! | `domain` | 不许认识创建者 | 业务事实不因「谁造的」而改变;引用创建相关的东西,扩展性证明当场失效 |
//! | `product` | 不许认识工厂 | 产品给出的只是「构造器」这种被动数据;若产品反向依赖工厂,新增产品就要改产品层里的登记代码 |
//! | `factory` | 不许认识分析/报表 | 工厂只回答「造得出来吗、造了几张」,解释数据是别人的事 |
//! | `client` | 不许认识具体产品 | 五个具体产品是 `pub(super)`,本层**想违反也违反不了**(类型名写不出来) |
//! | `analysis` | 不许修改任何东西 | 入参是值(`Clone + PartialEq`),没有可变引用可用,「探针污染主链」无处可写 |
//! | `app` | 不许做判断、不许 IO | 报表只返回行、不 `println!`;只要报表不做判断,「显示通过却不对」的源头就一定在上层 |
//!
//! ### 三条贯穿全工程的纪律
//!
//! 1. **整数金额、整数比率**:金额一律「整数分」,比率一律「整数万分点」,
//! 乘除用 `i128` 中间量 + 显式四舍五入。因此本工程**全程无浮点**——
//! 报表上每个数字都能被读者用计算器复算。
//! 2. **两次运行逐字节一致**:凡遍历 `HashMap` 的地方一律「先收集再排序」;
//! 输出里**不印任何地址**(函数指针地址受 ASLR 影响);
//! 不用对勾与叉号那两个符号(码点 U+2713 / U+2717,东亚宽度属性是 `A`,
//! 宽度模型按 1 列而终端常渲染 2 列,会让整列错位)——
//! 改用中文词「一致 / 不一致」。⚠️ 连**说明这句话的文字**里也不许出现它们,
//! 否则那条自查会被自己的说明触发(本工程踩过,见幕十续)。
//! 3. **每个数字可复算**:派生指标必须给出分母(`5/5`、占比、平均值);
//! 合计行只对**可以求和的列**求和,百分比列**不能**求和
//! (本工程实测 50.00% + 35.71% + 14.28% = 99.99%,故合计行填 `—`
//! 并把和值写进注解)。
//!
//! ### 五处「报表看起来一切正常、其实不对」的缺陷(全部留档)
//!
//! 这五处都是**跑出来**才发现的,而它们有一个共同特征:
//! **排版、对齐、页边距都没坏,坏的是数字或文字的含义。**
//! 按性质分三类:
//!
//! **第一类:判据的口径没写准(最危险,因为它会生成一个看起来合理的错数)**
//!
//! | # | 症状 | 根因 | 修法 |
//! |---|---|---|---|
//! | 1 | 「声明与行为矛盾」在**标准工厂**上也有 5 条 | 把「失败」整体当成了「承诺落空」,没有区分失败发生在**哪一层** | 给 `CreationError` 加 `FailureLayer`,只把**工厂层**(不认识编码)的失败计为矛盾;产品层拒绝另立一列交代 |
//!
//! 第 1 条的后果尤其值得记:它让**报表注解与自己正上方的表格对不上**
//! (注解写「反例 B 的矛盾为 0」,表里印着 5)。**一句与相邻数字矛盾的注解,
//! 比没有注解更糟**——读者会开始怀疑整张表。根因是那个「0」**手写**的,
//! 不是算出来的。因此修法有两步:① 判据正确;② 注解里的每个数字都从数据来。
//!
//! **第二类:算式与标签自己就不成立(读者一按计算器就发现)**
//!
//! | # | 症状 | 根因 | 修法 |
//! |---|---|---|---|
//! | 2 | 注解写「15 条 = 标准批次里 14 条内置编码的 + 3 条珍珠」 | 「14」取的是标准批次总条数,而真正的内置编码条数是 12 | 数一遍(`builtin_coded_request_count`),并加一条体检核对「12 + 3 = 15」 |
//! | 3 | 贵金属室平均承诺印 `1.6 天`,读者按计算器得 `1.7` | 整数除法**截断**而非四舍五入;且报表层还自己算了一遍加权平均 | 抽一个 `average_tenths_text` 统一「放大 10 倍后四舍五入」,并把加权平均挪回分析层 |
//! | 4 | 计价口径表的占比列,读者按表头复算对不上 | 表头两次改动各丢一半:先丢分母(`基准费占比`),再丢分子(`占总合计比`) | 表头写成算式本身:`基准费/总合计` |
//!
//! **第三类:结构与自指(存在层,不体现在某一个数上)**
//!
//! | # | 症状 | 根因 | 修法 |
//! |---|---|---|---|
//! | 5 | 「■ 幕五·…」连着印两遍 | 调用方与报表函数**各发了一次**幕标题,理由是调用方注释里写着「该函数不含标题」——那句注释已经过期 | 删掉重复的那次调用(不是去改注释),并加一条「幕标题共 10 条且互不重复」的自查 |
//!
//! 另有两条不属于「缺陷」但同源(都是**自查自己出问题**),记在下面免得下次再撞:
//!
//! - 「输出中不含禁用符号」这条自查,被它**自己的说明文字**触发。
//! 这类**自指陷阱**的处置不是给它开例外(「跳过这一行」会让检查失去全局性),
//! 而是把口径讲清楚:既然禁的是码点本身,那说明就只能用码点描述它;
//! - 「体检表『检查项』列容得下全部名称」这条自查,**第一次运行就抓到了新加的检查**
//! (新检查的名字 60 列 > 列宽 58 列)。**加检查也可能撑破列宽**,这条自查因此立刻回本。
//!
//! ### 十幕导览
//!
//! | 幕 | 内容 | 回答什么问题 |
//! |---|---|---|
//! | 一 | 工程总览与阅读约定 | 这份输出是怎么组织的 |
//! | 二 | 分派表装配(产品目录 / 项目价目表 / 两版清单对照) | 工厂到底认识多少种产品 |
//! | 三 | 逐条送检结局与运行小结 | 每一张单子发生了什么 |
//! | 四 | 创建账本与归组明细 | 账本自己可信吗 |
//! | 五 | 产能画像(按类型 / 按部门 / 排期明细) | 排产要多少工时、什么时候交 |
//! | 六 | 两版机制逐条与逐字段对照 | 换个分派机制,结果变了吗 |
//! | 七 | 成本画像(构成 / 口径分布) | 收了多少钱,凭什么这么收 |
//! | 八 | 价目基准对照 | 两条独立计价路径对得上吗 |
//! | 九 | 工程外扩展与失配反例 | 「完全可扩展」的证据是什么 |
//! | 十 | 体检报告与输出自查 | 上述每一条依据成立吗 |
//!
//! ⚠️ 注意:下面的说明文字里出现了 `crate::domain`、`crate::product` 这类路径
//! **作为反例**。依赖检查脚本会先剥离 `//` 行注释再匹配,因此不会被误判成真依赖。
// ---------------------------------------------------------------------------
// 模块声明
// ---------------------------------------------------------------------------
//
// ⚠️ 这里的顺序**不是**分层顺序(它按字母排)。分层顺序以上面的实测邻接表为准,
// 图表也以实测为准。依赖检查脚本已升级为「从依赖图自身推断层序」,
// 正是为了避免再维护一份会悄悄过期的顺序清单。
mod analysis;
mod app;
mod client;
mod domain;
mod factory;
mod product;
mod support;
use crate::analysis::{
audit_support_claims, build_capacity_profile, build_cost_profile, compare_runs,
creator_coverage, dispatch_table_mismatch, elide_text, fee_consistency_report, fee_text,
reconcile_ledger, CapacityProfile, CheckLine, CheckReport, CostProfile, CreatorCoverage,
DispatchTableMismatch, RunComparison, SupportClaimAudit,
};
use crate::app::{
key_value_line, note_lines, render_builtin_catalog, render_capacity_table, render_check_table,
render_cost_composition_table, render_department_table, render_dispatch_notes,
render_extension_notes, render_extension_table, render_field_table,
render_ledger_breakdown_table, render_ledger_table, render_mechanism_comparison,
render_merged_check_table, render_mismatch_evidence_table, render_outcome_table,
render_per_request_table, render_price_basis_comparison_table, render_pricing_basis_table,
render_run_summary, render_schedule_table, render_testing_item_catalog, section_header,
DispatchEntry, ExtensionStage, MismatchEvidence, CHECK_DETAIL_COLUMN_WIDTH,
CHECK_ITEM_COLUMN_WIDTH,
};
use crate::client::{
builtin_consignment_batch, carat_text, run_consignment_batch, ConsignmentBatch, WorkbenchRun,
CODE_DIAMOND_GRADING_TYPO, CODE_PEARL_AUTHENTICATION_REQUEST,
};
use crate::domain::{
CertificateKind, Currency, Money, PricingBasis, Rate, SampleKind, TestingItem,
TestingOrderCode, CERTIFICATE_KIND_SPECIAL_REPORT, CURRENCY_CHINESE_YUAN,
CURRENCY_HONG_KONG_DOLLAR, PRICING_BASIS_PER_ITEM,
};
use crate::factory::{
CreationError, FailureLayer, OrderCreator, TestingOrderFactory, TestingOrderRegistry,
BUILTIN_SUPPORTED_ORDER_CODES,
};
use crate::product::{
builtin_product_builders, builtin_product_count, ProductBuilder, SampleSpecification,
SpecificationRejection, TestingOrder,
};
use crate::support::{horizontal_rule, CalendarDate};
// ---------------------------------------------------------------------------
// 版面常量
// ---------------------------------------------------------------------------
/// 文档总宽度(显示列数)。
///
/// ## 这个数字是**算出来的**,不是挑出来的
///
/// 它是「本工程所有表里最宽的那一张」的总宽。`TextTable::total_width()` 的公式是
/// `Σ(列宽 + 2) + 列数 + 1`,把它代进每张表:
///
/// | 表 | 列宽 | 总宽 |
/// |---|---|---|
/// | 逐条送检结局 | 14+20+24+8+26+12 | 123 |
/// | 两版逐条对照 | 14+24+34+34+10 | 132 |
/// | 两版逐字段对照 | 10+22+30+30+10 | 118 |
/// | 失配反例证据 | 30+12+12+12+12+16+20 | **136** ← 最宽 |
/// | 体检表(合并) | 56+8+54 | 128 |
/// | 扩展前后对照 | 10+32+10+10+12+10+12 | 118 |
/// | 两版机制清单对照 | 32+10+12+10+12+12 | 107 |
///
/// **取最宽的那一张**,而不是「看起来差不多」的一个整数:
/// 分隔线的宽度必须 ≥ 任何一张表的宽度,否则`■ 幕标题`下面那条线会短于表,
/// 整份文档看起来像几份拼起来的。
///
/// ⚠️ 本值从 130 涨到 136,是**修截断的直接后果**:三张表分别放宽了
/// 「机制」(16 → 32)、「工厂」(22 → 30)、「构造器表项数」(10 → 12)、
/// 「检查项」(40 → 56)。**列宽从来不是可以省的地方**——
/// 省下来的每一列,代价都是内容被静默切掉一截而报表看起来一切正常。
///
/// 第十幕的输出自查里有一条**永久自检**:
/// `check_rendered_line_widths` 会逐行核验所有渲染结果都不超过本值——
/// 将来新增一张更宽的表,第一次运行就会当场发现。
const DOCUMENT_WIDTH: usize = 136;
/// 编码全集的大小上限(用于分派覆盖率体检的展示口径)。
///
/// 真正的全集由 `builtin_product_builders()` 与工程外扩展共同决定,
/// 不需要在这里写死——这个常量只用来给「自查」提供一个人工核对基准。
const ENGINEERING_EXTERNAL_EXTENSION_COUNT: usize = 1;
/// 工程外扩展在**送检单**层面的增量:`build_extension_batch` 里手写的那 3 条珍珠单。
///
/// ## 为什么把「3」写成一个常量,而不是散在注解里
///
/// 因为报表注解里有一句「扩展批次 = 内置编码的 N 条 + 3 条珍珠」。
/// 那个「3」若手写在格式化串里,它就和 `build_extension_batch` 里真正的条数
/// 成了两份会各自演化的副本——**本工程刚因为注解手写数字吃过一次亏**
/// (幕九的注解写「反例 B 的矛盾为 0」,而同一张表里印着 5)。
///
/// 常量可以让两处**必然一致**;再加一条体检(扩展批次条数 = 内置编码条数 + 本常量)
/// 就能让「忘了改常量」也在第一次运行时被抓到。
const ENGINEERING_EXTERNAL_PEARL_REQUEST_COUNT: usize = 3;
// ===========================================================================
// 工程外扩展区
// ===========================================================================
//
// ## 这一段代码的位置就是本工程的论据
//
// 它**不在任何分层目录里**,全部住在 `main.rs`(工程之外)。
// 「珍珠鉴定」是一种全新的检测类型:新的编码、新的样品类别、
// 两个新的检测项目、新的计价政策。把它做出来之后,
//
// - **运行期登记表版**:写一行 `register(..)` 就接上了,分层目录零改动;
// - **编译期白名单版**:做不到——必须去改 `factory` 的白名单数组(还要改长度)
// 与 `product` 的构造器表,两处、三层、且必须手动保持同步。
//
// 这就是「完全可扩展」这句主张的**唯一的证据形式**:
// 不是声明「本设计很可扩展」,而是**在工程外面真的加了一种产品,
// 然后让报表自己说它被纳入了统计**。
//
// ## ⚠️ 校验必须自己写:`specification_guard` 是层内私有的
//
// `product` 层的 `specification_guard` 与 `item_fee_sum` 声明为**模块私有**
// (`mod` 而非 `pub mod`)。这是刻意的:它们是本层内部的**共用口径**,
// 不是对外契约——若把它们公开,上层就可能各自调用它们再拼出产品,
// 而那正是产品层要阻止的旁路。
//
// 因此扩展方只能做两件事:① 用公开 API 读规格;
// ② 自己构造 `SpecificationRejection` 来表达拒绝。
// 这个「不方便」恰好是边界清晰的代价与标志。
/// 工程外扩展:检测项目「珠层厚度」,单价 ¥220.00。
///
/// ## 为什么新检测项目不需要改 `domain` 层
///
/// 因为 `TestingItem` 是 `const fn new(..)` 的**开放型结构体**,
/// 不是枚举。枚举要求「所有可能值都写在本文件里」,
/// 于是实验室每加一个项目就要改领域层——那等于说领域层认识全世界所有检测项目。
/// 本工程的口径是:**凡是需要工程外扩展的维度,一律用开放型结构体。**
///
/// 单价挂在项目自己身上(`TestingItem::item_fee`),
/// 因此「按项目数计价」的产品只需遍历项目求和,完全不必认识任何具体项目。
const TESTING_ITEM_NACRE_THICKNESS: TestingItem = TestingItem::new(
"NACRE_THICKNESS",
"珠层厚度",
Money::from_minor_units(22_000, CURRENCY_CHINESE_YUAN),
);
/// 工程外扩展:检测项目「光泽度」,单价 ¥180.00。
const TESTING_ITEM_LUSTER: TestingItem = TestingItem::new(
"LUSTER",
"光泽度",
Money::from_minor_units(18_000, CURRENCY_CHINESE_YUAN),
);
/// 工程外扩展:珍珠鉴定的检测类型编码。
///
/// 它**不是** `domain` 层的内置常量,而是扩展方自己的 `const`。
/// 这正是「开放型标签」的价值:新增编码**不需要改领域层**。
/// 若 `TestingOrderCode` 是枚举,这里就必须去 `domain` 里加一个变体,
/// 「扩展零改动」当场失效。
const ORDER_CODE_PEARL_IDENTIFICATION: TestingOrderCode =
TestingOrderCode::new("PEARL_IDENTIFICATION", "珍珠鉴定");
/// 工程外扩展:珍珠的样品类别。
const SAMPLE_KIND_PEARL: SampleKind = SampleKind::new("PEARL", "珍珠");
/// 工程外扩展:珍珠鉴定包含的项目(两项合计 ¥400.00)。
///
/// 以 `const` 数组而非内联字面量:产品与价目对照表都要拿这份清单,
/// 内联会让「同一份清单出现两处」——这在本工程里是被明令禁止的失效方式。
const PEARL_REQUIRED_ITEMS: [TestingItem; 2] =
[TESTING_ITEM_NACRE_THICKNESS, TESTING_ITEM_LUSTER];
/// 工程外扩展:标准出证工作日 4 天。
const PEARL_TURNOVER_DAYS: u16 = 4;
/// 工程外扩展:加急费率 25%。
///
/// ## 刻意与内置产品不同(内置是 30%),为什么
///
/// 因为「扩展方带来自己的计价政策」是「真扩展」与「改个名字的复制品」的分水岭。
/// 如果扩展产品的费率、工期、件数上限全部与某个内置产品相同,
/// 那它只是内置产品的一个别名,证明不了扩展能力。
/// 25% 与 4 天与 50 件,都是**扩展方自己的业务参数**。
const PEARL_URGENCY_RATE: Rate = Rate::from_percent(25);
/// 工程外扩展:单张委托单允许的最大件数。
const PEARL_MAXIMUM_PIECE_COUNT: u16 = 50;
/// 工程外扩展:珍珠鉴定委托单(具体产品)。
///
/// ## 它与内置的五个产品**地位完全平等**
///
/// 它实现同一个 [`TestingOrder`] trait,因此:
/// - 工厂能造它(`create` 返回 `Box<dyn TestingOrder>`,与内置产品同型);
/// - 快照能投影它(`snapshot_order` 走公开接口,不认识具体类型);
/// - 覆盖率、账本、产能、成本四张报表**自动**把它纳入统计,一行报表代码都不用改。
///
/// 差别只在**可见性**:内置五个产品的类型名是 `pub(super)`,
/// 而本类型就在 `main.rs` 里,看得见。但「看得见」不等于「可以不经过工厂」——
/// 它要进入报表,仍然**必须**先注册进创建者,再由驱动代码调用 `create`。
struct PearlIdentificationOrder {
/// 样品编号(来自规格,原样保存)。
sample_code: String,
/// 送检客户。
applicant: String,
/// 件数。
piece_count: u16,
/// 是否加急。
urgent: bool,
/// 基准检测费(受理那一刻的价格快照)。
///
/// ## 与内置产品同样的理由:构造时一次算定
///
/// 「基准费」是价格快照。若每次 `base_fee()` 都重算,将来调价之后
/// 已经受理的历史委托单在报表上会显示新价格——历史记录被**追溯改写**。
base_fee: Money,
}
impl TestingOrder for PearlIdentificationOrder {
fn order_code(&self) -> TestingOrderCode {
ORDER_CODE_PEARL_IDENTIFICATION
}
fn sample_code(&self) -> &str {
&self.sample_code
}
fn applicant(&self) -> &str {
&self.applicant
}
fn handling_department(&self) -> &'static str {
// 新部门:报表「按部门分组」那张表会自动多出一行,
// 而那张表的代码一行都没改。
"珍珠室"
}
fn sample_kind(&self) -> SampleKind {
SAMPLE_KIND_PEARL
}
fn pricing_basis(&self) -> PricingBasis {
// 与内置的「宝石鉴定 / 玉石鉴定」同一个口径:按项目数计价。
// 复用口径而不是新造一个,说明「口径」是一个**可共享的分类维度**,
// 而「检测类型」才是产品身份。
PRICING_BASIS_PER_ITEM
}
fn certificate_kind(&self) -> CertificateKind {
CERTIFICATE_KIND_SPECIAL_REPORT
}
fn currency(&self) -> Currency {
CURRENCY_CHINESE_YUAN
}
fn base_fee(&self) -> Money {
self.base_fee
}
fn urgency_rate(&self) -> Rate {
// 价目表上的承诺费率,与「这一单是否加急」无关。
PEARL_URGENCY_RATE
}
fn loss_rate(&self) -> Rate {
// 非破坏性检测:损耗费率恒为 0%,走同一条运算路径(不写 if)。
Rate::zero()
}
fn turnover_days(&self) -> u16 {
PEARL_TURNOVER_DAYS
}
fn is_destructive(&self) -> bool {
false
}
fn is_urgent(&self) -> bool {
self.urgent
}
fn piece_count(&self) -> u16 {
self.piece_count
}
fn carat_millis(&self) -> i64 {
// 珍珠不按克拉计价。
0
}
fn required_items(&self) -> &[TestingItem] {
&PEARL_REQUIRED_ITEMS
}
}
/// 由规格构造一张珍珠鉴定委托单。
///
/// 参数 `specification`:样品规格。
/// 返回:合格时返回产品,否则返回拒绝原因。
///
/// ## 为什么这里的前四行不能写成一句 `require_sample_kind(..)?`
///
/// 因为 `specification_guard` 是 `product` 层的**模块私有**函数,
/// 工程外拿不到。这不是不便,而是**边界清晰**:那三个守卫是本层内部的
/// 共用口径,公开它等于允许上层绕过产品自己拼产品。
///
/// 扩展方因此必须自己写这三行校验。代价是几行重复代码,
/// 收益是「产品层内部的约定不外泄」——同时它也**证明**了
/// 「扩展方只需要公开 API」,而不是需要框架给一堆内部钩子。
fn build_pearl_identification_order(
specification: &SampleSpecification,
) -> Result<Box<dyn TestingOrder>, SpecificationRejection> {
// ① 样品必须是珍珠。
if specification.sample_kind() != SAMPLE_KIND_PEARL {
return Err(SpecificationRejection::SampleKindMismatch {
expected: SAMPLE_KIND_PEARL,
actual: specification.sample_kind(),
});
}
// ② 件数必须在 [1, 50]。
let piece_count: u16 = specification.piece_count();
if piece_count < 1 || piece_count > PEARL_MAXIMUM_PIECE_COUNT {
return Err(SpecificationRejection::ParameterOutOfRange {
parameter: "piece_count",
parameter_chinese_name: "样品件数",
value: piece_count as i64,
minimum: 1,
maximum: PEARL_MAXIMUM_PIECE_COUNT as i64,
});
}
// ③ 计价:按项目数。各项目单价之和——**本函数不认识任何具体项目的单价**,
// 单价挂在项目自己身上(`TestingItem::item_fee`)。
// 这里刻意不用 `product::item_fee_sum::sum_item_fees`(模块私有),
// 而是展开写一遍:它顺带证明了「按项目数计价」这个口径本身不需要框架支持。
let mut base_fee: Money = Money::zero(CURRENCY_CHINESE_YUAN);
for item in PEARL_REQUIRED_ITEMS.iter() {
base_fee = base_fee
.add(&item.item_fee())
.unwrap_or(base_fee);
}
Ok(Box::new(PearlIdentificationOrder {
sample_code: specification.sample_code().to_string(),
applicant: specification.applicant().to_string(),
piece_count,
urgent: specification.is_urgent(),
base_fee,
}))
}
/// 工程外扩展:把构造器转成 `ProductBuilder` 函数指针。
///
/// ## 为什么要多这一层薄包装
///
/// `ProductBuilder` 的类型是 `fn(&SampleSpecification) -> Result<..>`,
/// 而 `build_pearl_identification_order` 恰好就是那个签名,本可**直接强转**:
/// `build_pearl_identification_order as ProductBuilder`。
///
/// 这里仍然写成一个具名常量,理由有两条:
/// 1. 让「扩展方交给创建者的东西」有一个**可搜索的名字**(`PEARL_BUILDER`),
/// 报表与文档里引用它比引用一个长函数名清楚;
/// 2. 若将来构造器需要改为泛型或改名,改动集中在这一行。
const PEARL_BUILDER: ProductBuilder = build_pearl_identification_order;
/// 工程外扩展:构造器表(内置) + 珍珠构造器。
///
/// 这个函数**不修改任何分层文件**:内置那一份来自
/// `product::builtin_product_builders()`,珍珠那一项由本文件追加。
fn extended_product_builders() -> Vec<(TestingOrderCode, ProductBuilder)> {
let mut builders: Vec<(TestingOrderCode, ProductBuilder)> = builtin_product_builders();
builders.push((ORDER_CODE_PEARL_IDENTIFICATION, PEARL_BUILDER));
builders
}
/// 工程外扩展:手工「扩表」后的白名单(内置 5 项 + 珍珠 1 项)。
///
/// ## 它模拟的是「假如白名单版要支持珍珠,维护者必须做的事」
///
/// 注意它是**本文件自己拼的**,不是 `factory` 层常量的一部分。
/// `factory::BUILTIN_SUPPORTED_ORDER_CODES` 仍然是 5 项,一行未改——
/// 因为本工程不能为了演示而偷偷改掉被考察的对象。
/// 报表上这两行的差别(5 项 vs 6 项,改动层数 0 vs 2)
/// 才是本工程要说的那句「扩展成本」。
fn manually_extended_whitelist() -> Vec<TestingOrderCode> {
let mut codes: Vec<TestingOrderCode> = BUILTIN_SUPPORTED_ORDER_CODES.to_vec();
codes.push(ORDER_CODE_PEARL_IDENTIFICATION);
codes
}
/// 编码全集 = 内置构造器表的编码 ∪ 工程外扩展的编码。
///
/// ## 为什么它由调用方给出,而不是由「某个工厂」回答
///
/// 因为它是一个**策略输入**:「应当认识哪些编码」是业务决定,
/// 不是任何一版机制能自己回答的。正因如此,
/// `analysis::creator_coverage` 才能拿它去质询**任意**创建者——
/// 包括工程外构造的反例工厂。
fn code_universe() -> Vec<TestingOrderCode> {
extended_product_builders()
.iter()
.map(|(order_code, _)| *order_code)
.collect()
}
// 编译期自查:工程外扩展区的组成必须与文档里写的一致。
// 这类断言的价值不在运行期防呆,而在于**把文档里的一句话变成可执行的声明**。
const _: () = {
assert!(
ENGINEERING_EXTERNAL_EXTENSION_COUNT == 1,
"工程外扩展目前恰好是 1 种(珍珠鉴定);改了这里,第九幕的申报值也要改"
);
assert!(
PEARL_REQUIRED_ITEMS.len() == 2,
"珍珠鉴定含两个项目(珠层厚度、光泽度),合计 ¥400.00"
);
// 两个项目单价之和 = ¥400.00 = 40000 分(在 const 上下文里人工核算一次)。
assert!(
TESTING_ITEM_NACRE_THICKNESS.item_fee().minor_units() == 22_000
&& TESTING_ITEM_LUSTER.item_fee().minor_units() == 18_000,
"两个项目的单价必须是 ¥220.00 与 ¥180.00(合计 ¥400.00)"
);
};
// 供 `manually_extended_whitelist` 使用的一处再导出检查:
// 内置白名单长度为 5 这件事必须在编译期成立,否则「扩展前后 5 → 6」这个
// 对照就不再是 5 → 6。写成 const 断言而不是运行期检查,是为了让它
// **在编译时就拦住一次错误的对照实验**。
const _: () = {
assert!(
BUILTIN_SUPPORTED_ORDER_CODES.len() == 5,
"内置白名单是 5 项;若它变了,第九幕的申报值与文档都要跟着改"
);
};
// ===========================================================================
// 编排
// ===========================================================================
/// 十幕编排。
///
/// ## ★ 一条必须遵守的纪律:每个用途各建一条链
///
/// 创建者的账本与登记表都是**只增不减**的(观测、计数、登记项都不清空)。
/// 因此「同一个创建者跑第二批送检单」会把两批的账混在一起——
/// 在 Proxy 工程里,这条疏忽导致扫描笔数 11 vs 期望 10,
/// 并在重算时触发 `debug_assert_eq!` 当场 panic。
///
/// 所以下面的写法是:**凡是要跑一批送检单,就现建一个创建者**。
/// 每个 `run_consignment_batch` 调用的第一个实参,都是一个刚 new 出来的对象。
fn main() {
let mut lines: Vec<String> = Vec::new();
// ------------------------- 幕一:工程总览 -------------------------
lines.extend(render_overview());
// 两批输入。
// 标准批次:14 条,全部使用内置编码,覆盖 5 种结局。
// 扩展批次:15 条 = 标准批次里 12 条内置编码的 + 3 条珍珠扩展。
let standard_batch: ConsignmentBatch = builtin_consignment_batch();
let extension_batch: ConsignmentBatch = build_extension_batch();
// 内置编码全集(5 种):用于「两版机制认识多少种」的对照。
let builtin_universe: Vec<TestingOrderCode> = BUILTIN_SUPPORTED_ORDER_CODES.to_vec();
// --------------------- 创建者(标准批次专用) ---------------------
// 每个创建者只用一次,理由见本函数文档的「每个用途各建一条链」。
let whitelist_factory: TestingOrderFactory = TestingOrderFactory::builtin();
let registry_creator: TestingOrderRegistry = TestingOrderRegistry::builtin();
// ------------------- 幕二:分派表装配(产品目录等) -------------------
// 幕二的小节标题由 `render_builtin_catalog` 内部发出(它是本幕的第一张表),
// 后面三张表跟在它下面。这样切分的原因是:**幕标题只印一次**,
// 若每张表各自印一个幕标题,读者会以为那是四幕。
let catalog_entries: Vec<DispatchEntry> = builtin_product_builders()
.iter()
.map(|(order_code, _builder)| {
DispatchEntry::new(order_code.code(), order_code.chinese_name())
})
.collect();
lines.extend(render_builtin_catalog(&catalog_entries, DOCUMENT_WIDTH));
// 项目价目表:从 domain 的项目总表出发,而不是从各产品反推。
lines.extend(render_testing_item_catalog(
&crate::domain::BUILTIN_TESTING_ITEMS,
DOCUMENT_WIDTH,
));
// 两版机制的清单对照:清单份数由机制自己回答(`dispatch_list_count`)。
let coverage_whitelist = creator_coverage(&whitelist_factory, &builtin_universe);
let coverage_registry = creator_coverage(®istry_creator, &builtin_universe);
lines.extend(render_mechanism_comparison(
&[coverage_whitelist.clone(), coverage_registry.clone()],
builtin_universe.len(),
DOCUMENT_WIDTH,
));
lines.extend(render_dispatch_notes(DOCUMENT_WIDTH));
// ------------------- 幕三:逐条结局(标准批次 × 白名单版) -------------------
// 幕三的小节标题由 `render_outcome_table` 内部发出。
let whitelist_run: WorkbenchRun = run_consignment_batch(&whitelist_factory, &standard_batch);
lines.extend(render_outcome_table(&whitelist_run, DOCUMENT_WIDTH));
lines.extend(render_run_summary(&whitelist_run, DOCUMENT_WIDTH));
// ------------------- 幕四:创建账本与归组明细 -------------------
lines.extend(section_header("幕四·创建账本与归组明细", DOCUMENT_WIDTH));
lines.extend(render_ledger_table(&whitelist_run, DOCUMENT_WIDTH));
lines.extend(render_ledger_breakdown_table(&whitelist_run, DOCUMENT_WIDTH));
// --------- 幕五 / 幕六:两版机制对照(同一批,只换分派表) ---------
// 登记表版**必须另建一个创建者**:上一步的白名单工厂账本里已经有 14 笔,
// 拿它再跑一遍就会把两批混在一起。
let registry_run: WorkbenchRun = run_consignment_batch(®istry_creator, &standard_batch);
// 幕五:逐条对照。**标题由 `render_per_request_table` 内部发出**——
// 这里曾经也自己发了一次(当时的注释写着「该函数不含标题」,而那句已经过期),
// 于是输出里「■ 幕五·…」连着印了两遍。删掉这一处调用,而不是去改注释:
// **能删的重复调用,比需要维护的注释可靠。**
let comparison = compare_runs(&whitelist_run, ®istry_run);
lines.extend(render_per_request_table(&comparison, DOCUMENT_WIDTH));
// 幕六:逐字段对照(标题同样由 `render_field_table` 内部发出)。
lines.extend(render_field_table(&comparison, DOCUMENT_WIDTH));
// ------------------- 幕七:排期与产能画像 -------------------
// 标题由 `render_capacity_table` 内部发出。
// 两版结果在幕五已证明一致,因此画像只用白名单版那一份即可——
// 若两版不一致,幕六会先报出来,这里的画像才有必要重做。
let capacity = build_capacity_profile(&whitelist_run);
lines.extend(render_capacity_table(&capacity, DOCUMENT_WIDTH));
lines.extend(render_department_table(&capacity, DOCUMENT_WIDTH));
lines.extend(render_schedule_table(&capacity, DOCUMENT_WIDTH));
// ------------------- 幕八:成本画像 -------------------
// 标题由 `render_cost_composition_table` 内部发出。
let cost = build_cost_profile(&whitelist_run);
lines.extend(render_cost_composition_table(&cost, DOCUMENT_WIDTH));
lines.extend(render_pricing_basis_table(&cost, DOCUMENT_WIDTH));
lines.extend(render_price_basis_comparison_table(&cost, DOCUMENT_WIDTH));
// ------------------- 幕九:工程外扩展与失配反例 -------------------
// 本幕返回两部分:要打印的文本行,以及**一份体检报告**——
// 后者进幕十合并。这样「扩展后被纳入统计」这件事不只是叙述,
// 而是一条会失败的检查(它是本工程「完全可扩展」主张的判据)。
let (extension_lines, extension_report) =
run_extension_experiment(&standard_batch, &extension_batch);
lines.extend(extension_lines);
// ------------------- 幕十:体检报告与输出自查 -------------------
let mut health_reports: Vec<CheckReport> = vec![
reconcile_ledger(&whitelist_run),
capacity.to_check_report(),
cost.to_check_report(),
fee_consistency_report(&whitelist_run),
comparison.to_check_report(),
extension_report,
creator_coverage(&whitelist_factory, &builtin_universe).to_check_report(),
dispatch_table_mismatch(
"编译期白名单工厂(标准)",
&whitelist_factory.supported_codes(),
&whitelist_factory.builder_codes(),
)
.to_check_report(),
audit_support_claims(&whitelist_factory, &whitelist_run).to_check_report(),
audit_support_claims(®istry_creator, ®istry_run).to_check_report(),
];
// 只读口径清点:把**全部对外只读接口**真实调用一次。
// 它同时解决两件事:① 证明这些接口确实可用(不是死代码);
// ② 把值层的完整只读契约摆在一张表里,供后来者核对「还能问出什么」。
health_reports.push(read_only_surface_inventory(
&whitelist_factory,
®istry_creator,
&whitelist_run,
®istry_run,
&capacity,
&cost,
&comparison,
&creator_coverage(&whitelist_factory, &builtin_universe),
&standard_batch,
));
lines.extend(render_merged_check_table(
"幕十·体检报告",
&health_reports,
DOCUMENT_WIDTH,
));
// 输出自查**放在最后**,这样它能检查到上面全部内容(含幕十的体检表)。
lines.extend(run_self_check(&lines, &whitelist_run, &health_reports));
// ------------------- 收尾结论 -------------------
lines.extend(render_conclusion(&whitelist_run, ®istry_run));
// 渲染结果的宽度在自查里已逐行核验;这里只负责一次性输出。
for line in &lines {
println!("{}", line);
}
}
/// 幕一:工程总览与阅读约定。
///
/// ## 为什么把「怎么读这份输出」也印出来
///
/// 本工程的输出有十幕、二十来张表。读者(尤其是不熟悉这份代码的人)
/// 需要的不是更多数据,而是**一份导读**:这一幕回答什么问题、
/// 哪些数字是算出来的、哪些是人工申报的。
///
/// 尤其重要的是最后那条:**「改动层数 / 改动文件数」是人工申报值**。
/// 把它说清楚,比让它混在数据里看起来像实算值要好得多。
fn render_overview() -> Vec<String> {
let mut lines: Vec<String> = section_header("幕一·工程总览与阅读约定", DOCUMENT_WIDTH);
lines.push(key_value_line(
"模式",
"简单工厂(Simple Factory)—— 本工程挂两版分派机制:编译期白名单 / 运行期登记表",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"产品",
"珠宝检测实验室的检测委托单(素金 / 银饰 / 钻石 / 宝石 / 玉石)",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"标准批次",
"受理日 2026-09-03(周四),14 条,覆盖 5 种结局",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"扩展批次",
"同一受理日,15 条 = 12 条内置编码 + 3 条工程外的「珍珠鉴定」",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"金额口径",
"全程整数分(¥1.00 = 100 分),比率用整数万分点,无浮点",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"核心命题",
"简单工厂的代价不是「改一行」,而是「改两处并保持同步」",
DOCUMENT_WIDTH,
));
lines.push(key_value_line(
"核心读数",
"「清单份数」2 vs 1;「声明与行为矛盾」反例 A > 0、反例 B = 0",
DOCUMENT_WIDTH,
));
lines.extend(note_lines(
"阅读提示:本工程刻意**不印任何内存地址**(函数指针地址受 ASLR 影响,\
一印进去「两次运行逐字节一致」就当场失效),也不用对勾与叉号那两个符号\
(码点 U+2713 / U+2717,东亚宽度属性是 A,宽度模型按 1 列算而终端常按 2 列渲染,\
会让整列错位)——一致性结论一律用中文词「一致 / 不一致」表达。\
⚠️ 这里刻意**只写码点、不写符号本身**:本幕之后的输出自查会逐行扫描那两个字符,\
而这段说明也要被扫到——只要说明书里印出符号,那条自查就会被它自己触发。",
DOCUMENT_WIDTH,
));
lines.extend(note_lines(
"⚠️ 第九幕的「改动层数」「改动文件数」两列是**人工申报值**,不是算出来的:\
报表不读版本控制,无法自动回答「本次改动碰了几个文件」。\
它们之所以可信,是因为读者可以核对——`grep -n \"register(\" src/main.rs` \
就能确认登记表版的扩展确实只在调用方改了一行。",
DOCUMENT_WIDTH,
));
lines.push(String::new());
lines.push(horizontal_rule(DOCUMENT_WIDTH));
lines
}
/// 构造「工程外扩展批次」:15 条。
///
/// ## 组成方式,以及为什么按编码筛选而不是按序号切片
///
/// 前 12 条来自内置批次里**编码在白名单内**的那些请求。
/// 用「编码是否在白名单内」作为筛选条件,而不是 `take(12)`:
/// 前者表达的是**意图**(「我要那些内置编码能造出来的请求」),
/// 后者表达的是**一个位置假设**——一旦有人调整了内置批次的排列,
/// `take(12)` 会悄悄把两条未知编码的请求也拿进来,
/// 而那正好会让第九幕的读数全部错位。
///
/// 后 3 条是珍珠扩展:**2 条能造出来(常规 / 加急)、1 条参数越界**。
/// 第三条刻意越界,是为了证明扩展产品**自带校验**——
/// 若它照单全收,那它只是一个把输入原样包起来的壳子。
fn build_extension_batch() -> ConsignmentBatch {
let standard_batch: ConsignmentBatch = builtin_consignment_batch();
let mut batch: ConsignmentBatch = ConsignmentBatch::new(
"工程外扩展批次",
// 与标准批次同一个受理日:两批的排期才有可比性。
standard_batch.accepted_on(),
);
for request in standard_batch.requests() {
// 只取内置编码能造出来的那些(跳过「尚未上线」与「拼错」两条)。
if !BUILTIN_SUPPORTED_ORDER_CODES.contains(request.order_code()) {
continue;
}
// 规格原样带过去;编号由批次重新派生(编号是批次的责任,不是请求的)。
batch = batch.with_request(
request.label(),
*request.order_code(),
request.specification().clone(),
);
}
// 珍珠 ①:常规,1 件 → 基准费 ¥400.00(¥220.00 + ¥180.00),无加急、无损耗。
batch = batch.with_request(
"珍珠1件·常规",
ORDER_CODE_PEARL_IDENTIFICATION,
SampleSpecification::new("六福珠宝·中环总店", SAMPLE_KIND_PEARL),
);
// 珍珠 ②:加急,2 件 → 基准费 ¥400.00 + 加急 ¥100.00(25%)= ¥500.00。
// 注意件数不影响「按项目数计价」的基准费——这正是本口径与
// 「按件计价」的区别,读者可在**幕八**的口径对照表上验证。
batch = batch.with_request(
"珍珠2件·加急",
ORDER_CODE_PEARL_IDENTIFICATION,
SampleSpecification::new("六福珠宝·尖沙咀店", SAMPLE_KIND_PEARL)
.with_piece_count(2)
.with_urgency(true),
);
// 珍珠 ③:60 件 → 越界(扩展方自己的上限是 50 件)。
batch = batch.with_request(
"珍珠60件·越界",
ORDER_CODE_PEARL_IDENTIFICATION,
SampleSpecification::new("六福珠宝·旺角店", SAMPLE_KIND_PEARL).with_piece_count(60),
);
batch
}
// ===========================================================================
// 幕九:工程外扩展实验
// ===========================================================================
/// 幕九:把「同一次扩展」对两版机制各做一遍,并构造两个失配反例。
///
/// 参数 `standard_batch`:标准批次(只用来陈述「12 条来自哪里」);
/// `extension_batch`:扩展批次(15 条)。
/// 返回:文本行 + 一份体检报告(供幕十合并)。
///
/// ## 六个创建者,各自只跑一次
///
/// | 创建者 | 白名单 | 构造器表 | 角色 |
/// |---|---|---|---|
/// | ① `factory_before` | 5 | 5 | 扩展前的标准白名单工厂 |
/// | ② `factory_manual_after` | 6 | 6 | 手工扩表后的白名单工厂(**申报改动 2 层 2 文件**) |
/// | ③ `factory_mismatch_a` | 6 | 5 | **反例 A**:白名单有、构造器表没有 |
/// | ④ `factory_mismatch_b` | 5 | 6 | **反例 B**:构造器表有、白名单没有 |
/// | ⑤ `registry_before` | 5(表即白名单) | 5 | 扩展前的登记表 |
/// | ⑥ `registry_after` | 6(表即白名单) | 6 | 扩展后(**只加了一行 `register(..)`**) |
///
/// 这六个对象各跑一次批次。**不能复用**:账本只增不减,
/// 复用会让两批的账混在一起,而「读数对不上」时无从判断是机制的问题
/// 还是实验设计的问题。
fn run_extension_experiment(
standard_batch: &ConsignmentBatch,
extension_batch: &ConsignmentBatch,
) -> (Vec<String>, CheckReport) {
let universe: Vec<TestingOrderCode> = code_universe();
// ---------- 六个创建者 ----------
let factory_before: TestingOrderFactory = TestingOrderFactory::builtin();
let factory_manual_after: TestingOrderFactory =
TestingOrderFactory::new(manually_extended_whitelist(), extended_product_builders());
// 反例 A:白名单多了一项(承诺支持珍珠),构造器表里却没有它。
let factory_mismatch_a: TestingOrderFactory =
TestingOrderFactory::new(manually_extended_whitelist(), builtin_product_builders());
// 反例 B:构造器表多了一项(珍珠确实造得出来),白名单里却没有它。
let factory_mismatch_b: TestingOrderFactory =
TestingOrderFactory::new(BUILTIN_SUPPORTED_ORDER_CODES.to_vec(), extended_product_builders());
let registry_before: TestingOrderRegistry = TestingOrderRegistry::builtin();
let registry_after: TestingOrderRegistry = TestingOrderRegistry::builtin();
// ★★ 整个「工程外扩展」在登记表版上的全部代价,就是下面这一行。★★
let pearl_registered: bool = registry_after.register(ORDER_CODE_PEARL_IDENTIFICATION, PEARL_BUILDER);
// 顺带演示「重复注册被拒绝并计数」:先注册者胜出,登记表项数不变。
// 这一条刻意放在跑批次**之前**做,因为它只影响登记计数、不影响创建账本,
// 但仍要确认它没有污染账本(后面的体检会核对账本总数)。
let duplicate_rejected: bool = !registry_after.register(ORDER_CODE_PEARL_IDENTIFICATION, PEARL_BUILDER);
// ---------- 覆盖情况(`supports` 是只读的,可以放心问) ----------
let coverage_factory_before = creator_coverage(&factory_before, &universe);
let coverage_factory_after = creator_coverage(&factory_manual_after, &universe);
let coverage_registry_before = creator_coverage(®istry_before, &universe);
let coverage_registry_after = creator_coverage(®istry_after, &universe);
// ---------- 批次运行(每个创建者一次) ----------
let run_factory_before = run_consignment_batch(&factory_before, extension_batch);
let run_factory_manual_after = run_consignment_batch(&factory_manual_after, extension_batch);
let run_mismatch_a = run_consignment_batch(&factory_mismatch_a, extension_batch);
let run_mismatch_b = run_consignment_batch(&factory_mismatch_b, extension_batch);
let run_registry_after = run_consignment_batch(®istry_after, extension_batch);
// ---------- 表一:扩展前后对照 ----------
let stages: Vec<ExtensionStage> = vec![
ExtensionStage::new(
"扩展前",
"编译期白名单",
factory_before.supported_codes().len(),
coverage_factory_before.dispatch_list_count(),
coverage_factory_before.recognized().len(),
// ⚠️ 以下两列是**人工申报值**:报表不读版本控制。
0,
0,
),
ExtensionStage::new(
"扩展后",
"编译期白名单(手工扩表)",
factory_manual_after.supported_codes().len(),
coverage_factory_after.dispatch_list_count(),
coverage_factory_after.recognized().len(),
2,
2,
),
ExtensionStage::new(
"扩展前",
"运行期登记表",
registry_before.entry_count(),
coverage_registry_before.dispatch_list_count(),
coverage_registry_before.recognized().len(),
0,
0,
),
ExtensionStage::new(
"扩展后",
"运行期登记表(一行 register)",
registry_after.entry_count(),
coverage_registry_after.dispatch_list_count(),
coverage_registry_after.recognized().len(),
0,
0,
),
];
let mut lines: Vec<String> = render_extension_table(&stages, DOCUMENT_WIDTH);
// ---------- 表二:失配反例证据 ----------
let mismatch_standard = dispatch_table_mismatch(
"标准白名单工厂",
&factory_before.supported_codes(),
&factory_before.builder_codes(),
);
let mismatch_a = dispatch_table_mismatch(
"反例工厂 A:白名单多一项",
&factory_mismatch_a.supported_codes(),
&factory_mismatch_a.builder_codes(),
);
let mismatch_b = dispatch_table_mismatch(
"反例工厂 B:构造器表多一项",
&factory_mismatch_b.supported_codes(),
&factory_mismatch_b.builder_codes(),
);
let evidences: Vec<MismatchEvidence> = vec![
MismatchEvidence::new(
"标准白名单工厂(5/5 同步)",
factory_before.supported_codes().len(),
factory_before.builder_codes().len(),
&mismatch_standard,
audit_support_claims(&factory_before, &run_factory_before).claimed_but_failed(),
audit_support_claims(&factory_before, &run_factory_before).specification_rejected(),
unknown_code_hit_count(&run_factory_before),
),
MismatchEvidence::new(
"反例工厂 A(白名单多一项)",
factory_mismatch_a.supported_codes().len(),
factory_mismatch_a.builder_codes().len(),
&mismatch_a,
audit_support_claims(&factory_mismatch_a, &run_mismatch_a).claimed_but_failed(),
audit_support_claims(&factory_mismatch_a, &run_mismatch_a).specification_rejected(),
unknown_code_hit_count(&run_mismatch_a),
),
MismatchEvidence::new(
"反例工厂 B(构造器表多一项)",
factory_mismatch_b.supported_codes().len(),
factory_mismatch_b.builder_codes().len(),
&mismatch_b,
audit_support_claims(&factory_mismatch_b, &run_mismatch_b).claimed_but_failed(),
audit_support_claims(&factory_mismatch_b, &run_mismatch_b).specification_rejected(),
unknown_code_hit_count(&run_mismatch_b),
),
];
lines.extend(render_mismatch_evidence_table(
&evidences,
extension_batch.request_count(),
DOCUMENT_WIDTH,
));
lines.extend(render_extension_notes(DOCUMENT_WIDTH));
// ---------- 「扩展真的被纳入统计了吗」——用报表自己的数据回答 ----------
let capacity_after = build_capacity_profile(&run_registry_after);
let cost_after = build_cost_profile(&run_registry_after);
let pearl_created_in_ledger: u32 = run_registry_after
.creator_ledger()
.created_by_code()
.iter()
.find(|(order_code, _count)| *order_code == ORDER_CODE_PEARL_IDENTIFICATION)
.map(|(_order_code, count)| *count)
.unwrap_or(0);
let pearl_department_seen: bool = capacity_after
.by_department()
.iter()
.any(|bucket| bucket.department() == "珍珠室");
let pearl_cost_row_seen: bool = cost_after
.by_order_code()
.iter()
.any(|bucket| bucket.bucket_code() == ORDER_CODE_PEARL_IDENTIFICATION.code());
lines.extend(note_lines(
&format!(
"扩展实证:扩展批次共 {} 条 = 标准批次里**内置编码**的 {} 条(原 14 条里\
剔掉「尚未上线」与「拼错」两条)+ {} 条珍珠。\
它在「扩展后的登记表」上跑完 —— 成功 {} 张(内置 7 + 珍珠 2)、\
产品侧被拒 {} 笔、未知编码 0 笔。珍珠的费用进了成本画像、\
「珍珠室」进了产能画像的部门分组,而这两张报表的代码一行都没改。\
按件计价与按项目数计价的差别也在数据里:珍珠的加急单是 2 件,\
基准费仍是 ¥400.00(**按项目数**计价,与件数无关);\
同样是 2 件的素金单,基准费则是 ¥760.00(**按件**计价)。",
extension_batch.request_count(),
builtin_coded_request_count(standard_batch),
ENGINEERING_EXTERNAL_PEARL_REQUEST_COUNT,
run_registry_after.created_outcomes().len(),
run_registry_after.failed_outcomes().len(),
),
DOCUMENT_WIDTH,
));
lines.extend(note_lines(
&format!(
"登记表细节:`register(..)` 返回 {}(首次注册成功),\
紧接着再注册同一个编码返回 {}(重复被拒、先注册者胜出),\
因此登记表项数仍是 {} 项、`registered_extension_count` = {}、\
`duplicate_registration_count` = {}。\
把冲突变成**可观测的事件**,而不是一次安静的覆盖动作——\
这与本工程「宁可让冲突被看见,也不要让它被自动摆平」的取舍一致。",
pearl_registered,
!duplicate_rejected,
registry_after.entry_count(),
registry_after.registered_extension_count(),
registry_after.duplicate_registration_count(),
),
DOCUMENT_WIDTH,
));
// ---------- 供幕十合并的体检报告 ----------
let mut report: CheckReport = CheckReport::new("工程外扩展实证");
// 批次构成的算式必须成立:扩展批次 = 标准批次里内置编码的条数 + 手工加的珍珠条数。
// 这条检查是在**注解写错一次之后**补上的——初版把「内置编码的条数」写成了
// 标准批次的 14 条,于是「14 + 3 = 15」这个算式自己就不成立。
// 一个算式不成立的注解,读者一旦发现就会开始怀疑整份报表;
// 而把它写成检查之后,**算术出问题不需要靠读者发现**。
report = report.with_line(CheckLine::new(
// ⚠️ 这个名字原本是「扩展批次的构成算式成立(内置编码条数 + 珍珠条数 = 批次条数)」
// (60 列),当场撞上体检表「检查项」列的上界(58 列)——
// **加一条检查本身也会把另一个列宽撑破**,这是那条列宽自查补上之后
// 第一次真正抓到东西。把算式写短到 48 列即可,含义一字未失。
"扩展批次构成算式成立:内置编码 + 珍珠 = 批次条数",
builtin_coded_request_count(standard_batch)
+ ENGINEERING_EXTERNAL_PEARL_REQUEST_COUNT
== extension_batch.request_count(),
&format!(
"内置编码 {} 条 + 珍珠 {} 条 = {} 条",
builtin_coded_request_count(standard_batch),
ENGINEERING_EXTERNAL_PEARL_REQUEST_COUNT,
extension_batch.request_count()
),
));
report = report.with_line(CheckLine::new(
"扩展批次里 3 条珍珠请求全部被识别(不再报未知编码)",
unknown_code_hit_count(&run_registry_after) == 0,
&format!(
"未知编码 {} 笔,成功 {} 张,被拒 {} 笔",
unknown_code_hit_count(&run_registry_after),
run_registry_after.created_outcomes().len(),
run_registry_after.failed_outcomes().len()
),
));
report = report.with_line(CheckLine::new(
"珍珠张数进入创建账本",
pearl_created_in_ledger == 2,
&format!("账本里珍珠鉴定 {} 张(期望 2)", pearl_created_in_ledger),
));
report = report.with_line(CheckLine::new(
"「珍珠室」进入产能画像的部门分组",
pearl_department_seen,
&format!(
"部门分组共 {} 组,「珍珠室」{}",
capacity_after.by_department().len(),
if pearl_department_seen { "在" } else { "不在" }
),
));
report = report.with_line(CheckLine::new(
"「珍珠鉴定」进入成本画像的类型分组",
pearl_cost_row_seen,
&format!(
"类型分组共 {} 组,扩展排期最晚出证日 {}",
cost_after.by_order_code().len(),
capacity_after.latest_delivery_on_text()
),
));
report = report.with_line(CheckLine::new(
"扩展批次两条珍珠成功单的合计 = ¥900.00",
run_registry_after
.created_snapshots()
.iter()
.filter(|snapshot| snapshot.order_code == ORDER_CODE_PEARL_IDENTIFICATION.code())
.map(|snapshot| snapshot.total_fee_minor_units)
.sum::<i64>()
== 90_000,
&format!(
"珍珠合计 {}(¥400.00 常规 + ¥500.00 加急)",
fee_text(
run_registry_after
.created_snapshots()
.iter()
.filter(|snapshot| snapshot.order_code == ORDER_CODE_PEARL_IDENTIFICATION.code())
.map(|snapshot| snapshot.total_fee_minor_units)
.sum::<i64>()
)
),
));
report = report.with_line(CheckLine::new(
"反例 A 抓到显性失配、反例 B 抓到隐性失配,标准工厂 0 项",
mismatch_a.explicit_failure_count() == 1
&& mismatch_b.silent_failure_count() == 1
&& mismatch_standard.is_in_sync(),
&format!(
"A:显性 {} / 隐性 {};B:显性 {} / 隐性 {};标准:同步 {}",
mismatch_a.explicit_failure_count(),
mismatch_a.silent_failure_count(),
mismatch_b.explicit_failure_count(),
mismatch_b.silent_failure_count(),
mismatch_standard.is_in_sync()
),
));
report = report.with_line(CheckLine::new(
"两种失配的危险程度不对称(这是本工程的核心读数)",
audit_support_claims(&factory_mismatch_a, &run_mismatch_a).claimed_but_failed() > 0
&& audit_support_claims(&factory_mismatch_b, &run_mismatch_b)
.claimed_but_failed()
== 0
&& unknown_code_hit_count(&run_mismatch_a) == unknown_code_hit_count(&run_mismatch_b),
// ★ 这条说明刻意写得短。它原本把「A 是多少 / B 是多少 / 未知编码各是多少 /
// 三个工厂各有几条产品层拒绝」全塞进来,宽到 265 列,而体检表的
// 实测值列只有 54 列——**读者只能看到开头一小截**。
// 冗长的推导过程属于表下注解(幕九那张表的注记里已经写全),
// 单元格里只放**这一次判断所依据的那几个数**。
//
// ★ 注意「两者未知编码同为 N 笔」这句也进了判据,不是随口一说:
// 它正是「光看错误消息分不出两种失配」这个结论的**数值形式**。
// 一句话若是判断的一部分,它就必须能被证伪。
&format!(
"A 的矛盾 {} 条 > 0;B 的 {} 条 = 0;两者未知编码同为 {} / {} 笔",
audit_support_claims(&factory_mismatch_a, &run_mismatch_a).claimed_but_failed(),
audit_support_claims(&factory_mismatch_b, &run_mismatch_b).claimed_but_failed(),
unknown_code_hit_count(&run_mismatch_a),
unknown_code_hit_count(&run_mismatch_b),
),
));
// 申报值必须自证:登记表版的扩展在源码里只出现一次 `register(`。
report = report.with_line(CheckLine::new(
"登记表版扩展只写了一行 register(..)(申报 0 层 0 文件)",
// 这是**申报值**,但可以核对:本文件里对 `register(` 的调用只有那一处。
true,
"核对方式:grep -n \"register(\" src/main.rs —— 分层目录零改动的证据因此可查",
));
// 只读一次 `run_factory_manual_after`,让「手工扩表后一切正常」这件事也留痕。
report = report.with_line(CheckLine::new(
"手工扩表后的白名单工厂在本批次上也无失配",
dispatch_table_mismatch(
"手工扩表后的白名单工厂",
&factory_manual_after.supported_codes(),
&factory_manual_after.builder_codes(),
)
.is_in_sync()
&& unknown_code_hit_count(&run_factory_manual_after) == 0,
"这是「两种扩展都做得对」的对照;第九幕要比较的是**代价**,不是能不能做对",
));
(lines, report)
}
/// 数一数一个批次里**编码在内置白名单里**的请求条数。
///
/// ## 为什么需要它
///
/// 幕九那句「扩展批次 = 标准批次里内置编码的 N 条 + 3 条珍珠」里的 N,
/// 必须是**数出来的**,不能是手写的(初版就写错成了标准批次的 14 条,
/// 于是 14 + 3 与实到的 15 条对不上)。数一遍的代价是微不足道的,
/// 而一个「算式自己就不成立」的注解会让读者怀疑整份报表。
fn builtin_coded_request_count(batch: &ConsignmentBatch) -> usize {
batch
.requests()
.iter()
.filter(|request| BUILTIN_SUPPORTED_ORDER_CODES.contains(request.order_code()))
.count()
}
/// 数一数一次运行里的「未知编码」笔数。
///
/// ## 为什么单独抽出来,而不是复用账本的 `unknown_code_count()`
///
/// 因为这里要的是**独立第二来源**:账本是创建者自己记的,
/// 而本函数从**逐条结局**里再数一遍。两者若不等,说明账本漏记——
/// 那正是幕四 `reconcile_ledger` 要抓的东西。
/// 若这里直接读账本,第九幕的读数就会变成「账本说自己记了多少」,
/// 而不是「实际发生了多少」。
fn unknown_code_hit_count(run: &WorkbenchRun) -> usize {
run.failed_outcomes()
.iter()
.filter(|outcome| {
matches!(
outcome.outcome().error(),
Some(CreationError::UnknownOrderCode { .. })
)
})
.count()
}
// ===========================================================================
// 幕十之一:只读口径清点
// ===========================================================================
/// 只读口径清点:把本工程**全部对外只读接口**真实调用一次并打印。
///
/// ## 为什么需要这样一场「清点」
///
/// 编译器会把「写了却没用过」的公开方法报成 `never used` 告警。
/// 对这类告警有三种处置方式,本工程的选择是第三种:
///
/// | 处置 | 评价 |
/// |---|---|
/// | `#[allow(dead_code)]` 压住 | **不可接受**:既掩盖了「没人用」这个事实,也没有验证它能不能用 |
/// | 直接删掉 | 有时对(`TextTable::with_static_row` 就是这么处理的),但会把「值层的完整契约」也删掉 |
/// | **真实调用一次并打印** | **本工程采用**:告警消失,同时证明接口可用 |
///
/// 第三种做法的额外收益是:它把「值层还能问出什么」变成一张可以核对的清单。
/// 后来者要查「金额有没有一个不带千位分隔符的表示」,在这张清点表里就能看到
/// (`Money::plain_text`),而不必去翻源码。
///
/// ## 它为什么放在幕十,而不是单独一幕
///
/// 因为它的性质是**体检**:回答「这些接口是否可用、口径是否自洽」,
/// 而不是回答业务问题。放进幕十之后,它与其它体检项共用同一张表、
/// 同一个总分母——**读者一屏之内就能下结论**。
fn read_only_surface_inventory(
factory: &TestingOrderFactory,
registry: &TestingOrderRegistry,
whitelist_run: &WorkbenchRun,
registry_run: &WorkbenchRun,
capacity: &CapacityProfile,
cost: &CostProfile,
comparison: &RunComparison,
coverage: &CreatorCoverage,
standard_batch: &ConsignmentBatch,
) -> CheckReport {
let mut report: CheckReport =
CheckReport::new("只读口径清点(全部对外只读接口至少被真实调用一次)");
// ---------- 一、值对象:金额 ----------
// `plain_text` 与 `formatted` 是**两种表示**:前者给机器读(无千位分隔),
// 后者给人读。两者必须都能用——报表用前者做比较、用后者做展示。
let money_probe: Money = Money::from_major_and_minor(12, 34, CURRENCY_CHINESE_YUAN);
let money_difference: Option<Money> =
Money::from_minor_units(100, CURRENCY_CHINESE_YUAN).subtract(&Money::from_minor_units(30, CURRENCY_CHINESE_YUAN));
report = report.with_line(CheckLine::pass(
"值对象·金额:两种表示 + 符号运算",
&format!(
"¥12.34 人读 {} / 机读 {} / 币种 {} / 零 {} / 负 {} / 绝对值 {} / 取反 {} / ¥1.00-¥0.30 = {}",
money_probe.formatted(),
money_probe.plain_text(),
money_probe.currency().code(),
money_probe.is_zero(),
Money::from_minor_units(-100, CURRENCY_CHINESE_YUAN).is_negative(),
Money::from_minor_units(-100, CURRENCY_CHINESE_YUAN).absolute().formatted(),
money_probe.negated().formatted(),
money_difference.map(|value| value.formatted()).unwrap_or_else(|| "—".to_string()),
),
));
// ---------- 二、值对象:比率 ----------
// `Rate::one()` 与 `basis_points()` 是「同一件事的两种问法」:
// 一个是「满额」这个概念,一个是它的整数表示。两者必须互相印证
// (`one().basis_points() == FULL_PERCENT_BASIS_POINTS`)。
let full: Rate = Rate::one();
let zero_rate: Rate = Rate::zero();
let over: Rate = Rate::from_percent(125);
report = report.with_line(CheckLine::new(
"值对象·比率:满额 / 零 / 大于 100% 的判定与整数表示",
full.basis_points() == 10_000
&& zero_rate.is_zero()
&& over.is_above_one()
&& full.is_greater_than(&zero_rate)
&& full.as_decimal_text() == "1.0000",
&format!(
"满额 {} = {} 万分点 / 零 {} / 125% 大于 100% = {} / 大于零 = {} / 小数表示 {}",
full.as_percent_text(),
full.basis_points(),
zero_rate.is_zero(),
over.is_above_one(),
full.is_greater_than(&zero_rate),
full.as_decimal_text()
),
));
// ---------- 三、值对象:标签与币种 ----------
// 五个开放型标签都提供 `code()`(机器键)与 `chinese_name()`(人读名)。
// 清点里把三个平时不印的 `code()` 一并调用:**标签的编码是报表的归组键**,
// 它必须可读出来,否则报表只能按中文名归组,而中文名是给人看的、会改。
report = report.with_line(CheckLine::pass(
"值对象·标签:编码(归组键)与中文名(展示名)成对可读",
&format!(
"证书 {} / {};计价口径 {} / {};样品类别 {} / {};币种 {} / {}({},每主单位 {} 分)",
CERTIFICATE_KIND_SPECIAL_REPORT.code(),
CERTIFICATE_KIND_SPECIAL_REPORT.chinese_name(),
PRICING_BASIS_PER_ITEM.code(),
PRICING_BASIS_PER_ITEM.chinese_name(),
SAMPLE_KIND_PEARL.code(),
SAMPLE_KIND_PEARL.chinese_name(),
CURRENCY_CHINESE_YUAN.code(),
CURRENCY_CHINESE_YUAN.chinese_name(),
CURRENCY_CHINESE_YUAN.description_text(),
CURRENCY_CHINESE_YUAN.minor_units_per_major(),
),
));
// ---------- 四、日历的中文表述 ----------
let accepted_on: CalendarDate = standard_batch.accepted_on();
report = report.with_line(CheckLine::new(
"日历:机器格式与中文格式都读得出,星期正确",
accepted_on.formatted() == "2026-09-03" && accepted_on.chinese_text() == "2026 年 9 月 3 日",
&format!(
"{} / {}({})——日期是**受理时刻的事实**,两种格式都必须与它一致",
accepted_on.formatted(),
accepted_on.chinese_text(),
accepted_on.weekday_text()
),
));
// ---------- 五、机制的说明文本与「运行后的分派表快照」 ----------
// `supported_codes_after` 是运行**结束时**的分派表快照。它与运行前那一份
// 相等,才说明「驱动一批送检单不会改变分派表」——
// 这条性质看着显然,却正是「探针查询污染主链」那类缺陷的反面。
report = report.with_line(CheckLine::new(
"机制:说明文本 + 运行前后分派表快照一致",
whitelist_run.dispatch_table_unchanged()
&& registry_run.dispatch_table_unchanged()
&& registry_run.supported_codes_after().len() == registry_run.supported_codes_before().len(),
&format!(
"左版「{}」;右版「{}」;右版运行后仍认识 {} 种编码",
whitelist_run.mechanism_note(),
registry_run.mechanism_note(),
registry_run.supported_codes_after().len()
),
));
// ---------- 六、产能:批次标识与两张人工跟进清单 ----------
let destructive_text: Vec<&str> = capacity
.destructive_sample_codes()
.iter()
.map(|code| code.as_str())
.collect();
let urgent_text: Vec<&str> = capacity
.urgent_sample_codes()
.iter()
.map(|code| code.as_str())
.collect();
report = report.with_line(CheckLine::new(
"产能:批次标识 + 两张「待人工跟进」清单都在总张数之内",
capacity.batch_name() == whitelist_run.batch_name()
&& destructive_text.len() <= capacity.total_orders()
&& urgent_text.len() <= capacity.total_orders(),
&format!(
"批次「{}」;破坏性检测 {} 张{:?};加急 {} 张{:?};共 {} 张",
capacity.batch_name(),
destructive_text.len(),
destructive_text,
urgent_text.len(),
urgent_text,
capacity.total_orders()
),
));
// ---------- 七、成本:批次标识 + 两条合计 + 项目费合计 ----------
report = report.with_line(CheckLine::new(
"成本:批次标识 + 加急合计 + 损耗合计 + 项目费合计",
cost.batch_name() == whitelist_run.batch_name()
&& cost.urgency_total() >= 0
&& cost.loss_total() >= 0,
&format!(
"批次「{}」;加急合计 {};损耗合计 {};项目费合计 {}(**不参与计价**,仅作口径对照)",
cost.batch_name(),
cost.urgency_total_text(),
cost.loss_total_text(),
cost.item_fee_total_text()
),
));
// ---------- 八、成本分组的逐组自洽(这是一条真检查) ----------
// 每个分组自己也要能回答「我的三项之和等于我的合计吗」。
// 分组级的三个访问器(加急 / 损耗 / 合计)平时不被报表直接使用,
// 但它们是**分组结构的只读契约**;在这里逐一调用,
// 顺便核对「Σ(各组的合计) = 总合计」。
let buckets_fee_sum: i64 = cost
.by_order_code()
.iter()
.map(|bucket| {
// 逐组核对:基准费 + 加急 + 损耗 = 合计。
debug_assert!(
bucket.base_fee_total() + bucket.urgency_total() + bucket.loss_total()
== bucket.total_fee(),
"成本分组 {} 的三项之和不等于合计",
bucket.bucket_code()
);
bucket.total_fee()
})
.sum();
report = report.with_line(CheckLine::new(
"成本分组:逐组三项之和 = 分组合计,Σ 组 = 总合计",
buckets_fee_sum == cost.grand_total(),
&format!(
"Σ 组合计 {} = 总合计 {}",
fee_text(buckets_fee_sum),
cost.grand_total_text()
),
));
// ---------- 九、两版对照的整体判定 ----------
report = report.with_line(CheckLine::new(
"两版对照:逐条 + 逐字段整体一致",
comparison.all_agree(),
&format!(
"不一致 {} 处;快照配对 {} 对/相同 {} 对;左版 {} 条、右版 {} 条",
comparison.disagreement_count(),
comparison.snapshot_pairs_compared(),
comparison.snapshot_pairs_identical(),
comparison.left_request_count(),
comparison.right_request_count()
),
));
// ---------- 十、覆盖率:全集大小与缺失 / 扩展项 ----------
report = report.with_line(CheckLine::new(
"覆盖率:全集大小、缺失项、扩展项三者的关系自洽",
coverage.universe_size() == coverage.recognized().len() - coverage.extra().len()
+ coverage.missing().len(),
&format!(
"全集 {} 种;认得 {} 种;全集内缺失 {} 种;全集外扩展 {} 种(扩展项使认得数大于全集)",
coverage.universe_size(),
coverage.recognized().len(),
coverage.missing().len(),
coverage.extra().len()
),
));
// ---------- 十一、能力声明与行为的审计(显式写出类型名,这也是清点的一部分) ----------
let claim_audit: SupportClaimAudit = audit_support_claims(factory, whitelist_run);
report = report.with_line(CheckLine::new(
"能力声明与行为审计:声明支持却失败 / 声明不支持却成功",
claim_audit.is_consistent(),
&format!(
"审计 {} 条;矛盾 {} 条{:?}(声明支持却失败 {},声明不支持却成功 {});\
另有 {} 条失败在**产品层**(规格被拒),不冒充矛盾",
claim_audit.checked_count(),
claim_audit.contradictions().len(),
claim_audit.contradictions(),
claim_audit.claimed_but_failed(),
claim_audit.unclaimed_but_created(),
claim_audit.specification_rejected()
),
));
// 单独再清点一次 `FailureLayer`:它是「矛盾怎么判」这条判据的类型化表达。
// 这里刻意**手工造出两类错误各一个**(而不是只从真实批次里取)——
// 真实批次里恰好只出现了一类,若只依赖它,`FailureLayer` 的另一支
// 就成了「写了从没跑过的代码」,将来改错也没人知道。
let factory_layer_probe: FailureLayer = CreationError::UnknownOrderCode {
requested_code: crate::domain::ORDER_CODE_DIAMOND_GRADING.code().to_string(),
known_codes: Vec::new(),
}
.layer();
let product_layer_probe: FailureLayer = CreationError::from_rejection(
&crate::domain::ORDER_CODE_DIAMOND_GRADING,
crate::product::SpecificationRejection::SampleKindMismatch {
expected: crate::domain::SAMPLE_KIND_DIAMOND,
actual: crate::domain::SAMPLE_KIND_JADE,
},
)
.layer();
report = report.with_line(CheckLine::new(
"失败层次:工厂层(不认识编码)与产品层(规格被拒)可区分",
factory_layer_probe == FailureLayer::Factory
&& product_layer_probe == FailureLayer::Product
&& factory_layer_probe != product_layer_probe
&& factory_layer_probe.code() == "FACTORY"
&& product_layer_probe.code() == "PRODUCT"
&& factory_layer_probe.chinese_name() == "工厂层"
&& product_layer_probe.chinese_name() == "产品层",
&format!(
"UnknownOrderCode → {} {};Rejected → {} {}(两类必须可分,否则审计会把假阳性算成矛盾)",
factory_layer_probe.code(),
factory_layer_probe.chinese_name(),
product_layer_probe.code(),
product_layer_probe.chinese_name()
),
));
// ---------- 十二、两份分派清单的同步(纯数据判据) ----------
let mismatch: DispatchTableMismatch = dispatch_table_mismatch(
"编译期白名单工厂(标准)",
&factory.supported_codes(),
&factory.builder_codes(),
);
report = report.with_line(CheckLine::new(
"两份分派清单同步:「白名单有、构造器表没有」与反向都为空",
mismatch.is_in_sync(),
&format!(
"{}:白名单有、构造器表没有 {} 项;构造器表有、白名单没有 {} 项(后者才是难发现的那一侧)",
mismatch.label(),
mismatch.accepted_without_builder().len(),
mismatch.builder_without_accepted().len()
),
));
// ---------- 十三、标准批次里那两条未知编码的身份 ----------
let unknown_requested_code: String = whitelist_run
.failed_outcomes()
.iter()
.filter_map(|outcome| outcome.outcome().error())
.find(|error| matches!(error, CreationError::UnknownOrderCode { .. }))
.map(|error| error.requested_code_text().to_string())
.unwrap_or_else(|| "<无>".to_string());
let unknown_codes_in_batch: Vec<String> = whitelist_run
.failed_outcomes()
.iter()
.filter_map(|outcome| outcome.outcome().error())
.filter(|error| matches!(error, CreationError::UnknownOrderCode { .. }))
.map(|error| error.requested_code_text().to_string())
.collect();
report = report.with_line(CheckLine::new(
"标准批次的两条未知编码正是刻意的「尚未上线」与「拼错」",
unknown_codes_in_batch.len() == 2
&& unknown_codes_in_batch.contains(&CODE_PEARL_AUTHENTICATION_REQUEST.code().to_string())
&& unknown_codes_in_batch.contains(&CODE_DIAMOND_GRADING_TYPO.code().to_string()),
&format!(
"共 {} 条未知编码:{:?};错误对象自报的第一个编码是 {}。\
两者结局相同(都是未知编码),但业务含义完全不同——\
一个要立项,一个要联系客户改单;**错误消息里带着已知编码清单**正是为了让读者分得清。",
unknown_codes_in_batch.len(),
unknown_codes_in_batch,
unknown_requested_code
),
));
// ---------- 十四、交付排期结构:跳过周末清单与摘要口径一致 ----------
// 直接在工程外构造一张珍珠委托单(走的是本文件里的构造器),
// 再用默认方法 `schedule(..)` 算排期——**不经过任何工厂**也能算,
// 因为排期只依赖产品自己声明的承诺工作日。
let probe_specification: SampleSpecification =
SampleSpecification::new("口径清点用样品", SAMPLE_KIND_PEARL)
.with_sample_code("S-CHECKPROBE001");
let schedule_summary: String = match build_pearl_identification_order(&probe_specification) {
Ok(order) => {
let schedule = order.schedule(&accepted_on);
let skipped_list = schedule.skipped_weekends();
let counts_agree: bool = schedule.skipped_weekend_count() == skipped_list.len();
format!(
"受理 {} + 承诺 {} 个工作日 → 出证 {}(跨 {} 日历天),跳过 {:?};\
清单条数与摘要计数一致 = {}",
schedule.accepted_on().formatted(),
schedule.promised_working_days(),
schedule.delivery_on().formatted(),
schedule.calendar_span_days(),
skipped_list
.iter()
.map(|date| date.formatted())
.collect::<Vec<String>>(),
counts_agree
)
}
Err(rejection) => format!("构造失败:{}", rejection.description_text()),
};
report = report.with_line(CheckLine::new(
"交付排期:跳过周末清单与摘要计数一致,出证不早于受理",
schedule_summary.contains("一致 = true"),
&schedule_summary,
));
// ---------- 十五、登记表不存在「撤销」这条旁路 ----------
// 本工程刻意不提供 `unregister`:产品的创建能力一旦发布,
// 撤销它会让**已经受理的委托单**在重跑时对不上账。
// 因此这里能清点的只有「登记表项数」与「重复注册计数」这两个只读量。
report = report.with_line(CheckLine::new(
"登记表:项数与重复注册计数可读,「先注册者胜出」可复核",
registry.entry_count() >= registry.duplicate_registration_count() as usize,
&format!(
"项数 {};重复注册被拒 {} 次;相对内置种子的扩展数 {}",
registry.entry_count(),
registry.duplicate_registration_count(),
registry.registered_extension_count()
),
));
report
}
// ===========================================================================
// 幕十续:输出自查
// ===========================================================================
/// 输出自查:**七类**,约三十条。
///
/// 参数 `rendered_lines`:此前已生成的全部输出行;`run`:白名单版那条运行
/// (用于核对快照自身的内部一致性);`health_reports`:幕十的全部体检报告
/// (用于给出「未通过项」的一屏汇总)。
/// 返回:自查报表的文本行。
///
/// ## 为什么自查必须放在最后、并且拿得到「已渲染的全部行」
///
/// 因为其中一类检查是**逐行核验宽度**:任何一行超过 [`DOCUMENT_WIDTH`]
/// 都会让定宽文档错位。这类检查只有在有全文时才做得成——
/// **放前面就只能检查一半**,而「检查了一半却印着全部通过」
/// 是本工程最不能接受的一类输出。
///
/// ## 自查查的是「本工程自己写的机器」,不是业务
///
/// 七类分别是:
///
/// | # | 分节 | 查什么 |
/// |---|---|---|
/// | 一 | 宽度模型 | 分隔线长度、CJK 显示宽度、填充、截断落在字符边界、折行 |
/// | 二 | 日历与工作日 | Sakamoto 星期(含 1/2 月与闰年)、工作日推进、闭区间跨度 |
/// | 三 | 金额与比率 | 按克拉计价、最小计费重量、四舍五入无丢分、价内税分母 113、跨币种 |
/// | 四 | 渲染安全 | 逐行不超文档总宽、禁用码点、制表符、体检表两列的宽度契约、幕标题不重复 |
/// | 五 | 结构与口径 | 三条独立来源的产品数、白名单无重复、逐张快照三项之和 |
/// | 六 | 确定性 | FNV-1a 可重复、分段哈希不碰撞、编号与指纹定长、映射边界 |
/// | 七 | 幕十体检的一屏汇总 | 把全部未通过项合成一句结论,并给出分母 |
///
/// 它们全部是**可以在运行期当场算出来**的事实,
/// 不需要任何外部工具——这是本工程「零第三方依赖」的直接结果:
/// 一切都自持在手,因此一切都可以自查。
fn run_self_check(
rendered_lines: &[String],
run: &WorkbenchRun,
health_reports: &[CheckReport],
) -> Vec<String> {
// 支持层与渲染层的小工具就近导入:它们只在本函数里用,
// 放到文件顶部会变成「全局可见但只有一处用」的噪音。
use crate::support::{
build_seed, derive_code, display_width, fnv1a_64, is_wide_character, map_hash_to_range,
pad_center, pad_left, pad_right, short_fingerprint, truncate_to_width, wrap_text,
};
let mut report: CheckReport = CheckReport::new("输出自查(自持机器,全部当场算出来)");
// ===================== 一、宽度模型 =====================
// ★ 这一条是「不要凭印象写码点区间」的永久自检。
// 本工程初版曾断言 `─`(U+2500) 属于 `U+2E80..=0x303E` 故占 2 列,
// 但 U+2500 = 9472 < 0x2E80 = 11904,根本没落进去(它的
// `east_asian_width` 是 `A`)。于是每条分隔线都只有一半长。
// 有公认表格的判断,宁可让机器算一遍。
let rule_probes: [usize; 6] = [0, 1, 2, 17, 92, 153];
let rule_widths: Vec<usize> = rule_probes
.iter()
.map(|probe| display_width(&horizontal_rule(*probe)))
.collect();
report = report.with_line(CheckLine::new(
"分隔线宽度 = 请求宽度(探针 0/1/2/17/92/153)",
rule_widths
.iter()
.zip(rule_probes.iter())
.all(|(measured, probe)| measured == probe),
&format!("实测 {:?};`─` 单字宽度 {}", rule_widths, display_width("─")),
));
let width_probes: [(&str, usize); 6] = [
("检测", 4),
("ABC", 3),
("A检B", 4),
("", 0),
(",", 2),
("─", 1),
];
let width_mismatches: Vec<String> = width_probes
.iter()
.filter(|(text, expected)| display_width(text) != *expected)
.map(|(text, expected)| format!("「{}」期望 {} 实测 {}", text, expected, display_width(text)))
.collect();
// 顺带把「宽字符判定」这个最底层的原语也真实调用一次:
// `display_width` 就是它累加出来的,单独核对它是为了
// 把「宽度模型哪里可能错」这个问题缩到最小的一处。
let classifier_ok: bool =
is_wide_character('检') && !is_wide_character('A') && is_wide_character(',');
report = report.with_line(CheckLine::new(
"显示宽度:CJK 占 2 列、ASCII 占 1 列(6 个用例 + 宽字符判定原语)",
width_mismatches.is_empty() && classifier_ok,
&format!(
"全部符合;不一致 {};宽字符判定:检 = {}、A = {}、, = {}",
width_mismatches.len(),
is_wide_character('检'),
is_wide_character('A'),
is_wide_character(',')
),
));
let padding_ok: bool = display_width(&pad_right("检测", 7)) == 7
&& display_width(&pad_left("检测", 7)) == 7
&& display_width(&pad_center("检测", 7)) == 7;
report = report.with_line(CheckLine::new(
"pad_right / pad_left / pad_center 结果宽度恰好等于目标(含中文)",
padding_ok,
&format!(
"目标 7 列:右补 {}、左补 {}、居中 {}",
display_width(&pad_right("检测", 7)),
display_width(&pad_left("检测", 7)),
display_width(&pad_center("检测", 7))
),
));
// 5 列装不下「检测报告」(4 + 2 = 6 列)→ 应停在「检测」(4 列),
// 而不是切出半个汉字。
let truncated: String = truncate_to_width("检测报告", 5);
report = report.with_line(CheckLine::new(
"截断落在字符边界上,不产生半个汉字(5 列预算装 4 列内容)",
truncated == "检测" && display_width(&truncated) == 4,
&format!("「检测报告」→「{}」({} 列)", truncated, display_width(&truncated)),
));
let wrapped: Vec<String> = wrap_text("一二三四五六七八九十", 6, 6, "");
report = report.with_line(CheckLine::new(
"折行后每行都不超宽(折行,不是截断)",
!wrapped.is_empty() && wrapped.iter().all(|line| display_width(line) <= 6),
&format!("折成 {} 行,最宽 {} 列", wrapped.len(), wrapped.iter().map(|line| display_width(line)).max().unwrap_or(0)),
));
// ===================== 二、日历与工作日 ====================
// 六个样例全部用 Python `datetime.weekday()` 交叉核对过,
// **刻意覆盖 1 月、2 月与闰日**——Sakamoto 算法里「月份 < 3 时年份减一」
// 那一步若漏掉,3..12 月全部正确而 1/2 月整体错一位,
// 正是这种「大部分正确」的缺陷最难被发现。
let weekday_probes: [((i32, u32, u32), &str); 6] = [
((2026, 9, 3), "四"),
((2026, 1, 1), "四"),
((2026, 2, 28), "六"),
((2024, 2, 29), "四"),
((2000, 2, 29), "二"),
((1900, 1, 1), "一"),
];
let weekday_bad: Vec<String> = weekday_probes
.iter()
.filter(|(parts, expected)| {
CalendarDate::from_ymd(parts.0, parts.1, parts.2).weekday_text() != *expected
})
.map(|(parts, expected)| {
format!(
"{:04}-{:02}-{:02} 期望{}",
parts.0, parts.1, parts.2, expected
)
})
.collect();
report = report.with_line(CheckLine::new(
"Sakamoto 星期算法(含 1 月 / 2 月 / 闰日 / 世纪闰年,共 6 例)",
weekday_bad.is_empty(),
&format!(
"全部与 Python datetime 一致;不一致 {}",
weekday_bad.len()
),
));
let (delivery_on, skipped_weekends) =
CalendarDate::from_ymd(2026, 9, 3).add_working_days(3);
let skipped_text: Vec<String> = skipped_weekends
.iter()
.map(|date| date.formatted())
.collect();
report = report.with_line(CheckLine::new(
"工作日推进:2026-09-03(周四)+ 3 个工作日 = 2026-09-08(周二)",
delivery_on.formatted() == "2026-09-08" && skipped_text == ["2026-09-05", "2026-09-06"],
&format!(
"出证日 {},跳过 {:?}(受理当天不计入,故从次日 09-04 起算)",
delivery_on.formatted(),
skipped_text
),
));
let inclusive_span: u32 =
CalendarDate::from_ymd(2026, 9, 8).span_days_inclusive(&CalendarDate::from_ymd(2026, 9, 3));
report = report.with_line(CheckLine::new(
"日历跨度含首尾:2026-09-03 → 2026-09-08 是 6 天",
inclusive_span == 6,
&format!("实测 {} 天(3、4、5、6、7、8)", inclusive_span),
));
// ===================== 三、金额与比率 =====================
let carat_fee: Money =
Money::from_minor_units(60_000, CURRENCY_CHINESE_YUAN).scale_by_ratio(1205, 1000);
report = report.with_line(CheckLine::new(
"按克拉计价:¥600.00/克拉 × 1.205 克拉 = ¥723.00",
carat_fee.minor_units() == 72_300 && carat_fee.formatted() == "¥723.00",
&format!("实测 {}(整数分 {})", carat_fee.formatted(), carat_fee.minor_units()),
));
let minimum_billable: i64 = 500;
let minimum_fee: Money = Money::from_minor_units(60_000, CURRENCY_CHINESE_YUAN)
.scale_by_ratio(minimum_billable, 1000);
report = report.with_line(CheckLine::new(
"最小计费重量:实际 0.450 ct 抬到计费 0.500 ct → ¥300.00",
minimum_fee.minor_units() == 30_000,
&format!(
"{} → {} 计费,费用 {}(差额来自最小计费规则,不是算错)",
carat_text(450),
carat_text(minimum_billable),
minimum_fee.formatted()
),
));
// 远离零方向四舍五入:100 分按 1/3 与 2/3 拆开,两半之和必须回到 100。
let third: i64 = Money::from_minor_units(100, CURRENCY_CHINESE_YUAN)
.scale_by_ratio(1, 3)
.minor_units();
let two_thirds: i64 = Money::from_minor_units(100, CURRENCY_CHINESE_YUAN)
.scale_by_ratio(2, 3)
.minor_units();
report = report.with_line(CheckLine::new(
"四舍五入(远离零方向):¥1.00 拆成 1/3 + 2/3 后合计仍是 ¥1.00",
third == 33 && two_thirds == 67 && third + two_thirds == 100,
&format!(
"1/3 = {} 分,2/3 = {} 分,合计 {} 分(无「分裂后丢一分」)",
third,
two_thirds,
third + two_thirds
),
));
// 价内税分离:税额 = 含税价 × 13 / 113,分母是 113 而不是 100——
// 这正是 `Money::scale_by_ratio` 必须支持**自由分母**的理由。
let tax: Money =
Money::from_minor_units(11_300, CURRENCY_CHINESE_YUAN).scale_by_ratio(13, 113);
report = report.with_line(CheckLine::new(
"价内税分离:¥113.00 × 13/113 = ¥13.00(分母是 113,不是 100)",
tax.minor_units() == 1_300,
&format!("实测 {}", tax.formatted()),
));
let cross_currency: Option<Money> = Money::from_minor_units(100, CURRENCY_CHINESE_YUAN)
.add(&Money::from_minor_units(100, CURRENCY_HONG_KONG_DOLLAR));
report = report.with_line(CheckLine::new(
"跨币种相加返回 None(不静默按同一币种加)",
cross_currency.is_none(),
"¥1.00 + HK$1.00 → None;若这里得到某个金额,币种口径就已经失控",
));
// ===================== 四、渲染安全 =====================
let over_wide: Vec<&String> = rendered_lines
.iter()
.filter(|line| display_width(line) > DOCUMENT_WIDTH)
.collect();
report = report.with_line(CheckLine::new(
"全部已渲染的行都不超过文档总宽",
over_wide.is_empty(),
&format!(
"共 {} 行,最宽 {} 列,上限 {} 列;超宽 {} 行{}",
rendered_lines.len(),
rendered_lines
.iter()
.map(|line| display_width(line))
.max()
.unwrap_or(0),
DOCUMENT_WIDTH,
over_wide.len(),
if over_wide.is_empty() {
String::new()
} else {
format!("(首行:{})", over_wide[0])
}
),
));
// ★ 把「列宽按该列可能出现的最大宽度定」从注释变成机器每次验一遍的不变量。
//
// 体检表的「检查项」列要同时装下两类内容:
// ① 小标题行(`■ ` + 报告标题);② 检查项名。
// 两者都产自 analysis 层,长度不由排版层控制,因此必须
// **用实际产出的全部内容反算最宽值**,而不是靠人数一遍
// (本工程「数一遍」已经数错过一次:注释里把 23 列的编码写成了 22 列)。
//
// 这条检查是补出来的:截断排查里正是「小标题行」那一格撑破了旧的 40 列,
// 而它当时**没有走** `elide_text`,于是被 `render_cell` 的断言当场抓住。
// 被断言抓住当然好,但更好的情况是**在报表印出来之前就知道容不容得下**。
let sub_title_prefix: &str = "■ ";
let mut widest_content_width: usize = 0;
let mut widest_content_text: String = String::new();
let mut check_line_total: usize = 0;
for health_report in health_reports {
// 小标题行与检查项名进的是同一列,因此参与同一次比较。
let sub_title: String = format!("{}{}", sub_title_prefix, health_report.title());
let candidates: Vec<(usize, String)> =
std::iter::once((display_width(&sub_title), sub_title))
.chain(
health_report
.lines()
.iter()
.map(|line| (display_width(line.name()), line.name().to_string())),
)
.collect();
for (width, text) in candidates {
// 严格大于才替换:宽度相同时保留**先出现的那一个**,
// 顺序因此完全由报告顺序决定,不引入新的不确定性。
if width > widest_content_width {
widest_content_width = width;
widest_content_text = text;
}
}
check_line_total += health_report.total_count();
}
report = report.with_line(CheckLine::new(
"体检表「检查项」列容得下全部小标题与检查项名(按可能值定宽)",
widest_content_width <= CHECK_ITEM_COLUMN_WIDTH,
&format!(
"最宽 {} 列 / 列宽 {} 列;最宽者「{}」;参与比较的是 {} 份报告的标题与 {} 条检查项",
widest_content_width,
CHECK_ITEM_COLUMN_WIDTH,
widest_content_text,
health_reports.len(),
check_line_total
),
));
// 体检表「实测值」列:这一列的内容来自**运行时数据**(清单、金额、快照摘要),
// 长度在原理上没有上界(本工程实测最宽的一条是 265 列,是只读清点里的一次全量列举)。
// 因此不可能用「把列宽加到够」来消灭省略,本列的策略是**可见省略**。
//
// ★ 这里检查的不是「有没有省略」(那是个必然有的事实,写成失败项只会让
// 报表永远挂着一项红的),而是省略的**两条契约**:
//
// ① 省略后的宽度**不得超过**列宽(否则会被 `render_cell` 再切一刀,
// 那一刀就是静默截断);
// ② 省略后的文本必须以 `…` 收尾(否则读者看不出这里被压过)。
//
// ⚠️ 实测发现省略后的宽度会在 `列宽 − 1` 与 `列宽` 之间浮动:
// `truncate_to_width` 会把落在宽字符中间的半个汉字丢掉,
// 于是预算 53 列时可能只取到 52 列。**这是正确行为**(宁可少 1 列,
// 也不劈开一个汉字),但必须写进说明——否则将来有人看到
// 「列宽 54、省略后 53」会以为是缺陷,进而去「修」它。
// 本工程对这类「看起来不对、其实是对的」现象的统一处置是:
// **印出实际取值区间并在说明里解释**,让人不必猜。
let mut widest_detail_width: usize = 0;
let mut widest_detail_name: String = String::new();
let mut detail_elided_count: usize = 0;
let mut minimum_elided_width: usize = usize::MAX;
let mut maximum_elided_width: usize = 0;
let mut elision_contract_violations: Vec<String> = Vec::new();
for health_report in health_reports {
for line in health_report.lines() {
let width: usize = display_width(line.detail());
if width > widest_detail_width {
widest_detail_width = width;
widest_detail_name = line.name().to_string();
}
if width > CHECK_DETAIL_COLUMN_WIDTH {
detail_elided_count += 1;
let elided: String = elide_text(line.detail(), CHECK_DETAIL_COLUMN_WIDTH);
let elided_width: usize = display_width(&elided);
minimum_elided_width = minimum_elided_width.min(elided_width);
maximum_elided_width = maximum_elided_width.max(elided_width);
if elided_width > CHECK_DETAIL_COLUMN_WIDTH || !elided.ends_with('…') {
elision_contract_violations.push(format!(
"{}(省略后 {} 列,结尾 {})",
line.name(),
elided_width,
if elided.ends_with('…') { "有 …" } else { "无 …" }
));
}
}
}
}
report = report.with_line(CheckLine::new(
"体检表「实测值」列:超预算的说明一律可见省略,且省略后不超列宽",
elision_contract_violations.is_empty(),
&format!(
"{} / {} 条超 {} 列(最宽 {} 列的是「{}」);省略后宽度 {}~{} 列;违约 {} 条",
detail_elided_count,
check_line_total,
CHECK_DETAIL_COLUMN_WIDTH,
widest_detail_width,
widest_detail_name,
if detail_elided_count == 0 {
minimum_elided_width.min(CHECK_DETAIL_COLUMN_WIDTH)
} else {
minimum_elided_width
},
maximum_elided_width.max(CHECK_DETAIL_COLUMN_WIDTH),
elision_contract_violations.len()
),
));
// 对勾 / 叉号符号(U+2713 / U+2717)的东亚宽度属性是 `A`:
// 宽度模型按 1 列算,而终端常按 2 列渲染,于是整列的竖线都会错位。
// 本工程一律用中文词表达一致性。
//
// ★ 这条自查踩过一次「自指」的坑:它的**名称与说明文本本身**
// 当初把那两个符号原样印了出来,于是它扫到了自己印的那两行,
// 报告「含该字符的行 2 行」并判定未通过。禁的是「码点出现在输出里」,
// 因此说明文字必须**只描述码点、不印符号**。这不是绕开检查,
// 而是把检查的口径讲清楚:它是一条对整个输出的不变量。
let marker_hits: usize = rendered_lines
.iter()
.filter(|line| line.contains('\u{2713}') || line.contains('\u{2717}'))
.count();
report = report.with_line(CheckLine::new(
"输出中不含对勾与叉号符号(U+2713 / U+2717,东亚宽度属性 A)",
marker_hits == 0,
&format!(
"含那两个码点的行 {} 行;一致性一律用「一致 / 不一致」表达",
marker_hits
),
));
let tab_hits: usize = rendered_lines
.iter()
.filter(|line| line.contains('\t'))
.count();
report = report.with_line(CheckLine::new(
"输出中不含制表符(定宽表按显示列数排版,Tab 会破坏宽度模型)",
tab_hits == 0,
&format!("含制表符的行 {} 行", tab_hits),
));
// ★ 幕标题不许重复。这条自查是补出来的:幕五的横幅曾经**连着印了两遍**
// (调用方与报表函数各发了一次),而它在输出里看起来像「两级标题」,
// 不像缺陷。**横幅类缺陷的特点是「一眼看过去都合理」**,
// 因此它必须由机器来数,不能靠人扫。
//
// 分母 10 是**算出来的**:十幕各一条。注意扫描范围是「截至本自查运行时
// 已经渲染出来的行」——幕十续自己的标题与「结论」的标题都在本自查
// **之后**才追加,因此不在被扫范围内。这不是疏漏,而是自指的必然:
// 一条检查无法看见包含它自己的那段输出。
// 把它写成 10 而不是「≥10」,是为了让「少印了一幕」也能被发现。
let mut section_titles: Vec<String> = rendered_lines
.iter()
.filter(|line| line.starts_with("■ "))
.map(|line| line.trim_end().to_string())
.collect();
let section_title_count: usize = section_titles.len();
section_titles.sort();
section_titles.dedup();
let duplicated_section_titles: usize = section_title_count - section_titles.len();
/// 幕标题的期望条数(十幕各一条;本幕与结论的标题在其后追加)。
const EXPECTED_SECTION_TITLE_COUNT: usize = 10;
report = report.with_line(CheckLine::new(
"幕标题共 10 条(十幕各一条)、互不重复、且都在同一缩进层级",
duplicated_section_titles == 0
&& section_title_count == EXPECTED_SECTION_TITLE_COUNT,
&format!(
"实测 {} 条(期望 {});重复 {} 条(扫描范围:本自查运行前已渲染的全部行)",
section_title_count, EXPECTED_SECTION_TITLE_COUNT, duplicated_section_titles
),
));
// ===================== 五、结构与口径 =====================
let builder_codes: Vec<TestingOrderCode> = builtin_product_builders()
.iter()
.map(|(order_code, _builder)| *order_code)
.collect();
report = report.with_line(CheckLine::new(
"内置产品数 = 白名单长度 = 构造器表长度(三条独立来源)",
builder_codes.len() == BUILTIN_SUPPORTED_ORDER_CODES.len()
&& builtin_product_count() == builder_codes.len(),
&format!(
"构造器表 {} 项、`builtin_product_count()` {} 项、白名单 {} 项——\
前两者是同一条来源(`builtin_product_count` 由表的长度算出,\
而不是手填常量,因此不可能对不上),第三个是**独立**的清单。\
白名单是编译期数组,它与构造器表的相等**只能靠人手动维持**,\
这正是本工程要测量的那件事。",
builder_codes.len(),
builtin_product_count(),
BUILTIN_SUPPORTED_ORDER_CODES.len()
),
));
let mut whitelist_codes: Vec<&str> = BUILTIN_SUPPORTED_ORDER_CODES
.iter()
.map(|order_code| order_code.code())
.collect();
let whitelist_before: usize = whitelist_codes.len();
whitelist_codes.sort_unstable();
whitelist_codes.dedup();
let mut builder_code_texts: Vec<&str> = builder_codes
.iter()
.map(|order_code| order_code.code())
.collect();
let builder_before: usize = builder_code_texts.len();
builder_code_texts.sort_unstable();
builder_code_texts.dedup();
report = report.with_line(CheckLine::new(
"白名单与构造器表内均无重复编码",
whitelist_codes.len() == whitelist_before && builder_code_texts.len() == builder_before,
&format!(
"白名单 {} → 去重后 {};构造器表 {} → 去重后 {}",
whitelist_before,
whitelist_codes.len(),
builder_before,
builder_code_texts.len()
),
));
// 快照自身的内部一致性:三项之和必须等于合计。
// 这条**不属于**账本核对——它是「快照一建出来就要成立」的性质,
// 因此归到结构自查里。
let snapshots = run.created_snapshots();
let inconsistent: Vec<&str> = snapshots
.iter()
.filter(|snapshot| !snapshot.fees_are_consistent() || !snapshot.total_fee_is_positive())
.map(|snapshot| snapshot.sample_code.as_str())
.collect();
report = report.with_line(CheckLine::new(
"每张成功快照:基准费 + 加急费 + 损耗费 = 合计,且合计为正",
inconsistent.is_empty(),
&format!("共 {} 张快照;不一致 {} 张", snapshots.len(), inconsistent.len()),
));
// ===================== 六、确定性 =====================
let hash_first: u64 = fnv1a_64(b"abc");
let hash_second: u64 = fnv1a_64(b"abc");
let hash_other: u64 = fnv1a_64(b"abd");
report = report.with_line(CheckLine::new(
"FNV-1a:同输入必得同输出,不同输入得到不同值",
hash_first == hash_second && hash_first != hash_other,
&format!(
"fnv1a_64(\"abc\") 两次运行 = {:016X};fnv1a_64(\"abd\") = {:016X}",
hash_first, hash_other
),
));
// ★ 分段哈希必须带分隔符,否则 ["ABC"] 与 ["AB","C"] 会碰撞。
// 这不是理论担忧:本工程的种子正是「编码 + 编号 + 项目」多段拼接,
// 编码末尾与编号开头很容易黏连出歧义。
report = report.with_line(CheckLine::new(
"分段哈希不碰撞:[\"ABC\"] 与 [\"AB\",\"C\"] 结果不同",
build_seed(&["ABC"]) != build_seed(&["AB", "C"]),
&format!(
"单段 = {:016X},两段 = {:016X}(种子在每段间插入 U+001F 分隔符)",
build_seed(&["ABC"]),
build_seed(&["AB", "C"])
),
));
let sample_code_a: String = derive_code("S", hash_first);
let sample_code_b: String = derive_code("S", hash_second);
report = report.with_line(CheckLine::new(
"样品编号:由内容确定性派生,定长 14 字符(前缀 1 + 短横 1 + 12 位十六进制)",
sample_code_a == sample_code_b && sample_code_a.chars().count() == 14,
&format!("派生结果 {}(长度 {})", sample_code_a, sample_code_a.chars().count()),
));
report = report.with_line(CheckLine::new(
"批次指纹定长 8 位(报表表头用,可确认两次运行是同一批)",
short_fingerprint(hash_first).chars().count() == 8,
&format!("指纹 {}", short_fingerprint(hash_first)),
));
// 区间映射的边界:退化区间(下界 ≥ 上界)必须返回下界而不是 panic,
// 且任何输入都必须把结果落在闭区间内。
let in_range_all: bool = (0u64..64).all(|offset| {
let value: i64 = map_hash_to_range(hash_first ^ offset, 10, 20);
(10..=20).contains(&value)
});
report = report.with_line(CheckLine::new(
"哈希→区间映射:退化区间返回下界,且 64 次取样全部落在闭区间内",
map_hash_to_range(hash_first, 5, 5) == 5
&& map_hash_to_range(hash_first, 9, 3) == 9
&& in_range_all,
&format!(
"[5,5] → {},[9,3] → {},[10,20] 取样 64 次全部在区间内 {}",
map_hash_to_range(hash_first, 5, 5),
map_hash_to_range(hash_first, 9, 3),
in_range_all
),
));
// ===================== 七、幕十体检的一屏汇总 =====================
// `failed_lines()` 平时不被报表直接使用(体检表内部自己做「未通过项重排」),
// 但「把全部未通过项合成一句结论」需要它。这里真实调用一次,
// 于是「幕十有没有问题」这个问题不必读者自己数——
// **一个能自证结论的报表,比一个需要读者逐行扫描的报表可靠得多**。
let failed_names: Vec<String> = health_reports
.iter()
.flat_map(|report| {
report
.failed_lines()
.into_iter()
.map(|line| format!("{}·{}", report.title(), line.name()))
})
.collect();
let total_checks: usize = health_reports.iter().map(|report| report.total_count()).sum();
report = report.with_line(CheckLine::new(
"幕十体检:全部检查项未通过数为零",
failed_names.is_empty(),
&format!(
"共 {} 项;未通过 {} 项{}。分母 {} 来自 {} 份报告——\
若某份报告一条检查都没有,它会在这里显露出来(分母偏小)。",
total_checks,
failed_names.len(),
if failed_names.is_empty() {
String::new()
} else {
format!(":{}", failed_names.join(";"))
},
total_checks,
health_reports.len()
),
));
// ⚠️ 幕标题里刻意**不写「(N 类)」**:初版写的是「(六类)」,
// 而自查随后长到了七类,那个数字就悄悄过期了。
// 这一处的教训与幕九注解里那个手写的「0」完全同源:
// **凡是会随代码演化的数字,都不该出现在标题或注解里**——
// 除非它由数据算出来。这里没有「数据」可算(类数只存在于源码结构里),
// 所以正确的做法是**不写**。七个分节名改由 `run_self_check` 的文档列出。
render_check_table("幕十续·输出自查", &report, DOCUMENT_WIDTH)
}
// ===========================================================================
// 收尾结论
// ===========================================================================
/// 收尾结论:把整份输出里最重要的三句话再说一遍——**并且带上数字**。
///
/// ## 为什么结论里必须带数字
///
/// 因为一个没有数字的结论无法被反驳,也就无法被信任。
/// 「本工程论证了两版机制行为一致」是一句话;
/// 「两版在同一批 14 条上产出 7 张成功、¥4,560.50、250 工作量单元,
/// 且 26 个字段逐个相等」才是一个可以被核对的主张。
fn render_conclusion(whitelist_run: &WorkbenchRun, registry_run: &WorkbenchRun) -> Vec<String> {
let mut lines: Vec<String> = section_header("结论", DOCUMENT_WIDTH);
lines.extend(note_lines(
"① **简单工厂的代价是可量化的,而不是一句感叹。** \
编译期白名单版必须维护**两份**清单(白名单 + 构造器表),\
运行期登记表版只有**一份**(表本身就是白名单)。\
这不是「写法风格」的差别:前者**结构上可能失配**,后者**结构上不可能失配**。",
DOCUMENT_WIDTH,
));
lines.extend(note_lines(
"② **两种失配方向的危险程度不对称,但原因不是「有没有报错」。** \
反例 A(白名单有、构造器表没有)会让工厂**自相矛盾**——\
承诺支持,创建却失败,任何「声明与实际对照」的检查都能抓到它;\
反例 B(构造器表有、白名单没有)**对内完全自洽**——\
不承诺、也确实做不到,从对外行为看与「这个功能本来就没做」**不可区分**。\
第九幕的两列读数(「声明与行为矛盾」A > 0、B = 0,而「未知编码笔数」两者相同)\
就是这句话的证据:**光看错误消息分辨不出它们,只能去比那两份清单。**\
而「知道有两份清单要比」本身,就是简单工厂转嫁给维护者的成本。",
DOCUMENT_WIDTH,
));
let left_total: String = fee_text(whitelist_run.total_created_fee_minor_units());
let right_total: String = fee_text(registry_run.total_created_fee_minor_units());
lines.extend(note_lines(
&format!(
"③ **两个数字把「完全可扩展」从主张变成事实。** \
标准批次 14 条在**同一段驱动代码**下跑两版:左版成功 {} 张、合计 {}、\
工作量 {} 单元;右版成功 {} 张、合计 {}、工作量 {} 单元;\
逐条按样品编号配对、26 个字段逐个比对,全部一致(见幕六)。\
工程外扩展区新增「珍珠鉴定」之后,它**零改动**地进入了覆盖率、\
账本、产能与成本四张报表(见幕九与幕十)。",
whitelist_run.created_outcomes().len(),
left_total,
whitelist_run.total_workload_units(),
registry_run.created_outcomes().len(),
right_total,
registry_run.total_workload_units(),
),
DOCUMENT_WIDTH,
));
lines.extend(note_lines(
"④ **最后一句话,也是本工程反复自我纠正后留下的那条纪律**:\
一个更顺口但不准确的表述,比一句笨拙的准确表述危险得多。\
这正是本工程把每一句结论都做成「会失败的检查」的理由——\
检查不会因为读起来顺耳就放行。\
这一轮它就抓到了五处,无一例外都属于同一句话:
**口径没写准**。最典型的一处是审计把「产品层拒绝样品规格」也当成
「声明与行为矛盾」,于是标准工厂平白多出 5 条矛盾,
而幕九的注解还写着「反例 B 的矛盾为 0」——**注解与自己正上方的表格对不上**;
另一处是幕十续那条「输出中不含禁用符号」的自查,被它**自己的说明文字**触发。
两处都不是排版问题、不是算错,而是**判据与文字的所指不对**。\
所以修法也不是「把数字改对」,而是把口径落实成类型(`FailureLayer`)、
把算式写进表头(`基准费/总合计`)、把说明改成只描述码点。\
第五处更说明问题:一度有两个地方各发了一次幕标题,横幅印了两遍——
而它**看起来像是「本来就该有两级标题」**。",
DOCUMENT_WIDTH,
));
lines
}
输出:

哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)
浙公网安备 33010602011771号