背景
docs/LET_TIME_SEE.md §5.3(智能工厂轨道)把验收标准写死了一条硬要求:任一产线状态
可由台账日志重放还原。把孪生层现有件底盘点一遍,四条(三条腿 + 一个内核)里只到位两条半:
- 已到位:数据面契约
zjchina_core::twin(Entity/EntityState/WorldSnapshot/TelemetrySample/BindingTable/TimelineFrame)、 内容地址zjchina-ledger::twin::frame_cid(sha256 + canonical 信封,时间不进哈希)、 单帧时间插值回放twin::replay_at(纯函数),以及一条 ROS2sensor_msgs/JointState手写解析(twin/feed.rs,零 ROS SDK)。 - 缺失:①数据源腿(真 ROS2 桥 / rosbag 回放)、②时间轴腿(帧落台账 + 时间窗查询)、 ③仿真内核腿——框架完全没有。
③ 之所以是真缺口,是因为「由台账重放还原」只解决了看历史;没有仿真内核,
就没有「不看现场也能先把产线跑起来」的能力:新工位布局、缓冲位分配、瓶颈搬迁、
停机率对产出的影响,全靠真机试错。而这恰是无锡美的双高端工厂(出处见
docs/LET_TIME_SEE.md)现场最贵的一类问题。
本决策要定的是三件事:DES 内核放哪、它的真源是什么、以及它与既有孪生契约的边界。
选项
- A. 新开独立 crate
zjchina-sim。 概念最干净(引擎不是契约)。但 ADR-0006 已确立 「契约不跨节点边界、不为契约开新 crate」;而本内核产出物就是WorldSnapshot, 是纯逻辑(CSR wasm / SSR native 双端可跑)、零外部依赖。为一个只依赖zjchina-core的纯算法模块开 crate,换来的是拆分成本而非隔离收益。 - B. 引擎与契约都放
zjchina-core/src/sim/(采纳)。 与twin/同构:契约 DTO- 纯函数同处 core(
twin/bind.rs::apply_sample、twin/timeline.rs::replay_at已是先例)。 逃生舱留好——引擎与 DTO 分文件,将来真要独立成 crate,成本 = 一个Cargo.toml。
- 纯函数同处 core(
- C. 复用 twin 的
Binding/BindingTable把仿真当「遥测源」。 概念重复:Binding是「外部遥测字段 → 实体属性」的解析器,而仿真内部的属性是引擎自己算出来的, 硬套一层绑定表只是把常量搬进配置文件。不采纳(但拓扑/身份复用Entity/Scene, 见「拍板」)。
证据
twin/的既有约定可直接继承:apply_sample的文档注释已明确「全局seq/prev_cid由追加方管理,不在本函数」——仿真的帧同样只出内容,盖章交给 ledger 侧。frame_canonical信封含seq与prev(zjchina-ledger/src/twin.rs,其测试断言 「seq 属于内容」)。故 CID 对帧是位置指纹而非纯内容指纹;周期采样不会撞 CID, 但也不能被误读为内容指纹——这决定了本轮采用周期采样 + 单一赋值点,把 「变化触发帧 + 心跳帧」推后,届时要先让「无变化」成为内容的一部分。timeline.rs::interpolate只遍历前一帧的实体(该函数对b帧新增实体不补位): 所以不能造瞬态在制品实体,否则插值回放会「丢件」。在制品只能作为聚合标量 (wip)挂在工位上。这是既有代码反向约束设计的一个实例。- 确定性是回放与 CID 的前提:跨端浮点(Box–Muller 的
sin/cos、指数的ln) 在 wasm 与 macOS libm 间可能差 1 ULP,取整后事件序分叉。故时间量一律整数毫秒、 抽样即取整,且 PRNG 自持(splitmix64),不引rand——PRNG 算法一变「同种子同结果」 的全量历史结果就变,自持才可控。这条同时满足 AGENTS.md 依赖准入(零新增依赖)。 - 消费侧样板已存在:
/Users/zjchina/factorywatch(独立仓,汽车总装 11 工位看板) 的fw-render/model.rs自带State{Run,Idle,Maint,Fault}/Line{station,takt,output, progress,state}/Board{kpis,lines,alarms,agvs,devices}与自研xorshift32, 注释明确写着「将来接真源时字段对齐zjchina_core::twin即可」——本内核的投影层 就是为这条对接准备的验证环。 - OEE 的窗口口径必须排除暖机:
availability与performance的分母若含warmup_ms,起步空线会被算成「停机」。故指标窗口 =horizon_ms − warmup_ms。
拍板
拍板者 zj(AI 协作者 TRAE 参与现状盘点与选项对比,拍板权属作者)。日期:2026-10-15。
① 落点:zjchina-core/src/sim/(选项 B),不新开 crate;lib.rs 只加 pub mod sim;,
不做平铺 re-export(Entity/Scene 等价名称过泛),统一 zjchina_core::sim::* 前缀,
与 twin 的既有命名纪律一致。
② 真源与投影:事件为真源,帧为投影。
- 真源 =
SimEvent{ts_ms, seq, station_id, kind, product_id}事件流,(ts_ms, seq)构成 全序 → 同刻事件顺序确定(回放、逐步审计、逐步 diff 都靠它); - 投影 = 每
sample_period_ms采一帧的WorldSnapshot序列;帧的seq/prev_cid留空,由追加方(ledger 侧)盖章后算frame_cid; - 指标由相位区间累计 fold 得出:每站任一时刻只处于
Busy/Blocked/Starved/Down之一,故busy + blocked + starved + down == 指标窗口是结构性事实,而不是一句断言。
③ 建模口径(写死,避免反复):
- 缓冲按边:
StationSpec::out_buffer= 本站与下游之间可存放的成品件数。「按站入口缓冲」 对首站无意义,出口缓冲才能表达「慢下游反压上游」(本来靠in_buffer建模的话, 末站积压这条最常见的整线反压根本表达不出来)。 - 末站例外:末站的下游是出货口,出货视为即时消耗,故末站完工即刻下线、
其
out_buffer不参与建模(P1 不建模出货节拍;等出货节拍成为真需求再引入, 届时末站缓冲才有意义)。 - 故障日历式 MTBF(与是否在加工无关),加工中断后保留剩余工时(不重置)。
performance可 >1(标称节拍抽样偏低时),不 clamp;blocked / starved 归 performance 损失,不归 availability。- 依赖准入:零新增依赖;时间量整数毫秒;
LineSpec::validate()拦坏输入(返Err不 panic)。
④ 通用化红线(不可越):仿真只做通用化场景验证,样例 LineSpec 用虚构的通用
工位名,不带任何真实工厂数据、节拍、涉密信息(仓库零工厂信息);web 层永不进控制
闭环,硬实时留在 DDS/PLC 域;工厂数据不出内网。数据源腿(rclrs/rosbag)与时间轴腿
(帧落台账 + 时间窗查询)本轮不做,与本内核互不阻塞——内核只依赖既有契约,
故二者可并行推进、独立回滚。
⑤ 冻结(P2 再评估):变化触发帧/心跳帧、返修回流(破串联假设,需重做状态机)、
AGV/物料(连续运动,不硬塞离散事件)、多线/并行工位/资源竞争、factory 台账 channel 的
DDL 迁移、C++ 侧消费、大屏页面与 SSE、spec_cid。