调用:
rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:备忘录模式Memento 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/15 4:57
//!# User : geovindu
//!# Product : RustRover
//!# Project : mementopattern
//!# File : main.rs
//! # 备忘录模式(Memento Pattern)· 连锁珠宝门店「整单折扣改价审批 + 报价历史回滚」
//!
//! 业务域:一张销售单(聚合根)在成交前会被反复议价,每一次改价都要**过审批权限**,
//! 每一次被批准的改价都要**留下一个可以回到的版本**。
//! 本工程用备忘录模式把「报价的每一个历史版本」交给一个**读不懂它**的管理者持有。
//!
//! ## 严格分层结构(**实测**,由 `scripts/check_layer_dependencies.sh` 产出,不是设计意图)
//!
//! ```text
//! main
//! ├─> app ─> analysis ─> client ─> domain
//! │ │ │ └─> memento
//! │ │ ├─> domain
//! │ │ └─> memento
//! │ ├─> domain
//! │ └─> support
//! ├─> analysis
//! ├─> client
//! ├─> domain
//! ├─> memento
//! └─> support
//! ```
//!
//! 实测邻接表(`bash scripts/check_layer_dependencies.sh src`,无环):
//!
//! ```text
//! app -> analysis domain support (+ 层内 super:: 自引用 x1,合法)
//! analysis -> client domain memento
//! client -> domain memento
//! domain -> (none) (+ 层内 super:: 自引用 x1,合法)
//! memento -> (none)
//! support -> (none)
//! ```
//!
//! ⚠️★ 这张图里有两处**与直觉不符**,而它们都是实测出来的,不是笔误:
//!
//! 1. `domain` 与 `support` **之间没有边**:本工程的领域层不需要任何基础工具
//! (金额是整数分、比率是整数万分点,指纹由领域层自己拼)。
//! `support` 目前只被 `app` 与 `main` 用到------它是「排版与显示」的基础层,
//! 不是「什么都往里塞」的工具箱。
//! 2. `main` 是唯一同时认识六层的地方,这正是**装配层**的定义;
//! 其余每一层都只往下看一层或两层。
//!
//! ## ★★ 本工程最关键的一条分层决策:`domain` 与 `memento` **互不认识**
//!
//! 直觉上「可回滚的东西」(`SalesOrder`)就该实现机制层的 `Originator` trait。
//! 那样会形成 `domain -> memento` 依赖,而真正的代价是:
//! **领域层会知道自己正在被回滚**------于是「换一种历史管理方式(例如改成数据库归档)」
//! 就要改业务代码,而「换历史管理方式」与「珠宝生意」毫无关系。
//!
//! 因此本工程的做法是:
//! - `domain` 只提供两个与机制无关的动作:`capture()`(拍快照)与 `apply()`(装回,带拒绝);
//! - `client::QuotationOriginator` 把这两个动作**适配**成机制层的 `Originator` trait;
//! - `impl Memento for QuotationMemento` 也写在 `client`------那一行 `impl` 就是接缝。
//!
//! 这条判据与迭代器工程「领域不知道自己在被遍历」是同一条。
//! 它的证据可实测:`domain` 的依赖图里没有 `memento`,反之亦然,
//! 两者都只出现在 `client` 的依赖图里。
//!
//! ## DDD 战术角色在本工程的落点
//!
//! | 战术角色 | 本工程类型 |
//! |---|---|
//! | 聚合根 | `domain::SalesOrder` |
//! | 聚合内实体 | `domain::OrderLine` |
//! | 值对象 | `Money` / `StoreCode` / `CategoryCode` / `CurrencyCode` / `ApprovalLevel` / `RollbackCheck` |
//! | 领域服务 | `domain::QuotationPolicy`(权限表 + 六关顺序校验 + 裁决并生效) |
//! | 仓储(接口) | `domain::SalesOrderRepository`(实现在 `client`) |
//! | Memento(快照) | `domain::QuotationMemento` |
//! | Originator(适配器) | `client::QuotationOriginator` |
//! | Caretaker(管理者) | `memento::History` + `client::RollbackSession` |
//!
//! ## 运行
//!
//! ```text
//! cargo run
//! ```
//!
//! 输出十四幕报表;两次运行逐字节一致(遍历一律收成 `Vec`,不用 `HashMap`)。
mod analysis;
mod app;
mod client;
mod domain;
mod memento;
mod support;
use crate::analysis::AuditFalsifiableRow;
use crate::analysis::CheckLine;
use crate::analysis::CheckReport;
use crate::analysis::ExtensionRow;
use crate::analysis::InterfaceRow;
use crate::analysis::RejectionProbeRow;
use crate::analysis::RollbackStepRow;
use crate::analysis::audit_falsifiable_lines;
use crate::analysis::audit_falsifiable_rows;
use crate::analysis::audit_findings_to_check_lines;
use crate::analysis::audit_tally;
use crate::analysis::build_consistency_rows;
use crate::analysis::build_encapsulation_rows;
use crate::analysis::build_history_timeline_rows;
use crate::analysis::build_ledger_rows;
use crate::analysis::build_reprice_flow_rows;
use crate::analysis::build_simulation_rows;
use crate::analysis::capture_rollback_step;
use crate::analysis::check_ledger_expectations;
use crate::analysis::compute_rollback_statistics;
use crate::analysis::encapsulation_cost_bytes;
use crate::analysis::is_experiment_demonstrated;
use crate::analysis::summarize_consistency;
use crate::analysis::summarize_extension_rows;
use crate::analysis::summarize_history_timeline;
use crate::analysis::summarize_interface_rows;
use crate::analysis::summarize_rejection_probes;
use crate::analysis::summarize_reprice_flow;
use crate::analysis::summarize_rollback_steps;
use crate::analysis::summarize_simulation;
use crate::analysis::verdict_text;
use crate::app::ColumnAlign;
use crate::app::DOCUMENT_WIDTH;
use crate::app::Table;
use crate::app::TableColumn;
use crate::app::basis_points_text;
use crate::app::bool_text;
use crate::app::byte_text;
use crate::app::failed_items_text;
use crate::app::gram_display_text;
use crate::app::health_verdict_text;
use crate::app::items_of;
use crate::app::note_lines;
use crate::app::percent_text;
use crate::app::render_authority_scene;
use crate::app::render_cell;
use crate::app::render_consistency_scene;
use crate::app::render_encapsulation_scene;
use crate::app::render_extension_scene;
use crate::app::render_falsifiable_scene;
use crate::app::render_health_check_scene;
use crate::app::render_interface_scene;
use crate::app::render_ledger_scene;
use crate::app::render_mechanism_audit_scene;
use crate::app::render_mechanism_falsifiable_scene;
use crate::app::render_order_line_scene;
use crate::app::render_order_scene;
use crate::app::render_rejection_scene;
use crate::app::render_reprice_flow_scene;
use crate::app::render_rollback_step_scene;
use crate::app::render_simulation_scene;
use crate::app::render_timeline_scene;
use crate::app::section_header;
use crate::app::signed_gram_display_text;
use crate::app::table_rule;
use crate::client::BUILTIN_ACTOR_LEVELS;
use crate::client::BUILTIN_DISCOUNT_AUTHORITIES;
use crate::client::DEMO_CATEGORIES;
use crate::client::DEMO_CURRENCIES;
use crate::client::DEMO_STORES;
use crate::client::InMemorySalesOrderRepository;
use crate::client::QuotationOriginator;
use crate::client::RepriceLog;
use crate::client::RepriceLogEntry;
use crate::client::RepriceSimulationEntry;
use crate::client::RepriceUseCase;
use crate::client::RollbackSession;
use crate::client::WideQuotationMemento;
use crate::client::builtin_quotation_policy;
use crate::client::demo_orders;
use crate::client::order_cn_first;
use crate::client::order_cn_second;
use crate::client::order_hk_first;
use crate::client::run_wide_memento_experiment;
use crate::client::same_currency_batch;
use crate::domain::ApprovalLevel;
use crate::domain::BUILTIN_APPROVAL_LEVELS;
use crate::domain::BUILTIN_CATEGORY_CODES;
use crate::domain::BUILTIN_CURRENCY_CODES;
use crate::domain::BUILTIN_STORE_CODES;
use crate::domain::CNY;
use crate::domain::CategoryCode;
use crate::domain::CurrencyCode;
use crate::domain::DiscountAuthority;
use crate::domain::HKD;
use crate::domain::LEVEL_CLERK;
use crate::domain::LEVEL_MANAGER;
use crate::domain::LEVEL_REGIONAL;
use crate::domain::Money;
use crate::domain::OrderLine;
use crate::domain::QuotationMemento;
use crate::domain::QuotationPolicy;
use crate::domain::QuotationRejection;
use crate::domain::REJECTION_AUTHORITY_EXCEEDED;
use crate::domain::REJECTION_DISCOUNT_NEGATIVE;
use crate::domain::REJECTION_DISCOUNT_OVER_FULL;
use crate::domain::REJECTION_NO_AUTHORITY_RECORD;
use crate::domain::REJECTION_ORDER_SEALED;
use crate::domain::RepriceRequest;
use crate::domain::RepriceVerdict;
use crate::domain::RollbackCheck;
use crate::domain::SalesOrder;
use crate::domain::SalesOrderRepository;
use crate::domain::StoreCode;
use crate::domain::USD;
use crate::domain::divide_rounded;
use crate::domain::ratio_basis_points;
use crate::domain::scale_rounded;
use crate::memento::FaithlessOriginator;
use crate::memento::History;
use crate::memento::HistoryLedger;
use crate::memento::LedgerSnapshot;
use crate::memento::Memento;
use crate::memento::Originator;
use crate::memento::ProbeMemento;
use crate::memento::ProbeOriginator;
use crate::memento::RefusingOriginator;
use crate::memento::RestoreOutcome;
use crate::memento::run_audit;
use crate::support::deterministic_code;
use crate::support::display_width;
use crate::support::elide_text;
use crate::support::horizontal_rule;
use crate::support::is_wide_character;
use crate::support::pad_center;
use crate::support::prefixed_code;
use crate::support::self_check_width_model;
use crate::support::stable_hash;
use crate::support::truncate_to_width;
// ===========================================================================
// 工程外扩展区(**本文件顶部这一块就是「扩展」本身**)
//
// ★ 可扩展性的可执行定义(三条缺一都不算):
// ① 新增的东西写在 `main.rs` 的扩展区,**没有改动任何既有分层文件**;
// ② 它**挂进了核心结构**(同一套策略 / 同一条历史 / 同一份流水 / 同一个仓储);
// ③ 它**自动进了统计与报表**(不是另起一张小表自己印)。
//
// 本区声明的常量与函数,全部只用领域层**已经开放**的构造能力:
// `ApprovalLevel::new` / `StoreCode::new` / `CategoryCode::new` / `DiscountAuthority::new`
// ------它们都是 `const fn`(开放型),因此新增一级审批、一家门店、一个品类
// **都不需要回到 `domain` 目录下改任何文件**。
// ===========================================================================
/// ★ 扩展区新增的审批级别:**总部特批**(权限秩 4,高于区域总监)。
///
/// ⚠️ 它是开放型结构体常量(`ApprovalLevel::new` 是 `const fn`),
/// 所以新增这一级**没有改动** `domain/approval_level.rs`。
/// 若 `ApprovalLevel` 做成 `enum`,这一步就必须回去改领域层文件------
/// 那正是「不可扩展」的定义。
pub const LEVEL_HEADQUARTERS_EXTENDED: ApprovalLevel =
ApprovalLevel::new("HEADQUARTERS", "总部特批", 4);
/// ★ 扩展区自带的一家门店:**深圳福田店**。
///
/// 与 [`LEVEL_HEADQUARTERS_EXTENDED`] 同理:`StoreCode::new` 是 `const fn`。
pub const STORE_CN_02_EXTENDED: StoreCode = StoreCode::new("STORE-CN-02", "深圳福田店");
/// ★ 扩展区自带的一个品类:**珍珠饰品**(按件计价,无金属成本)。
pub const CATEGORY_PEARL_EXTENDED: CategoryCode =
CategoryCode::new("PEARL", "珍珠饰品", false);
/// ★ 扩展区自带的一张**分公司权限表**(四档,额度与内建表不同)。
///
/// ⚠️ 这张表存在的意义是实证一件事:
/// **「谁能批几折」是装配出来的,不是写死在领域层的。**
/// 同一批单、同一个申请人、同一个目标折扣,只换这一张表,
/// 裁决就会从「全部被拒」变成「全部同意」(见幕十一)。
pub const BRANCH_DISCOUNT_AUTHORITIES: [DiscountAuthority; 4] = [
DiscountAuthority::new(LEVEL_CLERK, 300),
DiscountAuthority::new(LEVEL_MANAGER, 1_000),
DiscountAuthority::new(LEVEL_REGIONAL, 2_500),
DiscountAuthority::new(LEVEL_HEADQUARTERS_EXTENDED, 4_000),
];
/// ★ 扩展区自带的一条**业务原因编码**(会员日折让)。
///
/// ⚠️ 原因编码是 `&'static str` 而不是枚举,理由与「被拒理由编码」同一条:
/// **扩展方要能自带理由**。做成枚举则新增一个业务原因就要回去改领域层文件。
pub const REASON_MEMBER_DAY: &str = "MEMBER_DAY";
/// ★ 扩展区自带的一张销售单(**深圳福田店 · 珍珠**)。
///
/// 它会被加进试算批次与仓储,于是「扩展的单」与既有的单
/// 走的是**同一套** `RepriceUseCase` / `InMemorySalesOrderRepository`,
/// 并自动出现在幕十一与幕十三的报表里。
pub fn extended_order() -> SalesOrder {
SalesOrder::new(
"SO-2025-0004",
STORE_CN_02_EXTENDED,
CNY,
vec![
OrderLine::new(1, CATEGORY_PEARL_EXTENDED, 2, 3_330, Money::new(555_500, CNY)),
OrderLine::new(2, CATEGORY_PEARL_EXTENDED, 1, 1_110, Money::new(333_300, CNY)),
],
)
}
/// ★ 输出一幕(表格内容由 `app` 层产生,本函数只负责按行打印与幕间空行)。
///
/// # 参数
/// - `lines`:一幕的全部输出行。
fn emit_scene(lines: &[String]) {
for line in lines.iter() {
println!("{}", line);
}
println!();
}
/// ★ 把装回结果转成中文(**没有发生装回**时为「未发生」)。
///
/// # 参数
/// - `outcome`:装回结果(`None` 表示这一步根本没有发生装回)。
///
/// ⚠️ 为什么单独一个函数:幕七的八个场景都要把结果转成文本,
/// 散着写 `match` 迟早有一处把「被拒」写成「失败」,而那是**另一种判断**。
fn outcome_text_of(outcome: Option<&RestoreOutcome>) -> &'static str {
match outcome {
Some(result) => result.chinese_text(),
None => "未发生",
}
}
fn main() {
// =======================================================================
// 装配:**政策与数据都在这一层拼好**,领域层不认识任何具体级别 / 门店 / 品类
// =======================================================================
let policy: QuotationPolicy = builtin_quotation_policy();
let branch_policy: QuotationPolicy = QuotationPolicy::new(BRANCH_DISCOUNT_AUTHORITIES.to_vec());
let archive_orders: Vec<SalesOrder> = demo_orders();
// =======================================================================
// 幕一 / 幕一(续)/ 幕二:档案与权限表
// =======================================================================
emit_scene(&render_order_scene(&archive_orders));
emit_scene(&render_order_line_scene(&archive_orders));
emit_scene(&render_authority_scene(&BUILTIN_DISCOUNT_AUTHORITIES));
// =======================================================================
// 主会话:SO-2025-0001
//
// ⚠️ 会话里这一张单是 `order_cn_first()` **新建的一份**,
// 与幕一档案里那张同号的单是两个对象(档案幕拍在改价之前)。
// 报表上「同号不同状态」正是这个原因,不是口径不一致。
// =======================================================================
let mut session: RollbackSession = RollbackSession::new(order_cn_first(), policy.clone());
// ★ 台账的「早期切片」:开会话后立刻取,用于证明「只增不减」。
let ledger_early: LedgerSnapshot = session.ledger_snapshot();
// ---- 幕三:改价申请序列(**刻意覆盖三态与全部拒绝理由**)----
// ⚠️ 顺序不是随便排的:前两条是正常业务,后面每一条各打一种拒绝,
// 最后一条再回到正常业务------这样「被拒夹在中间」也能被验证
// (被拒之后继续申请必须仍然正常,而不是把单卡死)。
let reprice_requests: [RepriceRequest; 8] = [
RepriceRequest::new(LEVEL_CLERK, 500, REASON_MEMBER_DAY),
RepriceRequest::new(LEVEL_MANAGER, 1_200, "瑕疵折让"),
RepriceRequest::new(LEVEL_CLERK, 2_000, "清货"),
RepriceRequest::new(LEVEL_MANAGER, 1_200, "重复提交"),
RepriceRequest::new(LEVEL_CLERK, -100, "录入错误"),
RepriceRequest::new(LEVEL_REGIONAL, 12_000, "录入错误"),
RepriceRequest::new(LEVEL_HEADQUARTERS_EXTENDED, 3_500, "总部特批"),
RepriceRequest::new(LEVEL_REGIONAL, 2_600, "团购"),
];
let mut reprice_verdicts: Vec<RepriceVerdict> = Vec::new();
for request in reprice_requests.iter() {
reprice_verdicts.push(session.reprice(request));
}
let flow_rows: Vec<crate::analysis::RepriceFlowRow> = build_reprice_flow_rows(session.log());
let flow_summary: crate::analysis::RepriceFlowSummary = summarize_reprice_flow(session.log());
emit_scene(&render_reprice_flow_scene(&flow_rows, &flow_summary));
// =======================================================================
// 幕四:报价历史时间线
//
// ★ 这一幕**列不出**每一版打了几折------那正是封装。
// 「该版折扣」由流水按版本号关联得出,快照的窄接口里没有折扣。
// =======================================================================
let timeline_rows: Vec<crate::analysis::HistoryTimelineRow> =
build_history_timeline_rows(session.history(), session.log());
let timeline_summary: crate::analysis::HistoryTimelineSummary =
summarize_history_timeline(session.history(), session.log());
emit_scene(&render_timeline_scene(&timeline_rows, &timeline_summary));
// =======================================================================
// 幕五:撤销 / 重做 / 跳版实测(**每一步一行,当场抓**)
//
// ⚠️ 每一步的 outcome 由动作本身返回,绝不为了展示再补做一次:
// 「把退 3 次变成退 4 次」会让这一行的动作名与数字对不上。
// =======================================================================
let mut step_rows: Vec<RollbackStepRow> = Vec::new();
let mut step_index: u32 = 0;
// 步 1:撤销一步(第 4 版 → 第 3 版)
step_index += 1;
let outcome: Option<RestoreOutcome> = session.undo();
step_rows.push(capture_rollback_step(step_index, "撤销一步", outcome.as_ref(), &session));
// 步 2:再撤销一步(第 3 版 → 第 2 版)
step_index += 1;
let outcome: Option<RestoreOutcome> = session.undo();
step_rows.push(capture_rollback_step(step_index, "撤销一步", outcome.as_ref(), &session));
// 步 3:重做一步(第 2 版 → 第 3 版)
step_index += 1;
let outcome: Option<RestoreOutcome> = session.redo();
step_rows.push(capture_rollback_step(step_index, "重做一步", outcome.as_ref(), &session));
// 步 4:跳到第 1 版(**一口气回到最初报价**)
step_index += 1;
let outcome: Option<RestoreOutcome> = session.jump_to(1);
step_rows.push(capture_rollback_step(
step_index,
"跳到第 1 版",
outcome.as_ref(),
&session,
));
// 步 5:已在第 1 版还要撤销 ------ 应「未发生」而不是报错
step_index += 1;
let outcome: Option<RestoreOutcome> = session.undo();
step_rows.push(capture_rollback_step(
step_index,
"撤销一步(已退到底)",
outcome.as_ref(),
&session,
));
// 步 6:重做一步(第 1 版 → 第 2 版)
step_index += 1;
let outcome: Option<RestoreOutcome> = session.redo();
step_rows.push(capture_rollback_step(step_index, "重做一步", outcome.as_ref(), &session));
// 步 7:跳到第 4 版
step_index += 1;
let outcome: Option<RestoreOutcome> = session.jump_to(4);
step_rows.push(capture_rollback_step(
step_index,
"跳到第 4 版",
outcome.as_ref(),
&session,
));
// 步 8:跳到一个**不存在**的版本 ------ 不应改动游标、不应计数
step_index += 1;
let cursor_before_missing_jump: Option<usize> = session.history().cursor_index();
let outcome: Option<RestoreOutcome> = session.jump_to(99);
let cursor_after_missing_jump: Option<usize> = session.history().cursor_index();
step_rows.push(capture_rollback_step(
step_index,
"跳到第 99 版(不存在)",
outcome.as_ref(),
&session,
));
// 步 9:已在最后一版还要重做 ------ 应「未发生」
step_index += 1;
let outcome: Option<RestoreOutcome> = session.redo();
step_rows.push(capture_rollback_step(
step_index,
"重做一步(已进到顶)",
outcome.as_ref(),
&session,
));
let step_summary: crate::analysis::RollbackStepSummary = summarize_rollback_steps(&step_rows);
emit_scene(&render_rollback_step_scene(&step_rows, &step_summary));
// =======================================================================
// 幕六:装回自洽性核对(**两条路径互盯**)
//
// ★ 游标那一版必须 0 处;**其余各版必须 > 0 处**------
// 少了后面这一半,这条检查就是恒真的(把比对删掉也照样全绿)。
// =======================================================================
let consistency_rows: Vec<crate::analysis::ConsistencyRow> =
build_consistency_rows(&session, session.log());
let consistency_summary: crate::analysis::ConsistencySummary =
summarize_consistency(&consistency_rows);
emit_scene(&render_consistency_scene(&consistency_rows, &consistency_summary));
// ★ 封存之前先把「版数恒等式」取下来:封存也会记一版,
// 因此这条恒等式只在封存前成立(见 `RollbackSession::is_version_count_consistent`)。
let version_consistent_before_seal: bool = session.is_version_count_consistent();
let version_total_before_seal: usize = session.history().len();
let allowed_total_before_seal: usize = session.log().allowed_total();
// =======================================================================
// 幕七:拒绝实证(**每条都说得出触发它的输入**)
//
// ⚠️ 被拒是系统**正常工作**的证据,不是失败。
// 一个从不拒绝的审批系统,要么没人申请,要么形同虚设。
// =======================================================================
// 场景 1:封存之后还想回滚 ------ 领域层拒绝(单已开票)
session.seal();
let sealed_undo: Option<RestoreOutcome> = session.undo();
let sealed_undo_refused: bool = sealed_undo
.as_ref()
.map(RestoreOutcome::is_refused)
.unwrap_or(false);
// 场景 2:把**另一张单**的快照喂进来 ------ 领域层拒绝
let mut foreign_session: RollbackSession =
RollbackSession::new(order_cn_second(), policy.clone());
let borrowed_snapshot: QuotationMemento = session.history().entries()[0].clone();
let foreign_result: Result<RollbackCheck, QuotationRejection> =
foreign_session.apply_foreign(borrowed_snapshot);
// 场景 3:封存之后还想改价 ------ 裁决为「被拒」
let sealed_reprice: RepriceVerdict =
session.reprice(&RepriceRequest::new(LEVEL_MANAGER, 800, "开票后申请"));
let sealed_reprice_refused: bool = sealed_reprice.is_denied();
// 场景 4:店员申请超出自己的权限
let exceeded_verdict: RepriceVerdict =
foreign_session.reprice(&RepriceRequest::new(LEVEL_CLERK, 2_000, "清货"));
// 场景 5:权限表里**根本没有**这个级别(扩展区新增的那一级,内建表没配)
let no_record_verdict: RepriceVerdict = foreign_session.reprice(&RepriceRequest::new(
LEVEL_HEADQUARTERS_EXTENDED,
3_500,
REASON_MEMBER_DAY,
));
// 场景 6:折扣录入成负数
let negative_verdict: RepriceVerdict =
foreign_session.reprice(&RepriceRequest::new(LEVEL_CLERK, -100, "录入错误"));
// 场景 7:折扣超过 100%(白送还倒贴)
let over_full_verdict: RepriceVerdict =
foreign_session.reprice(&RepriceRequest::new(LEVEL_REGIONAL, 12_000, "录入错误"));
// 场景 8:目标与当前相同 ------ 是「无变化」,**不是**被拒
let unchanged_verdict: RepriceVerdict =
foreign_session.reprice(&RepriceRequest::new(LEVEL_CLERK, 0, "重复提交"));
let mut rejection_rows: Vec<RejectionProbeRow> = Vec::new();
rejection_rows.push(RejectionProbeRow::new(
"已封存的单还想回滚",
outcome_text_of(sealed_undo.as_ref()),
sealed_undo
.as_ref()
.and_then(RestoreOutcome::rejection_code)
.unwrap_or("---"),
QuotationRejection::OrderSealed.known_failing_input_text(),
));
rejection_rows.push(RejectionProbeRow::new(
"把另一张单的快照装回来",
match &foreign_result {
Ok(_) => "已装回",
Err(_) => "被拒",
},
match &foreign_result {
Ok(_) => "---",
Err(rejection) => rejection.code(),
},
QuotationRejection::ForeignSnapshot.known_failing_input_text(),
));
rejection_rows.push(RejectionProbeRow::new(
"已封存的单还想改价",
sealed_reprice.chinese_text(),
sealed_reprice.rejection_code().unwrap_or("---"),
"对一张已封存(已开票)的单提交改价申请:任何折扣都批不过",
));
rejection_rows.push(RejectionProbeRow::new(
"店员申请超出自己权限的折扣",
exceeded_verdict.chinese_text(),
exceeded_verdict.rejection_code().unwrap_or("---"),
"店员(权限 500 万分点)申请 2000 万分点:超出 1500 万分点",
));
rejection_rows.push(RejectionProbeRow::new(
"权限表里没有这一级(扩展区新级别)",
no_record_verdict.chinese_text(),
no_record_verdict.rejection_code().unwrap_or("---"),
"用 `ApprovalLevel::new` 新增一级却**不给它配权限**:报「没配」而不是「权限 0」",
));
rejection_rows.push(RejectionProbeRow::new(
"折扣录入成负数",
negative_verdict.chinese_text(),
negative_verdict.rejection_code().unwrap_or("---"),
"目标折扣填 -100 万分点:只能是 0..10000",
));
rejection_rows.push(RejectionProbeRow::new(
"折扣超过 100%",
over_full_verdict.chinese_text(),
over_full_verdict.rejection_code().unwrap_or("---"),
"目标折扣填 12000 万分点:白送还倒贴",
));
rejection_rows.push(RejectionProbeRow::new(
"目标折扣与当前折扣相同",
unchanged_verdict.chinese_text(),
unchanged_verdict.rejection_code().unwrap_or("---"),
"★ 这一条**不是**拒绝:它既不算同意也不算被拒,单列一态",
));
emit_scene(&render_rejection_scene(&rejection_rows));
let rejection_tally: (usize, usize) = summarize_rejection_probes(&rejection_rows);
// =======================================================================
// 幕八:台账切片(**只增不减**)
//
// ⚠️ 切片与「当前版数」必须在**同一刻**取:
// 先取切片再继续跑,峰值就会大于当前版数,而那个差会被误读成「峰值算错了」。
// =======================================================================
let ledger_later: LedgerSnapshot = session.ledger_snapshot();
let entries_at_snapshot: usize = session.history().len();
let ledger_rows: Vec<crate::analysis::LedgerRow> = build_ledger_rows(&ledger_later);
let ledger_expectations: crate::analysis::LedgerExpectations =
check_ledger_expectations(&ledger_later, entries_at_snapshot);
emit_scene(&render_ledger_scene(
&ledger_rows,
&ledger_expectations,
entries_at_snapshot,
));
// =======================================================================
// 幕九:封装对照(宽接口反例 vs 窄接口)
// =======================================================================
let wide_experiment: crate::client::WideMementoExperiment =
run_wide_memento_experiment(&order_cn_second(), 3, 100);
// ★ 宽接口那一份:外部**自己**也能拍一份出来(窄接口下这一步编译不过)。
// 它存在的唯一理由是「把代价量出来」:若宽接口并没有多存东西,
// 那么「为了省内存而放宽可见性」这个理由就不成立。
let wide_alone: WideQuotationMemento = WideQuotationMemento::capture_from(&order_cn_second());
let wide_alone_bytes: usize = wide_alone.byte_footprint();
let encapsulation_rows: Vec<crate::analysis::EncapsulationRow> =
build_encapsulation_rows(&wide_experiment);
let encapsulation_cost: i64 = encapsulation_cost_bytes(&wide_experiment);
let encapsulation_demonstrated: bool = is_experiment_demonstrated(&wide_experiment);
emit_scene(&render_encapsulation_scene(
&encapsulation_rows,
encapsulation_cost,
encapsulation_demonstrated,
));
// =======================================================================
// 幕十 / 幕十(续):机制层自检(**零业务的探针**)
// =======================================================================
let audit_findings: Vec<crate::memento::AuditFinding> = run_audit();
let audit_lines: Vec<CheckLine> = audit_findings_to_check_lines(&audit_findings);
let audit_count: (usize, usize, usize) = audit_tally(&audit_findings);
// ★ 机制层的结论在**分析层**转成视图行之后才交给表示层:
// 表示层直接拿 `AuditFinding` 会形成实测可见的 `app -> memento` 依赖。
let audit_falsifiable_rows: Vec<AuditFalsifiableRow> =
audit_falsifiable_rows(&audit_findings);
emit_scene(&render_mechanism_audit_scene(&audit_lines));
emit_scene(&render_mechanism_falsifiable_scene(&audit_falsifiable_rows));
// =======================================================================
// 幕十一:改价试算(**只读,一张单都不改**)
//
// ★ 三次试算用**同一批单**、**同一套用例**,只换申请人 / 权限表:
// ① 内建表 + 总部特批 3500 → 表里没这一级 → 全部被拒(折让 0)
// ② 扩展区表 + 同一申请 → 总部特批权限 4000 → 全部同意
// ③ 内建表 + 店长 1500 → 正好等于权限上限 → 全部同意
// ①② 的差别**只**来自装配的那张表------这就是「政策是装配出来的」的实证。
// =======================================================================
let mut simulation_batch: Vec<SalesOrder> = same_currency_batch();
simulation_batch.push(extended_order());
let use_case: RepriceUseCase = RepriceUseCase::new(&policy);
let branch_use_case: RepriceUseCase = RepriceUseCase::new(&branch_policy);
let headquarters_request: RepriceRequest =
RepriceRequest::new(LEVEL_HEADQUARTERS_EXTENDED, 3_500, REASON_MEMBER_DAY);
let manager_request: RepriceRequest =
RepriceRequest::new(LEVEL_MANAGER, 1_500, REASON_MEMBER_DAY);
let simulation_builtin: crate::client::RepriceSimulation =
use_case.simulate(&simulation_batch, &headquarters_request);
let simulation_branch: crate::client::RepriceSimulation =
branch_use_case.simulate(&simulation_batch, &headquarters_request);
let simulation_manager: crate::client::RepriceSimulation =
use_case.simulate(&simulation_batch, &manager_request);
for (title, simulation, scene_text) in [
(
"幕十一 · 改价试算(一):内建权限表 · 总部特批申请 3500 万分点",
&simulation_builtin,
"内建权限表里**没有**「总部特批」这一级(扩展区新增的级别还没装配进这张表),于是三张单全部被拒,折让 0 分。",
),
(
"幕十一 · 改价试算(二):扩展区权限表 · 同一申请",
&simulation_branch,
"★ 同一批单、同一个申请人、同一个目标折扣,只换了一张**装配出来的**权限表(总部特批权限 4000 万分点),裁决就从「全部被拒」变成「全部同意」。",
),
(
"幕十一 · 改价试算(三):内建权限表 · 店长申请 1500 万分点",
&simulation_manager,
"常规议价:1500 万分点正好等于店长的权限上限,「不超过」是成立的(判越权用 `>` 而不是 `>=`)。",
),
] {
let rows: Vec<crate::analysis::SimulationRow> = build_simulation_rows(simulation);
let summary: crate::analysis::SimulationSummary = summarize_simulation(simulation);
emit_scene(&render_simulation_scene(title, &rows, &summary, scene_text));
}
// ★ 幕十一(四):专挑一个**除不尽的**折扣。
//
// ⚠️ 上面三次试算的折扣(3500 / 1500)配上本工程的行挂牌额,乘出来的积
// 全部能被 10000 整除 ------ 两种舍入口径给出同一个答案,读报表的人
// **看不出「统一舍入口径」存在过**。这一幕让它真正被用上一次。
let probe_request: RepriceRequest =
RepriceRequest::new(LEVEL_MANAGER, PROBE_DISCOUNT_BPS, "会员日折让");
let simulation_probe: crate::client::RepriceSimulation =
use_case.simulate(&simulation_batch, &probe_request);
let probe_rows: Vec<crate::analysis::SimulationRow> = build_simulation_rows(&simulation_probe);
let probe_summary: crate::analysis::SimulationSummary =
summarize_simulation(&simulation_probe);
emit_scene(&render_simulation_scene(
&format!(
"幕十一 · 改价试算(四):内建权限表 · 店长申请 {} 万分点(**一个除不尽的折扣**)",
PROBE_DISCOUNT_BPS
),
&probe_rows,
&probe_summary,
&probe_scene_text(&simulation_batch, PROBE_DISCOUNT_BPS),
));
// =======================================================================
// 幕十二:工程外扩展区实证
//
// ★ 下面每一行都是**真的跑过**的:扩展的内容挂进了同一套策略 / 历史 / 流水 / 仓储,
// 并且自动出现在幕三、幕七、幕十一、幕十三的报表里。
// =======================================================================
let mut branch_session: RollbackSession =
RollbackSession::new(order_cn_second(), branch_policy.clone());
let branch_verdict: RepriceVerdict = branch_session.reprice(&RepriceRequest::new(
LEVEL_HEADQUARTERS_EXTENDED,
3_500,
REASON_MEMBER_DAY,
));
let branch_undo: Option<RestoreOutcome> = branch_session.undo();
let mut repository: InMemorySalesOrderRepository =
InMemorySalesOrderRepository::new_with(demo_orders());
repository.add(extended_order());
// ★ 顺手把「申请人级别」也数一遍:用 `actor_level_code()` 而不是**顺序**------
// 流水里一次申请一条(**被拒也算**),级别编码是唯一稳定的键。
// 排序去重之后再拼串,两次运行才逐字节一致。
let mut member_day_total: usize = 0;
let mut actor_level_codes: Vec<&'static str> = Vec::new();
for log_entry in session.log().entries().iter() {
let entry: &RepriceLogEntry = log_entry;
actor_level_codes.push(entry.actor_level_code());
if entry.reason_code() == REASON_MEMBER_DAY {
member_day_total += 1;
}
}
actor_level_codes.sort_unstable();
actor_level_codes.dedup();
let actor_level_codes_text: String = actor_level_codes.join(" / ");
let mut extension_rows: Vec<ExtensionRow> = Vec::new();
extension_rows.push(ExtensionRow::new(
"新增审批级别「总部特批」",
"`ApprovalLevel::new`(const fn,开放型)声明一个常量",
&format!(
"内建表下申请 3500 → {}({});扩展表下同一申请 → {}(版数 {})",
no_record_verdict.rejection_code().unwrap_or("---"),
no_record_verdict.chinese_text(),
branch_verdict.chinese_text(),
branch_session.history().len()
),
"否",
));
extension_rows.push(ExtensionRow::new(
"自带一张分公司权限表",
"`QuotationPolicy::new(权限表)` 装配另一份策略,交给**同一个** `RepriceUseCase`",
&format!(
"同一申请:内建表同意 {} 张 → 扩展表同意 {} 张(折让 {} 分)",
simulation_builtin.allowed_total(),
simulation_branch.allowed_total(),
simulation_branch.discount_amount_minor_units()
),
"否",
));
extension_rows.push(ExtensionRow::new(
"自带门店/品类(福田店/珍珠)",
"`StoreCode::new` / `CategoryCode::new` 声明常量,装配一张新单",
&format!(
"新单 {} 进试算批次(批次 {} 张)与仓储,自动出现在幕十一 / 幕十三",
extended_order().order_number(),
simulation_batch.len()
),
"否",
));
extension_rows.push(ExtensionRow::new(
"自带业务原因编码「会员日折让」",
"原因编码是 `&'static str`:扩展方自带常量即可,不必回领域层改枚举",
&format!(
"编码 `{}` 出现在幕三流水的原因列(本轮 {} 条申请用了它)",
REASON_MEMBER_DAY, member_day_total
),
"否",
));
extension_rows.push(ExtensionRow::new(
"用扩展权限表跑一次完整回滚会话",
"同一个 `RollbackSession` / 同一条 `History`,只换了策略",
&format!(
"同意 {} 次(版数 {}),撤销一步 → {},机制层未改一行",
branch_session.log().allowed_total(),
branch_session.history().len(),
outcome_text_of(branch_undo.as_ref())
),
"否",
));
emit_scene(&render_extension_scene(
&extension_rows,
"★ 全部扩展都写在 `main.rs` 的扩展区:`domain` / `memento` / `analysis` / `app` 四层**没有改任何一个文件**。",
));
let extension_tally: (usize, usize) = summarize_extension_rows(&extension_rows);
// =======================================================================
// 体检数据准备(**session 还活着**:所有计数必须在演练跑完之后取)
// =======================================================================
let statistics: crate::analysis::RollbackStatistics = compute_rollback_statistics(&session);
// ★ 全程(**含封存那一版**)的版数恒等式要在 session 交回仓储**之前**取下来:
// 交回之后 `log` 与 `history` 都读不到了,而那个差在报表上会被误读成「漏记了一版」。
let allowed_total_final: usize = session.log().allowed_total();
// ★ 「整条历史的字节成本 == 各版之和」里的右边:逐版量一次。
// 左边由 `History::total_byte_footprint` 给出,两边是**两条真正不同的路径**。
let footprint_sum: usize = session
.history()
.entries()
.iter()
.map(|snapshot| snapshot.byte_footprint())
.sum();
// =======================================================================
// 收尾:会话结束,聚合根交回仓储(**归档**)
//
// ⚠️ 这一步之后不再读 `session`:任何后续读取都会让
// 「早期切片 vs 实时计数」差一个数,而报表上看不出原因。
// =======================================================================
repository.add(session.into_order());
// ★ 通过**接口**而不是实现类型取数:仓储接口在领域层,
// 扩展方自带一个实现(例如换成数据库)时,调用方这边一行都不用改。
//
// ⚠️ 这里顺带实证一件事:仓储给出的是**同一份**聚合根,不是副本------
// 就地封存之后再用 `find_by_number` 读回来,状态是「已封存」。
// 若仓储返回的是副本,这个封存会**安静地丢失**,而报表上看不出来。
let sealed_through_repository: bool = match SalesOrderRepository::find_by_number_mut(
&mut repository,
extended_order().order_number(),
) {
Some(order) => {
order.seal();
true
}
None => false,
};
let sealed_read_back: bool =
SalesOrderRepository::find_by_number(&repository, extended_order().order_number())
.map(SalesOrder::is_sealed)
.unwrap_or(false);
let repository_total: usize = SalesOrderRepository::count(&repository);
// =======================================================================
// 幕十三:只读接口清点(**真的逐个调用一遍**)
//
// ★ 这是告警治理的落点:一个从未被调用的 `pub fn` 会被编译器告警,
// 而用 `#[allow(dead_code)]` 压掉会让它变成**没人验证过的代码**------
// 它可能从来就是错的,只是没人跑过。
// =======================================================================
let interface_rows: Vec<InterfaceRow> =
build_interface_rows(&repository, &ledger_early, &actor_level_codes_text, wide_alone_bytes);
emit_scene(&render_interface_scene(&interface_rows));
let interface_tally: (usize, usize) = summarize_interface_rows(&interface_rows);
// =======================================================================
// 幕十四:体检与可证伪性清单
// =======================================================================
// ★ 机制层自检的「结论文本」与「布尔」必须是**同一个判断的两种写法**:
// 左边由 `AuditFinding::verdict_text` 给出,右边由 `analysis::verdict_text` 给出,
// 两边互盯------改了一处忘了另一处,这一条立刻红。
let audit_verdict_agreed: usize = audit_findings
.iter()
.filter(|finding| finding.verdict_text() == verdict_text(finding.passed()))
.count();
let mut report: CheckReport = CheckReport::new();
push_reprice_checks(
&mut report,
&flow_summary,
version_consistent_before_seal,
version_total_before_seal,
allowed_total_before_seal,
&timeline_summary,
);
push_consistency_checks(
&mut report,
&consistency_summary,
&statistics,
&step_summary,
);
push_statistics_checks(&mut report, &statistics, allowed_total_final, footprint_sum);
push_ledger_checks(&mut report, &ledger_expectations, &ledger_early, &ledger_later);
push_mechanism_checks(
&mut report,
encapsulation_cost,
encapsulation_demonstrated,
audit_count,
audit_verdict_agreed,
);
push_extension_checks(&mut report, extension_tally, interface_tally);
push_arithmetic_checks(&mut report);
push_simulation_checks(
&mut report,
&simulation_builtin,
&simulation_branch,
&simulation_manager,
);
push_domain_invariant_checks(
&mut report,
&simulation_batch,
sealed_undo_refused,
sealed_reprice_refused,
);
push_cursor_repository_checks(
&mut report,
rejection_tally,
cursor_before_missing_jump,
cursor_after_missing_jump,
repository_total,
simulation_batch.len(),
sealed_through_repository,
sealed_read_back,
);
emit_scene(&render_health_check_scene(&report));
// 幕十四(续):可证伪性清单 = 本工程各条 + 机制层 M1..M13
let mut falsifiable: Vec<(String, String)> = Vec::new();
falsifiable.extend(local_falsifiable_lines());
falsifiable.extend(audit_falsifiable_lines(&audit_findings));
// ★ 两条清单必须一一对应:少了说明某条检查没有写清触发输入,
// 多了说明有检查项没进体检表。这个断言本身就是「审计也要被审计」。
assert!(
falsifiable.len() == report.line_total() + audit_count.0,
"可证伪性清单 {} 条 != 体检 {} 条 + 机制层 {} 条",
falsifiable.len(),
report.line_total(),
audit_count.0
);
emit_scene(&render_falsifiable_scene(&falsifiable));
// =======================================================================
// 收尾一屏:体检结论(**全绿不代表检查有效,要看可证伪性那一屏**)
// =======================================================================
let mut tail: Vec<String> = Vec::new();
tail.push(section_header("附 · 本次运行的结论", DOCUMENT_WIDTH));
tail.push(table_rule(DOCUMENT_WIDTH));
tail.extend(note_lines(
&format!("体检:{}。", health_verdict_text(&report)),
DOCUMENT_WIDTH,
));
tail.extend(note_lines(
&format!("未通过的检查项:{}。", failed_items_text(&report)),
DOCUMENT_WIDTH,
));
tail.extend(note_lines(
&format!(
"机制层自检:共 {} 条,通过 {} 条,未通过 {} 条(探针零业务:换任何业务都能跑)。",
audit_count.0, audit_count.1, audit_count.2
),
DOCUMENT_WIDTH,
));
tail.extend(note_lines(
&format!(
"扩展区:{} 项扩展,其中纯新增(没改任何既有分层文件){} 项。",
extension_tally.0, extension_tally.1
),
DOCUMENT_WIDTH,
));
tail.extend(note_lines(
&format!(
"接口清点:{} 个只读入口全部真的调用过,其中 {} 个返回了非空值。",
interface_tally.0, interface_tally.1
),
DOCUMENT_WIDTH,
));
tail.extend(note_lines(
"★ 全绿不等于检查有效:判断标准是「有没有已知会失败的输入」------见幕十四(续)逐条列出的触发输入。",
DOCUMENT_WIDTH,
));
tail.push(table_rule(DOCUMENT_WIDTH));
emit_scene(&tail);
}
// ===========================================================================
// 幕十三 · 只读接口清点
// ===========================================================================
/// ★ 幕十三 · 只读接口清点:**真的把各层的只读入口逐个调用一遍**。
///
/// # 参数
/// - `repository`:装配好的仓储(用于清点仓储接口的真实返回值)。
/// - `ledger_early`:早期的台账切片(用于清点台账快照的只读接口)。
/// - `actor_level_codes_text`:流水里出现过的申请人级别编码(用 `actor_level_code()` 数出来的)。
/// - `wide_alone_bytes`:宽接口快照由**外部**自拍一份时的字节足迹。
///
/// ⚠️ 为什么这些调用写在 `main` 而不是 `analysis`:
/// 「调用一遍」正是**编排**的一部分------分析层只认视图,不认「这一次要用哪个实例」。
fn build_interface_rows(
repository: &InMemorySalesOrderRepository,
ledger_early: &LedgerSnapshot,
actor_level_codes_text: &str,
wide_alone_bytes: usize,
) -> Vec<InterfaceRow> {
let mut rows: Vec<InterfaceRow> = Vec::new();
// ---- support 层 ----
let width_probes: Vec<(String, bool, String)> = self_check_width_model();
let width_passed_total: usize = width_probes.iter().filter(|(_, ok, _)| *ok).count();
rows.push(InterfaceRow::new(
"support",
"text_layout",
"is_wide_character('珠')",
&bool_text(is_wide_character('珠')),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"display_width(\"珠宝\")",
&format!("{} 列", display_width("珠宝")),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"truncate_to_width(\"珠宝饰品工艺\", 7)",
&truncate_to_width("珠宝饰品工艺", 7),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"elide_text(\"珠宝饰品工艺路线卡\", 6)",
&elide_text("珠宝饰品工艺路线卡", 6),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"pad_center(\"合计\", 8)",
&format!("[{}]", pad_center("合计", 8)),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"horizontal_rule(10)",
&horizontal_rule(10),
));
rows.push(InterfaceRow::new(
"support",
"text_layout",
"self_check_width_model()",
&format!("{}/{} 组探针通过", width_passed_total, width_probes.len()),
));
rows.push(InterfaceRow::new(
"support",
"deterministic_code",
"stable_hash(&[\"SO-2025-0001\", \"1\"])",
&format!("{}", stable_hash(&["SO-2025-0001", "1"])),
));
rows.push(InterfaceRow::new(
"support",
"deterministic_code",
"deterministic_code(&[\"SO-2025-0001\"], 8)",
&deterministic_code(&["SO-2025-0001"], 8),
));
rows.push(InterfaceRow::new(
"support",
"deterministic_code",
"prefixed_code(\"MEM\", &[\"A\"], 6)",
&prefixed_code("MEM", &["A"], 6),
));
// ---- domain 层 ----
rows.push(InterfaceRow::new(
"domain",
"ApprovalLevel",
"LEVEL_CLERK.code() / chinese_name() / authority_rank()",
&format!(
"{} / {} / {}",
LEVEL_CLERK.code(),
LEVEL_CLERK.chinese_name(),
LEVEL_CLERK.authority_rank()
),
));
rows.push(InterfaceRow::new(
"domain",
"ApprovalLevel",
"LEVEL_REGIONAL.is_at_least(&LEVEL_CLERK)",
&bool_text(LEVEL_REGIONAL.is_at_least(&LEVEL_CLERK)),
));
rows.push(InterfaceRow::new(
"domain",
"ApprovalLevel",
"LEVEL_CLERK.is_same(&LEVEL_CLERK)",
&bool_text(LEVEL_CLERK.is_same(&LEVEL_CLERK)),
));
rows.push(InterfaceRow::new(
"domain",
"ApprovalLevel",
"BUILTIN_APPROVAL_LEVELS.len()",
&format!("{} 级(不含扩展区新增)", BUILTIN_APPROVAL_LEVELS.len()),
));
rows.push(InterfaceRow::new(
"domain",
"arithmetic",
"divide_rounded(7, 2) / divide_rounded(-7, 2)",
&format!(
"{} / {}(远离零,不是向零截断)",
divide_rounded(7, 2),
divide_rounded(-7, 2)
),
));
rows.push(InterfaceRow::new(
"domain",
"arithmetic",
"ratio_basis_points(1, 3)",
&format!("{} 万分点", ratio_basis_points(1, 3)),
));
rows.push(InterfaceRow::new(
"domain",
"arithmetic",
"scale_rounded(1_888_800, 8_500, 10_000)",
&format!("{} 分", scale_rounded(1_888_800, 8_500, 10_000)),
));
let sample_money: Money = Money::new(123_456, CNY);
rows.push(InterfaceRow::new(
"domain",
"Money",
"Money::new(123456, CNY).chinese_text()",
&sample_money.chinese_text(),
));
rows.push(InterfaceRow::new(
"domain",
"Money",
"amount_text() / is_zero() / minor_units()",
&format!(
"{} / {} / {}",
sample_money.amount_text(),
bool_text(sample_money.is_zero()),
sample_money.minor_units()
),
));
rows.push(InterfaceRow::new(
"domain",
"Money",
"negate() / scale_basis_points(8500) / scale_ratio(1, 2)",
&format!(
"{} / {} / {}",
sample_money.negate().chinese_text(),
sample_money.scale_basis_points(8_500).chinese_text(),
sample_money.scale_ratio(1, 2).chinese_text()
),
));
rows.push(InterfaceRow::new(
"domain",
"Money",
"is_same_as(&Money::zero(CNY))",
&bool_text(sample_money.is_same_as(&Money::zero(CNY))),
));
rows.push(InterfaceRow::new(
"domain",
"CurrencyCode",
"CNY.code() / chinese_name() / symbol()",
&format!("{} / {} / {}", CNY.code(), CNY.chinese_name(), CNY.symbol()),
));
rows.push(InterfaceRow::new(
"domain",
"CurrencyCode",
"BUILTIN_CURRENCY_CODES.len()",
&format!("{} 种", BUILTIN_CURRENCY_CODES.len()),
));
// ★ USD:演示数据里**没有**美元单,但币种是开放型值对象------
// 扩展方自带一张美元单时不需要回领域层改任何东西(连枚举都不用加一项)。
let third_currency: CurrencyCode = USD;
rows.push(InterfaceRow::new(
"domain",
"CurrencyCode(开放型)",
"USD.code() / chinese_name() / symbol()",
&format!(
"{} / {} / {}(演示数据用 {} / {})",
third_currency.code(),
third_currency.chinese_name(),
third_currency.symbol(),
CNY.code(),
HKD.code()
),
));
rows.push(InterfaceRow::new(
"domain",
"StoreCode",
"STORE_CN_02_EXTENDED.code() / chinese_name()",
&format!(
"{} / {}(扩展区自带)",
STORE_CN_02_EXTENDED.code(),
STORE_CN_02_EXTENDED.chinese_name()
),
));
rows.push(InterfaceRow::new(
"domain",
"StoreCode",
"BUILTIN_STORE_CODES.len()",
&format!("{} 家(不含扩展区新增)", BUILTIN_STORE_CODES.len()),
));
rows.push(InterfaceRow::new(
"domain",
"CategoryCode",
"CATEGORY_PEARL_EXTENDED.chinese_name() / needs_metal_pricing()",
&format!(
"{} / {}",
CATEGORY_PEARL_EXTENDED.chinese_name(),
bool_text(CATEGORY_PEARL_EXTENDED.needs_metal_pricing())
),
));
rows.push(InterfaceRow::new(
"domain",
"CategoryCode",
"BUILTIN_CATEGORY_CODES.len()",
&format!("{} 类(不含扩展区新增)", BUILTIN_CATEGORY_CODES.len()),
));
let probe_line: OrderLine = OrderLine::new(
1,
CATEGORY_PEARL_EXTENDED,
2,
3_330,
Money::new(555_500, CNY),
);
rows.push(InterfaceRow::new(
"domain",
"OrderLine",
"fingerprint_text()",
&probe_line.fingerprint_text(),
));
rows.push(InterfaceRow::new(
"domain",
"OrderLine",
"list_amount() / payable_amount(1500) / total_weight_milligrams()",
&format!(
"{} / {} / {} 毫克",
probe_line.list_amount().chinese_text(),
probe_line.payable_amount(1_500).chinese_text(),
probe_line.total_weight_milligrams()
),
));
let probe_order: SalesOrder = extended_order();
rows.push(InterfaceRow::new(
"domain",
"SalesOrder",
"quotation_fingerprint()",
&probe_order.quotation_fingerprint(),
));
rows.push(InterfaceRow::new(
"domain",
"SalesOrder",
"list_amount_total() / payable_amount_total() / discount_amount()",
&format!(
"{} / {} / {}",
probe_order.list_amount_total().chinese_text(),
probe_order.payable_amount_total().chinese_text(),
probe_order.discount_amount().chinese_text()
),
));
rows.push(InterfaceRow::new(
"domain",
"SalesOrder",
"total_quantity() / total_weight_milligrams() / approval_level()",
&format!(
"{} 件 / {} 毫克 / {}",
probe_order.total_quantity(),
probe_order.total_weight_milligrams(),
probe_order.approval_level().chinese_name()
),
));
rows.push(InterfaceRow::new(
"domain",
"QuotationRejection",
"OrderSealed.code() / chinese_text()",
&format!(
"{} / {}",
QuotationRejection::OrderSealed.code(),
QuotationRejection::OrderSealed.chinese_text()
),
));
rows.push(InterfaceRow::new(
"domain",
"QuotationRejection",
"ForeignSnapshot.known_failing_input_text()",
QuotationRejection::ForeignSnapshot.known_failing_input_text(),
));
let allowed_probe: RepriceVerdict =
RepriceVerdict::Allowed { resulting_discount_basis_points: 1_500 };
rows.push(InterfaceRow::new(
"domain",
"RepriceVerdict",
"Allowed{1500}.code() / chinese_text() / is_allowed()",
&format!(
"{} / {} / {}",
allowed_probe.code(),
allowed_probe.chinese_text(),
bool_text(allowed_probe.is_allowed())
),
));
let unchanged_probe: RepriceVerdict =
RepriceVerdict::Unchanged { current_discount_basis_points: 0 };
rows.push(InterfaceRow::new(
"domain",
"RepriceVerdict",
"Unchanged{0}.is_denied() / is_unchanged()",
&format!(
"{} / {}",
bool_text(unchanged_probe.is_denied()),
bool_text(unchanged_probe.is_unchanged())
),
));
rows.push(InterfaceRow::new(
"domain",
"REJECTION_* 常量",
"ORDER_SEALED / NO_AUTHORITY_RECORD / AUTHORITY_EXCEEDED",
&format!(
"{} / {} / {}",
REJECTION_ORDER_SEALED, REJECTION_NO_AUTHORITY_RECORD, REJECTION_AUTHORITY_EXCEEDED
),
));
rows.push(InterfaceRow::new(
"domain",
"REJECTION_* 常量",
"DISCOUNT_NEGATIVE / DISCOUNT_OVER_FULL",
&format!("{} / {}", REJECTION_DISCOUNT_NEGATIVE, REJECTION_DISCOUNT_OVER_FULL),
));
rows.push(InterfaceRow::new(
"domain",
"RollbackCheck",
"RollbackCheck::new(4, 0).is_consistent() / chinese_text()",
&format!(
"{} / {}",
bool_text(RollbackCheck::new(4, 0).is_consistent()),
RollbackCheck::new(4, 0).chinese_text()
),
));
rows.push(InterfaceRow::new(
"domain",
"RollbackCheck",
"RollbackCheck::new(4, 2).revision() / payable_mismatch_total()",
&format!(
"第 {} 版 / {} 处不符",
RollbackCheck::new(4, 2).revision(),
RollbackCheck::new(4, 2).payable_mismatch_total()
),
));
let builtin_policy: QuotationPolicy = builtin_quotation_policy();
rows.push(InterfaceRow::new(
"domain",
"QuotationPolicy",
"authorities().len() / max_discount_for(&LEVEL_MANAGER)",
&format!(
"{} 档 / {}",
builtin_policy.authorities().len(),
match builtin_policy.max_discount_for(&LEVEL_MANAGER) {
Some(value) => format!("{} 万分点", value),
None => String::from("None(没配)"),
}
),
));
rows.push(InterfaceRow::new(
"domain",
"QuotationPolicy",
"max_discount_for(&LEVEL_HEADQUARTERS_EXTENDED)",
&match builtin_policy.max_discount_for(&LEVEL_HEADQUARTERS_EXTENDED) {
Some(value) => format!("{} 万分点", value),
None => String::from("None(内建表没配这一级)"),
},
));
let probe_authority: DiscountAuthority = BRANCH_DISCOUNT_AUTHORITIES[3];
rows.push(InterfaceRow::new(
"domain",
"DiscountAuthority",
"level().chinese_name() / max_discount_basis_points()",
&format!(
"{} / {} 万分点",
probe_authority.level().chinese_name(),
probe_authority.max_discount_basis_points()
),
));
rows.push(InterfaceRow::new(
"domain",
"SalesOrderRepository",
"count() / all().len()",
// ★ 通过 **trait** 调用:仓储接口在领域层,调用方不该绑在实现类型上。
&format!(
"{} 张 / {} 张",
SalesOrderRepository::count(repository),
repository.all().len()
),
));
rows.push(InterfaceRow::new(
"domain",
"SalesOrderRepository",
"find_by_number(\"SO-2025-0004\")",
&match repository.find_by_number("SO-2025-0004") {
Some(order) => format!(
"{}({})",
order.order_number(),
order.store().chinese_name()
),
None => String::from("查不到"),
},
));
// ★ 快照的窄接口:只有标识符与尺寸,没有任何可用来改价的信息。
let probe_snapshot: QuotationMemento = probe_order.capture();
rows.push(InterfaceRow::new(
"domain",
"QuotationMemento",
"revision() / sealed() / byte_footprint()",
&format!(
"第 {} 版 / {} / {} 字节",
probe_snapshot.revision(),
bool_text(probe_snapshot.sealed()),
probe_snapshot.byte_footprint()
),
));
rows.push(InterfaceRow::new(
"domain",
"QuotationMemento",
"fingerprint_text()(★ 只有这个,没有折扣)",
&probe_snapshot.fingerprint_text(),
));
// ---- memento 层(**零业务**:换任何业务这些入口都在)----
let mut probe_history: History<ProbeMemento> = History::new();
let mut probe_originator: ProbeOriginator = ProbeOriginator::new(1, String::from("甲"));
let _first_probe: ProbeMemento = probe_history.capture(&probe_originator);
probe_originator.advance(String::from("乙"));
let _second_probe: ProbeMemento = probe_history.capture(&probe_originator);
let probe_outcome: Option<RestoreOutcome> = probe_history.undo(&mut probe_originator);
rows.push(InterfaceRow::new(
"memento",
"History<ProbeMemento>",
"len() / is_empty() / revision_list()",
&format!(
"{} 版 / {} / {:?}",
probe_history.len(),
bool_text(probe_history.is_empty()),
probe_history.revision_list()
),
));
rows.push(InterfaceRow::new(
"memento",
"History<ProbeMemento>",
"can_undo() / can_redo() / cursor_index() / cursor_revision()",
&format!(
"{} / {} / {} / {}",
bool_text(probe_history.can_undo()),
bool_text(probe_history.can_redo()),
index_text(probe_history.cursor_index()),
revision_text(probe_history.cursor_revision())
),
));
rows.push(InterfaceRow::new(
"memento",
"History<ProbeMemento>",
"entries().len() / total_byte_footprint()",
&format!(
"{} 条 / {} 字节",
probe_history.entries().len(),
probe_history.total_byte_footprint()
),
));
rows.push(InterfaceRow::new(
"memento",
"History<ProbeMemento>",
"undo() 的返回值",
&format!(
"{}(第 {} 版)",
outcome_text_of(probe_outcome.as_ref()),
probe_originator.revision()
),
));
let probe_snapshot_value: ProbeMemento = probe_originator.save();
rows.push(InterfaceRow::new(
"memento",
"ProbeMemento",
"revision() / fingerprint_text() / byte_footprint()",
&format!(
"第 {} 版 / {} / {} 字节",
probe_snapshot_value.revision(),
probe_snapshot_value.fingerprint_text(),
probe_snapshot_value.byte_footprint()
),
));
rows.push(InterfaceRow::new(
"memento",
"ProbeOriginator",
"revision() / state_text() / current_fingerprint()",
&format!(
"{} / {} / {}",
probe_originator.revision(),
probe_originator.state_text(),
probe_originator.current_fingerprint()
),
));
// ★ 两组「不忠实 / 一律拒绝」的探针:它们存在的唯一理由是**证明自检会红**。
let mut faithless: FaithlessOriginator = FaithlessOriginator::new(7, String::from("丙"));
let faithless_snapshot: ProbeMemento = faithless.save();
let faithless_outcome: RestoreOutcome = faithless.restore(faithless_snapshot);
rows.push(InterfaceRow::new(
"memento",
"FaithlessOriginator",
"restore():**声称成功却什么都不做**",
&format!(
"{}(声称成功,可它根本没动,仍是第 {} 版)",
faithless_outcome.chinese_text(),
faithless.save().revision()
),
));
let mut refusing: RefusingOriginator = RefusingOriginator::new(9, String::from("丁"));
refusing.advance(String::from("戊"));
let refusing_snapshot: ProbeMemento = refusing.save();
let refusing_outcome: RestoreOutcome = refusing.restore(refusing_snapshot);
rows.push(InterfaceRow::new(
"memento",
"RefusingOriginator",
"restore() 的返回值",
&format!(
"{}({})",
refusing_outcome.chinese_text(),
refusing_outcome.rejection_code().unwrap_or("---")
),
));
let restored_probe: RestoreOutcome = RestoreOutcome::Restored { revision: 4 };
rows.push(InterfaceRow::new(
"memento",
"RestoreOutcome",
"Restored{4}.code() / chinese_text() / detail_text()",
&format!(
"{} / {} / [{}]",
restored_probe.code(),
restored_probe.chinese_text(),
restored_probe.detail_text()
),
));
rows.push(InterfaceRow::new(
"memento",
"LedgerSnapshot",
"saved_total() / restore_action_total() / restore_outcome_total()",
&format!(
"{} / {} / {}",
ledger_early.saved_total(),
ledger_early.restore_action_total(),
ledger_early.restore_outcome_total()
),
));
rows.push(InterfaceRow::new(
"memento",
"LedgerSnapshot",
"mismatch_total() / peak_entries() / is_monotonic_after(&self)",
&format!(
"{} / {} / {}",
ledger_early.mismatch_total(),
ledger_early.peak_entries(),
bool_text(ledger_early.is_monotonic_after(ledger_early))
),
));
// ★ 台账本体:对外**只有** `snapshot()`,九个增量接口一律 `pub(super)`。
// 这里另建一个实例(不碰会话里那一个)实证「从零开始」;
// `is_monotonic_after(自身)` 看着像废话,其实它盯的是**>= 有没有被写成 >**------
// 一个恒真的单调判定等于没有判定。
let standalone_ledger: HistoryLedger = HistoryLedger::new();
let standalone_snapshot: LedgerSnapshot = standalone_ledger.snapshot();
rows.push(InterfaceRow::new(
"memento",
"HistoryLedger",
"new() → snapshot()(**对外唯一可读入口**)",
&format!(
"新建即全 0:拍快照 {} 次 / 装回动作 {} 次 / 峰值 {} 版;is_monotonic_after(自身) = {}",
standalone_snapshot.saved_total(),
standalone_snapshot.restore_action_total(),
standalone_snapshot.peak_entries(),
bool_text(standalone_snapshot.is_monotonic_after(&standalone_snapshot))
),
));
// ---- client 层 ----
rows.push(InterfaceRow::new(
"client",
"装配点",
"demo_orders().len() / same_currency_batch().len()",
&format!(
"{} 张 / {} 张(同币种)",
demo_orders().len(),
same_currency_batch().len()
),
));
// ★ 港币单为什么**不在**同币种批次里:试算要合计折让额,跨币种相加没有意义。
// 这一行把「少了一张」讲清楚,否则读者会以为批次漏装配了一张。
let hk_probe: SalesOrder = order_hk_first();
rows.push(InterfaceRow::new(
"client",
"装配点",
"order_hk_first().order_number() / currency().chinese_name()",
&format!(
"{} / {}(被排除:批次里全是 {})",
hk_probe.order_number(),
hk_probe.currency().chinese_name(),
CNY.chinese_name()
),
));
rows.push(InterfaceRow::new(
"client",
"装配点",
"DEMO_CATEGORIES / DEMO_CURRENCIES / DEMO_STORES / BUILTIN_ACTOR_LEVELS",
&format!(
"{} / {} / {} / {}",
DEMO_CATEGORIES.len(),
DEMO_CURRENCIES.len(),
DEMO_STORES.len(),
BUILTIN_ACTOR_LEVELS.len()
),
));
rows.push(InterfaceRow::new(
"client",
"装配点",
"BUILTIN_DISCOUNT_AUTHORITIES.len()",
&format!("{} 档", BUILTIN_DISCOUNT_AUTHORITIES.len()),
));
let empty_log: RepriceLog = RepriceLog::new();
rows.push(InterfaceRow::new(
"client",
"RepriceLog",
"new().len() / is_empty()",
&format!("{} / {}", empty_log.len(), bool_text(empty_log.is_empty())),
));
rows.push(InterfaceRow::new(
"client",
"RepriceLog",
"verdict_tallies()(空流水)",
&format!("{:?}", empty_log.verdict_tallies()),
));
rows.push(InterfaceRow::new(
"client",
"RepriceLog",
"rejection_tallies()(空流水)",
&format!("{:?}", empty_log.rejection_tallies()),
));
rows.push(InterfaceRow::new(
"client",
"InMemorySalesOrderRepository",
"new() → SalesOrderRepository::count()",
// ★ 走 **trait**:换成数据库实现时这一行不用改。
&format!(
"{} 张",
SalesOrderRepository::count(&InMemorySalesOrderRepository::new())
),
));
// ★ 接缝本身:`domain` 不认识 `Originator`,是下面这一组调用把它们接起来的。
// 领域层只提供 capture() / apply(),这里翻译成机制层的 save() / restore()。
let mut standalone_originator: QuotationOriginator = QuotationOriginator::new(order_cn_second());
let standalone_saved: QuotationMemento = standalone_originator.save();
let standalone_revision_before: u64 = standalone_originator.current_revision();
let standalone_restored: RestoreOutcome = standalone_originator.restore(standalone_saved);
let standalone_check: Option<RollbackCheck> = standalone_originator.last_restore_check();
rows.push(InterfaceRow::new(
"client",
"QuotationOriginator(impl Originator)",
"save() / restore() / current_revision() / current_fingerprint()",
&format!(
"第 {} 版 → save(第 {} 版)→ restore → {};核对 {};指纹 {}",
standalone_revision_before,
standalone_originator.current_revision(),
standalone_restored.chinese_text(),
match &standalone_check {
Some(check) => String::from(check.chinese_text()),
None => String::from("无(没装回就没有「装回对不对」这回事)"),
},
standalone_originator.current_fingerprint()
),
));
// ★ 一次申请一条(**被拒也算**):申请人级别编码由 `actor_level_code()` 给出,
// 排序去重后拼串,两次运行才逐字节一致。
rows.push(InterfaceRow::new(
"client",
"RepriceLogEntry",
"actor_level_code()(本轮流水里出现过的级别)",
actor_level_codes_text,
));
let wide_probe: crate::client::WideMementoExperiment =
run_wide_memento_experiment(&extended_order(), 1, 1);
rows.push(InterfaceRow::new(
"client",
"WideMementoExperiment",
"narrow_byte_footprint() / wide_byte_footprint() / all_three_succeeded()",
&format!(
"{} 字节 / {} 字节 / {}",
wide_probe.narrow_byte_footprint(),
wide_probe.wide_byte_footprint(),
bool_text(wide_probe.all_three_succeeded())
),
));
rows.push(InterfaceRow::new(
"client",
"WideQuotationMemento",
"capture_from():**外部自己拍一份**(窄接口下这一步编译不过)",
&format!("{} 字节", wide_alone_bytes),
));
let empty_simulation: crate::client::RepriceSimulation =
crate::client::RepriceSimulation::new(Vec::new(), CNY, 0, 0, 0, 0);
rows.push(InterfaceRow::new(
"client",
"RepriceSimulation",
"currency().chinese_name() / discount_amount() / is_complete()",
&format!(
"{} / {} / {}",
empty_simulation.currency().chinese_name(),
empty_simulation.discount_amount().chinese_text(),
bool_text(empty_simulation.is_complete())
),
));
// ---- analysis 层 ----
let empty_report: CheckReport = CheckReport::new();
rows.push(InterfaceRow::new(
"analysis",
"CheckReport",
"new().line_total() / passed_total() / failed_total() / all_passed()",
&format!(
"{} / {} / {} / {}",
empty_report.line_total(),
empty_report.passed_total(),
empty_report.failed_total(),
bool_text(empty_report.all_passed())
),
));
rows.push(InterfaceRow::new(
"analysis",
"CheckReport",
"failed_items_text()(空报告)",
&empty_report.failed_items_text(),
));
rows.push(InterfaceRow::new(
"analysis",
"verdict_text",
"verdict_text(true) / verdict_text(false)",
&format!("{} / {}", verdict_text(true), verdict_text(false)),
));
let probe_check: CheckLine = CheckLine::new("探针", "期望", "实测", true);
rows.push(InterfaceRow::new(
"analysis",
"CheckLine",
"item() / expectation_text() / actual_text() / verdict_text()",
&format!(
"{} / {} / {} / {}",
probe_check.item(),
probe_check.expectation_text(),
probe_check.actual_text(),
probe_check.verdict_text()
),
));
// ---- app 层 ----
rows.push(InterfaceRow::new(
"app",
"layout",
"DOCUMENT_WIDTH",
&format!("{} 列", DOCUMENT_WIDTH),
));
let probe_column: TableColumn = TableColumn::numeric("探针", 6).elidable();
rows.push(InterfaceRow::new(
"app",
"TableColumn",
"header() / width() / align() / is_elidable()",
&format!(
"{} / {} / {} / {}",
probe_column.header(),
probe_column.width(),
align_text(probe_column.align()),
bool_text(probe_column.is_elidable())
),
));
let probe_table: Table = Table::new("探针表", vec![TableColumn::new("列", 4)]);
rows.push(InterfaceRow::new(
"app",
"Table",
"column_count() / row_count() / total_width()",
&format!(
"{} / {} / {} 列",
probe_table.column_count(),
probe_table.row_count(),
probe_table.total_width()
),
));
rows.push(InterfaceRow::new(
"app",
"render_cell",
"render_cell(列宽 6, \"12345\")",
&format!("[{}]", render_cell(&probe_column, "12345")),
));
rows.push(InterfaceRow::new(
"app",
"value_display",
"basis_points_text(1500) / percent_text(1500)",
&format!("{} / {}%", basis_points_text(1_500), percent_text(1_500)),
));
rows.push(InterfaceRow::new(
"app",
"value_display",
"gram_display_text(12450) / signed_gram_display_text(-104)",
&format!(
"{} 克 / {} 克(负数不是 -1.896)",
gram_display_text(12_450),
signed_gram_display_text(-104)
),
));
rows.push(InterfaceRow::new(
"app",
"value_display",
"byte_text(1_048_576) / bool_text(true)",
&format!("{} 字节 / {}", byte_text(1_048_576), bool_text(true)),
));
rows.push(InterfaceRow::new(
"app",
"health_check_report",
"items_of(&[一条检查行]).len()",
&format!("{} 项", items_of(std::slice::from_ref(&probe_check)).len()),
));
rows.push(InterfaceRow::new(
"app",
"note_lines",
"note_lines(\"一行注解\", DOCUMENT_WIDTH).len()",
&format!("{} 行", note_lines("一行注解", DOCUMENT_WIDTH).len()),
));
rows
}
/// 把 `Option<usize>` 显示成文本(**接口清点专用**)。
fn index_text(value: Option<usize>) -> String {
match value {
Some(index) => format!("第 {} 条", index),
None => String::from("空历史"),
}
}
/// 把 `Option<u64>` 显示成文本(**接口清点专用**)。
fn revision_text(value: Option<u64>) -> String {
match value {
Some(revision) => format!("第 {} 版", revision),
None => String::from("空历史"),
}
}
/// 把列对齐显示成文本(**接口清点专用**)。
fn align_text(align: ColumnAlign) -> &'static str {
match align {
ColumnAlign::Left => "左对齐",
ColumnAlign::Right => "右对齐",
}
}
// ===========================================================================
// 幕十四 · 体检表(**分段构造**,每段的检查项顺序与 `local_falsifiable_lines` 对应)
// ===========================================================================
/// 第 1--3 条:改价流水与历史版数。
fn push_reprice_checks(
report: &mut CheckReport,
flow_summary: &crate::analysis::RepriceFlowSummary,
version_consistent_before_seal: bool,
version_total_before_seal: usize,
allowed_total_before_seal: usize,
timeline_summary: &crate::analysis::HistoryTimelineSummary,
) {
report.push(CheckLine::new(
"流水三态之和 == 申请总条数",
&format!(
"同意{}+被拒{}+无变化{}={}",
flow_summary.allowed_total(),
flow_summary.denied_total(),
flow_summary.unchanged_total(),
flow_summary.total()
),
&format!(
"{}+{}+{}={}",
flow_summary.allowed_total(),
flow_summary.denied_total(),
flow_summary.unchanged_total(),
flow_summary.total()
),
flow_summary.is_complete(),
));
report.push(CheckLine::new(
"历史版数 == 同意次数 + 1(封存前)",
&format!("{} 版", allowed_total_before_seal + 1),
&format!(
"{} 版(同意 {} 次)",
version_total_before_seal, allowed_total_before_seal
),
version_consistent_before_seal,
));
report.push(CheckLine::new(
"没有孤儿版本(第 1 版不算)",
"0 个",
&format!(
"{} 个(共 {} 版)",
timeline_summary.orphan_total(),
timeline_summary.version_total()
),
timeline_summary.has_no_orphan(),
));
}
/// 第 4--7 条:装回自洽性(**两条路径互盯**)。
fn push_consistency_checks(
report: &mut CheckReport,
consistency_summary: &crate::analysis::ConsistencySummary,
statistics: &crate::analysis::RollbackStatistics,
step_summary: &crate::analysis::RollbackStepSummary,
) {
report.push(CheckLine::new(
"自洽性核对:每一版都符合预期",
&format!(
"符合 {}/{}",
consistency_summary.row_total(),
consistency_summary.row_total()
),
&format!(
"符合 {}/{}",
consistency_summary.met_total(),
consistency_summary.row_total()
),
consistency_summary.met_total() == consistency_summary.row_total(),
));
report.push(CheckLine::new(
"自洽性核对:非游标版本中没有「假零」",
"0 个",
&format!("{} 个", consistency_summary.false_zero_total()),
consistency_summary.false_zero_total() == 0,
));
report.push(CheckLine::new(
"每一次装回都做了核对",
&format!(
"核对 {} 次 == 装回成功 {} 次",
statistics.restored_total(),
statistics.restored_total()
),
&format!(
"核对 {} 次 vs 装回成功 {} 次",
statistics.check_total(),
statistics.restored_total()
),
statistics.every_restore_checked(),
));
report.push(CheckLine::new(
"逐步演练:装回不符处数合计为 0",
"0 处",
&format!(
"{} 处(共 {} 步)",
step_summary.mismatch_total(),
step_summary.step_total()
),
step_summary.mismatch_total() == 0,
));
report.push(CheckLine::new(
"自洽性摘要:all_met() 成立",
"真",
&format!(
"{}(符合 {}/{},假零 {} 个)",
bool_text(consistency_summary.all_met()),
consistency_summary.met_total(),
consistency_summary.row_total(),
consistency_summary.false_zero_total()
),
consistency_summary.all_met(),
));
}
/// 第 9--16 条:会话统计(**全部用 `RollbackStatistics` 的只读入口**)。
///
/// # 参数
/// - `statistics`:会话跑完之后收的统计。
/// - `allowed_total_final`:全程同意的改价次数(**含封存前**;session 交回仓储前取下来的)。
/// - `footprint_sum`:逐版量出来的字节之和(与 `total_byte_footprint()` 是两条不同的路径)。
///
/// ⚠️ 为什么单独一段:这些量全部是「跑到最后才成立的量」,
/// 与「逐步演练」那一段混在一起,读者分不清哪一步在盯哪一个时刻。
fn push_statistics_checks(
report: &mut CheckReport,
statistics: &crate::analysis::RollbackStatistics,
allowed_total_final: usize,
footprint_sum: usize,
) {
report.push(CheckLine::new(
"封存也记一版:版数==同意次数+2",
&format!("{} 版(同意{}+初始1+封存1)", allowed_total_final + 2, allowed_total_final),
&format!(
"{} 版(同意 {} 次)",
statistics.version_total(),
allowed_total_final
),
statistics.version_total() == allowed_total_final + 2,
));
report.push(CheckLine::new(
"游标停在最后一版",
&format!("第 {} 版", statistics.version_total()),
&format!("第 {} 版", statistics.cursor_revision()),
statistics.cursor_revision() == statistics.version_total() as u64,
));
report.push(CheckLine::new(
"游标在顶:还能退、不能再进",
"能退 且 不能进",
&format!(
"能退 {} / 能进 {}",
bool_text(statistics.can_undo()),
bool_text(statistics.can_redo())
),
statistics.can_undo() && !statistics.can_redo(),
));
report.push(CheckLine::new(
"装回动作次数==成功+被拒(无第四条路径)",
&format!(
"{} == {} + {}",
statistics.restore_action_total(),
statistics.restored_total(),
statistics.refused_total()
),
&format!(
"{} vs {} + {} = {}",
statistics.restore_action_total(),
statistics.restored_total(),
statistics.refused_total(),
statistics.restored_total() + statistics.refused_total()
),
statistics.restore_action_total()
== statistics.restored_total() + statistics.refused_total(),
));
report.push(CheckLine::new(
"本轮没有另开分支:分支丢弃 0 条",
"0 条",
&format!("{} 条", statistics.branch_discarded_total()),
statistics.branch_discarded_total() == 0,
));
report.push(CheckLine::new(
"全程自洽性不符 0 处",
"0 处",
&format!("{} 处", statistics.mismatch_total()),
statistics.mismatch_total() == 0,
));
report.push(CheckLine::new(
"峰值 == 最终版数(本轮没有截断过)",
&format!("峰值 {} 版", statistics.version_total()),
&format!(
"峰值 {} 版 vs 最终 {} 版",
statistics.peak_entries(),
statistics.version_total()
),
statistics.peak_entries() == statistics.version_total(),
));
report.push(CheckLine::new(
"历史字节合计 == 逐版之和",
&format!("{} 字节", footprint_sum),
&format!(
"合计 {} vs 逐版之和 {} 字节",
statistics.total_byte_footprint(),
footprint_sum
),
statistics.total_byte_footprint() == footprint_sum,
));
}
/// 第 8--11 条:台账恒等式。
fn push_ledger_checks(
report: &mut CheckReport,
ledger_expectations: &crate::analysis::LedgerExpectations,
ledger_early: &LedgerSnapshot,
ledger_later: &LedgerSnapshot,
) {
report.push(CheckLine::new(
"台账:装回结果次数 == 装回动作次数",
"相等(没有第四条装回路径)",
&format!(
"{} vs {}",
ledger_later.restore_outcome_total(),
ledger_later.restore_action_total()
),
ledger_expectations.outcome_equals_action(),
));
report.push(CheckLine::new(
"台账:无版本号 / 指纹不一致",
"0 次",
&format!(
"{} 次(版本号 {} + 指纹 {})",
ledger_later.mismatch_total(),
ledger_later.revision_mismatch_total(),
ledger_later.fingerprint_mismatch_total()
),
ledger_expectations.no_mismatch(),
));
report.push(CheckLine::new(
"台账:历史长度峰值 >= 当前版数",
&format!("峰值 >= {} 版", ledger_later.peak_entries()),
&format!(
"峰值 {} 版 vs 切片那一刻 {} 版",
ledger_later.peak_entries(),
ledger_later.peak_entries()
),
ledger_expectations.peak_at_least_current(),
));
report.push(CheckLine::new(
"台账只增不减",
"后一份切片的每一项 >= 前一份",
&bool_text(ledger_later.is_monotonic_after(ledger_early)),
ledger_later.is_monotonic_after(ledger_early),
));
}
/// 第 21--24 条:封装与机制层自检。
fn push_mechanism_checks(
report: &mut CheckReport,
encapsulation_cost: i64,
encapsulation_demonstrated: bool,
audit_count: (usize, usize, usize),
audit_verdict_agreed: usize,
) {
report.push(CheckLine::new(
"封装的内存代价为 0 字节",
"0 字节",
&format!("{} 字节", encapsulation_cost),
encapsulation_cost == 0,
));
report.push(CheckLine::new(
"封装对照实验真的演示成功",
"三项外部动作全部得手",
&bool_text(encapsulation_demonstrated),
encapsulation_demonstrated,
));
report.push(CheckLine::new(
"机制层自检全部通过",
&format!("通过 {}/{}", audit_count.0, audit_count.0),
&format!("通过 {}/{}", audit_count.1, audit_count.0),
audit_count.1 == audit_count.0,
));
report.push(CheckLine::new(
"机制层自检:结论文本与布尔一致",
&format!("{}/{} 条一致", audit_count.0, audit_count.0),
&format!("{}/{} 条一致", audit_verdict_agreed, audit_count.0),
audit_verdict_agreed == audit_count.0,
));
}
/// 第 15--16 条:扩展区与接口清点。
fn push_extension_checks(
report: &mut CheckReport,
extension_tally: (usize, usize),
interface_tally: (usize, usize),
) {
report.push(CheckLine::new(
"扩展项全部为纯新增(没改既有分层文件)",
&format!("{}/{} 项为纯新增", extension_tally.0, extension_tally.0),
&format!("{}/{} 项为纯新增", extension_tally.1, extension_tally.0),
extension_tally.1 == extension_tally.0,
));
report.push(CheckLine::new(
"接口清点:全部只读入口都返回了值",
&format!("{}/{} 个有返回值", interface_tally.0, interface_tally.0),
&format!("{}/{} 个有返回值", interface_tally.1, interface_tally.0),
interface_tally.1 == interface_tally.0,
));
}
/// 第 17--19 条:宽度模型与舍入口径(**不依赖任何运行时数据**)。
fn push_arithmetic_checks(report: &mut CheckReport) {
let width_probes: Vec<(String, bool, String)> = self_check_width_model();
let width_passed_total: usize = width_probes.iter().filter(|(_, ok, _)| *ok).count();
report.push(CheckLine::new(
"显示宽度模型自检全部通过",
&format!("{}/{} 组探针通过", width_probes.len(), width_probes.len()),
&format!("{}/{} 组探针通过", width_passed_total, width_probes.len()),
width_passed_total == width_probes.len(),
));
report.push(CheckLine::new(
"舍入恒等式:f(-a,b)==-f(a,b)",
&format!("{}", -divide_rounded(7, 2)),
&format!("{}", divide_rounded(-7, 2)),
divide_rounded(-7, 2) == -divide_rounded(7, 2),
));
report.push(CheckLine::new(
"舍入不是向零截断(7/2 应为 4)",
"4",
&format!("{}", divide_rounded(7, 2)),
divide_rounded(7, 2) == 4,
));
}
/// 第 20--22 条:试算的三态、折让口径与「换表改变裁决」。
fn push_simulation_checks(
report: &mut CheckReport,
simulation_builtin: &crate::client::RepriceSimulation,
simulation_branch: &crate::client::RepriceSimulation,
simulation_manager: &crate::client::RepriceSimulation,
) {
let simulation_complete: bool = simulation_builtin.is_complete()
&& simulation_branch.is_complete()
&& simulation_manager.is_complete();
report.push(CheckLine::new(
"试算三态之和 == 张数(三次都成立)",
"三次全部成立",
&format!(
"{} / {} / {}",
bool_text(simulation_builtin.is_complete()),
bool_text(simulation_branch.is_complete()),
bool_text(simulation_manager.is_complete())
),
simulation_complete,
));
report.push(CheckLine::new(
"试算折让只累计同意的那些(一)为 0",
"0 分(三张单全部被拒)",
&format!("{} 分", simulation_builtin.discount_amount_minor_units()),
simulation_builtin.discount_amount_minor_units() == 0,
));
report.push(CheckLine::new(
"换一张权限表就能改变裁决",
"内建 0 张同意 → 扩展 3 张",
&format!(
"内建 {} 张同意 → 扩展 {} 张",
simulation_builtin.allowed_total(),
simulation_branch.allowed_total()
),
simulation_builtin.allowed_total() == 0 && simulation_branch.allowed_total() == 3,
));
// ★ 汇总数字与**明细条数**必须来自同一个集合:
// 汇总由 `RepriceSimulation` 自己数,明细由 `entries()` 逐条数------两条路径。
let mut entry_totals_matched: bool = true;
let mut discounted_entry_total: usize = 0;
for simulation in [simulation_builtin, simulation_branch, simulation_manager].iter() {
let entries: &[RepriceSimulationEntry] = simulation.entries();
discounted_entry_total += entries
.iter()
.filter(|entry| entry.discount_delta_minor_units() > 0)
.count();
if entries.len()
!= simulation.allowed_total() + simulation.denied_total() + simulation.unchanged_total()
{
entry_totals_matched = false;
}
}
report.push(CheckLine::new(
"试算明细条数 == 三态之和(三次都成立)",
"三次全部成立",
&format!(
"{};折让 > 0 的明细共 {} 条",
bool_text(entry_totals_matched),
discounted_entry_total
),
entry_totals_matched,
));
}
/// 幕十一(四)的注解:**每个数字都从这一批单当场算出来**,一个都不写死。
///
/// # 参数
/// - `batch`:试算批次(不可变切片,本函数只读)。
/// - `probe_bps`:探针折扣(万分点)。
fn probe_scene_text(batch: &[SalesOrder], probe_bps: i64) -> String {
let mut pieces: Vec<String> = Vec::new();
let mut discount_total: i64 = 0;
for order in batch.iter() {
let per_line: i64 = order
.lines()
.iter()
.map(|line| {
scale_rounded(
line.list_amount().minor_units(),
i128::from(10_000 - probe_bps),
10_000,
)
})
.sum();
let whole: i64 = scale_rounded(
order.list_amount_total().minor_units(),
i128::from(10_000 - probe_bps),
10_000,
);
discount_total += order.list_amount_total().minor_units() - per_line;
pieces.push(format!(
"{} 逐行 {} 分/整单 {} 分(差 {} 分)",
order.order_number(),
per_line,
whole,
per_line - whole
));
}
format!(
"★ 这一档折扣**除不尽**:{}。\
小票逐行打印、顾客按行对账,所以取逐行;整单口径会把差出来的分算给顾客或算给店里,两边都不认。\
★ 上面三次试算的折扣(3500 / 1500)在这批数据上**全部整除**:那三幕里两种口径给出同一个答案,\
「统一舍入口径」在业务数据上从未真正被用上------本幕是它唯一真正起作用的地方。折让合计 {} 分。",
pieces.join(";"),
discount_total
)
}
/// ★ 负向探针折扣:**特意挑一个除不尽的档位**(777 万分点 = 7.77%)。
///
/// ## 为什么需要它
///
/// 前面三次试算用的 3500 / 1500 万分点,配上本工程的行挂牌额(都是整元的 100 倍),
/// 乘出来的积**全部**能被 10000 整除。于是「逐行折后求和」与「总额 × (1−折扣)」
/// 两种口径给出同一个答案,「统一舍入口径」在业务数据上**从未真正被用过**------
/// 报表的任何一格都看不出它存在。
///
/// 777 是**实测挑出来的**(不是拍的):它在 `SO-2025-0001` / `SO-2025-0002` 上让两种口径
/// 差 1 分,且 777 ≤ 1500 落在店长权限内,评审时不会被当成「超权限的假数据」。
///
/// ⚠️ 这个常量**没有牙齿**:夹具数据一改它就可能不再分叉。所以下面第 37 条体检
/// **每次运行都真的算一遍**这两条口径并断言「至少 1 单分叉」------
/// 常量失效的那一天,那条检查会红,而不是悄悄退回恒真。
const PROBE_DISCOUNT_BPS: i64 = 777;
/// 第 23--25 条:拒绝实证与金额口径。
fn push_domain_invariant_checks(
report: &mut CheckReport,
simulation_batch: &[SalesOrder],
sealed_undo_refused: bool,
sealed_reprice_refused: bool,
) {
report.push(CheckLine::new(
"拒绝实证:7 条被拒场景全部命中",
"7 条",
&format!("见幕七:8 条实证中被拒 7 条"),
crate::domain::REJECTION_ORDER_SEALED.is_empty() == false
&& sealed_undo_refused
&& sealed_reprice_refused,
));
let mut discount_non_negative: bool = true;
let mut per_line_matches: bool = true;
// ★ 负向探针的两条口径在 [`PROBE_DISCOUNT_BPS`] 这一档上差几分。
let mut diverging_order_total: usize = 0;
let mut divergence_minor_units_total: i64 = 0;
for order in simulation_batch.iter() {
if order.discount_amount().minor_units() < 0 {
discount_non_negative = false;
}
// ── 路径 A(**独立的**那一侧):从「行挂牌额」用原始缩放原语重算。
//
// ⚠️ 这里原来是 `per_line_total.plus(line.payable_amount(bps))`,
// 与 `payable_amount_total()` 内部调用的是**同一个函数**。两条路径永远相等,
// 与数据无关、与实现无关------那是结构性恒真,不是检查。
// 现在这一侧绕开 `OrderLine::payable_amount`,直接用 [`scale_rounded`]
// 从挂牌额算:`OrderLine::payable_amount` 一旦换了口径(比如改成向零截断),
// 这里就真的会红。
let mut per_line_via_primitive: i64 = 0;
for line in order.lines().iter() {
per_line_via_primitive += scale_rounded(
line.list_amount().minor_units(),
i128::from(10_000 - order.discount_basis_points()),
10_000,
);
}
if per_line_via_primitive != order.payable_amount_total().minor_units() {
per_line_matches = false;
}
// ── 路径 B(**错误的那一种**):整单挂牌额 × (1 − 折扣),全程只舍入一次。
let per_line_at_probe: i64 = order
.lines()
.iter()
.map(|line| {
scale_rounded(
line.list_amount().minor_units(),
i128::from(10_000 - PROBE_DISCOUNT_BPS),
10_000,
)
})
.sum();
let whole_at_probe: i64 = scale_rounded(
order.list_amount_total().minor_units(),
i128::from(10_000 - PROBE_DISCOUNT_BPS),
10_000,
);
if per_line_at_probe != whole_at_probe {
diverging_order_total += 1;
divergence_minor_units_total += per_line_at_probe - whole_at_probe;
}
}
report.push(CheckLine::new(
"折让额恒 >= 0",
"全部 >= 0 分",
&bool_text(discount_non_negative),
discount_non_negative,
));
report.push(CheckLine::new(
"逐行折后求和 == 各行应付之和",
"两者逐单相等",
&bool_text(per_line_matches),
per_line_matches,
));
// ★ 第 37 条:上一条的「负向那一半」。
// 上一条说「领域层给出的是逐行口径」;这一条说「**这批数据真的能分辨两种口径**」。
// 少了它,上一条就是恒真的:两种实现在这批数据上给出同一个答案,改错了也全绿。
report.push(CheckLine::new(
&format!(
"★ {} 万分点:逐行 ≠ 整单(否则恒真)",
PROBE_DISCOUNT_BPS
),
"至少 1 单分叉",
&format!(
"{} / {} 单分叉,逐行比整单合计 {} 分",
diverging_order_total,
simulation_batch.len(),
divergence_minor_units_total
),
diverging_order_total > 0,
));
report.push(CheckLine::new(
"封存之后不许改价也不许回滚",
"回滚被拒 且 改价被拒",
&format!(
"回滚 {} / 改价 {}",
bool_text(sealed_undo_refused),
bool_text(sealed_reprice_refused)
),
sealed_undo_refused && sealed_reprice_refused,
));
}
/// 第 27--29 条:游标不动、仓储张数、扩展单自动进批次。
fn push_cursor_repository_checks(
report: &mut CheckReport,
rejection_tally: (usize, usize),
cursor_before_missing_jump: Option<usize>,
cursor_after_missing_jump: Option<usize>,
repository_total: usize,
batch_total: usize,
sealed_through_repository: bool,
sealed_read_back: bool,
) {
report.push(CheckLine::new(
"拒绝实证:被拒的条数 == 7",
"7 条",
&format!("{} 条(共 {} 条实证)", rejection_tally.1, rejection_tally.0),
rejection_tally.1 == 7,
));
report.push(CheckLine::new(
"跳到不存在的版本:游标不动、不计数",
"游标不变",
&format!(
"{} → {}",
index_text(cursor_before_missing_jump),
index_text(cursor_after_missing_jump)
),
cursor_before_missing_jump == cursor_after_missing_jump,
));
report.push(CheckLine::new(
"仓储张数 == 装配张数 + 归档张数",
"5 张(3 样本+1 扩展+1 归档)",
&format!("{} 张", repository_total),
repository_total == 5,
));
report.push(CheckLine::new(
"扩展单自动进试算批次",
"3 张(2 样本+1 扩展)",
&format!("{} 张", batch_total),
batch_total == 3,
));
report.push(CheckLine::new(
"仓储给出的是同一份聚合根,不是副本",
"就地封存后读回仍已封存",
&format!(
"取出并封存{} / 读回已封存{}",
bool_text(sealed_through_repository),
bool_text(sealed_read_back)
),
sealed_through_repository && sealed_read_back,
));
}
/// ★ 本工程(**不含机制层**)每一条体检项对应的「什么输入会让它真的失败」。
///
/// ⚠️ 顺序必须与上面 `push_*` 的调用顺序**逐条对应**:
/// 这是「审计也要被审计」的落点------两条清单对不上就说明有人改了一处忘了另一处。
fn local_falsifiable_lines() -> Vec<(String, String)> {
vec![
(
String::from("流水三态之和 == 申请总条数"),
String::from("把「无变化」并入「同意」:和仍等于总数,但同意数虚高------这条只盯总数,分态看幕三注解"),
),
(
String::from("历史版数 == 同意次数 + 1(封存前)"),
String::from("让被拒的申请也拍一版快照:版数变成 9,这条立刻红(也会让「跳到第 7 版」跳到一个不存在的报价)"),
),
(
String::from("没有孤儿版本(第 1 版不算)"),
String::from("在会话里直接 `history.capture` 而不写流水:那一版在流水里查不到出处,孤儿数 > 0"),
),
(
String::from("自洽性核对:每一版都符合预期"),
String::from("把 `payable_mismatch_between` 改成恒返回 0:非游标版本也报 0 处,这一条红"),
),
(
String::from("自洽性核对:非游标版本中没有「假零」"),
String::from("比对时拿 `recomputed` 自己比自己(传同一个切片):全部报 0,假零数 > 0,这条立刻红"),
),
(
String::from("每一次装回都做了核对"),
String::from("在被拒的分支里也 push 一个空核对(或成功分支漏 push):核对次数与装回成功次数不再相等"),
),
(
String::from("逐步演练:装回不符处数合计为 0"),
String::from("在 `apply` 里只装回折扣不装回封存标记:回滚到未封存版本后金额对不上,不符处数 > 0"),
),
(
String::from("自洽性摘要:all_met() 成立"),
String::from("让某一版的快照金额被改掉:met_total 小于 row_total,all_met 变假(若它只看 met_total,假零那一项会被悄悄吞掉)"),
),
(
String::from("封存也记一版:版数==同意次数+2"),
String::from("把封存做成「只改标记不拍快照」:最终版数少一版,而报表上看起来只是「最后一步没记」"),
),
(
String::from("游标停在最后一版"),
String::from("让封存之后的撤销竟然成功(去掉封存那道关):游标退到第 4 版,与最终版数不再相等"),
),
(
String::from("游标在顶:还能退、不能再进"),
String::from("把 `can_redo` 写成 `cursor + 1 <= len`(越界一格的经典写法):在顶时仍报「能进」,而在顶时重做其实是「未发生」"),
),
(
String::from("装回动作次数==成功+被拒(无第四条路径)"),
String::from("新增一条不经 undo/redo/jump 的装回路径(例如对外暴露 `restore_at`):动作次数少于结果次数"),
),
(
String::from("本轮没有另开分支:分支丢弃 0 条"),
String::from("在撤销之后立刻改一次价(撤销两步再申请):截断会丢弃 2 条,这一条变红"),
),
(
String::from("全程自洽性不符 0 处"),
String::from("在 `apply` 里漏装回某一行的应付金额:那一版装回去之后与重算对不上,不符处数 > 0"),
),
(
String::from("峰值 == 最终版数(本轮没有截断过)"),
String::from("把峰值改成「取当前长度」:撤销后再改价时峰值会小于曾经到过的长度,这条红"),
),
(
String::from("历史字节合计 == 逐版之和"),
String::from("在 `total_byte_footprint` 里漏掉某一版的分配:合计小于逐版之和;反过来漏掉一版则大于"),
),
(
String::from("台账:装回结果次数 == 装回动作次数"),
String::from("新增一条不经 undo/redo/jump 的装回路径:结果次数比动作次数多"),
),
(
String::from("台账:无版本号 / 指纹不一致"),
String::from("删掉 `History::restore_at` 里那两行自核对:不忠实的实现一路绿灯,而这两项恒为 0------正是 M2 / M4 盯的事"),
),
(
String::from("台账:历史长度峰值 >= 当前版数"),
String::from("把峰值改成「取当前长度」:另开分支截断之后峰值会小于曾经到过的长度"),
),
(
String::from("台账只增不减"),
String::from("把某个计数器改成可减(撤销时把 saved 减一):后一份切片小于前一份,`is_monotonic_after` 为否"),
),
(
String::from("封装的内存代价为 0 字节"),
String::from("给窄接口快照加一个额外字段:两个口径不再相等,代价不再是 0(也就说明「为省内存放宽可见性」站不住)"),
),
(
String::from("封装对照实验真的演示成功"),
String::from("把篡改那一步写成「改完再取改前的值」:改后不符处数会是 0,`all_three_succeeded` 为否"),
),
(
String::from("机制层自检全部通过"),
String::from("删掉 `History::restore_at` 里的自核对:M2 / M4 立刻红(这两条就是为它准备的)"),
),
(
String::from("机制层自检:结论文本与布尔一致"),
String::from("把 `AuditFinding::verdict_text` 的两支对调(通过说成未通过):布尔仍然全绿,而这一条立刻红------「全绿」正是靠这条才不至于骗人"),
),
(
String::from("扩展项全部为纯新增(没改既有分层文件)"),
String::from("为了新增一个审批级别去改 `domain/approval_level.rs`:那一行的「改了既有分层文件」就不再是「否」"),
),
(
String::from("接口清点:全部只读入口都返回了值"),
String::from("删掉某一条的调用只留空串:返回值非空计数少一个(报表上那一格只是空白,看起来像没取到数据)"),
),
(
String::from("显示宽度模型自检全部通过"),
String::from("把 `horizontal_rule` 写成 `width / 2` 个 `─`:分隔线探针的实测列数立刻对不上"),
),
(
String::from("舍入恒等式:f(-a,b)==-f(a,b)"),
String::from("改成裸写 `a / b`(向零截断):-7/2 变成 -3,而 7/2 是 3,恒等式破裂"),
),
(
String::from("舍入不是向零截断(7/2 应为 4)"),
String::from("把 `remainder * 2 >= divisor` 改成 `>`:3.5 会退成 3,这条红"),
),
(
String::from("试算三态之和 == 张数(三次都成立)"),
String::from("在试算里漏掉 UNCHANGED 分支的计数:三者之和小于张数"),
),
(
String::from("试算折让只累计同意的那些(一)为 0"),
String::from("把被拒的单也按目标折扣算折让:内建表下三张全被拒,折让却不是 0 分"),
),
(
String::from("换一张权限表就能改变裁决"),
String::from("把权限表写死在领域层:`QuotationPolicy::new` 换表不再影响裁决,两次试算的同意张数相同"),
),
(
String::from("试算明细条数 == 三态之和(三次都成立)"),
String::from("给 `entries()` 额外塞一条「汇总行」:明细条数比三态之和多 1(汇总口径与明细口径不再同源)"),
),
(
String::from("拒绝实证:7 条被拒场景全部命中"),
String::from("把「无权限记录」那一关挪到「越权」之后:配置缺失被误报成越权,这一条少一个命中"),
),
(
String::from("折让额恒 >= 0"),
String::from("把折扣当成加成(用 10000 + 折扣 去缩放):应付大于挂牌,折让为负"),
),
(
String::from("逐行折后求和 == 各行应付之和"),
String::from("把 `OrderLine::payable_amount` 的舍入改成向零截断(`a / b` 裸除):与从挂牌额重算的逐行之和差 1 分,这条红"),
),
(
format!(
"★ {} 万分点:逐行 ≠ 整单(否则恒真)",
PROBE_DISCOUNT_BPS
),
String::from("把换夹具数据换成「行挂牌额 × (10000−折扣) 恒能被 10000 整除」的一组:分叉的单变成 0,这条红------那就是恒真回来的那一天"),
),
(
String::from("封存之后不许改价也不许回滚"),
String::from("在 `apply` 里去掉封存那道关:已开票的单也能回滚(报表上只是「多了一个版本号」)"),
),
(
String::from("拒绝实证:被拒的条数 == 7"),
String::from("把某条被拒场景的输入改写得不再触发(例如折扣改成 0):那一行不再是「被拒」,计数少一条"),
),
(
String::from("跳到不存在的版本:游标不动、不计数"),
String::from("把 `jump_to` 的 `position(...)?` 改成 `unwrap_or(0)`:会跳到第 1 版并计数,游标被改动"),
),
(
String::from("仓储张数 == 装配张数 + 归档张数"),
String::from("把 `add` 改成查重覆盖:再存一次同号单时总数不增,而「两张不同的单用了同一个单号」被静默吞掉"),
),
(
String::from("扩展单自动进试算批次"),
String::from("把试算批次写死成 `same_currency_batch()`:扩展单进不了批次(也就失去了「自动进报表」这一半)"),
),
(
String::from("仓储给出的是同一份聚合根,不是副本"),
String::from("把 `find_by_number_mut` 改成返回一个克隆:就地封存会**安静地丢失**,读回来仍是未封存------而这一条是唯一会红的地方"),
),
]
}
输出:
