# 步骤 ① 交付报告 · 虚拟时钟内核 > **对应**:设计文档 §7.3 虚拟时钟调度器、§0.4 P3、§7.4 T1–T5;定位文件 §7 施工顺序第 ① 步 > **状态**:✅ 已完成并验证 ─ **35 项测试全绿**、宿主半 build + typecheck 全绿 > **owner**:session-24ca6e69 ┃ **互不交叉**:前端场景由 session-40f8f716 并行推进 --- ## 1. 交付物 | 文件 | 内容 | |---|---| | `src/contracts/**`(5 个) | **冻结契约**:时间原语 / 虚拟设备 / 装配状态 SSOT / 前后端协议 | | `src/core/vclock/virtual-clock.ts` | ★ 虚拟时钟调度器 | | `src/core/devices/deterministic.ts` | ★ 验收专用确定性设备 | | `src/index.ts` | 宿主半入口(替换掉脚手架的守护循环,见 §5) | | `tests/**`(4 个,35 项) | 时钟单测 / 时钟验收 / T6 事件循环 / 入口冒烟 | | `scripts/build.sh` | 修正依赖链接(见 §5.3) | **跑法**(无需编译、无需 bash): ```bash npm test # node --test "tests/**/*.test.ts" —— Node 24 原生类型擦除 npm run typecheck # 宿主半类型检查 npm run typecheck:client # 前端半类型检查(前端 owner 建的 tsconfig.client.json) npm run build # 需 DSH_CHECKOUT + bash(见 §5.3) ``` --- ## 2. 验收结果(实测数字) | 判据 | 标准 | 实测 | 结论 | |---|---|---|---| | **T1** | 5 次读数按序递增 | `readings == [1,2,3,4,5]` | ✅ 逐字成立 | | **T2** | 虚拟时间推进量一致 | 第 5 次读数发生在 t=4.0(误差 <1 片);循环结束 t=5.0 精确 | ✅ | | **T3** | 慢周期设备周期事件无丢失 | 1s 设备 1 次 + 10ms 设备 **100 次**,两台各被 tick **1000 片** | ✅ | | **T5** | 加速模式 1 秒虚拟时间 < 50ms | **0.13–0.32ms**(1000 片) | ✅ 余量 150× | | **T6** | 单次 advance 期间宿主事件循环最大阻塞 < 50ms | p50 **1.04ms** / p95 **1.19ms** / p99 **2.20ms** | ✅ | ### T6 实测分布(每片 20µs,`advance(10.0)`,10000 片) | 让出预算 | p50 | p95 | p99 | max | 总耗时 | |---:|---:|---:|---:|---:|---:| | 0(关闭) | 16.2 | **207.7** | 207.7 | 207.7 | 212ms | | **1(默认)** | 1.04 | **1.19** | 2.20 | 16.0 | 220ms | | 2 | 2.04 | 2.24 | 14.85 | 15.1 | 217ms | | 4 | 4.06 | 9.18 | 15.07 | 15.1 | 217ms | | 8 | 8.08 | 16.16 | 16.17 | 16.2 | 213ms | **三条结论**: 1. **关闭让出时阻塞随总耗时线性增长** —— 1022ms 的推进就阻塞 1022ms。这正是文档§7.3 伪码的真实后果,也是定位文件把它列为**新增最高风险**的原因。 2. **p50 精确跟随预算** —— 让出机制按设计工作,预算是可直接调的自变量。 3. **max 有一个 ~16ms 的环境地板**,任何预算都压不下去(Node 定时器/GC 抖动),**不是时钟引入的阻塞**。⇒ **判据必须盯分位数**。只盯 max 会把环境抖动误读成时钟缺陷,并让 CI 偶发红灯 —— 这正是文档 §0.4 批评过的「用概率性行为验证确定性判据」。 **默认值定为 1ms**(原拟 4ms):4ms 预算的 p95 已达 9.18ms、p99 达 15ms,几乎吃掉一个 60fps 帧;1ms 预算的 p95 只有 1.19ms,而代价仅 ~3% 总耗时(220ms vs 213ms)。 --- ## 3. 设计文档的三处自相矛盾(已发现并解决) > 这三处不解决,P3 的验收断言**永远不可能成立**。 ### 矛盾一:`tick` 每次 +1 vs 每片都 tick - §0.4 的 `DeterministicTestDevice.tick` 每次 `+1` - §7.3 的 `advance(1.0)` 在 step=1ms 下要 tick **1000 次** - ⇒ 首读会是 1000 而非 1,`[1,2,3,4,5]` 永不成立 **解决**:按**采样周期**结转(累加虚拟时间,满 `period` 秒 +1 采样),而非按 tick 计数。 **副产品**:P3 因此变成一条**更强的**判据 —— 它同时验证「1000 个 1ms 时间片必须精确累加为 1.0 秒」。 ### 矛盾二:初值 0 还是 1 文档测试循环是**先读、后睡**,第 1 次读发生在任何 sleep 之前。初值取 0 则首读为 0。 **解决**:初值取 **1** —— 真实传感器上电即产出首个有效读数,所以 t=0 已有第 1 个采样。这是物理正确,不是凑数。 ### 矛盾三:T2 的 `clock.now ≈ 4.0` §7.4 的循环是 **5 次** `read()+sleep(1)`,结束时虚拟时刻是 **5.0**,不是 4.0。 **解决**:4.0 是**第 5 次读数发生的时刻**,不是结束时刻。按此前者验收,并额外断言结束时精确等于 5.0。 --- ## 4. 相对文档伪码的三处强化 | # | 文档 §7.3 伪码 | 本实现 | 理由 | |---|---|---|---| | 1 | `now` 是浮点秒 | **整数微秒为权威**,浮点仅视图 | `0.001` 累加 1000 次在浮点下 **≠ 1.0**(测试里先证伪了这条前置假设)。残差会累积,最终让「确定性读数」多跳一格 | | 2 | 同步忙循环 | 按**墙钟预算**让出事件循环 | 新增判据 T6;「算得快就不让出」(T5 不受损)、「算得久就让出」(T6 得到保证) | | 3 | 「加速模式用事件优先队列」 | 设备以 `nextEventIn()` **声明可跳空** | **只要有一台设备返回 `null`(需要每片)就绝不跳空** —— 积分型模型(BME280 漂移)结果与片数相关,跳空会改变结果。安全优先 | 另有两处工程加固: - **并发 `advance` 串行化**(内部 Promise 链),不让两次推进交错破坏设备状态;且某次抛错不会卡死后续调用。 - **跳空必须给最后一个时间片留位置** —— 否则 skip 恰好等于剩余量时循环直接 break,设备**永远拿不到终点那一刻的 tick**,末次采样会错。 --- ## 5. 环境与工具链的三项发现 ### 5.1 ★ Node 24 原生跑 TypeScript,步骤① **零依赖** `node --test "tests/**/*.test.ts"` 直接跑 `.ts`(Node 24 类型擦除)。 配合 `tsconfig.json` 的 `allowImportingTsExtensions` + `rewriteRelativeImportExtensions`,**同一份源码**既能被 node 直跑、又能被 tsc 编译成 `lib/`(`.ts` 导入自动改写成 `.js`)。 ⇒ 内核单测**不需要** npm install、不需要 bash、不需要 build。这是 §10「内核零依赖宿主 API」带来的直接收益。 ### 5.2 脚手架的两处模板 bug | # | 位置 | 问题 | 处置 | |---|---|---|---| | 1 | `src/client/index.ts` | `component: () => ({ render(){} })` —— **形状是错的**。真实契约是 `SlotComponent

= (props: P) => ReactNode`(`packages/client/ui-slots/src/index.ts:370`),即 React 函数组件 | 前端 owner 已按 React.createElement 重写 | | 2 | `src/index.ts` | 模板残留的**守护 agent 循环**(每 60s 唤醒一次 LLM),与本插件毫无关系 | 已替换为真正的宿主半入口 | > 这两处建议回修 `dev_scaffold_plugin` —— 生成物在新插件上会直接误导。 ### 5.3 ★ `vendor/*` 是源码态,运行时链接必须另找 - checkout 的 `vendor/schemastery`、`vendor/cordis` **只有 `lib/types/`,没有 JS 产物**。 - 插件里 `cordis` / `dsh-tools` / `dsh-llm` / `dsh-system-prompt` **全是 `import type`**,编译期擦除,只要有 `.d.ts` 就够。 - 但 **`schemastery` 是值导入**(`import z from ...` 用于 Config),必须有 JS。 ⇒ 直接链 checkout 会让插件被宿主加载时报 `ERR_MODULE_NOT_FOUND: .../schemastery/lib/index.mjs`。 - 另:checkout 版是 **3.18.1**,连 `package.json` 声明的 peer 范围 `^3.18.2` 都不满足。 **修正**:`build.sh` 改为**优先链接 profile 里宿主实际加载的已构建副本**(`$DSH_PROFILE_DIR/node_modules/...`,实测 3.18.4,满足 peer 范围),找不到才回退 checkout 并告警。 **构建环境备忘**:`DSH_CHECKOUT=D:\dsh\deepseek-harness`;bash 在 `E:\Git\bin\bash.exe`(**不在 PATH**,但存在)。 --- ## 6. 宿主半入口自检 `src/index.ts` 启动时跑一次内核自检并打日志,把 T1 的核心断言在**真实宿主环境**里再验一遍: ``` [dsh-breadboard] 虚拟时钟内核就绪(step=0.001s, yieldBudgetMs=1ms) [dsh-breadboard] 内核自检通过:readings=[1,2,3,4,5] tickCount=5000 ``` 自检失败**只记日志、不抛** —— 插件加载不应因自检失败而中断。 --- ## 7. 下一步(定位文件 §7 施工顺序) | 步 | 内容 | 状态 | |---|---|---| | ① | 虚拟时钟 + 确定性设备 | ✅ **本报告** | | ② | BridgeServer(TCP) + `bridge_client.py`,打通 IPC,验证 T3 跨进程 | ⬜ 下一步 | | ③ | BME280 寄存器级行为模型 | ⬜ | | ④ | `test-project/` 适配改造(P1/P5) | ⬜ | | ⑤ | SimEngine + 工具注册(P6) | ⬜ 工具参数方言已查明:**JSON Schema**(非 schemastery),见 `00-宿主API核实.md` | | ⑥ | HostAdapter 接真实 DSH | ⬜ | | ⑦ | 前端 Three.js 场景 | 🔄 前端 owner 并行推进中 | **②的前置已就绪**:`SubprocessHandle` 契约、`bridge_client.py` 的 NDJSON 协议风格(对齐 DSH 自带 Python SDK)、以及 TCP+token 的通道选型。