fix(approval): round-67 审批迁移 fail-closed 与 grant digest 归一化(R67-B1)

- 迁移 grant 只持久化 token_digest,修复 raw token 直写 token_hash 导致 P3 grant 不可消费的 CRITICAL 缺陷
- 损坏 JSON/schema/记录 fail-closed,不再静默当空源;文件缺失维持空源语义
- 全部写入单 Session 事务 flush/commit,后段异常整体回滚
- 幂等重建:不可变身份冲突拒绝覆盖、单调状态只前进不回退
- legacy raw-token 主键行一致时原子修复、冲突 fail-closed、已知 digest 禁二次哈希
- grant 缺 pending 时重建终态 APPROVED request 满足外键
- history 完整 canonical event digest 去重,近似事件保留
- APS_APPROVAL_MIGRATION_QUIESCED=1 停写门禁 + 同源锁 + 源 SHA-256 漂移回滚
- 验证:focused 19 passed、审批套件 88 passed、全量黄金 1065 passed/1 既有失败(与本轮无关)、ruff/diff-check 通过
This commit is contained in:
z.zhang 2026-08-20 10:09:04 +08:00
parent 3a73fdd766
commit e200074612
7 changed files with 2057 additions and 1 deletions

View File

@ -1,9 +1,844 @@
# 变更登记簿(CHANGELOG)
## 2026-08-20 — Round 67 审批迁移 Fail-Closed 与 Grant Digest 归一化(R67-B1)
- **类型**:fix + safety + test + docs
- **做了什么**(`server/agent_core/approval_migrate.py` 重写,矩阵 117 行剩余项):
- **Grant digest 归一化(CRITICAL 修复)**:迁移 grant 只持久化 `token_digest(raw_token)`,不再把文件后端 raw token 主键直接写入 DB `token_hash`;迁移后 active P3 grant 经 `DatabaseApprovalStore.consume_grant` 可消费一次、重放失败(修复前迁移产物完全不可消费)。
- **损坏 fail-closed(CRITICAL 修复)**:`load_file_store` 文件缺失维持空源语义;文件存在但 JSON 损坏 / 顶层 schema 错误 / pending/grant/history 记录非法(缺 expiresAtEpoch/action/tenantUuid/status/decidedAtEpoch)一律抛 `MigrationError`,不再静默当空源报告成功。
- **单事务原子性**:全部 request/grant/事件写入在同一个 SQLAlchemy Session 内 flush/commit;后段故障(故障注入验证)三表整体回滚。
- **幂等语义重建**:不可变身份字段(action/power/paramsHash/tenant/project/owner)冲突即 fail-closed 拒绝覆盖;单调状态只前进不回退——DB 端已 CONSUMED/EXPIRED 不被源端 ACTIVE 回退,源端更新则单调前进 DB(revision+1)。
- **legacy 修复**:旧版缺陷产物(raw-token 主键行)一致时在事务内原子 delete+insert 修复为 digest 行;不一致或 raw/digest 双行冲突 fail-closed;源键本身已是 64 位 hex digest 且 DB 已知时按已迁移跳过,绝不二次哈希。
- **终态 request 重建**:grant 缺 pending request 且 DB 无该 request 时,依据 grant 重建终态 APPROVED request(`migratedTerminalRequest=True` 诚实标注)满足外键与唯一约束。
- **history canonical digest 去重**:完整事件 canonical JSON SHA-256 替代旧六字段去重;同秒同状态但 approver/note/payload 不同的事件全部保留,完全重复事件幂等去重。
- **停写门禁**:迁移要求 `APS_APPROVAL_MIGRATION_QUIESCED=1`,持有与文件后端相同的 `{source}.lock` 进程锁直到提交;flush 后复核源文件 SHA-256,漂移即整体回滚。
- **影响分析**:GitNexus MCP 工具在本运行环境不可用,按 round-67 工作计划既有门禁(`load_file_store` LOW / `migrate_file_to_db` MEDIUM)+ 人工复核——两函数无生产端接线,仅测试调用,爆炸半径 LOW,无 HIGH/CRITICAL。
- **验证**:focused `test_approval_migration.py` **19 passed**(新增 14 项:digest-only、P3 消费一次/重放失败、损坏/schema/记录 fail-closed、身份冲突、单调不回退/源前进、legacy 修复/冲突、digest 键不再哈希、近似事件保留、源漂移回滚、后段故障回滚、停写门禁);审批相关套件(migration + database_store + store + delegate + transfer + role_policies + gov_api + harness_p3)**88 passed**;全量黄金三批 **1065 passed / 1 failed**(`test_preference_features::test_extract_features_from_world` 为 DXF-01 轮已记录的既有失败,与本轮无关);ruff 本轮两文件全绿;`git diff --check` passed。
- **数据保护**:全程使用临时审批文件与临时 SQLite;`server/data/**` 与 `.env` Git 状态干净,现场 `approvals.json`/`master.db` 未触碰。
- **文档同步**:本节、`docs/product/plan-completion-matrix.md`(矩阵 117 行证据与 §11 优先级)、`docs/round-67-approval-migration-state.md`。
- **遗留/边界**:数据库权威时钟、TTL/CAS 跨主机语义(R67-B2)、真实 MySQL 多主机断连/死锁故障注入、SAP outbound P3 evidence envelope、folder.schedule TOCTOU、Automation G4 payload 绑定仍属后续 P0;真实跨主机停写编排是部署验收,不在本地 SQLite 冒充完成。
- **发布动作**:本地 commit(用户授权);无 push/merge/publish。
---
## 2026-08-06 — DXF-01 工程图纸自动识别(解析→候选→P2 确认→主数据)
- **类型**:feature + test + docs
- **做了什么**:
- 新增 `server/aps_domain/dxf_drawing.py`(moduleId: domain-dxf-drawing):DXF → ParsedDrawing(版本/范围/图层/实体统计/标题栏/位置号/技术说明),字段级 source+confidence;标题栏三通道(块属性 0.9 / 右下角中文标签 0.6 / 文件名 0.5),文件名与图号交叉校验互证提置信。
- 主数据候选安全边界:物料编码/名称来自标题栏(类型/单位为推断值封顶 0.6);BOM 仅位置号索引(38 个),子件编码/名称/数量全部 missing;工艺路线 steps 空;材质/板厚/工时/外协/采购属性图纸未承载一律显式 missing + requiresConfirm,绝不编造。
- SVG 预览:ezdxf SVGBackend 白底渲染模型空间(样例 2.2MB DXF → 1.3MB SVG / 约 1.3 秒),drawingId 缓存复用(FIFO 8 份,上传临时文件随淘汰清理)。
- 多模态接线:`DxfDrawingExtractor`(kind=dxf_drawing, source=DXF_PARSE)注册进 default_registry;candidates_to_batches 对 dxf_drawing 显式 fail closed 并指引 /api/dxf/stage。
- 网关:`POST /api/dxf/parse`(上传)· `/parse-path`(本地路径)· `GET /api/dxf/{drawingId}/svg` · `GET /api/dxf/discover`(工程目录发现)· `POST /api/dxf/stage`(候选 → master.material/bom/routing.upsert 的 P2 确认卡,唯一执行路径仍是 /api/actions/confirm)。审计:dxf.parse(P0/TOOL)+ dxf.stage.*(P2/GATE,携带 drawingId 关联)。
- 依赖:ezdxf>=1.4(requirements.txt 与 pyproject.toml 同步)。
- **样例验证**:`demand/dxf-samples/5060102101-001-e.dxf`(MTU 12V 183 TB32 发动机安装图,AC1015 德英双语)——图号 506 010 21 01(与文件名互证 0.95)、标题 Motor 12V 183 TB32、比例 1:5、版本 e(文件名后缀+B_FREI 互证)、适用机型 OM 444 LA;WERKSTOFF 未填写保持空并标记 missing;位置号 38 个(14.001/06.020/15.108…);技术说明 9 条。
- **前端**:`apps/web/src/drawing/DrawingPanel.tsx`(moduleId: web-drawing,antd)——上传/路径解析/工程目录扫描、SVG 预览(滚轮缩放+拖拽平移+图层开关经 `?layers=` 服务端过滤渲染)、标题栏字段表(置信度徽标+来源)、物料候选可编辑表单 → `postDxfStage` P2 卡 → `postConfirm` 批准/驳回、BOM 位置号 missing 标注、工艺路线待确认提示;App.tsx 新增 RightTool `drawing` + 右侧导轨按钮 + Ctrl+K 命令面板条目。
- **验证**:`tests/golden/test_dxf_drawing.py` 14 passed(含图层过滤变体缓存与 `?layers=` 端点);受影响回归 test_multimodal + test_multimodal_image + test_master_data 合计 50 passed(两处注册表断言同步更新);全量黄金 1040 passed / 1 failed(test_preference_features 为既有失败,import server.knowledge.preferences,与本轮改动无关);`npm run build`(tsc -b + vite build)passed;ruff 本轮新增文件全绿(app.py 46 项与 multimodal.py TRY004 为既有基线,未扩散)。
- **文档同步**:gateway/app.py 端点清单、docs/architecture/harness.md 只读端点备案 + 确认暂存端点备案、本节。
- **影响分析**:GitNexus MCP 工具在本运行环境不可用,改走人工影响分析——新增模块零调用方;multimodal.py 三处增量分支(registry/build_raw/_value_preview/candidates_to_batches 错误提示);app.py 纯增量端点;两处 golden 断言扩展。爆炸半径 LOW,无 HIGH/CRITICAL。
- **遗留/边界**:真实德文明细表(Stückliste)块内表格识别、中文 CAD 标题栏样例覆盖、图元级选择高亮与字段↔图元双向定位、几何长度/面积推导下料尺寸均未实现,不宣称;自然语言入口(「识别这个图纸」意图路由)未接入意图管线;低置信字段必须人工确认的制度由 P2 门禁保证。
- **发布动作**:无 commit/push/merge/publish。
---
## 2026-08-03 — Round 66 Windows 求解器进程隔离与 Fail-Closed
- **类型**:fix + architecture + test + docs
- **做了什么**:
- CP/HYBRID native 优化迁入独立 child,采用 `aps.solver-process.v1`;父进程校验退出、fatal marker、timeout、协议、requestId/digest 与 runtime identity 后才物化。
- fatal/timeout/非法响应均 `UNAVAILABLE`、0 PO/WO、不可发布;父进程与 8003 存活、业务世界原子,HYBRID 不静默降级 RULE。
- runtime probe 增加来源、版本、user-site、fatal+exit0 与内部 timeout 门禁;默认 PATH Anaconda 稳定拒绝,`.venv`/`.sidecar-venv` 各连续 5 次通过。
- 源码 Sidecar `--solver-child -I` 真实冒烟首次发现隔离 Python 移除源码 import root;经 GitNexus LOW impact 修复为只 bootstrap 已解析的 trusted source root,并新增 `test_source_solver_child_entry_runs_under_isolated_python`。修复后 CP `OPTIMAL`、1 PO/10 WO/10 slots、协议 v1、`runtimeSafe=true`。
- **真实运行**:复制 `world.json` 的 CP/HYBRID 均 `OPTIMAL`、1 PO/10 WO/10 operationSlots;MOM 复制数据求解器虽 `OPTIMAL`,但受现有主数据阻断为 0 PO/WO、1 slot/1 conflict,不宣称完成现场主数据。fatal 注入 CP/HYBRID 均失败关闭;timeout 15.633 秒并回收 grandchild。
- **验证**:post-fix focused 80 passed;最终全量黄金 1029 passed / 2 warnings(300.23 秒);Node 60 passed;Web build、ruff、compile、node check、diff-check passed。
- **复审补强**:大请求改用自动清理临时文件作为 child stdin,避免 Windows 管道背压使 deadline 失效;严格拒绝越界 slot 与订单/产品错绑,并补错误 pipeline/status 回归。
- **首轮审计修复**:`AUDIT: FAIL` 暴露 success response 摘要未验证与 `OPTIMAL + 空 entries` 假绿;现强制重算 `responseDigest`,校验条目完整排列/业务字段、pipeline/status 和可行槽位覆盖,新增 16 个负向用例。
- **服务/UI**:8003 PID 21576、5173 PID 9808 healthy;浏览器 title=`APS 智能排产工作台`、root=true、无 warning/error。
- **数据保护**:`world.json` SHA-256 `C6E7FF090719DB505AF5C3B0D8ADD376CBA7843D6E6982A68C1BA02C342052FA`、MOM SHA-256 `6B64ADF6F29A43A518D89718610EE71FAD60E86D3F38CD5CFF26F1740CC53F7D` 前后不变;`server/data/**`、`.env` 无 Git 状态。`master.db` 可能更新 license `last_seen` 等运行元数据,不用文件 hash 冒充业务不变。
- **遗留/边界**:现有冻结 EXE 仅旧 `--probe-child` 通过,未重建,不能宣称新 `--solver-child` 已打包;仍需构建后冒烟、干净 Windows、签名/Defender/真实 CI。下一 P0 为全量 P2/P3 evidence 封套与审批迁移可靠性。
- **最终独立复审**:`AUDIT: PASS`;2 MiB 请求且 child 不读 stdin 的真实 deadline 复现为 0.736 秒,越界/错绑 slots 与错误 pipeline/status 均失败关闭。
- **文档同步**:`plan.md`、完成矩阵、`scheduling-v1.md`、Round 66 工作/并行计划、状态与运行证据。
- **发布动作**:无 commit/push/merge/publish。
---
## 2026-08-03 — Round 65 端到端闭环 APS 排产内核
- **类型**:feature + fix + safety + docs
- **做了什么**:统一商务订单/BOM/库存/供应/工艺/设备/班组/模具/层级负荷准入与 V2 校验;修复原子排产、P2 发布、P3 MES、WMS Saga、外部 Skill、sourceHash、订单页真实状态和订单 GET 副作用。
- **证据门禁**:P2/P3 evidence 绑定完整 V2 problem/solution/validation、活动/工单身份及当前多资源快照;维保、能力、班组、模具、寿命与层级容量漂移失败关闭。
- **前端/E2E**:6 阶段读取持久化闭环和精确 gate;MOM 刷新恢复阻断,正向 fixture 经 P2 发布、P3 双人审批并产生 MES 外部工单。
- **验证**:`pytest tests/golden -q` = 979 passed;`npm run test:node` = 60 passed;Web build passed;真实隔离 Playwright = 2 passed。
- **现场保护**:旧版订单 GET 曾在 2026-08-03 10:10:24 写现场 world;现已改为幂等投影 + 深拷贝预览。当前 MOM hash `6b64adf6...53f7d` 在隔离验收前后保持一致,未擅自回退。
- **文档同步**:`plan.md`、完成矩阵、features、scheduling-v1、harness、modules、锐扬 runbook、Round 65 状态与运行证据。
- **遗留**:39 缺工艺、67 模板/缺能力、可信到货承诺与真实 MES/WMS/SAP 凭据不可伪造;最终独立复审 `AUDIT: PASS`;未提交、未合并、未发布。
> **大管家的账本。** 每次改代码、加功能、改功能、修缺陷,同一轮必须追加一节。
> 写法见 [docs/README.md §2.2](./README.md)。旧名:`PROGRESS.md`(已迁入本文件)。
---
## 2026-08-02 — 锐扬 Excel 结构化诊断与真实行号收口(round-63)
- **类型**:bugfix + data quality observability。
- **解析纠偏**:设备编码别名采用“最后一个非空值优先”,后续空列不再覆盖已有编码;补 `固定资产编号` 别名。47 行原解析噪声恢复为有效设备行。
- **结构分类**:`02-工艺模型` Excel 第 5 行二级序号表头、`07-设备模型` 4 行内嵌 section 归为 `ignorable_section`;非排产整块归为 `NON_SCHEDULING_BLOCK/ignored`。
- **诊断契约**:batch / preview / folder 新增 `diagnostics`、`diagnosticCounts`、`diagnosticsVersion=1`,每条含 sheet、真实 Excel 行号、parserIndex、kind、code、severity、classification、message、rawSummary;旧 errors/warnings/count 字段保持兼容。
- **现场结果**:锐扬目录(主表 + `订单-样例.csv`)为 **有效 1404 / blocking 1 / skippedRows 974**;diagnostics 为 ignored 979 / warning 181 / blocking 1。唯一阻塞定位到 `07-设备模型` Excel 第 130 行“3.8吨锂电叉车”,三个业务编码均空;出厂编号不冒充设备编码。
- **用户可读输出**:文件问题列直接显示真实 Excel 行号;Markdown 追加 5 行结构忽略、134 台 GENERAL 兜底、47 行别名恢复说明。
- **回归**:结构化 XLSX 缩比测试、folder 聚合契约、日期稳定性与既有导入/指导测试通过;全量黄金基线随 round-64 一并验证为 910 passed。
## 2026-08-02 — MRP 多层 BOM 自制/采购纠偏 + 分阶段界面(round-64)
- **类型**:bugfix + UX semantics(订单分解 / MRP)。
- **根因**:旧逻辑把“有默认工艺路线”错误等同于“自制”,并要求 `MAKE && hasRouting` 才继续展开;锐扬 MOM 导入又只为前 80 个自制候选生成模板路线,导致缺路线半成品被改写为采购并停止下钻。前端同时隐藏 `make`、把 DRAFT 建议标成“采购订单”,分解后还自动跳采购页。
- **后端修复**:显式 `procurementType` 优先;BOM 父项/成品/半成品即使缺工艺仍按 MAKE,生成 `missingRouting` 阻断提示并继续多层展开;同订单同物料聚合后只扣一次库存/在途;持久化独立 `makeSuggestions`,不写入/伪造 `productionOrders`;路线进一步区分 READY / TEMPLATE / MISSING。
- **前端修复**:新增“自制建议”页签,分解后默认打开;“生产订单”改为“已排生产”,“采购订单/委外订单”改为“采购建议/委外建议”;模板工艺、缺工艺分别标识,明确建议与真实生产订单的阶段边界。
- **真实验证**:`MOM--00280` 完整展开为 **106 项唯一自制建议 / 124 条采购建议 / 0 委外**;其中 **67 项仅模板工艺待确认、39 项缺工艺路线**;124 条采购全部为 `RAW_MATERIAL`,采购中半成品为 0;`B0050121-22750` 已从采购移入自制,且继续展开出原材料 `C0030800-04179`。
- **回归**:`tests/golden/test_mrp.py`、`tests/golden/test_sourcing_decompose.py` 聚焦 **10 passed**;Web `npm run build` 通过;8003/5173 均 200;在真实浏览器完成重分解和页签/文案核验。
- **边界**:当前 67 条路线来自导入模板,仅允许试排,不应视为现场已确认工艺;39 条缺路线项必须补齐主数据后才能形成可执行生产工单。
## 2026-08-01 — trace_chain 接入 schedule.run
## 2026-08-01 — trace_chain 跨引擎可复算(round-16 补充)
## 2026-08-01 — EXTERNAL 引擎接入 trace_chain + 前端追溯 UI(round-16b)
## 2026-08-01 — 对话状态与世界状态成对快照(round-17)
## 2026-08-01 — 审计完整性告警通知渠道(round-18)
## 2026-08-01 — 审计完整 Merkle 树(round-19)
## 2026-08-01 — 审计 append-only 守护:跨进程锁 + 清理策略(round-20)
## 2026-08-01 — 偏好学习升级:项目隔离/解释/重置/冷启动(round-21)
## 2026-08-01 — Explore 与 Runtime 数据边界(round-22)
## 2026-08-01 — L0-L3 Plan 重放能力(round-23)
## 2026-08-01 — 统一工具运行时(LLM 只有提议权,round-24)
## 2026-08-01 — 分支树:项目->会话->分支三级树(round-25)
## 2026-08-01 — 分支切换自动恢复锚定检查点(round-27)
## 2026-08-01 — 偏好解释 UI + stub 可复算修复(round-28)
## 2026-08-01 — 主数据工艺树升级为树形表格 + 物料分类/来源修复(round-29,产品向 sub agent 实施)
## 2026-08-01 — L0-L3 Plan 批准/驳回审计(round-30)
## 2026-08-01 — Agent 自动编排:plan 节点驱动 workflow 门禁(round-31)
## 2026-08-01 — 偏好时间衰减加权模型(round-32)
## 2026-08-01 — Explore 通道扩展:敏感性/鲁棒性接入统一边界(round-33)
## 2026-08-01 — 多智能体并行交付:会话引用/四层上下文/自动化G0-G4/分支树UI/时间线微观层(round-34,3 个并行智能体)
## 2026-08-02 — 多智能体并行交付·第二批:CP-SAT 工序级/算法库+参数优化/MCP总线+异步任务(round-35,3 个并行智能体)
- **类型**:feature(多方向并行,plan.md §9.2/§9.5/§9.8/§7/§6.8)
- **做了什么**(三个智能体并行,写范围互不重叠):
- **方向 D(Gauss)· 矩阵 84**:`cp_engine.py` 工序级模型——每工序 OptionalIntervalVar 按工艺路线串联(C1)、同工位 NoOverlap(C2)、换型矩阵 next-link 单链(C10)、冻结窗 FixedSizeIntervalVar(C11);solverMeta 增 operationSlots/totalChangeoverMin/frozenCount。
- **方向 E(Ramanujan)· 矩阵 86/87**:`algolib.py` 算法库注册表(A-E 分类+元数据+健康+版本+种子);`param_opt.py` 参数优化闭环(训练/验证隔离→回放→灰度→退化回滚+敏感性衔接)。
- **方向 F(Sartre)· 矩阵 75/88**:`mcp_bus.py` MCP 插件总线(manifest/工具契约/权限 fail-closed/健康/启停/版本兼容+Skill 桥接)+ 网关 /api/mcp/plugins*;`async_jobs.py` JobQueue 异步重算 + /api/jobs* + SkillConsole「MCP 插件」「异步任务」页签。
- **为什么**:按 plan.md 蓝图继续拆分边界并行推进矩阵 Missing/Partial 项。
- **验证**:三方向黄金测试 32 项(cp 6 + algolib 5 + param_opt 8 + mcp_bus 8 + async_jobs 5)通过;前端 `npm run build` 通过;CP/HYBRID 冒烟 OPTIMAL 且契约不变。
- **边界**:Cumulative 班组容量、ML/元启发资产落地、网关 P2 门禁接线、真实 MES/WMS 适配器、分布/置信区间留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第三批:LNS 插单/IIS 冲突归因/前端世界分区+diff+回放(round-36,3 个并行智能体)
- **类型**:feature(多方向并行,plan.md §9.11/§9.2b/§4.5/§9.4)
- **做了什么**(三个智能体并行,写范围互不重叠):
- **方向 G(Locke)· 矩阵 90**:`server/aps_domain/lns.py` LNS 插单局部修复器——固定窗口(默认急单交期 ±36h,`timeWindowHours=72`)内最小扰动邻域搜索(候选槽 + 插入右移级联,窗口外订单冻结 `windowOuterMovedCount=0`),扰动度量 movedOrderCount/shiftedHours/tardinessDelta/disturbanceRatio;受影响数/扰动率/窗口宽度超阈值 → ESCALATE 升级信号;`rush.evaluate` 附加 `impact.lns`(P1 沙盒,`payload["lns"]=False` 可关)、`rush.apply` 出卡携带 lns(P2 门禁)、`execute_confirmed` 路由 LOCAL→`apply_lns_local`(DRAFT+LNS_RUSH+LOCAL_REPAIR)/ ESCALATE→`apply_rush` 全量重排(audit 记 mode+escalateReasons);`test_rush_lns.py` 9 项。
- **方向 H(Lovelace)· 矩阵 89**:`server/aps_domain/attribution.py` 冲突归因——`detect_iis()` 约束级 IIS 最小冲突集(黄金用例验证删除任一约束后恢复可行,可行输入返回空+说明)、`attribution_for_schedule()` 结构化归因报告(evidence conflictId+versionId+日志描述可复核)、中文归因文案(约束名/订单号/资源/短日期/解开方式,不含 IIS/shadow 等数学术语)、近似影子价代理量(isShadowPriceProxy 显式标注,真实对偶价需 LP 重解);只读复用 `hard_blocking_conflicts`/`get_constraint_profile`,未改既有接口;`test_attribution.py` 7 项。
- **方向 I(Sagan)· 矩阵 54/55**:前端 Explore 与 Runtime 视觉分区(`WorldModeBadge.tsx` 世界种类徽标+只读角标、ViewportPanel 工具栏徽标+画布着色 world-explore/runtime/analysis、时间线根节点徽标、FlexPanel「沙盒试排」徽标);`WorldDiffView.tsx` 两个世界并排对比(基准 A × 对比 B:当前世界/版本快照/检查点/回放定位,订单工序时间/设备/模具/KPI 逐字段 diff 高亮+变化汇总);`AlgoReplayPanel.tsx` ALGO_RUN 动作链逐步回放(复位/上一步/播放/下一步/末步,traceSummary 链+检查点锚点+「对比此步前后世界」联动);`npm run build` 无类型错误。
- **为什么**:按 plan.md 蓝图继续把 Missing/Partial 矩阵项分发给并行智能体,保持写范围互不重叠、智能体可互相沟通。
- **验证**:新增黄金测试 16 项(rush_lns 9 + attribution 7)+ 相关回归全绿;全量回归 `618 passed`(隔离 `APS_KNOWLEDGE_PATH` 规避知识库环境污染);后端 8003 以新代码重启健康、前端 5173 构建通过。
- **边界**:真实 LP/CP 对偶影子价需线性化模型重解;CP-SAT 原生 IIS 与 flex 轨归因接入;跨产线 LNS 邻域需引擎 forcedLineId;网关 rush.apply 透传 lns 参数;检查点世界侧投影接口 `GET /api/checkpoints/{pairId}`(前端已降级展示);分支合并世界 diff、@知识 引用、滚动摘要 LLM 压缩、策略特征规则归纳留待后续轮。
- **类型**:feature(多方向并行,plan.md §4.7/§4.8/§5.3/§4.6/§6.10)
- **做了什么**(三个智能体并行,写范围互不重叠):
- **方向 A(Dirac)· 矩阵 63/64**:`server/agent_core/session_ref.py`(`@会话/@方案/@版本/@报告` 引用解析 + 按时点快照取值 + 证据链 refs + 删除/无权显式失败);`server/agent_core/context_policy.py`(四层上下文 ContextBudget + 滚动摘要可溯源 + pin 钉住)。
- **方向 B(Ptolemy)· 矩阵 76**:`server/agent_core/automation.py`(G0-G4 档位状态机 + AutomationGate 升权门禁 P3 双人 SOD + 事件/定时 cron/阈值三类规则 + 调度器 pause/resume/replay + AUTOMATION 审计)。
- **方向 C(Huygens)· 矩阵 51/54**:前端 `BranchPanel.tsx` 分支树 UI(fork/rename/switch/discard/merge + 活动高亮)+ `TimelineRail.tsx` 时间线微观层(Plan/ALGO_RUN traceSummary/审计证据 refs)+ 分支 API client。
- **为什么**:按 plan.md 蓝图拆分边界分发多智能体并行推进矩阵 Missing/Partial 项。
- **验证**:三方向黄金测试 20 项(session_ref 5 + context_policy 6 + automation_gears 6 + branch_tree 3)通过;前端 `npm run build` 通过;浏览器实测分支树 fork/merge/discard 与微观层正常。
- **边界**:接线(上下文到对话链路、自动化到网关定时/真实动作、引用自动触发)、`@知识` 引用、调度器持久化、两世界 diff 留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第四批:检查点投影/`@知识`引用/滚动摘要LLM/偏好特征规则(round-37,3 个并行智能体 + 主控 L66)
- **类型**:feature(多方向并行,plan.md §4.4/§8.1/§8.3/§8.5)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控实现矩阵 66):
- **方向 J(Darwin)· 矩阵 54 收口**:`checkpoints.world_projection(pair_id)` 世界侧轻量投影(summary+固定/柔性工单+产能池,仓内数据零改动)+ `GET /api/checkpoints/{pairId}`(缺失 404 中文)与 `GET /api/checkpoints?pairIds=&branchIds=&sessionId=`(分支锚定批量投影);前端 `fetchCheckpointWorld` + `WorldDiffView` 检查点 B 侧优先投影逐字段全量 diff,失败回退元数据/近似快照降级;`test_checkpoint_projection.py` 5 项。
- **方向 K(Maxwell)· 矩阵 63 收口**:`session_ref.py` 第 5 类 `@知识` 引用(强制命中:命中→正文+出处+证据 ref,未命中→显式中文 RefResolutionError 不编造,未接线→显式 not-wired);`resolve_ref/resolve_all` 新增 keyword-only `knowledge_lookup`(既有 4 类零破坏);workflow `handle_intent` 入口自动解析注入(`_refs` + SESSION 审计 evidenceRefs,失败 fail-closed);`test_ref_knowledge.py` 9 项。
- **方向 L(Galileo)· 矩阵 64 收口**:`summarizer.py` LLM 滚动摘要压缩生成方(`compress_scroll_summary` 固定中文提示词 + ModelProvider.chat_text,不可用/异常/超预算→确定性回退绝不编造);`context_policy.scroll_summary` 可选 summarizer(compressedBy 溯源);gateway chat 每轮预算门控钩子(autoSummarize 开关,try/except 兜底不阻断对话);越权上下文 fail closed 测试补齐;`test_scroll_summary_llm.py` 14 项 + `test_context_policy.py` +3 项。
- **L66(主控实现)· 矩阵 66 收口**:`preferences.py` 多特征规则归纳——`extract_features(world)`(订单数/VIP占比/急单占比/品种数/硬冲突数)、分箱条件规则 `induced_rules`(采用2/试排1 条件得分+支持度)、`preferred_strategy_with_features`(规则投票命中→回退全局)、`explain_features` 中文规则依据;workflow/app 两处 `record()` 调用点接线 features;`test_preference_features.py` 6 项。
- **为什么**:按 plan.md 蓝图继续把矩阵剩余 Partial 项收口,保持多智能体并行、写范围互不重叠。
- **验证**:round-37 相关黄金测试 61 项通过;全量回归 `656 passed`(隔离 `APS_KNOWLEDGE_PATH` 规避知识库环境污染);前端 `npm run build` 无类型错误;后端 8003 新代码重启健康。
- **边界**:`test_guidance.py::test_workflow_guidance_and_kb_miss` 为既有知识库漂移环境失败(默认 knowledge.json 库容漂移使 miss 查询可命中,隔离路径下全绿);summaries 历史上限裁剪、@知识 标题首条子串命中依赖标题质量、检查点批量投影按分支锚定留待后续轮。
- **类型**:feature / security
- **做了什么**:`run_sensitivity` 与 `run_monte_carlo` 改为经 `explore_boundary.run_explore` 统一通道执行(fn 只拿深拷贝沙盒、主干永不外泄写引用,内部子沙盒互不污染);保留既有函数签名与输出结构(sensitivity rows/monteCarlo、monte carlo outcomes)。
- **为什么**:矩阵 55 行「Explore 与 Runtime 数据边界」剩余项「敏感性/鲁棒性等其他 Explore 工具逐一接入统一通道」。
- **验证**:`test_explore_boundary_extension.py` 3 项(sensitivity 零污染、monte carlo 零污染、只读视图 fail closed)通过;真实世界数据冒烟:sensitivity 返回 rows/monteCarlo、monte carlo 3 trials、主干逐字节不变;场景相关 15 项全绿。
- **边界**:前端 Explore 与 Runtime 视觉分区留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第五批:分支diff/plan回退/WMS缺料闭环/工具运行时收口/蒙特卡洛配置(round-38,3 个并行智能体 + 主控 L88)
- **类型**:feature(多方向并行,plan.md §4.4/§4.5/§9.9/§6.10/§5.3)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控实现矩阵 88):
- **方向 M(Gibbs)· 矩阵 51/52 收口**:`branches.compute_world_diff`(两分支锚定 checkpoint 世界侧稳定 JSON 差异:版本/KPI/工序时间/柔性版本/产能池,merge 响应附 diff + 审计落库,前端 BranchPanel 合并前预览);PlanStore 分支维度——`plans_snapshot`/`restore_for_branch` 可恢复接口(plan 视图随 checkpoint 归档物化,分支切换自动回退/恢复,latest/node_at/versions/create/transition 视图感知,无锚定完全兼容);`test_branch_merge_diff.py` 4 项 + `test_plan_branch_restore.py` 5 项。
- **方向 N(Euler)· 矩阵 74/77 收口**:`wms_stub.py` MockWMS(库存台账+缺料事件流+幂等+单调 inventoryVersion);`wms_events.py` 事件消费管线(eventId 全局一次消费/乱序 seq 缓冲/重复证据、库存版本进证据链、WMS-EVENT 冲突同步);缺料→重排端到端闭环:事件→影响半径(沙盒差分只读)→Explore 方案卡(复用 flex.reschedule P2)→人工确认(L4 重排)→MES 下发(P3 双人 SOD)→回执(wmsReceipts+审计全链证据);`POST /api/wms/events` + `GET /api/wms/replay` + `GET /api/wms/inventory`;`test_wms_events.py` 3 项 + `test_wms_replay.py` 3 项。
- **方向 O(Zeno)· 矩阵 111 收口**:`tool_runtime.run_tool_async` 异步入口;scenario.apply 由直连 handle_intent 改为经工具运行时(未登记拒绝+TOOL 审计,响应契约保留;source-level 盘点确认其为唯一直连端点);GovConsole「工具调用」视图(TOOL 审计条目:意图/等级/执行者/时间/结果 放行·确认卡·拒绝);`test_tool_runtime_gateway.py` 5 项。
- **L88(主控实现)· 矩阵 88 收口**:`robustness.py` 可配置分布(normal/uniform/triangular 逐维 + shockProbability)、`correlation`(-1..1 需求-效率扰动相关,rho=0 随机流与旧实现种子兼容)、`confidence_level` 经验分位置信区间(tardinessCI/mean/std)+ config 透明元信息;`test_monte_carlo_config.py` 7 项。
- **为什么**:按 plan.md 蓝图继续把矩阵 Missing/Partial 项收口,多智能体并行、app.py 按区段划分(merge/switch、WMS 事件、scenario.apply 互不重叠)。
- **验证**:round-38 相关黄金测试 57 项通过;全量回归 `683 passed`(隔离 `APS_KNOWLEDGE_PATH` 规避知识库环境污染);前端 `npm run build` 无类型错误;后端 8003 新代码重启健康(/api/wms/* 已注册)。
- **边界**:Saga/补偿(矩阵 78/119)留待下轮(幂等键/P3 门禁/回执审计已打地基);WMS 为 Mock 形态(真实适配器需集成环境);plan 视图为 store 级单例(多会话并行切换以最后一次为准);scenario.apply 未登记拒绝返回 200+拒绝文案(契约保留)。
- **类型**:feature / algorithm
- **做了什么**:`PreferenceStore` 从简单计数升级为**时间衰减加权评分**:每样本得分 = 来源权重(采用2/试排1)× 时间衰减(2^(-age_days/半衰期14天)),偏好随最近样本漂移、久远信号自然衰减;评分舍入 2 位小数保持当天信号≈整数的既有语义;`explain` 置信度增加「最高分与次高分接近 → medium」判定。
- **为什么**:矩阵 66 行「偏好学习影响默认策略」剩余项「偏好模型升级为权重回归/规则归纳」的权重回归部分。
- **验证**:`test_preference_decay.py` 4 项(新样本主导、半衰期≈0.5、当天≈整数、来源权重叠加)通过;既有偏好测试 10 项全绿。
- **边界**:策略特征规则归纳(多特征回归:订单结构/客户等级/约束 → 策略)留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第六批:Saga补偿/13类约束目录/trace断链可视化/算法目录收口(round-39,3 个并行智能体 + 主控 L86)
- **类型**:feature(多方向并行,plan.md §5.3/§9.2a/§9.5/§9.10/§10.4)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控实现矩阵 86):
- **方向 P(Hypatia)· 矩阵 78/119 收口**:`saga.py` SagaCoordinator——状态机(PENDING/RUNNING/WAITING_HUMAN/SUCCEEDED/FAILED/COMPENSATING/COMPENSATED/MANUAL_TAKEOVER)+ SagaStep(幂等键/超时/重试/补偿/gate)+ 恢复队列(world["sagas"] 持久化 + resume_all 重放)+ 补偿注册表(失败逆序补偿链,补偿失败→人工接管)+ 写前快照回滚锚点;WMS 缺料闭环接入(run_shortage_saga 事件级 dedupe)+ `mes.cancel_dispatch`/`cancel_work_order` 幂等补偿;`/api/saga/*` 列表/详情/retry/compensate/takeover;`test_saga.py` 13 项 + `test_saga_wms_closure.py` 6 项。
- **方向 Q(Jason)· 矩阵 113 收口**:13 类约束目录齐全(C1-C13,新增 C9、C12 归位、goldenCase/kindConfigurable)+ `constraint_catalog()`;四引擎(RULE/CP/HYBRID/GA)硬约束语义一致黄金测试(EQUIPMENT/CAPACITY/MATERIAL 三态,未发现不一致);`ConstraintProfileDenied` 门禁拒绝三类关闭硬约束路径 + write_audit DENIED(LLM 提案同门禁);`test_constraint_catalog.py` 19 项 + `test_engine_constraint_semantics.py` 19 项。
- **方向 R(Poincare)· 矩阵 114 剩余收口**:`verify_audit_trace` 审计链断链检测(expected/actual/reason)+ workflow 落 traceItems(canonical 全量条目精确重算)+ `/api/gov/audit` traceAlerts + `GET /api/gov/trace/verify` + GovConsole ⚠断链徽标/顶部提示条;**EXTERNAL 真实 HTTP skill 端到端验收**(uvicorn stub + HTTP skill → flex.schedule.external 真实 POST → trace 可复算);`test_trace_break_visual.py` 6 项 + `test_trace_external_http.py`。
- **L86(主控实现)· 矩阵 86 收口**:`algolib.py` 目录完整性——LNS 随 round-36 交付翻转为 available=True(entrypoint + test_rush_lns.py 黄金测试)、ML/元启发未落地资产如实登记 available=False(健康检查如实报告);`test_algolib_catalog.py` 4 项(A-E 齐全/ML元启发如实登记/query 过滤)。
- **为什么**:按 plan.md 蓝图继续把矩阵 Missing/Partial 项收口,多智能体并行、app.py 按区段划分(/api/saga、/api/gov、无冲突)。
- **验证**:round-39 相关黄金测试 111 项通过;全量回归 `755 passed`(隔离 `APS_KNOWLEDGE_PATH` 规避知识库环境污染;Poincare 标记的批量顺序相关失败未在全量中出现);前端 `npm run build` 无类型错误;后端 8003 新代码重启健康(/api/saga/*、/api/gov/trace/verify 已注册)。
- **边界**:P3 双人确认自动代行需生产接真实操作员授权(auto_approve=False 停 WAITING_HUMAN 人工确认);旧审计记录无 traceItems 仅结构校验(新记录哈希级比对生效);约束拒绝分支 store.save 持久化建议补;真实 MES/WMS 适配器(矩阵 73/74 现场对接)与 WORM/加密、信创、CI/CD 等外部环境依赖项留待后续。
- **类型**:feature / governance
- **做了什么**:新增 `server/agent_core/plan_orchestration.py`:P2 动作出确认卡时自动创建 L0 意图→L1 策略→L2 决策节点链(`stage_plan_node`,同 action 父链幂等复用);批准/驳回时流转 L2 节点状态(`decide_plan_node`,APPROVED/FAILED,按 confirmId 匹配);接入 harness.stage_confirmation 与 workflow.execute_confirmed(尽力而为,失败仅记日志不阻断门禁);修复 PlanStore `_load` 的 regenCount 校验(状态流转版本 regenCount 不变,原校验要求连续递增会误判 corrupt)。
- **为什么**:矩阵 110 行「L0-L3 Plan」剩余项「接入 Agent 自动编排(plan 节点驱动 workflow 分支)」。
- **验证**:`test_plan_orchestration.py` 4 项(出卡建链、批准/驳回流转、重启加载有效、父链复用)通过;API 端到端:排产→发布出卡→批准→L2 节点 DRAFT→APPROVED v2(历史不可变);plan 相关 20 项全绿。
- **边界**:L3 动作级节点驱动具体执行(编排链进一步细分)留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第七批:自动化网关驱动/NSGA-II多目标/可重生流水线/桌面壳决策(round-40,3 个并行智能体 + 主控 L98/L110)
- **类型**:feature(多方向并行,plan.md §6.10/§9.7/§10.2/§14)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控决策收口):
- **方向 S(Ptolemy)· 矩阵 76 收口**:`automation.py` 业务动作注册表(commit→schedule.publish 发布、reschedule→flex.reschedule 重排,经 AutomationGate 升权 + 既有门禁;未登记 P2/P3 动作 G4 也按 P3 fail-closed 拦截)+ 调度器持久化(宽容加载重启恢复)+ 网关定时驱动(POST /api/automation/tick + lifespan 后台自动 tick,可配开关);`run_automation_intent` 走 execute_confirmed 真实执行;`test_automation_gateway.py` 6 项 + `test_automation_gears.py` +2。
- **方向 T(Archimedes)· 矩阵 91 收口**:`rebuild_orchestrator.py` 可重生流水线——候选构建(scan_modules+黄金测试映射)、隔离运行(subprocess + APS_* 随机临时目录跑 pytest 子集)、安全扫描(S1 阻断 + S2/S3/S4 告警)、灰度(GRAY shadow + gray_probe)、失败自动回滚(黄金失败/灰度探针失败恢复基线)、审计;registry.py 候选版本登记 + 激活指针;`/api/rebuild/*` 4 端点;`test_rebuild_pipeline.py` 10 项 + `test_registry.py` 6 项。
- **方向 U(Pauli)· 矩阵 85 收口**:`nsga2_engine.py` 真实 NSGA-II——快速非支配排序+拥挤度+锦标赛+精英保留、工序级染色体(OX/PMX 保可行性+工序交换/资源迁移变异)、3 目标(加权总延迟/容量冲突/负载均衡)输出 Pareto 解集、`nsga2_defaults` 参数模板 + manifest、`get_engine("NSGA2")` 集成 + `solve_nsga2`、`nsga2_solutions_to_cards` 衔接 rank_scenarios、大算例基线(35 订单 50 代 <15s 不劣于单目标 GA);时间预算确定性修复(小预算跑满固定代数与 GA 口径一致);algolib nsga2 条目翻转为可用;`test_nsga2_engine.py` 11 项。
- **L98(主控决策)· 桌面壳收口**:架构决策——接受 Electron 为当前桌面壳(apps/desktop 已实现 + 打包冒烟),plan.md 目标态同步修订(Tauri 列为备选不阻塞),矩阵 98 置 Done。
- **L110(主控补正)**:Plan 不可变/可版本化/可局部重生(矩阵 110 剩余=无,状态补正 Done)。
- **为什么**:按 plan.md 蓝图继续把矩阵剩余项收口,多智能体并行、app.py 按区段划分(/api/automation、/api/rebuild,无冲突)。
- **验证**:round-40 相关黄金测试 65 项通过;全量回归 `791 passed`(隔离 `APS_KNOWLEDGE_PATH` 规避知识库环境污染;NSGA-II 时间预算确定性修复后全绿);后端 8003 新代码重启健康(/api/automation/status、/api/rebuild 已注册);文档门禁通过。
- **边界**:自动化规则 CRUD UI、NSGA-II 冲突目标物化层对齐、rebuild 激活版本物理替换、真实 MES/WMS 适配器、信创/CI-CD/WORM 等外部环境依赖项留待后续。
- **类型**:feature / governance
- **做了什么**:`PlanStore.transition_status`:Plan 状态流转(DRAFT→APPROVED/FAILED/RUNNING/SUPERSEDED 等合法边),追加新版本承载状态变化(version+1、regenCount 不变、inputsHash/payload 不变保证历史不可变),evidenceRefs 追加 `status:<新状态>`、note 写入 payload._note(审计留痕);非法流转抛 `PlanTransitionError`;`POST /api/plans/{id}/approve` / `/reject` 端点。
- **为什么**:矩阵 110 行「L0-L3 Plan」剩余项「批准/驳回审计」。
- **验证**:`test_plan_transition.py` 4 项(批准追加版本+证据、驳回带 note、非法流转拒绝、连续流转历史不变量)通过;plan 相关 16 项全绿。
- **边界**:Agent 自动编排(plan 节点驱动 workflow 分支)留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第八批:CP Cumulative/参数优化门禁/前端E2E+Sidecar冒烟+CI矩阵/审批迁移(round-41,3 个并行智能体 + 主控 L112)
- **类型**:feature(多方向并行,plan.md §9.2a/§9.8/§14.3/§3.3)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控实现矩阵 112):
- **方向 V(Herschel)· 矩阵 84 收口**:`cp_engine.py` Cumulative 班组/工装聚合容量(C12:跨工位并行工序占用 ≤ 可用容量,teamId/toolingId 主数据接线,personnel+剖面双闸,solverMeta.cumulative 含 peakConcurrent/unwired 诚实报告);gap(objective/bestBound/None 语义)+ 时限(确定性超时 FEASIBLE 契约)+ 性能基线(n=12 <30s);`test_cp_cumulative.py` 8 项。
- **方向 W(Bacon)· 矩阵 87 收口**:`param_opt.py` propose(P1 触发)+ record_observation 线上观测(连续劣化自动回滚+审计+落盘 paramObservations)+ P2 门禁接线(promote 经 harness 确认卡,验证回放 finalize GRAY→FULL,拒绝留 GRAY;harness _POWER_MAP 登记 param.*);`/api/params/*` 5 端点;`test_param_opt_gateway.py` 9 项 + `test_param_opt.py` +5。
- **方向 X(Boole)· 矩阵 118 部分 + 99 本地部分**:完整前端 E2E(apps/web/e2e + @playwright/test,preview 模式,3 用例含登录→跑一版柔性排产→结果块→世界对比/柔性工作台→命令面板,`npm run test:e2e` 本地 3 passed);`scripts/smoke-sidecar-local.mjs` 本地冒烟 5/5(20 次冷启动/端口冲突/崩溃重启/父进程看门狗/中文空格路径);`.github/workflows/ci.yml` 5 job 构建矩阵(lint/前端构建 Win+Ubuntu/后端 golden 隔离 Win+Ubuntu/E2E/SBOM);`docs/development/e2e.md`。
- **L112(主控实现)· 矩阵 112 部分收口**:`server/agent_core/approval_migrate.py` 文件→DB 存量迁移——幂等(重复 confirmId/token_hash/事件内容去重跳过)、dry_run 不动库、缺文件空源、未配 database 后端 fail closed、迁移后经 database 后端可读回;`test_approval_migration.py` 6 项。
- **为什么**:按 plan.md 蓝图继续把矩阵剩余项收口;E2E 实测发现 dev 模式 React StrictMode 吞 refreshWorld/captureSessionResult 状态更新(preview 生产构建不受影响,已记录 docs/development/e2e.md 建议前端跟进)。
- **验证**:round-41 相关黄金测试 93 项通过;全量回归 `818 passed`(隔离 `APS_KNOWLEDGE_PATH`);前端 `npm run test:e2e` 3 passed + `npm run build` 通过;sidecar 本地冒烟 5/5;后端 8003 新代码重启健康(/api/params 已注册)。
- **边界**:签名/SBOM 上链/Defender 入 CI 与干净离线机安装冒烟(需外部证书/验证机);WORM 归档与 MySQL 实机故障注入(外部环境);观测入口为接口驱动(每次排产自动采样后续一行接线);CI workflow 本地 YAML 校验通过但未在真实 GitHub 仓库跑通。
- **类型**:feature / fix / product
- **做了什么**:主数据「工艺路线」由 antd Tree 改为 **树形表格**(列:名称/编码/类型/来源/用量/图号,行展开折叠);后端 `build_process_tree` 修复三缺陷——BOM 去重(flexBom 与经典 bomItems 双份合并去重)、半成品推导(名称含组件/总成/支架且非标准件 → typeLabel=半成品)、工序补名称(flexOperations/operations 补齐 operationName);节点新增 `typeLabel`/`sourcingLabel`(自制/委外/采购)/`operationName` 等字段(契约只增不删)。
- **为什么**:现场计划员反馈——要树形表格而非纯树;物料必须分原材料/半成品/成品;来源必须分生产(自制)/委外/采购,否则无法排产判断。
- **验证**:`build_process_tree("28200003654300")` 去重 0 重复(22 物料)、半成品 3 项、工序缺名 0、来源缺失 0;`npm run build` 通过;sourcing/master 相关 16 项测试通过;浏览器实测外壳产品树形表格 578 行、列头/徽标/层级/展开折叠正常。
- **边界**:无工艺路线的推导半成品来源按既有规则显示采购(若现场确认为厂内自制需在 infer_material_sourcing 增加按 typeLabel 覆盖);默认展开策略可按现场反馈收紧。
---
## 2026-08-02 — 多智能体并行交付·第九批:多模态低置信确认/WORM归档模拟/前端StrictMode修复/roadmap对齐(round-42,3 个并行智能体 + 主控文档收口)
- **类型**:feature(多方向并行,plan.md §6.7/§10.4/§12)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控 roadmap 对齐):
- **方向 Y(Carver)· 矩阵 101 框架落地**:`multimodal.py` 多模态管线——Extractor 协议 + ExtractorRegistry(未注册 kind 显式报错不编造,register() 为真实 ASR/VLM 接入入口)+ stub 提取器(文件导入/文本模板)+ **低置信确认门禁**(不完整显式拒绝、高置信直通、低置信 P2 确认卡,确认前绝不写世界状态)+ 审计(extract/stage/apply/reject);`/api/multimodal/extract|ingest`;`test_multimodal.py` 20 项。
- **方向 Z(Goodall)· 矩阵 104/116/112 WORM 部分**:`audit_ledger.py` WORM 归档模拟——archive_old 归档即密封(`.worm-manifest.json`:sha256+merkleRoot+coverage+readonly,chmod 只读 + 密封写守卫,同月不覆写自动分流)+ `verify_archive()`(篡改/缺失/merkle 复算不一致检出 + summary);`GET /api/gov/archive/verify` + `POST /api/gov/archive/run`;`test_audit_worm.py` 7 项。
- **方向 AA(Hooke)· 矩阵 118 dev E2E 修复**:根因定位——StrictMode 模拟卸载把 `alive` ref 永久置 false(effect 体为空只返回 cleanup)+ `captureSessionResult` 在 setWorld 更新器内(杂质);修复:alive effect 体内复位 + worldRef 镜像把捕获移出更新器;**dev 模式 E2E 3 passed**(修复前必失败)+ preview 3 passed 无回退;docs/e2e.md 缺陷状态更新。
- **主控·文档收口**:roadmap.md 对齐(13 类约束目录已落地、NSGA-II/CP Cumulative 已交付、引擎演进更新);矩阵 101 由 Missing 转 Partial(框架完成、真实 ASR/VLM 外部)。
- **为什么**:按 plan.md 蓝图继续推进矩阵剩余项;修复 E2E 发现的真实前端缺陷(dev 模式视口不可用)。
- **验证**:round-42 相关黄金测试 62 项通过;全量回归 `846 passed`(隔离 `APS_KNOWLEDGE_PATH`);前端 `npm run build` 通过 + E2E preview/dev 双路径 3 passed;后端 8003 新代码重启健康(/api/multimodal/*、/api/gov/archive/* 已注册)。
- **边界**:真实 ASR/VLM 模型服务、物理 WORM 介质、加密/密钥管理、MySQL 实机、签名/Defender/离线机安装与真实 GitHub CI 跑通(均需外部环境);低置信卡复用 import.commit 动作名(专用 multimodal.apply P2 卡需登记 _POWER_MAP 并同步 harness.md,留待后续轮)。
- **类型**:feature / fix
- **做了什么**:设置中心「个人偏好」页新增偏好学习卡片:当前默认策略、置信度(高/中/冷启动)、原因文案、得分明细、重置按钮;后端新增 `GET /api/settings/preferences`(explain,缺省取活动项目维度)与 `POST /api/settings/preferences/reset`;修复 `algo_skill_stub._stub_input_fingerprint` 误纳含时间戳的 problemId 导致同输入两次运行 runId 不同、EXTERNAL trace 链不可复算(矩阵 114 行可复算性回归缺陷)。
- **为什么**:矩阵 66 行剩余项「UI 展示偏好解释」;全量回归偶发 `test_trace_external::test_external_flex_trace_recomputable_same_input` 失败,定位为 problemId 时间戳污染 runId。
- **验证**:`test_tool_runtime + test_trace_external` 连续 3 次通过;EXTERNAL/trace 相关 13 项通过;TestClient 端到端:冷启动→3 次排产→DELIVERY_FIRST(high, 2:1) 项目维度偏好。
- **边界**:偏好模型升级为权重回归/规则归纳留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第十批:审计归档加密/Defender冒烟+离线包校验/桌面升级回滚/测试门禁修复(round-43,3 个并行智能体 + 主控)
- **类型**:feature(多方向并行,plan.md §10.4/§14.1/§12)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控测试门禁修复):
- **方向 BB(Franklin)· 矩阵 104/116 加密部分**:`audit_ledger.py` 归档加密模拟——AES-256-GCM 认证加密(cryptography,scrypt 口令派生或 env 原始密钥),manifest 双层哈希(ciphertextHash+plaintextHash)+ 解密校验/密钥缺失或错误显式失败/密文篡改检出,未配置密钥 fail-closed 不静默;`test_audit_encryption.py` 12 项。
- **方向 CC(Singer)· 矩阵 99/118 本地部分**:`scripts/defender-scan.mjs`(MpCmdRun 静态扫描 Sidecar/win-unpacked/NSIS 安装包 4/4 clean,含非 ASCII 路径 temp-copy 兜底与 EICAR 威胁解析验证)+ `scripts/offline-install-check.mjs`(SHA-256 清单生成/校验/portable/dry-run 安装路径);`tests/node/packaging-smoke.test.mjs` 12 项 + `docs/development/packaging-smoke.md`。
- **方向 DD(Faraday)· 矩阵 103 升级回滚部分**:`packaging/upgrade-manifest.schema.json` + `build-upgrade-manifest.mjs`(清单生成/校验)+ `apps/desktop/updater.cjs`(校验→备份 copy/listing→NSIS 静默安装→健康检查→失败自动回滚+本地审计 JSONL,可选上报 /api/gov/audit)+ IPC(check-for-update/apply-update/rollback 走 isTrustedIpcEvent);`apps/desktop/test/updater.test.cjs` 15 项 + `updater-smoke.mjs` 4 项。
- **主控·测试门禁修复**:`test_guidance.py::test_workflow_guidance_and_kb_miss` 长期环境失败根治——测试内隔离知识库路径(APS_KNOWLEDGE_PATH 临时文件 + 清全局缓存),不再依赖已漂移的 server/data/knowledge.json(5/5 通过)。
- **为什么**:按 plan.md 蓝图继续推进矩阵剩余项;根治每个全量轮次唯一的既有失败(知识库漂移)。
- **验证**:round-43 相关测试 37 pytest + 26 node(updater 22 + packaging-smoke 12 去重后统计)通过;全量回归 `858 passed`(隔离 `APS_KNOWLEDGE_PATH`,test_guidance 自隔离);Defender 扫描 4/4 clean;离线安装包校验 PASS。
- **边界**:签名 Authenticode/SBOM 上链/provenance、WORM 物理介质、HSM/KMS 密钥管理、Defender for Endpoint CI 门禁与实时防护演练、干净离线机实机安装、真实 GitHub CI 跑通、NSIS 真装冒烟(均需外部环境/证书/验证机)。
- **类型**:feature
- **做了什么**:`_create_checkpoint` 建档后把 checkpointId 锚定到当前活动分支(分支↔快照联动);`POST /api/sessions/{id}/branches/{bid}/switch` 升级为 P1→P2:目标分支有锚定 checkpoint 时自动成对恢复世界+对话侧(restored=true),无锚点仅切换 active_node(restored=false);fork 继承父分支锚点。
- **为什么**:矩阵 52 行剩余项「分支切换时自动恢复锚定 checkpoint(成对切换联动)」;P1 安全探索验收「任意分支切换后对话和世界逐字段一致」。
- **验证**:`test_branch_checkpoint_link.py` 3 项(建档锚定分支、切换锚定分支自动恢复版本数、无锚点仅切换)通过;端到端:chat 建档→锚定→fork→污染→切换恢复版本数回锚定时。
- **边界**:PlanStore 可恢复接口(plan 版本随分支切换回退)留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第十一批:CI 6-job 扩展/health 版本+审计token/NSIS真装冒烟/文档同步(round-44,3 个并行智能体 + 主控)
- **类型**:feature(多方向并行,plan.md §14.1/§12/§10.4)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控收口):
- **方向 EE(Bernoulli)· 矩阵 103/118 CI 部分**:CI 新增 `node-tests` job(Win+Ubuntu)——updater+sidecar+packaging-smoke 34 项 + sidecar 本地冒烟 + offline verify + Defender best-effort;`pyproject.toml` 声明 `cryptography>=42`;根 `package.json` 新增 `test:node`(本地 npm run test:node 34/34)。
- **方向 FF(Popper)· 矩阵 103 衔接**:`/api/health` 补 `version`(APS_APP_VERSION env 注入,web null 兜底)+ sidecar 一次性审计 token(重启重置、不落盘不进子进程 env)→ main `resolveAuditOptions` → updater Bearer+nonce 客户端链路闭环;probeHealth 版本比对显式化(无 version 退化 200 检查);`test_health_version.py` 3 项 + node +5 项。
- **方向 GG(Kant)· 矩阵 99/118 真装冒烟**:`scripts/nsis-install-smoke.mjs` NSIS 临时目录真装冒烟(/S /D=随机临时目录,2 次 PASS:33.6s/30.9s、567MB 解包、electron-builder 布局校验、卸载清理、无系统残留;验证 updater /S /D= 约定)+ `tests/node/nsis-smoke.test.mjs` 13 项 + `docs/development/nsis-smoke.md`。
- **主控·收口**:requirements.txt 补 cryptography(CI backend-tests 依赖同步);features.md 补 round-36~44 交付速览表;test_guidance 知识库隔离修复已在 round-43 完成并纳入本轮全量验证。
- **为什么**:按 plan.md 蓝图收口矩阵剩余本地可做项,把桌面升级/审计/打包链路与 CI 门禁衔接闭环。
- **验证**:全量回归 `861 passed`(隔离 APS_KNOWLEDGE_PATH);node 门禁 `npm run test:node` 34/34 + nsis-smoke 13/13 + updater 27 + sidecar 冒烟 5/5;CI YAML 6 job 校验通过;NSIS 真装 2 次 PASS;8003 新代码重启健康(health 含 version 字段)。
- **边界**:服务端审计上报端点(POST + Bearer 校验)、升级后 probeHealth nonce 续接、签名/SBOM 上链、Defender for Endpoint CI 门禁、干净离线机实机安装、真实 GitHub CI 跑通、perMachine 路径(均需外部证书/验证机/仓库);真实 MES/WMS 适配器、信创、ASR/VLM、WORM 物理介质、HSM/KMS、MySQL 实机等外部项维持矩阵标注。
## 2026-08-01 — 主数据工艺路线树产品升级(round-26,产品向 sub agent 实施)
- **类型**:feature / product
- **做了什么**:主数据「工艺路线」页签树重做:根=选中工艺路线的成品(金色「成品」徽标+自制+用量×N+图号),每个物料节点下先挂它自己工艺路线的工序序列(工序徽标+seq+自制/委外,按序),再挂 BOM 子件;原料/外购件为叶子(「原料+采购」);默认只展开到工序层、半成品可折叠;空态/异常有 Alert 不白屏。后端 `build_process_tree` 只增不减补充展示辅助字段(typeLabel/hasRouting/isLeafMaterial/unit);前端新增 `processTreeNodeTitle`/`processTreeToNodes` 与 `.pt-badge--*`/`.pt-src--*` 样式。
- **为什么**:现场计划员需要一眼看清「成品 → 半成品 → 每层各自生成哪些工序」,此前树全部平铺无层级语义。
- **验证**:`npm run build --prefix apps/web` 通过;浏览器实测外壳树 467 节点、折叠/展开正常、原料叶子无展开箭头;golden 测试 535 全绿;impact 分析 build_process_tree LOW。
- **边界**:空态 Alert 未在真实数据触发验证;更深层 BOM 子件默认折叠,可在产品迭代中调整默认展开深度。
---
## 2026-08-02 — 多智能体并行交付·第十二批:审计上报端点/HTTP MES适配器/图片元数据提取/信创依赖矩阵(round-45,3 个并行智能体 + 主控 L102)
- **类型**:feature(多方向并行,plan.md §3.6/§7/§6.7/§14.2)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控实现矩阵 102):
- **方向 HH(Helmholtz)· 矩阵 103 收口**:`POST /api/gov/audit/events` 审计上报端点(desktop nonce 门禁双通道鉴权/web 登录 401/非法载荷 400/幂等 idempotencyKey 去重,复用 write_audit 落链)+ updater 上报新契约(sha256 确定性 key)+ probeHealth/applyUpdate nonce 续接(升级后健康检查带新网关 nonce,403 fail-soft 不阻塞);`test_audit_events_ingest.py` 7 项 + node 30/30。
- **方向 II(Socrates)· 矩阵 73/75 适配器框架**:`server/integrations/mes_http.py` HttpMesClient——config-driven(base_url/token/超时/重试 env)、幂等 idemKey、指数退避重试、报工回执/状态轮询/幂等撤单(saga 补偿)、MesHttpError 显式失败、未配置 fail-closed;`mes.py` env 切换客户端(stub 默认);MCP 总线注册 mes.http(dispatch P3/status P0/report P2/cancel P2,默认 deny);本地 HTTP stub 黄金测试;`test_mes_http.py` 9 项 + `test_mcp_bus.py` +1。
- **方向 JJ(Lorentz)· 矩阵 101 框架扩展**:`ImageMetaExtractor`——文件名/元数据级结构化候选(订单/物料/日期/附件类型,置信 ≤0.6 + requiresVlm 不编造、9 位数字守卫),走既有低置信 P2 确认门禁;真实 VLM 经 register(replace=True) 覆盖切换;`test_multimodal_image.py` 10 项。
- **L102(主控实现)· 信创依赖矩阵**:`docs/architecture/platform-matrix.md`(Python/OS/架构/依赖 wheel 覆盖 + 信创关键依赖标注)+ `scripts/platform-deps-check.py`(python/平台/架构/requirements 全依赖检测,本地 16/16 PASS)——矩阵 102 由 Missing 转 Partial。
- **为什么**:按 plan.md 蓝图收口矩阵剩余本地可做项(Missing 已归零);为真实 MES 对接与信创安装提供配置化框架与检测工具。
- **验证**:全量回归 `888 passed`(隔离 APS_KNOWLEDGE_PATH);node 门禁 30/30 + updater-smoke 4/4;集成组 91 项通过;8003 新代码重启健康(audit/events 端点 web 401/desktop nonce 双通道验证);文档门禁通过。
- **边界**:真实 MES 工厂对接(填配置+现场联调)、真实 ASR/VLM/OCR、麒麟/统信四组合实机安装、签名/SBOM 上链、Defender for Endpoint 门禁、干净离线机实机、真实 GitHub CI、main.cjs 新 nonce 传递接线(均需外部环境/证书/后续轮)。
- **类型**:feature
- **做了什么**:新增 `server/state/branches.py`(BranchStore):每会话一棵分支树(tree_root/active_node/branches),支持 fork(从活动分支派生,继承 checkpoint 锚点)/rename/switch/discard/merge,状态机(主干与活动分支不可丢弃、已合并/已丢弃不可再操作),JSON 持久化 + 并发锁;gateway 新增 `/api/sessions/{id}/branches` 系列端点(GET/POST fork/PATCH rename/switch/discard/merge)并写 GATE 审计(branch.fork/switch/merge/discard);成对快照 `conversationSide` 纳入分支树。
- **为什么**:矩阵 51 行「项目 -> 会话 -> 分支三级树及分支生命周期」(Missing)与 52 行成对快照的前置依赖。
- **验证**:`test_branch_tree.py` 3 项(BranchStore 生命周期+持久化、gateway 全流程+审计、丢弃活动分支 409)通过;API 冒烟 fork/switch/merge/discard 全链路正常。
- **边界**:前端分支树 UI、分支合并世界差异 diff、分支切换自动恢复锚定 checkpoint 留待后续轮。
---
## 2026-08-02 — 多智能体并行交付·第十三批:升级nonce续接/MES就绪检查/审计统计视图/基线同步(round-46,3 个并行智能体 + 主控)
- **类型**:feature(多方向并行,plan.md §14.1/§7/§3.6)
- **做了什么**(三个并行智能体写范围互不重叠 + 主控基线同步):
- **方向 KK(Curie)· 矩阵 103 最后本地项收口**:`apps/desktop/upgrade-nonce.cjs` createUpgradeNonceCoordinator——sidecar restarted 事件→新 nonce→probeHealth 重验(expectedVersion opt-in 版本比对/退化 200 检查)→成功带新 nonce 上报审计(upgrade.health-verified)/失败本地 JSONL 留痕;安装期 deferred 重启补验 + maxPendingMs 过期兜底 + 状态机防漏;main.cjs 接线 + build.files 打包包含;`test/upgrade-nonce.test.cjs` 13 项(desktop 43/43)。
- **方向 LL(Copernicus)· 矩阵 73 现场对接准备**:`mes_http.py` readiness(probe)——configured/baseUrl/tokenPresent/connectivity/lastError/message 稳定字段,未配置 fail-closed 提示不 500、探测短超时绝不抛错(2xx ok/失败 lastError);`GET/POST /api/integrations/mes/readiness[/probe]`;`test_mes_readiness.py` 8 项。
- **方向 MM(Avicenna)· 矩阵 104 审计查询与报表前端**:GovConsole「审计统计」分区(AuditStatsView:分类计数/动作 Top8/近 7 天趋势/告警概览含 traceAlerts 断链明细,中文标签+空态提示,前端聚合 limit=300 零后端改动);`npm run build` 通过。
- **主控·基线同步**:矩阵「当前验证基线」更新为 round-45 取证(888 passed + node/E2E/Defender/NSIS 门禁)+ GitNexus 索引规模 8,772 节点。
- **为什么**:按 plan.md 蓝图收口矩阵最后本地可做项;升级审计闭环、MES 现场对接第一步、审计报表可读性全部产品化。
- **验证**:全量回归 `895 passed`(隔离 APS_KNOWLEDGE_PATH);node 门禁 55/55(desktop 43 + packaging-smoke 12)+ updater-smoke 4/4;mes readiness 相关 33 pytest;前端 `npm run build` 通过;8003 新代码重启健康(/api/integrations/mes/readiness 已注册,鉴权前置)。
- **边界**:真实 MES 工厂对接/端点路径现场联调、applyUpdate 途中重启竞态根治(updater nonceMismatch→pending 语义)、签名/SBOM 上链、Defender for Endpoint、干净离线机实机、真实 GitHub CI、WORM 物理介质、HSM/KMS、麒麟/统信四组合实机、ASR/VLM、真实 WMS(均需外部环境/证书/后续轮)。
- **类型**:feature / security
- **做了什么**:新增 `server/agent_core/tool_runtime.py`:LLM 提议意图的统一执行入口——`is_registered` 校验(写入类匹配 `_POWER_MAP`、只读类白名单),未登记意图拒绝执行并写 TOOL 审计(`tool.denied`/P3),已登记意图委托 `handle_intent`(P2/P3 仍走 harness 确认卡)并写 `tool.run` 审计;gateway 聊天路径 `_run_intent` 改为经 `run_tool` 执行。
- **为什么**:矩阵 111 行「LLM 只有提议权」剩余验收「所有 LLM 工具调用都经统一工具运行时;不能通过未登记 API 或直接 store 写入绕过」。
- **验证**:`test_tool_runtime.py` 4 项(未登记拒绝+审计、已登记直通+tool.run、只读白名单放行、登记判定)通过;冒烟验证 `unknown` 拒绝、`schedule.run` 直通且双审计落盘。
- **边界**:其余 gateway 直连端点(scenario.apply 等)接入工具运行时、前端工具调用可视化留待后续轮。
---
## 2026-08-02 — 升级链路竞态根治:applyUpdate nonceMismatch→pending(round-47,主控)
- **类型**:fix(plan.md §14.1 离线升级闭环;矩阵 103 最后本地小项)
- **做了什么**:`apps/desktop/updater.cjs` 健康检查失败分支区分「升级途中 sidecar 重启导致的 nonce 轮换」——403 且已携带 nonce(`details.nonceMismatch=true`)时**不再自动回滚**(安装已成功,仅健康检查 nonce 过期),返回 `{ok:false, deferredVerification:true, reason:'NONCE_MISMATCH'}` + 审计 `upgrade.health-pending`(PENDING),由 Electron main 的 upgrade-nonce 协调器以新 nonce 补验;无 nonce 的 403(普通故障)仍走自动回滚;`test/updater.test.cjs` 新增 2 项非对称用例(带 nonce 403→pending 不回滚 / 无 nonce 403→回滚),desktop node 45/45。
- **为什么**:round-46 KK 标注的最大残留竞态——sidecar 在 applyUpdate 内部健康检查前完成重启时,自带检查用旧 nonce 403 会误触发回滚;本轮根治(非 nonce 类失败仍回滚,安全语义不变)。
- **验证**:desktop node `45/45`;根 node 门禁(updater+packaging-smoke+nsis-smoke)全绿;全量回归 `895 passed`(隔离 `APS_KNOWLEDGE_PATH`);`node --check` 通过。
- **边界**:跨进程持久化 nonce 续接(应用重启即丢 pending)与签名/SBOM 上链、真实 GitHub CI、真实 MES/信创/离线机等外部项维持矩阵标注。
- **类型**:feature / governance
- **做了什么**:`server/agent_core/plan_runtime.py` 新增 `node_at`(时间线按版本取不可变节点,越界/不存在抛 PlanNotFoundError)与 `replay_verify`(L2 单节点/L3 动作级重放校验:给定输入重算 inputsHash 与目标版本比对,一致返回节点、不一致抛 PlanInputMismatchError);`server/gateway/plan_api.py` 新增 `GET /{plan_id}/versions/{version}`(时间线回放)与 `POST /{plan_id}/versions/{version}/replay-verify`(动作级重放校验)。
- **为什么**:矩阵 110 行「L0-L3 Plan」剩余项「时间线回放和 L2 单节点/L3 动作级重放」。
- **验证**:`test_plan_replay.py` 4 项(node_at 不可变版本、相同输入可复算、输入变更拒绝、L3 动作级重放)通过;既有 `test_plan_runtime.py` 8 项全绿。
- **边界**:Agent 自动编排(plan 节点驱动 workflow 分支)、批准/驳回审计留待后续轮。
---
## 2026-08-02 — 升级 pending 跨进程持久化(round-48,主控)
- **类型**:feature(plan.md §14.1 离线升级闭环;矩阵 103 稳健性增强)
- **做了什么**:`apps/desktop/upgrade-nonce.cjs` 协调器新增 `statePath` 持久化——setPending/markInstalled/settle/clear 均原子写 `upgrade-state.json`(tmp+rename,宽容失败);应用重启后新协调器恢复未过期 pending(status→verifying)等待新 sidecar 补验,过期自动丢弃;`main.cjs` 接线 `{APS_HOME}/upgrade-state.json`;测试 3 项(持久化往返/过期丢弃/settle 清空),desktop node 48/48、根 node 门禁 60/60。
- **为什么**:round-47 前 pending 为内存态,应用重启即丢续接上下文;持久化后升级验证可跨进程续接(配合 sidecar 新 nonce)。
- **验证**:desktop node 48/48;根 node 门禁 60/60;文档门禁 16 passed;`node --check` 通过(纯 node/Electron 接线,pytest 无影响)。
- **边界**:签名/SBOM 上链、真实 GitHub CI、真实 MES/信创/离线机、WORM 物理介质、HSM/KMS、MySQL 实机等外部项维持矩阵标注。
- **类型**:feature / security
- **做了什么**:新增 `server/aps_domain/explore_boundary.py`:`readonly_view` 主干世界只读视图(写操作/嵌套变异 fail closed 抛 PermissionError),`run_explore` 统一 Explore 通道(内部深拷贝沙盒,fn 只拿沙盒、主干永不外泄写引用);`scenario.compare_scenarios` 整体接入统一通道(baseline 经只读视图读取、策略级子沙盒深拷贝隔离)。
- **为什么**:矩阵 55 行「Explore 与 Runtime 数据边界」;此前深拷贝隔离是约定而非强制,Explore 工具理论上可绕过。
- **验证**:`test_explore_boundary.py` 5 项(顶层/嵌套写保护、沙盒隔离、嵌套变异隔离、越权测试)通过;既有 `test_scenario_compare_does_not_mutate_world` 等 18 项场景/边界测试全绿。
- **边界**:敏感性/鲁棒性等其他 Explore 工具逐一接入统一通道、前端 Explore 与 Runtime 视觉分区留待后续轮。
---
## 2026-08-02 — 外部验收执行手册(round-49,主控;goal blocked 期间准备性交付)
- **类型**:docs(plan.md §7/§14;为矩阵 10 个 Partial 行的外部验收固化执行步骤)
- **做了什么**:新建 `docs/product/external-acceptance.md`——按剩余行逐项给出前置条件/验收步骤/验收标准/代码锚点:真实 MES(readiness+联调)、真实 WMS(事件契约接入)、干净离线机安装(offline-check/NSIS/sidecar 冒烟)、Defender 实时防护+Endpoint 门禁、ASR/VLM 接入(Extractor register 契约)、信创四组合实机(platform-deps-check)、代码签名/SBOM 上链(upgrade-manifest 预留字段)、真实 GitHub CI(6-job workflow)、WORM 物理介质/HSM-KMS(audit_ledger 密封+加密)、MySQL 多主机(approval CAS 契约);附执行顺序建议(先授权提交固化 267 项改动→GitHub CI→真实 MES/WMS→其余)。
- **为什么**:goal 处于 blocked(剩余项全部需外部环境);本手册使资源到位后可「按单执行」,无需重新设计,为解除阻塞后的验收做直接准备。
- **验证**:文档门禁 4 passed;矩阵基线增加手册指针。
- **边界**:矩阵 10 个 Partial 行状态不变(仍需外部环境);无代码改动。
---
## 2026-08-02 — 最终验证 + 交付总览(round-50,主控;goal blocked 期间收尾)
- **类型**:docs(交付盘点;为提交授权与外部验收提供权威证据)
- **做了什么**:全量黄金测试最终复跑 **895 passed**(隔离 `APS_KNOWLEDGE_PATH`,确认 round-15~49 十五批累积工作树全绿);新建 `docs/product/delivery-summary.md`——总览指标(50 Done/10 Partial/0 Missing、895 测试、node/E2E/Defender/NSIS 多门禁)、15 批交付清单(50+ 并行 sub agent)、关键能力清单、剩余外部项(指向 external-acceptance.md)、提交授权建议。
- **为什么**:goal 处于 blocked(剩余 10 行全部需外部环境);交付总览让用户无需逐轮翻 CHANGELOG 即可评估 267 项未提交改动并决策授权提交。
- **验证**:全量回归 895 passed;文档门禁 4 passed;服务 8003/5173 健康。
- **边界**:矩阵状态不变(仍需外部环境/提交授权);无代码改动。
- **类型**:feature
- **做了什么**:`server/knowledge/preferences.py`:`record` 新增 `project_id` 维度(样本落 projectId);`preferred_strategy` 支持按项目过滤(个人全局与项目样本互不影响);新增 `explain`(得分明细 + cold/medium/high 置信 + 依据条数 + 原因文案)与 `reset`(按项目清样本);样本不足 3 条标记冷启动、并列最高分标记不确定;workflow `schedule.run` 与 gateway `scenario.apply` 调用点传入 `world_key` 实现项目隔离。
- **为什么**:矩阵 66 行「偏好学习影响默认策略」;此前仅简单“试排1分/采用2分”且无项目维度。
- **验证**:`test_preference_upgrade.py` 4 项(项目隔离、冷启动/充足解释、并列不确定、按项目重置)通过;既有 `test_m3_knowledge.py` 6 项全绿;端到端 schedule.run 带 world_key 冒烟通过。
- **边界**:前端偏好解释 UI、权重回归/规则归纳模型留待后续轮。
---
## 2026-08-02 — 现场数据保护与文档基线收口(round-62,主控取证 / A-DOC 回写)
- **类型**:verification / docs(不改代码与测试)
- **做了什么**:按当前工作区重新固化发布前证据口径:① 隔离测试环境下全量 `pytest` **907 passed**,现场 `master.db` 验证前后各表计数完全不变;② Playwright 默认目标 `127.0.0.1:8003` 时 fail-closed,只有显式设置 `E2E_ALLOW_LIVE_BACKEND=1` 才允许连接现场后端;③ `folder.schedule` 富卡显示 **有效 1357 / 问题 53 / 跳过 974**,跳过行明确不计入问题行;④ Web build 通过,Playwright 三种配置加载门禁通过;⑤ 同步矩阵、交付总览与最终外部验收手册的 round、测试数、工作树和 Partial 口径。
- **为什么**:round-49~61 文档把“最终验收需要外部资源”过度概括成“10 个 Partial 全部只能等待外部资源”,且同时存在 861/888/895/904 多套测试基线与 267/273 工作树计数。round-62 将口径改为:**10 个 Partial 的最终验收仍依赖真实外部环境,但其中仍有可本地推进的适配、持久化和发布门禁缺口;状态保持 Partial,不虚假标记 Done。**
- **验证**:全量 `pytest` 907 passed;现场 `master.db` 前后计数完全一致;Web build 通过;Playwright 三种配置加载门禁通过;`folder.schedule` 富卡取证为 1357/53/974,且 `skippedRows` 不计入 `totalErrors`。
- **边界**:A-DOC 仅修改 `docs/CHANGELOG.md`、`docs/product/plan-completion-matrix.md`、`docs/product/delivery-summary.md`、`docs/product/external-acceptance.md`;不修改代码/测试,不提交。矩阵仍为 **50 Done / 10 Partial / 0 Missing**。
## 2026-08-02 — 目录分析区分「非排产块」:问题行噪声收敛 1027→53(round-61,主控)
- **类型**:fix / UX(现场分析报告可读性)
- **做了什么**:`server/aps_domain/folder_pack.py` 新增 `_MOM_SKIP_BLOCKS`(生产模型/质检方案/仓储模型/人力资源/客户管理/供应商管理),`analyze_work_dir` 把命中非排产块的 sheet 问题行单独计入 `skipRows/skippedRows`,不再混入 `errorCount/totalErrors`;`_to_markdown` 在表格后追加脚注「另有 N 行来自非排产块…已跳过,不计入问题行」;`workflow.py` folder.schedule UIBlock 透传 `skippedRows`。
- **为什么**:锐扬 MOM 收集表的工厂/质检/仓储/人力/客户/供应商块本就不导入,此前 1027 问题行里 974 行来自这些块,会误导用户以为数据大面积出错;现在问题行只反映真实数据错误(02 表头行 1 + 07 设备缺编码 52)。
- **验证**:真实目录实测 `totalErrors 1027→53`、`skippedRows 974`;markdown 脚注与浏览器回复均正确(「另有 974 行来自非排产块…已跳过」);`test_folder_pack`/`test_importers`/`test_guidance` 16 项通过;全量黄金回归 `904 passed`。
- **边界**:跳过逻辑按 sheet 名匹配(锐扬模板名),其他模板同名块同样受益;不影响 okRows/确认卡语义。
## 2026-08-02 — 全链路排产就绪验证:订单样例入目录即可开排(round-60,主控)
- **类型**:验证(现场排产端到端闭环)
- **做了什么**:将 `订单-样例.csv`(订单号/客户/产品编码/数量/交期/优先级/加急,示例行 SO-RY-20260801)放入工程目录 `D:\ItemSpace\14.工业智核\锐扬` 后,浏览器实测「给我模拟排产一下」:目录分析识别订单 1 有效行,**排产必备全齐(订单✓1 / 物料✓1356 / 工艺路线✓446 / 设备✓87)**,结论「基本具备开排条件」、调试信息「可开排」,P2 确认卡正常生成(有效行合计 1357:订单 1 + 物料 1356 + 工艺 446 + 设备 87,批准后写入柔性主数据并试排一版,可回滚)。同步更新 `docs/product/ruiyang-data-runbook.md` 排产必备状态与验证记录。
- **为什么**:证明用户下一步(替换真实订单 → 带我排一版/根据这些数据排产 → P2 确认)端到端可用,现场数据接入全部打通。
- **验证**:浏览器实测如上;无代码改动;全量黄金 `904 passed` 基线不变。
- **边界**:样例行需替换为真实订单(产品编码须在 02/04 表内、交期非空);问题行 1027 多为非排产块(工厂/质检/仓储/人力等)不影响入库;矩阵状态不变(10 个 Partial 仍依赖外部资源)。
## 2026-08-02 — 外部验收手册增补资源清单收集表(round-59,主控)
- **类型**:docs(矩阵 10 个 Partial 外部项的落地准备)
- **做了什么**:`docs/product/external-acceptance.md` 新增「11. 资源清单收集表」——10 项外部验收各自「需要准备的资源 + 具体字段/获取方式」(MES base URL/token、WMS 事件接口、离线机镜像、DfE 门户、模型服务 URL/API key、信创四组合、签名证书/时间戳、GitHub 授权、WORM 介质/HSM-KMS、MySQL DSN),并给出并行推进建议;同步更新执行顺序中的工作树改动数(267→273)与 round-52~58 修复说明。
- **为什么**:外部资源是剩余矩阵项的唯一阻断;把「找谁要什么」固化成表,用户/现场可先行收集,资源到位即可按第 1-10 节执行。
- **验证**:文档渲染核对通过;无代码改动(不需要跑测试)。
- **边界**:资源到位后按单执行并回写矩阵状态;现场用户当前仍等订单表(订单-样例.csv)完成首次排产验收。
## 2026-08-02 — 订单导入支持「加急」列 + 锐扬数据接入手册/订单样例(round-58,主控)
- **类型**:feature / docs(现场订单表接入资产)
- **做了什么**:① `importers.py` 表头别名新增 `加急→isRush`(订单表常用列,导入后订单带急单标记);② 在工程目录放置 `订单-样例.csv`(列:订单号/客户/产品编码/数量/交期/优先级/加急,示例行改数即可用);③ 新增 `docs/product/ruiyang-data-runbook.md`:sheet→导入类型映射表、制造流程→工艺路线说明、订单表格式与必填项、排产必备清单现状(物料✓910/工艺✓446/设备✓87/订单缺)、工时缺失默认 1 分钟兜底、P2 确认与触发路径。
- **为什么**:现场排产唯一缺口是订单表;给计划员可直接填写的样例与手册,减少来回确认。
- **验证**:新增 `test_order_csv_rush_column_maps_to_is_rush`(加急列→isRush=True)通过;importers 6 项全绿;全量黄金回归 `903 passed`;订单样例 CSV 已按 utf-8-sig 落盘(Excel 可直接打开)。
- **边界**:订单样例行(SO-RY-20260801)仅作演示,导入前需替换/删除;工时仍缺(默认 1 分钟,需另行补表)。
## 2026-08-02 — 锐扬 02-工艺模型「制造流程」列提取为工艺路线(round-57,主控)
- **类型**:feature / fix(现场表自带工艺路线未被识别)
- **做了什么**:`server/aps_domain/importers.py` `_rows_from_xlsx` 检测「制造流程/工艺代码/工艺流程」列(锐扬 MOM 02-工艺模型第 17 列起,表头后次行为 01..12 序号),为每个数据行合成 `routing` 视图:`{productCode=ERP品号, seq=序号, operationCode/operationName=工艺代码}`(如 `ZP01组装`→ZP01/组装、`LM01POP拉铆`→LM01POP/拉铆),随 preview_file 按 routing 批校验入库。
- **为什么**:上一轮目录分析显示「工艺路线/工时 × 缺少」,但现场表的 02-工艺模型**自带制造流程**(每物料 3-12 步工艺代码),只是从未被提取;提取后排产必备仅剩「订单」缺口。
- **验证**:真实文件 `totalOk 910→1356`(+446 条工艺路线,样例 `A0050101-00280 seq=1 op=ZP01/组装`);浏览器实测「给我模拟排产一下」→ 物料 ✓1356 / 工艺路线 ✓446 / 设备 ✓87 / **仅缺订单**,P2 确认卡(导入并试排 · 1356 行)正常生成;黄金测试扩展 `test_xlsx_mom_template_multiblock_headers_soft_preview`(02 sheet 加制造流程列,断言 routing 视图 5 行 + 代码/名称解析);相关 10 项测试通过;全量黄金回归 `903 passed`。
- **边界**:工时(stdTimePerUnit)现场表无该列,入库路线无单件工时(排产时可用默认/后续补);订单仍缺(现场表无订单 sheet,且当前世界唯一订单 `MOM--00280` 交期为空)。
## 2026-08-02 — JMS 无角色用户默认 planner,P2 审批门禁可用(round-56,主控)
- **类型**:fix(真实 JMS 平台不返回角色字段,登录用户被审批角色门禁卡死)
- **做了什么**:`JmsAuthProvider._identity`(`server/auth/providers.py`)在 JMS profile 未返回 `roles`(真实平台实测无该字段)时,默认注入 `planner` 角色(可用 `JMS_AUTH_DEFAULT_ROLE` 覆盖,留空关闭);`.env` 增加 `JMS_AUTH_DEFAULT_ROLE=planner`。
- **为什么**:审批角色默认集为 planner/approver/admin,而真实用户张振 roles=[],导致任何 P2 动作(导入确认/发布/写主数据)都报「当前身份没有发起审批的角色权限」,产品不可用。
- **验证**:live 登录实测返回 `roles:["planner"]`;浏览器端「给我模拟排产一下」→ 目录分析(910 有效行)→ **P2 确认卡正常生成**(导入工程目录并试排 · 批准执行/驳回);`test_jms_auth_provider.py` 新增 3 项(无角色默认 planner / env 覆盖 / 有角色不被覆盖);全量黄金回归 `903 passed`。
- **边界**:JMS 后续返回真实角色时以角色为准;P3 双人 SOD 语义不变。
---
## 2026-08-02 — 锐扬 MOM 收集表模板导入:多行标题/分块表头自动识别(round-55,主控)
- **类型**:feature / fix(现场真实表 0 有效行 → 910 有效行)
- **做了什么**:`server/aps_domain/importers.py`:① `_rows_from_xlsx` 改为**表头行自动探测**——按规范化后命中已知字段映射最多的行(序号/编码/名称/单位/数量…)作为表头,跳过「物料清单」等标题行与分块模板(物料组块 + 物料主数据块),平铺表第 1 行即表头时行为不变;② 新增别名:ERP品号/财务编号/厂内编号→code、单套用量→quantity、物料简称→name、物料组名称→type、规格型号→spec。
- **为什么**:现场 `湖南锐扬MOM主数据收集表.xlsx`(9 个 sheet、多行标题/合并单元格)此前全部 2038 行校验失败(0 有效行),`给我模拟排产一下` 只能报「还不能排」;实际 02-工艺模型(物料清单)、04-物料模型、07-设备模型都含真实数据。
- **验证**:真实文件实测 `totalOk 0→910`(02 工艺模型 415 物料 + 04 物料模型 408 物料 + 07 设备 87 台,canCommit=True),剩余错误行均为非排产类块(工厂/质检/仓储/人力/客户/供应商);新增 `test_xlsx_mom_template_multiblock_headers_soft_preview` + `test_xlsx_flat_header_row_0_behavior_unchanged`;相关 14 项测试通过;全量黄金回归 `903 passed`;浏览器端「给我模拟排产一下」→ 目录分析显示 910 有效行并出 P2 确认卡。
- **边界**:02 工艺模型按物料入库(BOM 结构/层次暂不建父子边,工序工时需另行补表);订单/工艺路线工时仍缺(现场表本身没有这两块)。
## 2026-08-02 — 「模拟排产」类口令落空修复:直接执行而非引导文案(round-54,主控)
- **类型**:fix(意图规则快路漏匹配,用户现场复现「竟然不理解」)
- **做了什么**:`server/agent_core/intent.py` 的 `_is_imperative_schedule` 尾缀正则只支持 `排/排产/排一下/排程` 结尾,`排产一下`、`排产一版`、`模拟排产(一下)` 等口语全部漏匹配,落到 LLM 降级 → `assistant.py` 的「我先按你的话理解一下…想开排说带我排一版」通用引导(现场实测复现)。修复:正则扩展为 `(模拟|试)?排(产|程)?(一下|一版)?` 并加入 `模拟排产`、`试排一版/一下`、`排一下` 等独立词条;命中后走既有祈使排产路径(工程目录有表 → `folder.schedule`,否则 `flex.schedule`)。
- **为什么**:现场用户输入「给我模拟排产一下」得到的是四选一引导而非执行,是「竟然不理解」的直接根因;同批修复 `帮我排产一下` 等同类漏匹配。
- **验证**:快路实测 10 个新/旧祈使句全部 `flex.schedule`/`folder.schedule`;负向守护:`带我排一版`→wizard、`怎么排产`→wizard、`模拟报工`→mes.report、`帮我看看现在能不能排`→assistant.reply、`解析项目文件夹`→folder.analyze 均不被误吞;`test_assistant.py` 扩展祈使句清单 + 新增 `test_simulate_schedule_does_not_hijack_other_intents`;浏览器实测「给我模拟排产一下」→ 真实解析工程目录 `锐扬`(0 有效行/2038 问题行)并给出补数诊断,不再返回引导文案;全量黄金回归 `898 passed`。
- **边界**:若工程目录存在 Excel 但行校验不过,排产口令按产品规则走 `folder.schedule` 数据门禁(提示补齐数据);真实数据补齐后即可开排。矩阵状态不变。
## 2026-08-02 — OpenAPI 生成回归门禁(round-53,主控)
- **类型**:test(round-52 修复的防回归守护)
- **做了什么**:新增 `tests/golden/test_openapi_schema.py`(2 项):
1. `test_openapi_schema_generates_without_forward_ref_error`——`create_app().openapi()` 必须完整生成,且 12 个受影响路由(/api/import/commit、/api/templates/apply、/api/rag/query、/api/rebuild/*、/api/knowledge/import、/api/skills/*、/api/mcp/plugins/*、/api/jobs)都在 schema 中;
2. `test_affected_request_models_are_module_scope`——12 个请求模型必须可从 `server.gateway.app` 模块级导入,且 `__qualname__` 不含 `create_app`(直接编码 round-52 修复前的状态,修复前必失败)。
- **为什么**:round-52 修复了函数局部 Pydantic 模型导致 `/openapi.json` 500 的缺陷,但当时没有回归测试;本测试把该约束固化为黄金门禁,防止后续把模型类再写回函数局部。
- **验证**:新增 2 项通过(1.65s);相邻子集复跑 `test_contract_sync` + `test_health_version` 17 项全绿;live `GET /openapi.json` 200 不变。
- **边界**:矩阵状态不变(剩余 10 个 Partial 仍依赖外部资源)。
## 2026-08-02 — 修复 /openapi.json 500:12 个路由请求模型从 create_app 局部上移模块级(round-52,主控)
- **类型**:fix(FastAPI OpenAPI 生成崩溃修复)
- **做了什么**:定位 `GET /openapi.json` 返回 500(PydanticUserError:`TypeAdapter[Annotated[ForwardRef('ImportCommitRequest'), ...]] is not fully defined`)。根因:`server/gateway/app.py` 的 `create_app()` 函数体内定义了 12 个路由请求模型类(ImportCommitRequest/TemplateApplyRequest/RagQueryRequest/RebuildStartRequest/RebuildRollbackRequest/KnowledgeImportRequest/SkillRegisterRequest/SkillEnableRequest/McpPluginRegisterRequest/McpPluginEnableRequest/McpPermissionRequest/JobSubmitRequest),FastAPI 生成 OpenAPI schema 时按模块全局解析 ForwardRef 失败(报错只停在第一个,实际 12 个同隐患)。修复:12 个模型类整体去缩进上移为模块级定义(与既有 ~50 个 Request 模型同层),路由函数体零改动,类名经 Python 词法作用域照常解析;顺带使这些模型可被模块级 import。
- **为什么**:`/openapi.json` 500 破坏 Swagger 文档与基于 schema 的契约校验;属于不依赖外部资源、可本地修复的真实缺陷。
- **验证**:`create_app().openapi()` 生成成功(181 paths,此前抛 PydanticUserError);8003 重启后 `GET /openapi.json` 200(152KB)+ `/docs` 200;受影响路由黄金测试子集 78 项通过(importers/multimodal/multimodal_image/rebuild_pipeline/skill_console/mcp_bus/async_jobs/routing_templates/contract_sync,隔离 `APS_KNOWLEDGE_PATH`);**全量黄金回归 `895 passed`**(隔离 `APS_KNOWLEDGE_PATH`,与 round-50 基线一致);live 冒烟 5 个受影响端点均正常注册(401 登录态保护属预期);GitNexus 门禁 impact(import_commit) risk=LOW(direct 0/processes 0)。
- **边界**:未改任何路由逻辑;矩阵状态不变(剩余 10 个 Partial 仍依赖外部资源:真实 MES/WMS、ASR/VLM、信创实机、干净离线机、代码签名证书、真实 GitHub CI、WORM 介质、HSM/KMS、MySQL 多主机)。
## 2026-08-02 — 侧栏乱码会话清理 + E2E 测试卫生预防(round-51,主控)
- **类型**:fix + 测试卫生(现场工作区污染修复)
- **做了什么**:
1. **修复**:侧栏出现 `????` / `???????` 乱码任务——定位为早期 E2E 探针残留的空会话(e2e-probe-001/002,标题被存成字面 `?`,05:45/05:47 创建),非业务数据;经 `ProjectStore.delete_session` 正规删除,侧栏恢复正常(3 任务)。
2. **预防**:`apps/web/playwright.config.ts` 在本地默认指向现场 8003 且非 CI 时打印警告(E2E 会在现场工作区创建真实会话);`docs/development/e2e.md` 新增「3.5 数据隔离」节(隔离后端启动方式 + CI 一致性说明)。
- **为什么**:本地 E2E 冒烟(dev/preview)默认复用运行中的 8003 现场后端,测试创建的真实会话会污染用户工作区(曾产生乱码残留);预防后不会再出现。
- **验证**:`npm run build`(tsc -b + vite)通过;浏览器确认侧栏乱码任务已消失。
- **边界**:`柔性排产` 任务(e2e-probe-003,含 FV20260802-005 结果)同为 E2E 产物但被保留(用户可能在用),可在任务操作中删除;矩阵状态不变。
- **类型**:feature / security
- **做了什么**:新增 `server/agent_core/audit_filelock.py`:JSONL 追加写统一走 `locked_append`(进程内锁 + Windows msvcrt 区域锁,可配 `APS_AUDIT_LOCK_RETRY_SECONDS`),`audit_ledger.append` 与 `audit_mirror.append_event` 均切换接入,防多进程并发写行交错损坏证据;`audit_ledger.archive_old` 清理策略:超过保留窗口(`APS_AUDIT_LEDGER_KEEP_LAST`,默认 10000)的旧锚定记录移到独立 `archive/<yyyy-mm>.jsonl` 目录,分级存储而非删除,证据不丢失。
- **为什么**:矩阵 116 行「审计 append-only」剩余项「清理策略、跨进程锁强化」。
- **验证**:`test_audit_appendonly_guard.py` 4 项(4 线程×50 并发 append 200 行无丢失、归档窗口保留且证据归档可读、窗口内 no-op、镜像并发回归)通过;既有审计测试 33 项全绿。
- **边界**:WORM/加密留待后续轮(需外部介质/密钥管理)。
---
- **类型**:feature / security
- **做了什么**:新增 `server/agent_core/audit_merkle.py`:完整二叉树 Merkle 树(叶子=事件 hash,逐层两两 SHA-256 配对到根;空树根=创世根;奇数节点上提),`merkle_root` 确定性且对顺序/数量/内容变化断根;`merkle_proof` 生成 O(log n) 成员证明、`verify_proof` 沿 sibling 路径重建根校验;`audit_export.py` 的 JSON 导出携带 `merkleRoot`(与本地复算一致)。
- **为什么**:矩阵 104 行「全审计」剩余项「完整 Merkle 树」;此前仅链式双哈希聚合(audit_ledger.audit_root),无成员证明能力。
- **验证**:`test_audit_merkle.py` 6 项(确定性/敏感性、全索引 proof 校验、错误叶子拒绝、越界、单/空边界、导出携带根)通过;8 叶树 proof sibling 数=树高 3。
- **边界**:WORM 物理介质留待后续轮(需外部介质);Merkle 根当前随导出提供,尚未写入独立介质锚定。
---
- **类型**:feature / security
- **做了什么**:新增 `server/agent_core/audit_notify.py`:按配置渠道(`APS_AUDIT_ALERT_CHANNELS`,console/jsonl:<path>/webhook:<url>,逗号分隔)投递审计完整性告警;critical/warning 默认投递、info 默认跳过(`APS_AUDIT_ALERT_INCLUDE_INFO=1` 开启);同 code+message 在 `APS_AUDIT_ALERT_DEDUP_SECONDS`(默认 300s)窗口内防抖去重;`/api/gov/audit` 读取审计时联动投递(失败不阻断读取)。
- **为什么**:矩阵 104 行「全审计」剩余项「告警通知渠道」;此前 build_alerts 只聚合、无投递。
- **验证**:`test_audit_notify.py` 4 项(console 投递+防抖、jsonl 落盘、info 默认跳过、webhook 失败非致命);本地 HTTP 冒烟 webhook 成功投递(received 200)。
- **边界**:WORM 物理介质、完整 Merkle 树留待后续轮;通知仅内置 console/jsonl/webhook,邮件/IM 可经 webhook 对接。
---
- **类型**:feature / governance
- **做了什么**:`server/state/checkpoints.py` 的 `CheckpointStore.create()` 新增可选 `conversationSide` 完整对话侧快照参数(workspace+messages+plans);`server/aps_domain/workflow.py` 新增 `_create_checkpoint` 统一建档入口(自动收集对话侧)、`_restore_conversation_side`(回滚时成对恢复对话侧),全部 23 处 P2 写前建档与回滚分支接入;`test_m2_state.py` 增至 6 项(成对快照往返逐字段一致、不传对话侧向后兼容)。
- **为什么**:矩阵 52 行「对话状态与世界状态真正成对快照、成对切换和成对回滚」;此前仅存 world + conversationNote 摘要。
- **验证**:`test_m2_state.py` 6 passed;端到端:建档(含对话侧)→ 污染世界 → 回滚 → 世界侧逐字段恢复、对话侧快照保留。
- **边界**:活动分支(BranchNode 树)与 PlanStore 可恢复接口留待矩阵 51/110 行落地后纳入。
---
- **类型**:feature / governance
- **做了什么**:`server/aps_domain/workflow.py` 新增 `_build_external_trace`/`_external_audit_evidence`/`_external_inputs_hash`,ALGO_RUN `flex.schedule.external` 审计携带 traceChainHash/traceCount/traceSummary(schedule-version/run/algorithm 三条链,evidence 含 skill/run/schedule-version 引用);`server/integrations/algo_skill_stub.py` 的 runId 由随机 uuid 改为输入派生确定性哈希(可复算);前端 `GovConsole.tsx` AuditView 新增「可追溯链」列展示 traceChainHash/traceCount/traceSummary(details 可展开,`gov-trace` 样式)。
- **为什么**:矩阵 114 行剩余「EXTERNAL 引擎接入、前端追溯 UI」;EXTERNAL 柔性轨此前审计无 trace 链,stub runId 随机导致链不可复算。
- **验证**:`test_trace_external.py` 3 项(审计携带 traceChainHash/格式、evidence 三引用、同输入可复算)通过;`npm run build`(tsc + vite)通过;浏览器审计链面板实测 schedule.run 行显示 trace 链且可展开。
- **边界**:EXTERNAL 真实 HTTP skill 端到端验收、trace 断链可视化提示留待后续轮。
---
- **类型**:fix / governance
- **做了什么**:`_build_run_trace` 的链哈希去掉墙钟 `solveTimeSec`(改为保留在 ALGO_RUN 审计 rationale 顶层字段),使 CP/HYBRID 真实求解(墙钟 > 0)不再破坏链可复算;新增 `_run_audit_evidence` 把 `schedule-version:<id>` 引用并入审计 `evidence`(对齐 round-15 计划假设 2);`test_trace_run.py` 增至 6 项(新增全引擎 RULE/CP/HYBRID/GA 可复算 + evidence 含 schedule-version 引用、墙钟不参与链哈希)。
- **为什么**:worker 回传残余风险指出 CP/HYBRID 引擎链哈希含墙钟导致两次求解哈希不同,矩阵 114 行「其余引擎接入 trace_chain」被该问题阻塞。
- **验证**:`test_trace_run.py` 6 passed;RULE/CP/HYBRID/GA 同输入同种子两次求解 chainHash 一致;`solveTimeSec` 1.234 vs 5.678 时链哈希相同。
- **边界**:EXTERNAL 引擎接入、前端追溯 UI 留待后续轮。
---
- **类型**:feature / governance
- **做了什么**:`server/aps_domain/workflow.py` 的 `_run_schedule` 求解后经 `_build_run_trace` 构造 EvidenceItem 链(schedule-version/run/algorithm,含 `_run_inputs_hash` 输入包指纹),ALGO_RUN `schedule.run` 审计 rationale 新增 `traceChainHash`/`traceCount`/`traceSummary`;trace 构造失败返回空链摘要,不影响求解。
- **为什么**:矩阵 114 行剩余「各引擎实际写入 trace_chain」首个落地(schedule.run 首接入),验证 evidence.py 可用性。
- **验证**:`test_trace_run.py` 3 项(审计携带 traceChainHash 且格式正确、同输入/同种子/同版本可复算、元素/顺序/输入变化断链)通过;全量黄金测试 490+ passed;ruff 干净。
- **边界**:CP/GA/外部引擎独立写入 trace_chain、前端追溯 UI 留待后续轮。
---
## 2026-08-01 — 审计完整性告警
- **类型**:feature / security
- **做了什么**:新增 `server/agent_core/audit_alerts.py`(`build_alerts` 聚合链断裂 critical/锚定不匹配 warning/账本损坏 critical/未锚定 info/镜像未启用 info);`/api/gov/audit` 返回 `alerts`;前端 `AuditAlert` 类型 + AuditView 按 severity 展示告警条(样式 `.gov-alert-*`)。
- **为什么**:矩阵 104 行剩余「异常告警」;审计闭环最后一块。
- **验证**:`test_audit_alerts.py` 5 项(正常无告警、链断裂、锚定不匹配、未锚定/账本损坏、镜像源)通过;全量黄金测试固定运行时 490 passed;前端 tsc 通过;ruff 干净。
- **边界**:WORM 物理介质、完整 Merkle 树、告警通知渠道(邮件/IM)留待后续轮。
---
## 2026-08-01 — 审计合规导出与保留脱敏
- **类型**:feature / security / compliance
- **做了什么**:新增 `server/agent_core/audit_export.py`:`sanitize_event`(token/password/secret/key/authorization/credential 键值递归掩码)与 `export_events`(json/csv,含元信息);网关 `GET /api/gov/audit/export?format=json|csv` 导出当前作用域审计事件(脱敏)+ 链/锚定摘要。
- **为什么**:矩阵 104 行剩余「保留脱敏、合规导出」;审计闭环的收尾能力。
- **验证**:`test_audit_export.py` 6 项(敏感键掩码、json/csv 可解析、未知格式拒绝、网关端点、worldKey 非敏感保留)通过;全量黄金测试固定运行时 485 passed;ruff 干净。脱敏采用精确键集合,避免误掩 `worldKey` 等含 "key" 子串字段。
- **边界**:WORM 物理介质、完整 Merkle 树、异常告警、前端导出 UI 留待后续轮。
---
## 2026-08-01 — 审批转派(transfer)
- **类型**:feature / governance
- **做了什么**:`ApprovalBackend.transfer()`(文件 + 共享 database 双后端,DB 用 CAS 重试)+ `harness.transfer_confirmation()` + 网关 `POST /api/actions/transfer`;P2 可把待确认项转派给指定用户(转派后 owner=目标、原 owner/delegate 不可再审批),P3 拒绝转派保双人职责分离;历史事件 `TRANSFERRED` 记录 from/to。
- **为什么**:矩阵 P3「外部副作用二次确认」剩余项(转派审批);委托之后审批域的自然增量。
- **验证**:`test_approval_transfer.py` 5 项(owner 转派、非 owner 拒绝、P3 拒绝、转派给自己 no-op、DB 持久化)通过;全量黄金测试固定运行时 479 passed;ruff 干净。
- **边界**:批量转派、委托撤销、转派 UI 展示、MySQL 实机故障注入留待后续轮。
---
## 2026-08-01 — 统一 EvidenceItem 可追溯链
- **类型**:feature / governance
- **做了什么**:新增 `server/agent_core/evidence.py`:`EvidenceItem`(结构化证据记录:kind/ref/version/runId/engine/seed/inputsHash/knowledgeVersion/confirmId/note)、`evidence_ref`/`parse_evidence_ref`(`<kind>:<id>` 语法,kind 白名单防注入)、`trace_chain()`(规范化 JSON + SHA-256 链哈希,任何元素/顺序/内容变化断链,可复算)。
- **为什么**:矩阵 114 行剩余「统一 EvidenceItem;输入包、算法版本、种子、知识版本和用户确认可串成一条链」;此前 run-id/solverMeta/seed 分散在各引擎。
- **验证**:`test_evidence_item.py` 6 项(ref 往返、kind 白名单、链确定性、内容/顺序断链、schedule-version 兼容、dict 复算)通过;全量黄金测试固定运行时 474 passed;ruff 干净。
- **边界**:各引擎实际写入 trace_chain、前端追溯 UI 留待后续轮。
---
## 2026-08-01 — 审批委托(delegation)
- **类型**:feature / governance
- **做了什么**:`stage_confirmation(..., delegate_user_id=...)` 出卡时可指定被委托人;P2 单重审批作用域允许 owner 或被委托人操作(`_matches_current_scope`),P3 双人职责分离不受委托影响(委托不视为身份合并);`delegateUserId` 随 record 在文件/共享 database 双后端持久化。
- **为什么**:矩阵 P3「外部副作用二次确认」剩余项之一;此前审批只认 owner。
- **验证**:`test_approval_delegate.py` 5 项(delegate 可审批、非 delegate 拒绝、owner 仍可、P3 SOD 不变、DB 后端持久化)通过;全量黄金测试固定运行时 468 passed;ruff 干净。
- **边界**:转派、批量委托、委托撤销、MySQL 实机故障注入留待后续轮。
---
## 2026-08-01 — 契约生成自动化与差异检查门禁
- **类型**:feature / governance
- **做了什么**:新增 `scripts/generate_contract_types.py`:从 `shared/schemas/*.json` 生成 TS 类型声明(interface + 枚举 literal union,schema 为权威源);新增 `tests/golden/test_contract_generation.py` 5 项门禁(生成器对全部 7 个 schema 可执行、必填字段/枚举与 schema 一致、手写 types.ts 关键面 ⊆ schema、生成确定性)。
- **为什么**:矩阵 117 行剩余「契约生成自动化(schema → pydantic/TS)」;此前一致性靠手写 + 校验,无可复用生成源。
- **验证**:`test_contract_generation.py` 6 项(含 nullable 类型映射)通过;全量黄金测试固定运行时 462 passed;ruff 干净。
- **边界**:pydantic 端自动生成与复杂嵌套类型完整支持留待扩展;生成器不自动改写手写 types.ts(供人工采用/差异检查)。
---
## 2026-08-01 — 审计事件独立介质镜像(AuditMirror)
- **类型**:feature / security / governance
- **做了什么**:新增 `server/agent_core/audit_mirror.py`(`AuditMirror`,按租户/项目隔离 append-only JSONL,原子追加 + fsync);`APS_AUDIT_MIRROR=1` 时 `write_audit` 同步镜像每条审计事件(尽力而为,失败只记日志不阻断业务);`/api/gov/audit` 有镜像时以镜像为事件源(响应含 `source: mirror|world`),业务进程修改 world 无法抹掉独立介质证据。
- **为什么**:矩阵 116 行剩余「业务进程无法修改历史事件;独立介质」——此前审计事件只存世界内,改 world 即抹掉。
- **验证**:`test_audit_mirror.py` 6 项(追加/读取、隔离、改 world 不影响镜像、开关、write_audit 镜像、镜像事件链校验)通过;全量黄金测试固定运行时 457 passed;ruff 干净(gateway 8 处 F401/F811 为 HEAD 预存在)。
- **边界**:镜像默认关闭(`APS_AUDIT_MIRROR=1` 显式启用);`_mirror_event` 全异常兜底(严格尽力而为);WORM/加密、清理策略、跨进程锁强化留待后续轮。
---
## 2026-08-01 — 文档技术选型漂移门禁
- **类型**:docs / governance
- **做了什么**:修订 `docs/architecture/overview.md` 与 `docs/product/positioning.md` 的桌面技术选型描述——从「Tauri(后期/规划)」改为「Electron(当前实现,`apps/desktop`;Tauri 目标态待产品/架构决策)」,消除与代码事实(Electron 43.2.0)的漂移;新增 `tests/golden/test_doc_drift.py` 门禁:桌面描述必须含 Electron 或「待决策」标注、禁止孤立 Tauri 绝对描述、Hybrid 引擎描述与代码一致。
- **为什么**:矩阵 117 行点名「Hybrid、Tauri/Electron 文档描述漂移」;此前 overview/positioning 与 desktop.md 自相矛盾。
- **验证**:`test_doc_drift.py` 4 项通过;全量黄金测试固定运行时 451 passed;ruff 干净。
- **边界**:Tauri/Electron 终局决策仍待产品/架构;契约生成自动化留待后续轮。
---
## 2026-08-01 — 审计独立介质锚定(AuditLedger)
- **类型**:feature / security / governance
- **做了什么**:新增 `server/agent_core/audit_ledger.py`:`audit_root(events)`(对链哈希做双哈希聚合根)与 `AnchorLedger`(按租户/项目隔离的 append-only JSONL 账本,原子追加 + fsync);`/api/gov/audit` 返回 `anchor` 状态(已锚定/根/事件数/ok),新增 `POST /api/gov/audit/anchor` 显式锚定;与既有 `verify_audit_chain` 链式校验互补(事件体篡改由链校验捕捉、哈希序列/账本行删改由锚定捕捉)。
- **为什么**:矩阵「全审计 / 审计 append-only 且可检测篡改」P0 剩余项——此前审计只存世界内,无独立介质与外部锚定证明。
- **验证**:`test_audit_ledger.py` 6 项(根确定性/顺序数量敏感、锚定往返、hash 篡改检测、账本行删除检测、root+chain 互补、租户隔离)+ `test_audit_anchor_api.py` 1 项(端点端到端:GET 未锚定→POST 锚定→GET 已锚定)通过;全量黄金测试固定运行时 447 passed;ruff 干净。
- **边界**:审计事件仍存世界内(可被业务进程改写),独立存储迁移、WORM、Merkle 树、导出与告警留待后续轮。
---
## 2026-08-01 — 审批意见 UI 输入框(单条+批量)
- **类型**:feature / ui
- **做了什么**:GovConsole 待审批列表每条确认卡新增「审批意见」输入框,批量操作栏新增公共意见输入框;`postConfirm`/`postConfirmBatch` 支持 `note` 参数透传(接口能力此前已就绪,本次补齐前端入口);审批历史「意见」列由此真正可用。
- **为什么**:页面巡检员 WARN——接口支持 note 但 UI 无输入框,意见列恒显示「—」。
- **验证**:`test_confirm_batch.py` 5 项(新增单条 confirm API note 透传断言);全量黄金测试固定运行时 440 passed;前端 tsc 通过;ruff 干净。
- **边界**:无。
---
## 2026-08-01 — 批量审批(confirm-batch)
- **类型**:feature / governance
- **做了什么**:新增 `POST /api/actions/confirm-batch` 批量确认端点(同一决定应用于多条确认卡,逐条复用单条执行语义:证据校验、P3 双人、驳回、审计;单条失败隔离不阻断其余;令牌不存在/已失效明确标记失败);GovConsole 待审批列表新增多选/全选 + 批量批准/驳回;前端 `postConfirmBatch` 与类型同步。
- **为什么**:矩阵 P3「外部副作用二次确认」剩余项之一;此前待审批只能逐条操作。
- **验证**:`test_confirm_batch.py` 4 项(批量 P2 批准、P3 首次批准 secondConfirmRequired、批量驳回、单条失败隔离)通过;全量黄金测试固定运行时 439 passed;前端 tsc 通过;ruff 干净(gateway 8 处 F401/F811 为 HEAD 预存在)。
- **边界**:委托/转派审批、文件→DB 存量迁移、WORM 归档、MySQL 实机故障注入留待后续轮。
---
## 2026-08-01 — 审批意见(note)贯穿批准流程
- **类型**:feature / governance
- **做了什么**:`take_confirmation`/`execute_confirmed`/网关 `ConfirmRequest` 支持可选审批意见 `note`;`ApprovalStore.decide` 与 `DatabaseApprovalStore.decide` 将意见写入 approvals 记录与审批历史事件(驳回路径同样落库);管理台审批历史新增「意见」列展示;前端 `ApprovalActor.note` 类型同步。
- **为什么**:矩阵 P3「外部副作用二次确认」剩余验收项首个子项;此前审批历史只有时间/动作/结果,无决策依据。
- **验证**:双后端聚焦 46 passed(新增 note 批准/驳回断言);全量黄金测试固定运行时 435 passed;前端 `tsc -b` 通过;ruff 干净(gateway 8 处 F401/F811 为 HEAD 预存在,不在本轮改动范围)。
- **边界**:委托/转派/批量审批、文件→DB 存量迁移、WORM 归档、MySQL 实机故障注入留待后续轮。
---
## 2026-08-01 — 契约漂移门禁补全:公共排产契约纳入三方校验链
- **类型**:feature / governance / fix
- **做了什么**:把 `scheduling_problem` / `scheduling_solution` / `plan_node` 三个公共排产契约纳入 `tests/golden/test_contract_sync.py` 三方校验链(schema 字段/必填/枚举 ↔ Python 模型 ↔ 网关契约端点),并新增 schema 清单门禁(磁盘 schema 与测试覆盖集不一致即失败)与不兼容消费者漂移测试。修复真实契约漂移:`scheduling_dto.py` 的 `orders/routings/resources`(problem)与 `operations`(solution)、`plan_runtime.py` 的 `evidenceRefs` 按 schema required 去掉空默认值(fail-closed);移除预存在未用 import。
- **为什么**:矩阵 P0 批次点名「三层契约漂移门禁」;此前 4 个契约有校验,3 个公共排产契约漂移无门禁。
- **验证**:`test_contract_sync.py` 12 passed(原 6 + 新 6);依赖测试 28 passed;全量黄金测试固定运行时 ≥425 通过;ruff 干净。
- **边界**:契约生成自动化与 Hybrid/Tauri 文档漂移门禁留待后续轮次。
---
## 2026-08-01 — 证据链 v1:强关联扩展到全部 P2/P3
- **类型**:feature / security / governance
- **做了什么**:把 `mes.dispatch` 的「确认令牌 + 版本证据 + 写前快照、缺项拒绝」统一为全 P2/P3 执行协议。`stage_confirmation` 出卡时冻结 `evidenceRefs`(显式或按 `versionId` 派生 `schedule-version:<id>`)、`beforeSnapshot` 与 `beforeFingerprint`(输入快照指纹);`execute_confirmed` 在最终批准后、写世界前执行 `verify_pending_evidence`(版本证据绑定 / 前置快照存在 / 版本可解析,fail closed,失败写 DENIED 审计且不改世界);26 处写审计全部携带 `beforeSnapshot`+`evidenceRefs`。
- **为什么**:完成度矩阵 P0 批次点名「把强关联扩展到其余 P2/P3,统一 EvidenceItem、输入快照和版本协议」;此前只有 mes.dispatch 强绑定,其余 P2/P3 审计字段缺失、无漂移/缺项拒绝。
- **验证**:新增 `tests/golden/test_evidence_contract.py` 9 项(缺证据拒绝、快照缺失拒绝、版本失效拒绝、版本替换拒绝、审计字段断言、mes.dispatch 回归);全量黄金测试固定运行时 `424 passed`(原 397 + 新增);ruff 干净。
- **边界**:漂移检测采用对象域版本一致性(世界带版本表才解析),不做全局 world 指纹硬拒绝,避免审批期间 P0/P1 合法改世界的假阳性。
---
## 2026-07-31 — 跨主机共享审批存储首切片
- **类型**:feature / security / database
- **做了什么**:保留默认文件审批后端,新增显式 `APS_APPROVAL_BACKEND=database`;共享模式复用 MySQL `APS_DATABASE_URL`,以规范化 request/grant/event 表和 revision CAS 协调批准/驳回终态、P3 grant 签发及全局一次消费。最终批准、grant 和审批事件同事务提交,数据库只保存 token SHA-256;database 与 `APS_APPROVAL_PATH` 冲突、缺少 URL、方言不符或连接失败均失败关闭且不回退文件。Harness 外部 API、P2/P3、职责分离、TTL 和角色策略语义保持不变。
- **涉及路径**:`server/agent_core/approval_store.py`、`server/agent_core/approval_db_store.py`、`server/agent_core/harness.py`、`server/db/models.py`、`server/db/migrations/versions/20260731_03_shared_approval_store.py`、`tests/golden/test_approval_database_store.py`
- **验证**:SQLite 本地数据库合同/CAS/竞态/token 摘要/失败关闭专项与既有审批回归通过;这不是 MySQL 实机或真实多主机验收。
- **文档同步**:`.env.example`、`architecture/database.md`、`architecture/harness.md`、`product/implementation-spec.md`、`product/plan-completion-matrix.md`
- **遗留**:执行副作用仍存在批准状态提交后、真实执行前的崩溃窗口;待补 outbox/幂等执行状态机、文件存量迁移器、MySQL 多主机故障注入和 WORM 审计归档。
## 2026-07-31 — 按动作与等级细分审批角色策略
- **类型**:feature / security
- **做了什么**:新增 `APS_APPROVAL_ROLE_POLICIES` 严格 JSON 配置,可分别按精确 action 和 P2/P3 设置发起、批准、历史查看角色;每种能力按 action > power > 既有全局环境变量确定性解析;未知字段/action/等级、重复键和非法角色结构使整份配置失败关闭;待办、确认解析、批准和历史四条路径统一按动作授权;治理策略 API 保留原字段并追加角色、来源和配置校验状态;P3 双人审批及不同用户职责分离保持不变。
- **涉及路径**:`server/agent_core/harness.py`、`tests/golden/test_approval_role_policies.py`
- **验证**:角色策略专项 `10 passed`;审批/Harness/MES 聚焦回归 `45 passed`;最终在全新 `APS_DB_PATH` 的全量黄金 `397 passed in 168.33s`
- **文档同步**:`architecture/harness.md`、`product/features.md`、`product/plan-completion-matrix.md`
- **遗留**:审批意见、委托/转派/批量审批、跨主机共享存储和 WORM 审计归档仍待后续切片。
## 2026-07-31 — 固定 CPython Desktop Sidecar 与安装态守护
- **类型**:feature / release / security
- **做了什么**:桌面安装态不再调用系统 Python;新增 uv-managed CPython 3.13.12、哈希锁定的 Windows x64 依赖、PyInstaller onedir 构建和冻结产物冒烟。Sidecar 在进入 FastAPI 前校验不回显的请求侧 nonce,缺失/错误值返回 403;每次启动和重启轮换端口与 nonce,旧 origin 立即失效并冻结窗口直至新实例健康。Electron renderer 启用 sandbox,注入打包态 CSP,默认拒绝权限请求,限制导航/重定向与外链协议,校验 IPC sender,并使用单实例锁;Sidecar 固定单 worker、使用严格环境允许列表和 10 MiB 日志轮转,进程树及父进程看门狗继续负责退出回收。Electron/electron-builder 升级到 43.2.0/26.15.3,并固定已修复的 `brace-expansion` 5.0.9;根构建引擎同步要求 Node >=22.12.0。
- **涉及路径**:`server/sidecar.py`、`apps/desktop/main.cjs`、`apps/desktop/preload.cjs`、`apps/desktop/sidecar.cjs`、`apps/desktop/package.json`、`apps/desktop/package-lock.json`、`scripts/build-sidecar.mjs`、`scripts/smoke-packaged-desktop.mjs`、`packaging/aps-sidecar.spec`、`packaging/requirements-sidecar.lock`、`tests/golden/test_desktop_sidecar.py`
- **验证**:Sidecar Python 定向 `33 passed`;Node 生命周期定向 `7 passed`;`npm audit --prefix apps/desktop --registry=https://registry.npmjs.org` 为 0;`npm run build:desktop` 成功生成无根仓库泄漏的 NSIS;固定运行时 5 轮隔离求解、冻结 exe 自探针、父进程消失退出均通过;打包 Electron CDP 冒烟确认真实 Sidecar origin 与 preload bridge 可用,直接未授权访问健康端点返回 403。
- **遗留**:当前安装包未签名、使用默认 Electron 图标;尚未在无 Python/无网络的干净 Win10/11 x64、中文和空格安装路径上完成 20 次冷启动、Defender、升级/回滚与真实 APS 大算例验收;`plan.md` 的 Tauri 目标与当前 Electron 实现仍需产品/架构决策。
## 2026-07-31 — CycloneDX SBOM 与本地制品完整性清单
- **类型**:release / security
- **做了什么**:新增固定版本的 CycloneDX npm/Python 生成器,为 Web、Desktop 和冻结 Sidecar 依赖生成可重复、校验通过的 CycloneDX 1.6 SBOM;同时记录 SBOM 与现有安装器、Electron、Sidecar 制品的 SHA-256 和字节数。发布清单显式标记 `signed=false`,不把本地哈希清单冒充签名 provenance。
- **涉及路径**:`scripts/generate-sbom.mjs`、`package.json`、`.gitignore`
- **验证**:`npm run sbom` 生成三份非空且校验通过的 BOM 和 `dist/sbom/release-manifest.json`。
- **遗留**:尚未接入 CI、签名服务、透明日志或 SLSA provenance;生成物只有本地完整性证据,不构成可信发布证明。
## 2026-07-31 — 审批队列持久化、TTL 与 P3 双人职责分离
- **类型**:feature / security
- **做了什么**:新增原子 JSON 审批存储,持久化待审批动作、决策历史和一次性执行凭据;审批与凭据分别支持可配置 TTL,过期后拒绝并记录历史;P3 第二重必须由同租户/项目作用域内、具备配置角色的不同用户完成,同一用户重复审批显式拒绝;发起/批准/历史只读角色可分别配置,权限错误统一返回 403;记录冻结发起人的有效 `worldKey`,防止第二审批人的个人空间劫持执行目标;全部审计 actor 从认证身份派生,客户端原值仅作为 `requestedActor` 证据;冻结参数摘要和一次性消费语义跨进程重启保持不变;同机文件锁与落盘前刷新阻止多个 worker 重复签发;损坏或非法记录文件隔离为 `.corrupt-*` 并告警;治理台新增审批历史,待办/历史展示发起人、审批进度、等待/停留时长与到期时间。
- **涉及路径**:`server/agent_core/approval_store.py`、`server/agent_core/harness.py`、`server/aps_domain/workflow.py`、`server/gateway/app.py`、`apps/web/src/api/client.ts`、`apps/web/src/api/types.ts`、`apps/web/src/gov/GovConsole.tsx`、`tests/golden/test_approval_store.py`
- **验证**:审批存储定向含真实 `spawn` 多进程竞争;审批/MES/租户隔离/审计链定向 `41 passed`,覆盖错误个人世界、actor 伪造、角色 403 和坏 TTL 隔离;相关 Python Ruff 和 `npm run build:web` 成功。
- **遗留**:跨主机或不保证文件锁语义的网络文件系统仍需事务共享存储;按动作/等级细分角色策略、审批委托/转派/批量审批和 WORM 审计归档仍待后续切片。
## 2026-07-31 — 独立审计 P1/P2 整改
- **类型**:fix
- **做了什么**:P3 最终批准签发并消费一次性、租户/项目作用域内且绑定规范化审批参数摘要的内部执行凭据,替换 `track/versionId/evidenceRefs` 会被拒绝且不消耗合法凭据;MES 下发校验实际存在的前置快照、审批冻结版本和对应证据,只下发该版本,审批期间产生新版本不会漂移;预览最多展示 80 条但执行处理完整集合,只有全部工单均建立本地幂等链接后才标记整版已下发;固定轨通过生产订单 `schedulingVersionId` 反查工单;跨租户查询已知令牌不再泄漏第二确认状态。方案 UI 完全服从后端 `recommendationId` 并禁用 `HARD_REJECTED`;Pareto 仅在前沿内归一化。Plan HTTP API 不再接受可伪造的 `createdBy`,来源固定为 `USER`。ScheduleResult 的 `EXTERNAL`、必填字段和求解元数据同步到 JSON Schema/TypeScript。
- **涉及路径**:`server/agent_core/harness.py`、`server/aps_domain/mes.py`、`server/aps_domain/workflow.py`、`server/aps_domain/scenario_selection.py`、`server/gateway/plan_api.py`、`shared/schemas/schedule_result.schema.json`、`apps/web/src/api/types.ts`、`apps/web/src/chat/ChatPanel.tsx`
- **验证**:首轮审计整改跨模块定向 `43 passed`;参数替换/81 条边界整改专项 `14 passed`;最终全量黄金 `337 passed in 148.01s`;`npm run build:web` 成功。
- **遗留**:审批队列/执行凭据仍为单进程内存态;TTL、职责分离、跨重启恢复、真实 MES MCP、Saga/WORM 仍待后续切片。
## 2026-07-31 — Windows 求解器发布探针
- **类型**:test
- **做了什么**:新增隔离子进程探针,重复导入 NumPy/Pandas/OR-Tools 并运行最小 CP-SAT 求解;同时扫描标准输出和错误输出中的 `Windows fatal exception`、`Fatal Python error`、`0xc0000139`,避免原生加载异常被退出码 0 掩盖。
- **涉及路径**:`scripts/check_solver_runtime.py`、`tests/golden/test_solver_runtime_probe.py`
- **验证**:仓库 `.venv` 直接五轮探针通过;探针黄金用例通过;包含该用例的最终全量黄金 `337 passed in 148.01s`。PATH 首位 Anaconda Python 首轮以 `0xc0000139 / WinError 127` 被正确阻断,不属于可发布运行时。
- **遗留**:探针是发布阻断器,不替代对 Anaconda/Python 3.13 与二进制 wheel 组合的根因治理;正式打包环境应使用固定、可复现的 CPython sidecar。
## 2026-07-31 — SC-14 方案优选与蒙特卡洛鲁棒性首切片
- **类型**:feature
- **做了什么**:为五策略沙盒方案增加硬约束过滤、真实帕累托前沿、min-max 归一化加权排序和稳定并列规则;新增固定种子蒙特卡洛,每个样本真实重跑排产并输出验收达成比例及延期分位;场景卡和现有敏感性 API 仅追加字段,旧契约保持兼容。
- **涉及路径**:`server/aps_domain/scenario_selection.py`、`server/aps_domain/robustness.py`、`server/aps_domain/scenario.py`、`server/aps_domain/sensitivity.py`、`tests/golden/test_scenario_selection.py`
- **验证**:定向回归 `15 passed`;`python -m pytest tests/golden -q -p no:cacheprovider` → `325 passed`;相关 Python 文件 Ruff 与 `git diff --check` 通过。算法 agent 的独立运行曾打印 OR-Tools→pandas/pyarrow 的 Windows `0xc0000139` 环境异常栈,但主代理最终全量回归未复现,仍保留为发布环境风险。
- **文档同步**:`algorithm/solution-selection-v1.md`、`algorithm/README.md`、`algorithm/roadmap.md`
- **遗留**:NSGA-II 求解器、生产级数百样本并行、历史分布校准、更多扰动因子、偏好学习权重和 Sobol 仍待后续切片。
## 2026-07-31 — L0-L3 PlanNode 可重生运行时首切片
- **类型**:feature
- **做了什么**:新增 L0-L3 共用的不可变 `PlanNode` 契约与项目隔离的追加式 JSON 版本仓;重生保持同一 `planId`、递增 `version/regenCount` 并保留旧版;规范化 `inputsHash` 校验阻断输入漂移,默认重生 3 次后熔断;提供创建、最新版本、版本历史和重生四个最小 API。
- **涉及路径**:`server/agent_core/plan_runtime.py`、`server/gateway/plan_api.py`、`server/gateway/app.py`、`shared/schemas/plan_node.schema.json`、`tests/golden/test_plan_runtime.py`
- **验证**:`python -m pytest tests/golden/test_plan_runtime.py -q` → `8 passed`;加治理/认证/租户/会话回归集 → `40 passed`
- **文档同步**:`architecture/plan-runtime.md`、`architecture/harness.md`、`architecture/modules.md`、`product/features.md`
- **遗留**:尚未接入 Agent 自动分解、时间线 UI、审计事件写入与跨进程文件锁;本切片只负责治理模型、持久化和最小 API。
## 2026-07-31 — P3 外部副作用二次确认
- **类型**:fix
- **做了什么**:确认暂存记录增加审批步数;P3 第一次批准只写 GATE 审计并保留令牌,第二次批准才执行外部副作用;拒绝会在任一步骤立即终止;Web 确认卡在第一步完成后切换为“二次确认并执行”。MES 下发在最终执行前强制校验 `confirmId`、自动检查点 `beforeSnapshot` 和排产版本 `evidenceRefs`,并将三者写入审计链;缺失任一项即拒绝执行。P2 仍保持一次确认。
- **涉及路径**:`server/agent_core/harness.py`、`server/aps_domain/workflow.py`、`server/gateway/app.py`、`apps/web/src/api/client.ts`、`apps/web/src/chat/ChatPanel.tsx`、`tests/golden/test_harness_p3.py`
- **验证**:`pytest tests/golden/test_harness_p3.py tests/golden/test_gov_api.py tests/golden/test_mes.py -q` → `13 passed`,覆盖 Gateway 0→1 外部副作用次数、重复令牌不刷新、缺少门禁/快照/证据时拒绝;`npm run build:web` 成功。
- **文档同步**:`product/features.md`、`product/implementation-spec.md`、`architecture/harness.md`
- **遗留**:持久化审批队列、TTL、职责分离和真实 MES 适配器仍待后续切片。
## 2026-07-31 — 跨层契约版本与漂移门禁
- **类型**:fix
- **做了什么**:为意图、UI 块和视口命令统一增加 `interfaceVersion=1.0`;把 `intent.schema.json` 的封闭枚举同步到当前运行时意图全集;增加黄金测试阻断 Pydantic、JSON Schema 与 TypeScript 的版本及 UIBlock/ViewportCommand 枚举漂移;TypeScript 镜像增加兼容字段。公共 `/api/health` 现在返回同一版本,前端 `InterfaceGate` 在认证和业务界面挂载前完成握手,不兼容或服务不可用时显式阻断。
- **涉及路径**:`server/contracts.py`、`server/gateway/app.py`、`shared/schemas/*.json`、`apps/web/src/api/types.ts`、`apps/web/src/auth/InterfaceGate.tsx`、`apps/web/src/main.tsx`、`tests/golden/test_contract_sync.py`
- **验证**:契约+认证握手定向 `11 passed`;`npm run build:web` 成功。
- **文档同步**:`architecture/overview.md`
- **遗留**:其余公共排产契约仍需逐步纳入统一生成链;多版本兼容窗口和灰度升级策略尚未建立。
## 2026-07-31 — SC-03 GA 真实求解首切片
- **类型**:feature
- **做了什么**:新增固定种子的 `GeneticAlgorithmEngine`,联合搜索订单顺序与具备完整工位映射的候选产线;精英保留保证 RULE 顺序/首选线基线进入种群;代数预算由时限确定性推导,避免墙钟截断破坏固定种子复算;通过统一 `solverMeta` 留存种群、代数、目标值、耗时与种子;`get_engine("GA")` 不再由 RULE 代跑。
- **涉及路径**:`server/engines/ga_engine.py`、`server/engines/__init__.py`、`tests/golden/test_ga_engine.py`
- **验证**:`pytest tests/golden/test_ga_engine.py tests/golden/test_cp_engine.py tests/golden/test_rule_engine.py -q` → `24 passed`
- **文档同步**:`product/features.md`、`product/implementation-spec.md`、`algorithm/roadmap.md`、`algorithm/scheduling-v1.md`、`architecture/modules.md`
- **遗留**:方案层 Pareto 已落地;工序级 GA、NSGA-II 种群进化、LNS 和大算例性能基线仍待后续切片。
## 2026-07-24 — 桌面/Web 双形态与 ~/.aps(对标 Codex)

View File

@ -0,0 +1,167 @@
# Plan 完成度与验收矩阵
> 基线日期:2026-08-20(round-67,承接 round-66)
> 范围:根目录 `plan.md` 的 M-POC、M0-M6 与跨阶段治理要求。
> 判定顺序:当前可运行代码与黄金测试 > `docs/` 落地文档 > `plan.md` 目标态描述。
> 状态只使用 `Done`、`Partial`、`Missing`;Mock、代理实现和“首切片”不得标为 `Done`。
## 1. 当前验证基线
- GitNexus 当前索引:10,340 个符号、54,203 条关系、300 条执行流(round-66 当前索引)。
- 黄金测试命令:`.\.venv\Scripts\python.exe -X utf8 -m pytest tests/golden -q`。
- Round 66 最终全量黄金为 **1029 passed / 2 warnings**(最新源码,300.23 秒);post-fix focused 为 **80 passed**,覆盖源码 Sidecar `-I`、响应摘要与业务语义假绿负向用例。
- Round 67(2026-08-20):审批迁移 focused **19 passed**;审批相关套件 **88 passed**;全量黄金三批合计 **1065 passed / 1 failed**(`test_preference_features::test_extract_features_from_world` 为 DXF-01 轮已记录的既有失败,与 round-67 无关);ruff、diff-check 通过。
- Node **60 passed**;Web build、ruff、Python compile、Node syntax check、`git diff --check` 均通过。
- 实时服务:8003 PID `21576`、5173 PID `9808` healthy;浏览器 title=`APS 智能排产工作台`、root=true、console warning/error 为 0。
- 求解器隔离真实运行:复制 `world.json` 的 CP/HYBRID 均 `OPTIMAL`、1 PO / 10 WO / 10 operationSlots、协议 `aps.solver-process.v1`、`runtimeSafe=true`;fatal/timeout 均失败关闭且父进程存活。
- 源码 Sidecar `--solver-child -I` 首次冒烟发现源码 import root 被隔离;GitNexus LOW impact 修复为仅 bootstrap trusted source root,新测试覆盖,实际 CP `OPTIMAL`、1/10/10。现有冻结 EXE 未重建,不能把源码通过冒充打包通过。
- MOM 复制数据 CP/HYBRID 求解为 `OPTIMAL`,但现有主数据阻断导致 0 PO/WO、1 slot/1 conflict;这不是现场工厂主数据完成证明。
- 数据保护:`world.json` hash `C6E7FF...52FA`、MOM hash `6B64AD...3F7D` 前后不变,`server/data/**` 与 `.env` 无 Git 状态。`master.db` 可因 license `last_seen` 等运行元数据变化,不以文件 hash 声称数据库完全不变。
- 矩阵保持 **50 Done / 10 Partial / 0 Missing**。10 个 Partial 的最终验收仍依赖真实外部系统、设备、凭据或构建环境;最终外部步骤见 [external-acceptance.md](./external-acceptance.md),不得因本地首切片、源码冒烟或 Mock 证据虚假标记 `Done`。
## 2. M-POC:左说右动验证
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| 中文指令经确定性解析器转为结构化视口命令,不依赖 LLM | Done | `poc/workbench.html`:`parseNL`、`applyCommand` | 固定验收口令集回放结果稳定;离线打开可用 | 产品定义验收口令;开发维护解析与命令协议 |
| 甘特、负荷热力、交期承诺三视图可由同一命令通道切换 | Done | `poc/workbench.html`:`viewport.mode`、`renderGantt`、`renderLoad`、`renderDue` | 每种口令和手动页签产生同构命令与一致视图 | 产品验收交互一致性;前端负责渲染 |
| 过滤、聚焦、高亮、时间粒度和重置命令可联动右侧视口 | Done | `poc/workbench.html`:`viewport.filter/focus/highlight/timescale/reset` | 命令顺序组合可回放,重置后无残留视口状态 | 前端负责状态机;测试负责组合回放 |
| 甘特拖拽与自然语言口令使用同构命令 | Done | `poc/workbench.html` 拖拽处理与结构化 command | 拖拽吸附、冲突提示和命令载荷形成固定验收样例 | 产品定义调程语义;前端实现;算法校验可行性 |
## 3. M0:工程地基
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| Monorepo 骨架覆盖 Web、桌面、Python Gateway、Agent Core、领域、状态、共享契约和黄金测试 | Done | `apps/web/`、`apps/desktop/`、`server/`、`shared/`、`tests/golden/`、根 `package.json` | 新模块继续遵守分层边界,不从 UI 直接写世界状态 | 架构/开发负责 |
| UIBlock、ViewportCommand、Intent、排产问题/结果具备跨层类型契约 | Done | `shared/schemas/*.schema.json`、`server/contracts.py`、`apps/web/src/api/types.ts`、`server/aps_domain/scheduling_dto.py`、`server/agent_core/plan_runtime.py`;`test_contract_sync.py` 校验全部 7 个 schema 的字段/必填/枚举三方一致(含 scheduling_problem/scheduling_solution/plan_node),并有 schema 清单门禁与不兼容消费者漂移测试 | 无(本轮验收完成) | 开发负责契约生成与校验;测试负责漂移门禁 |
| 契约版本握手与不兼容显式失败 | Done | 公共 `/api/health` 返回 `interfaceVersion`;`InterfaceGate.tsx` 在认证和业务界面前严格比对本地版本,不兼容/不可用时阻断并显示可重试错误;契约与认证定向 `11 passed` | 保持不兼容版本回归测试;未来多版本窗口必须显式定义兼容策略 | 开发负责 |
| KIMI/DeepSeek/OpenAI 兼容模型可配置切换,失败可降级 | Done | `server/agent_core/providers.py`、`.env.example`、`tests/golden/test_gov_api.py` | 用受控端点做一次 KIMI、DeepSeek 契约测试;密钥不得出现在设置响应和日志 | 开发负责 Provider;测试负责契约与安全 |
| Gateway SSE 会话流打通 | Done | `server/gateway/app.py` 的 `/api/chat`、`StreamingResponse`;`apps/web/src/chat/ChatPanel.tsx` | 断线重连、取消、超时、事件顺序和大响应背压有集成测试 | 开发负责;测试负责网络故障注入 |
## 4. M1:核心排产闭环
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| 规则快路 + LLM JSON 两级意图识别,低置信进入澄清/降级 | Done | `server/agent_core/intent.py`、`dialog.py`;`test_dialog_clarify.py`、`test_schedule_wizard.py` | 核心生产口令离线可用;LLM 非法 JSON 不产生写动作 | 开发负责意图与对话;产品维护口令/澄清验收集 |
| Rule 引擎生成 DRAFT 版本、PO/WO、冲突与 KPI | Done | `server/engines/rule_engine.py`、`server/aps_domain/workflow.py`;`test_rule_engine.py` | 黄金算例固定工序顺序、资源独占、日历和交期结果 | 算法负责求解;开发负责装配与持久化 |
| 左说右动正式 UI:聊天驱动甘特/负荷/交期视图 | Done | `apps/web/src/chat/ChatPanel.tsx`、`viewport/ViewportPanel.tsx`、`server/contracts.py` | 浏览器端主旅程通过,命令和按钮行为一致 | 产品定义旅程;前端实现;测试做 E2E |
| P2 发布必须先出确认卡,批准后写入并刷新世界 | Done | `server/agent_core/harness.py`、`server/gateway/app.py`、`workflow.py`;`test_gov_api.py`、`test_constraints.py` | P2 无法绕过唯一确认入口;确认令牌一次性且绑定租户/项目/用户 | 开发负责门禁;审计/测试负责绕过测试 |
| 证据链 v1:排产结果包含 run-id,RAG/报告可携带来源 | Done | `server/contracts.py` 的 `evidenceRefs`;`harness.stage_confirmation` 冻结 `evidenceRefs`/`beforeSnapshot`/`beforeFingerprint`;`execute_confirmed` 统一证据强校验(版本证据绑定、前置快照存在、版本可解析,fail closed);全部 P2/P3 写审计携带 `beforeSnapshot`+`evidenceRefs`;`mes.dispatch` 强绑定保持不变;`tests/golden/test_evidence_contract.py` 9 项覆盖漂移/缺证据/版本替换/审计字段 | 无(本轮验收完成) | 开发负责证据基础设施;算法负责 run 元数据;产品定义可解释口径 |
| 设计系统 v1 与紧凑工作台交互 | Done | `apps/web/src/styles.css`、Ant Design 组件、`docs/product/features.md` | 桌面/常用浏览器关键视口无重叠;主要流程键盘和错误态可用 | 产品/设计验收;前端负责 |
## 5. M2:探索与治理
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| 项目和会话服务端持久化、成员权限与租户隔离 | Done | `server/state/projects.py`、`server/db/models.py`、`apps/web/src/projects/`;`test_workspace.py`、`test_auth_tenant_isolation.py`、`test_chat_ensure_session.py` | 并发成员操作、角色变更和跨设备恢复通过 E2E;数据库迁移可回滚 | 开发负责状态/auth;测试负责隔离与并发 |
| 项目 -> 会话 -> 分支三级树及分支生命周期 | Done | `server/state/branches.py` BranchStore + `/api/sessions/{id}/branches` 端点 + 前端 `BranchPanel.tsx` 分支树 UI(fork/rename/switch/discard/merge 操作、活动分支高亮、禁用规则与后端一致)+ `client.ts` 分支 API client;`test_branch_tree.py` 3 项;**分支合并世界差异 diff**:`compute_world_diff` 基于两分支锚定 checkpoint 世界侧计算稳定 JSON 差异(版本/KPI/工序时间/柔性版本/产能池+changed,深拷贝不改世界),merge 响应附 `diff` 并随 `branch.merge` 审计落库,前端合并前预览(接口不可用降级原确认);`test_branch_merge_diff.py` 4 项 | 无(本轮验收完成) | 开发负责状态模型/API/UI;产品定义生命周期 |
| 对话状态与世界状态真正成对快照、成对切换和成对回滚 | Done | `server/state/checkpoints.py` 成对快照(conversationSide)+ 分支切换成对恢复 + 回滚;**PlanStore 可恢复接口**:`plans_snapshot(branch_id, checkpoint_id)` / `restore_for_branch`(plan 视图随 checkpoint 归档物化,切换自动回退/恢复,latest/node_at/versions/create/regenerate/transition_status 视图感知,无锚定完全兼容);gateway switch 响应附 planView + 审计;`test_plan_branch_restore.py` 5 项 + `test_branch_checkpoint_link.py` 3 项 + `test_m2_state.py` 6 项 | 无(本轮验收完成;plan 视图为 store 级单例,多会话并行切换以最后一次为准——文档化简化) | 开发负责原子状态与事务;测试负责破坏/恢复验证 |
| P2/P3 前自动 Checkpoint,手动建档和回滚 | Done | `server/state/checkpoints.py`、`server/agent_core/harness.py`、`TimelineRail.tsx`;`test_checkpoint_roundtrip`、`test_checkpoint_capacity_eviction` | 外部事件重排前和所有写动作前均强制建档;回滚本身可撤销 | 开发负责;审计/测试核对覆盖面 |
| 时间线宏观/中观/微观导航、分支 diff 与回放 | Done | `TimelineRail.tsx` 版本/Checkpoint + 分支树(BranchPanel)+ 微观层分组;`AlgoReplayPanel.tsx` ALGO_RUN 动作链逐步回放(复位/上一步/播放/下一步/末步);`WorldDiffView.tsx` 两个世界并排对比(基准 A × 对比 B:当前世界/版本快照/检查点/回放定位)逐字段 diff 高亮;**检查点世界侧投影接口**:`checkpoints.world_projection(pair_id)` + `GET /api/checkpoints/{pairId}`(单查 200/404 中文)与 `GET /api/checkpoints?pairIds=&branchIds=&sessionId=`(分支锚定批量,缺失上报),固定轨工单/柔性工单/产能池投影;前端 `fetchCheckpointWorld` 优先投影做逐字段全量 diff,失败自动回退元数据+近似快照降级并保留提示(缓存+不重试);`test_checkpoint_projection.py` 5 项 + 回放面板/世界徽标;`npm run build` 无类型错误 | 无(本轮验收完成) | 产品定义信息层级;前端负责;开发提供审计投影 |
| Explore 与 Runtime 数据边界 | Done | `explore_boundary.py` 统一通道(`readonly_view` fail closed + `run_explore` 深拷贝沙盒,5+3 项测试);前端 `WorldModeBadge.tsx` 世界种类徽标(Runtime 主世界/沙盒试排/只读分析 + 只读角标)、ViewportPanel 工具栏徽标 + 画布按世界种类着色(world-explore/runtime/analysis)、时间线根节点徽标、FlexPanel「沙盒试排」徽标;Explore(flex/pool/compare/diff/plan)与 Runtime(gantt/load/util/due)一眼可辨 | 无(本轮验收完成;真实影子价需 LP 对偶重解) | 开发负责通道/权限;测试负责越权与污染测试 |
| 多策略方案卡组、采用后进入受控流程 | Done | `scenario.py` 输出唯一 `recommendationId`;`ChatPanel.tsx` 不再自行重排并禁用 `HARD_REJECTED`;`test_scenario_selection.py` 覆盖无推荐与前端门禁 | 方案输入、引擎版本、KPI 和证据持续可复现;采用动作有审计 | 算法负责方案生成;开发负责卡片与采用流程 |
## 6. M3:知识、上下文与偏好
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| RAG 资产、导入、混合检索和来源/版本返回 | Done | `server/knowledge/assets.py`、`ingest.py`、`retrieval.py`、`embedding.py`;`test_knowledge_ingest.py`、`test_embedding_fallback.py`、`test_m3_knowledge.py` | 审批状态、版本替换、租户隔离和未命中不编造持续通过 | 开发负责知识平台;产品维护回答验收集 |
| `@会话`、`@方案`、`@版本`、`@报告` 结构化引用并按时点取值 | Done | `server/agent_core/session_ref.py`:parse_refs 解析 4 类引用 + 第 5 类 `@知识`(强制命中:命中→资产正文+出处 assetId/title/kind/version+证据 ref,未命中→显式 RefResolutionError 中文「知识库未命中…(未编造)」,未接线→显式 not-wired 报错);`resolve_ref`/`resolve_all` 新增 keyword-only `knowledge_lookup`(既有 4 类调用零破坏);workflow `handle_intent` 入口自动解析注入本轮上下文(`_refs`)+ SESSION 审计 evidenceRefs,失败 fail-closed 中文明文回复;`test_ref_knowledge.py` 9 项 + `test_session_ref.py` 6 项 | 无(本轮验收完成) | 开发负责模型/解析;产品定义引用 UX |
| 固定/项目/会话/检索四层上下文、滚动摘要、钉住与预算 | Done | `server/agent_core/context_policy.py`:ContextBudget 四层限额 + 确定性 token 估算(超限 fail closed 可预测拒绝)、assemble_context 四层组装、pin_message 钉住、scroll_summary 带 sourceMessageIds/supersedes 链可溯源 + 可选 `summarizer`(落 compressedBy 溯源);**`summarizer.py` LLM 压缩生成方**(`compress_scroll_summary` 固定中文提示词 + ModelProvider.chat_text,provider 不可用/异常/超预算 → 确定性回退,绝不编造)+ gateway chat 每轮预算门控钩子(autoSummarize 配置开关,长会话开启,try/except 兜底不阻断对话);越权上下文 fail closed 测试补齐;`test_scroll_summary_llm.py` 14 项 + `test_context_policy.py` 9 项 | 无(本轮验收完成;summaries 历史上限裁剪留待后续) | 开发负责;测试做长会话与越权上下文测试 |
| 交期承诺看板与乐观/预计/悲观模拟 | Done | `apps/web/src/viewport/DueView.tsx`、`server/aps_domain/flex.py`;`test_kangni_flex.py` | 区间口径与产能/物料输入一致,关键缺口可追溯 | 算法负责区间计算;产品验收业务口径 |
| 偏好学习影响默认策略 | Done | `server/knowledge/preferences.py`:按用户/项目隔离、explain(cold/medium/high 置信)、reset、冷启动回退、**时间衰减加权评分**(采用2/试排1 × 2^(-age/半衰期14天));**多特征规则归纳**:`extract_features(world)`(订单数/VIP占比/急单占比/品种数/硬冲突数)、分箱条件规则 `induced_rules`(采用2/试排1 条件得分+支持度)、`preferred_strategy_with_features`(规则投票命中→回退全局)、`explain_features` 中文规则依据;workflow/gateway 调用点已传 world_key + features;`test_preference_features.py` 6 项 + 既有偏好测试 | 无(本轮验收完成) | 算法负责偏好模型;开发负责隔离/存储/UI |
| 日报、版本对比和方案报告基于冻结快照并可导出 | Done | `server/aps_domain/reports.py`、报告 API/UI;`test_m3_knowledge.py`、`test_master_query_kb.py` | 增加模板管理、交期承诺函 P2、Word/PDF 输出和审阅状态 | 产品定义模板;开发负责导出;审计校验快照口径 |
## 7. M4:外部集成与自动化
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| 真实 MES MCP 下发、状态和报工回流 | Partial | `mes_stub`/`mes.py`(P3 门禁+幂等+回执+补偿)+ **HTTP MES 适配器**(round-45:config-driven HttpMesClient,鉴权/幂等/超时/重试/回流/撤单/fail-closed,stub 测试)+ **对接就绪检查(round-46)**:`readiness(probe)`(configured/baseUrl/tokenPresent/connectivity/lastError/message 稳定字段,未配置 fail-closed 提示不 500,可选轻量连通探测短超时绝不抛错)+ `GET/POST /api/integrations/mes/readiness[/probe]`;`test_mes_readiness.py` 8 项 + `test_mes_http.py` 9 项 | 真实 MES 工厂对接(填 base_url+token,现场联调端点路径/报文;健康端点路径不同需调 _probe_health) | 开发/集成负责适配器;测试负责故障注入;产品验收闭环 |
| 真实 WMS 缺料/库存事件接入 | Done | `server/integrations/wms_stub.py` MockWmsClient(库存台账+缺料事件流+幂等 idem_key+单调 inventoryVersion)+ `server/aps_domain/wms_events.py` 事件消费管线(eventId 全局一次消费、乱序 seq 缓冲排空、重复写 `wms.event.duplicate` 证据、库存版本随事件更新并入证据链 `inventory-version:N`、flexMaterials 库存同步 + WMS-EVENT 冲突);`POST /api/wms/events`(幂等上报)/ `GET /api/wms/replay`(可重放)/ `GET /api/wms/inventory`(P0 只读);`test_wms_events.py` 3 项 + `test_wms_replay.py` 3 项 | 无(本轮验收完成,Mock 形态;真实 WMS 适配器接入留待集成环境) | 开发/集成负责;算法消费标准事件,不直接连 WMS |
| MCP 插件管理与权限面板 | Partial | `mcp_bus.py` 总线(manifest/工具契约/power 门禁/健康/启停/版本兼容)+ 前端 SkillConsole「MCP 插件」页签 + **mes.http 真实适配器注册(round-45)**:mes.http_dispatch(P3)/status(P0)/report(P2)/cancel(P2),默认 deny 需显式放行;`test_mcp_bus.py` 9 项 | 真实 MES/WMS 现场对接(配置+权限放行即可;端点路径/报文现场对齐) | 开发负责插件总线/UI;安全测试负责越权 |
| 房间内事件/定时/阈值自动化与 G0-G4 档位 | Done | `server/agent_core/automation.py`:Gear 状态机 + AutomationGate 升权门禁 + RuleExecutor(事件/定时 cron/阈值)+ **业务动作注册表**(BusinessActionBinding:commit→schedule.publish 发布、reschedule→flex.reschedule 重排,含意图槽位与门禁路径)+ **调度器持久化**(snapshot/restore/save/load,宽容加载,重启恢复)+ **网关定时驱动**(`POST /api/automation/tick` 手动 + lifespan 后台自动 tick,APS_AUTOMATION_DRIVER/TICK_SECONDS 可配)+ G4 受控自动经业务桥走真实动作(未登记 P2/P3 动作即使 G4 也按 P3 fail-closed 拦截,G2/G3 出确认卡);`run_automation_intent`(workflow,auto=True 出卡后经 execute_confirmed 执行,真实写入始终过 harness);`test_automation_gateway.py` 6 项 + `test_automation_gears.py` 8 项 | 无(本轮验收完成;规则 CRUD UI 与规则管理端点留待后续) | 产品定义档位语义;开发实现调度与治理 |
| 缺料触发重排端到端闭环 | Done | `wms_events.py` 全链:WMS 缺料(event.consume)→ 影响半径(深拷贝沙盒+PoolEngine 差分只读)→ Explore 方案卡(复用 `flex.reschedule` P2 动作,wmsTriggered 护栏防重复触发)→ 人工确认(execute_confirmed L4 重排 DRAFT)→ MES 下发(P3 双人 SOD 门禁 mes.dispatch)→ 回执(wmsReceipts 镜像 + 审计 `wms.mes.receipt` 带全链证据引用);`test_wms_events.py` 闭环用例(含驳回不重排) | 无(本轮验收完成,Mock 全链) | 开发负责事件编排和门禁;算法负责影响半径 |
| 跨内部世界状态与外部系统的 Saga/补偿 | Done | `server/aps_domain/saga.py` SagaCoordinator:状态机(PENDING/RUNNING/WAITING_HUMAN/SUCCEEDED/FAILED/COMPENSATING/COMPENSATED/MANUAL_TAKEOVER)+ SagaStep(name/action/idem_key/timeout_sec/max_retries/compensation_action/gate)+ 恢复队列(world["sagas"] 持久化、宽容加载、resume_all 重放)+ 补偿注册表(失败逆序补偿链,补偿失败→MANUAL_TAKEOVER)+ 超时/重试 + 审计(saga.step.* 含 idemKey/status/compensation)+ 写前快照回滚锚点;WMS 缺料闭环接入 `build_wms_shortage_saga`/`run_shortage_saga`(事件级 dedupe);`GET /api/saga`(中间态列表)/详情/retry/compensate/takeover;`test_saga.py` 13 项 + `test_saga_wms_closure.py` 6 项 | 无(本轮验收完成;P3 双人确认自动代行需生产接真实操作员授权,auto_approve=False 停 WAITING_HUMAN) | 开发/集成负责;产品定义业务补偿边界 |
## 8. M5:算法与求解升级
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| 真 CP-SAT 与 Hybrid 求解 | Done | `cp_engine.py` 工序级模型(C1 工艺串联/C2 NoOverlap/C10 换型/C11 冻结窗)+ **Cumulative 班组/工装聚合容量**(C12:跨工位并行工序占用 ≤ 班组/工装可用容量,teamId/toolingId 主数据接线,personnel+世界剖面双闸开关,solverMeta.cumulative 含 enabled/capacity/resources[peakConcurrent]/unwired 诚实报告);**gap/时限/性能基线**(objective/bestBound/gap 元数据含 None 语义、timeLimitSeconds 确定性超时 FEASIBLE 契约、n=12 固定算例 <30s 壁钟基线);`test_cp_cumulative.py` 8 项 + `test_cp_operation_level.py` 6 项 | 无(本轮验收完成;FEASIBLE 目标随 4 worker 壁钟略有波动,位级可复现需 num_search_workers=1+seed;物化层仍走 RuleEngine 班次占槽未统一) | 算法负责模型;开发仅负责引擎装配 |
| 真 GA/NSGA-II 多目标求解 | Done | `server/engines/nsga2_engine.py`(新):标准 NSGA-II——快速非支配排序(Deb 2002)+ 拥挤度距离(边界 inf+归一化边长)+ 锦标赛选择(rank 优先拥挤度次之)+ 精英保留;工序级染色体(sequence 沿工艺路线保序 + lines 产线,OX/PMX 交叉保可行性 + 工序交换/资源迁移变异);3 目标(加权总延迟/容量溢出冲突/负载均衡取负)输出 Pareto 解集;`nsga2_defaults` 参数模板 + `build_nsga2_manifest`(algolib manifest 对齐);`get_engine("NSGA2")` 集成 + `solve_nsga2` entrypoint;`nsga2_solutions_to_cards` 衔接 rank_scenarios;大算例基线(35 订单:50 代/40 种群 <15s,Pareto 互不支配且不劣于单目标 GA 基线);**时间预算确定性**(小预算跑满固定代数与 GA 口径一致,大预算墙钟兜底);algolib nsga2 条目翻转为可用;`test_nsga2_engine.py` 11 项 | 无(本轮验收完成;ScheduleResult.engineType 复用 GA 槽位以 solverMeta 标注 NSGA2,字面 Literal 扩展留待契约轮) | 算法负责;测试维护黄金算例 |
| 独立算法库注册表与 A-E 资产分类 | Done | `server/agent_core/algolib.py` AlgorithmRegistry:A-E 分类(启发式/精确/元启发/ML/集成)、AlgorithmManifest 元数据(输入输出 schema/版本/随机种子/测试/健康)、register/query/health/version/next_seed;**ML/元启发目录资产如实登记**(milp/nsga2/ml.* available=False + 健康检查如实报告未就绪);**LNS 随 round-36 交付翻转为真实可用**(entrypoint=server.aps_domain.lns:lns_local_repair + test_rush_lns.py 黄金测试);`test_algolib.py` 5 项 + `test_algolib_catalog.py` 4 项(A-E 齐全/ML元启发如实登记/query 过滤) | 无(本轮验收完成) | 算法定义元数据;开发实现注册/调用接口 |
| 参数优化闭环、评测、灰度生效和回滚 | Done | `server/agent_core/param_opt.py` ParameterOptimizer:split 隔离/回放/GRAY 灰度/退化自动 ROLLED_BACK/敏感性衔接 + **propose**(P1 触发)+ **record_observation 线上观测**(生产 KPI 对比生效前基线、连续劣化默认 2 次可配自动回滚 + 审计 param.experiment.auto_rolled_back、观测落盘 paramObservations)+ **P2 门禁接线**(promote 经 harness 确认卡:实验摘要/候选参数/回放 KPI,批准执行验证回放 finalize GRAY→FULL、拒绝留 GRAY;harness _POWER_MAP 登记 param.* 动作);`GET /api/params/experiments` + `POST /api/params/optimize|observations` + `POST /api/params/experiments/{id}/promote|rollback`;`test_param_opt_gateway.py` 9 项 + `test_param_opt.py` 13 项 | 无(本轮验收完成;观测入口为接口驱动,每次排产自动采样可后续一行接线) | 算法负责调优;开发负责版本和灰度;产品定义 KPI |
| What-if/Tornado/Monte Carlo 与鲁棒性评分 | Done | `sensitivity.py` OAT/Tornado + `robustness.py` 蒙特卡洛;**可配置分布/相关性/置信区间**:`distributions`(normal/uniform/triangular 逐维可配 + shockProbability)、`correlation`(-1..1 需求-效率扰动相关,rho=0 随机流与旧实现种子兼容)、`confidence_level`(经验分位置信区间 tardinessCI/mean/std + config 透明元信息);`async_jobs.py` JobQueue 异步重算 + `/api/jobs*` + SkillConsole「异步任务」页签;`test_monte_carlo_config.py` 7 项 + `test_async_jobs.py` 5 项 | 无(本轮验收完成) | 算法负责;开发负责异步任务与可视化 |
| 紧约束/影子价、IIS 与冲突归因 | Done | `server/aps_domain/attribution.py`:`detect_iis()` 约束级 IIS 最小冲突集检测(黄金用例验证删除任一约束后恢复可行;可行输入返回空+说明)、`attribution_for_schedule()` 结构化归因报告(证据 conflictId+versionId+日志描述可复核)、中文归因文案(约束名/订单号/资源/短日期/解开方式,不含数学术语)、近似影子价代理量(isShadowPriceProxy 显式标注);`test_attribution.py` 7 项 | 真实 LP/CP 对偶影子价需线性化模型重解;CP-SAT 原生 IIS(solverMeta 层)与 flex 轨接入留待算法侧;归因未接 /api 与前端(后续接线) | 算法负责;产品定义可理解的归因文案 |
| LNS 插单局部修复和影响半径 | Done | `server/aps_domain/lns.py`:`lns_local_repair`(P1 沙盒评估)/ `apply_lns_local`(P2 落主干,DRAFT+LNS_RUSH+LOCAL_REPAIR,窗口外订单冻结)/ `format_lns_text`+UI 块;固定窗口(默认交期±36h)内最小扰动(movedOrderCount/shiftedHours/tardinessDelta/disturbanceRatio 度量),受影响数/扰动率/窗口宽度超阈值 → ESCALATE 升级信号;`rush.evaluate` 附加 impact.lns、`rush.apply` 出卡携带 lns、`execute_confirmed` 路由 LOCAL→apply_lns_local / ESCALATE→apply_rush 全量重排(audit 记录 mode+escalateReasons);`test_rush_lns.py` 9 项 + test_rush.py 3 项兼容 | 跨产线邻域与强制选线修复需引擎 forcedLineId 支持;网关 rush.apply 透传 lns 参数(当前默认全量安全兜底) | 算法负责;开发负责事件编排和门禁 |
| 可重生算法流水线 | Done | `server/agent_core/rebuild_orchestrator.py`(新):可重生流水线执行器——候选构建(scan_modules 解析 moduleId + 源文件/依赖/黄金测试映射)、隔离运行(subprocess + 全部 APS_* 指向随机临时目录跑 pytest 子集)、安全扫描(S1 可重生声明阻断 + S2 动态执行/S3 硬编码路径/S4 未登记写路径告警)、灰度(GRAY shadow 并存 + gray_probe 扩展点)、失败自动回滚(黄金失败/灰度探针失败恢复基线 + autoRolledBack)、审计(rebuild.started/passed/failed/gray/rolled_back,链式哈希兼容);registry.py 候选版本登记 + 激活指针 + 基线恢复;`/api/rebuild/{module_id}` 触发/`GET /api/rebuild` 状态/promote/rollback;`test_rebuild_pipeline.py` 10 项 + `test_registry.py` 6 项 | 无(本轮验收完成;激活版本为注册表逻辑指针,物理源码替换需接发布机制) | 开发负责流水线;算法负责验收基准;测试拥有发布门禁 |
## 9. M6:双形态、私有化与完整治理
| 需求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| Web 与桌面共用前端 | Done | `apps/web/`、`apps/desktop/`、根构建脚本 | 双端同一版本前端的关键旅程一致 | 开发负责 |
| Tauri 桌面壳 | Done | **架构决策(round-40):接受 Electron 为当前桌面壳**——`apps/desktop/`(Electron 43.2.0 + electron-builder 26.15.3 + NSIS + Sidecar 随包分发,打包冒烟通过);`plan.md` 目标态已同步修订(桌面端=Electron 已实现,Tauri 列为未来迁移备选不阻塞);文档漂移门禁 `test_doc_drift.py` 通过;`docs/architecture/desktop.md` 与实现一致 | 无(本轮决策收口;Tauri 迁移需另行立项评估) | 产品/架构决策;开发实施 |
| 自包含 Python Sidecar、守护、随机端口和离线运行 | Partial | 本地冒烟全覆盖:sidecar-local(20 冷启动/端口冲突/崩溃重启/看门狗/中文空格路径 5/5)+ Defender 静态扫描 4/4 clean + 真实 APS 求解(黄金测试)+ **NSIS 真装临时目录冒烟(round-44 scripts/nsis-install-smoke.mjs:/S /D=随机临时目录安装 2 次 PASS,33.6s/30.9s、567MB 解包、布局校验、卸载清理、无系统残留,验证 updater /S /D= 约定)**;`tests/node/nsis-smoke.test.mjs` 13 项 | 干净 Win10/11 离线机实机安装(%LOCALAPPDATA% 落点/快捷方式/冷启动/升级回滚)、perMachine 路径、Defender 实时防护真实拦截演练(需外部验证机/策略) | 开发/发布工程负责;测试拥有安装门禁 |
| 桌面许可证和设备隔离 | Done | `server/auth/licenses.py`、数据库迁移、`test_desktop_license_auth.py` | 密钥轮换、吊销、时钟回拨、防复制和离线续期策略通过安全测试 | 开发/安全负责;产品定义授权策略 |
| 语音、图片、文件多模态与低置信确认 | Partial | `multimodal.py` 管线框架(Extractor 协议/注册表/低置信 P2 确认门禁)+ stub 文本/文件提取器 + **ImageMetaExtractor(round-45)**:文件名/元数据级结构化候选(订单/物料/日期/附件类型,置信 ≤0.6 + requiresVlm,无线索不编造返回空、9 位数字守卫),走既有低置信确认门禁;真实 VLM 经 register(replace=True) 覆盖切换(测试断言 stub→VLM);`test_multimodal_image.py` 10 项 | 真实 ASR/VLM/OCR 模型服务接入(注册接口已就绪,需外部模型);attachments 批次确认后域表写入语义(真实 VLM 接入时接线) | 产品定义交互;开发负责管线;模型/算法负责质量评测 |
| 国产化/信创多架构交付 | Partial | **依赖矩阵与检测脚本(round-45)**:`docs/architecture/platform-matrix.md`(Python >=3.11、Windows/Linux × x86-64/ARM64、ortools/cryptography/uvicorn 等平台 wheel 覆盖标注、信创关键依赖说明)+ `scripts/platform-deps-check.py`(检测 python/平台/架构/requirements 全依赖可导入与版本下限,本地 16/16 PASS);无 GPU 依赖 | 麒麟/统信 × x86-64/ARM64 四组合实机安装、求解性能基线、离线依赖包、安全软件兼容性(需外部信创环境) | 发布工程主责;算法验证求解器兼容性 |
| CI/CD、构建矩阵、签名、SBOM、漏洞扫描和离线升级 | Partial | 可复现本地构建+打包冒烟;CI 6-job 矩阵;离线升级+失败自动回滚(upgrade-manifest + updater.cjs);health version + 审计上报端点(round-45)+ 升级 nonce 续接收口(round-46)+ applyUpdate 竞态根治(round-47)+ **pending 跨进程持久化(round-48)**:upgrade-nonce.cjs statePath 持久化 upgrade-state.json(setPending/markInstalled/settle/clear 原子写盘),应用重启后新协调器恢复未过期 pending 为 verifying 并随新 sidecar 补验,过期自动丢弃;main.cjs 接线;node 48/48 | 签名 Authenticode/SBOM 上链/provenance(外部证书);真实 GitHub CI 跑通;NSIS 真装到用户落点 | DevOps/开发负责;测试拥有门禁规则 |
| 全审计:独立存储、Merkle/WORM、保留脱敏、导出和异常检测 | Partial | 链+锚定+镜像+脱敏导出+告警+通知+Merkle+WORM 归档模拟+归档加密(AES-256-GCM 双层哈希);**审计查询与报表前端(round-46)**:GovConsole「审计统计」分区(AuditStatsView:分类计数/动作 Top8/近 7 天趋势/告警概览含 traceAlerts 断链明细,中文标签+空态提示,前端聚合 limit=300 零后端改动);`test_audit_worm.py` 7 项 + `test_audit_encryption.py` 12 项 | WORM 物理介质(光盘/WORM 盘)与 HSM/KMS 真实密钥管理/轮换(需外部介质/密钥设施);审计量达万级后引入 /api/gov/audit/stats 端点 | 安全/开发负责;产品定义审计查询与报表 |
## 10. 跨阶段治理要求
| 治理要求 | 状态 | 当前证据路径 / 测试 | 剩余验收标准 | 建议责任边界 |
| --- | --- | --- | --- | --- |
| L0-L3 Plan 是不可变、可版本化、可局部重生对象 | Done | `server/agent_core/plan_runtime.py`、`server/gateway/plan_api.py`、`plan_node.schema.json`;HTTP 来源派生 USER 拒伪造;node_at 时间线版本化;replay_verify 动作级重放校验;transition_status 合法流转+审计;plan_orchestration 自动编排;round-38 分支维度回退(restore_for_branch);`test_plan_runtime.py` + `test_plan_replay.py` 4 + `test_plan_transition.py` 4 + `test_plan_orchestration.py` 4 + `test_plan_branch_restore.py` 5 项 | 无(矩阵 110 行收口,状态补正为 Done) | 开发负责状态/编排;产品定义审批;算法提供 L1/L2 建议 |
| LLM 只有提议权 | Done | Provider 只返回文本/JSON;Harness 控制已登记动作;`tool_runtime.py` 统一工具运行时(gateway 聊天唯一执行入口)+ **异步入口 `run_tool_async` 接入其余直连端点**(scenario.apply 由直连 handle_intent 改为经工具运行时,未登记拒绝+TOOL 审计,响应契约保留;盘点确认其为唯一直连端点,chat _run_intent 已门禁);**前端工具调用可视化**:GovConsole「工具调用」视图(TOOL 审计条目:意图/等级/执行者/时间/结果 放行·确认卡·拒绝,中文标签);`test_tool_runtime_gateway.py` 5 项(含 source-level 无直连绕过盘点断言) | 无(本轮验收完成;chat _run_intent 收敛到 run_tool_async 属整洁性事项,建议下轮) | 开发/安全负责 |
| P3 外部副作用二次确认 | Partial | 文件后端默认原子持久化 + 显式 database 后端(request/grant/event 规范化表、revision CAS、token 摘要、全局一次消费、失败关闭);P3 双人 SOD;**文件→DB 存量迁移(round-67 R67-B1 收口)**:grant 只落 `token_digest`、损坏/schema/记录非法 fail-closed、单事务整体回滚、不可变身份冲突拒绝、单调状态不回退、legacy raw-token 行原子修复/冲突拒绝、history canonical digest 去重、终态 request 重建、`APS_APPROVAL_MIGRATION_QUIESCED=1` 停写门禁 + 同源锁 + 源 SHA-256 漂移回滚;**WORM 归档模拟**(audit_ledger 归档即密封 + verify_archive);`test_approval_migration.py` 19 项 + `test_audit_worm.py` 7 项 | 下一 P0:R67-B2 数据库权威时钟与 TTL/CAS 跨主机语义;最终仍需 WORM 物理介质、MySQL 实机多主机与断连/死锁故障注入 | 产品定义确认 UX;开发负责门禁 |
| 硬约束不可被 LLM 或软权重关闭 | Done | `constraints.py` 13 类约束目录齐全(C1-C13:工艺先后序/工位独占/班次日历/维保窗口/选线/物料齐套/产能上限/交期/客户优先级/换型/冻结窗口/班组工装/SOP;新增 C9、C12 归位、goldenCase/kindConfigurable)+ `constraint_catalog()` 只读接口;四引擎(RULE/CP/HYBRID/GA)硬约束语义一致黄金测试(EQUIPMENT/CAPACITY/MATERIAL 三态一致,未发现不一致);**关闭请求被拒并审计**:`ConstraintProfileDenied` 门禁(enabled=False 落硬约束/硬→软降级/对硬约束设权重三类路径拒绝)+ write_audit DENIED(action=constraint.profile.save,LLM 提案同门禁);`test_constraint_catalog.py` 19 项 + `test_engine_constraint_semantics.py` 19 项 | 无(本轮验收完成;API/LLM 拒绝分支的 store.save 持久化建议补,属整洁性) | 算法负责语义;开发负责配置/门禁 |
| 结论、动作、报告可追溯和复算 | Done | `server/agent_core/evidence.py` 统一 EvidenceItem + trace_chain/verify_chain + **verify_audit_trace**(遍历 ALGO_RUN 审计链哈希逐条重算比对,产出 broken 列表 expected/actual/reason,旧记录无 traceItems 不误报);workflow `_build_run_trace`/`_build_external_trace` 落 traceItems(canonical 全量条目供精确重算);`/api/gov/audit` 扩展 traceAlerts/traceChecked/traceOk + `GET /api/gov/trace/verify`;GovConsole「可追溯链」列 ⚠断链徽标(期望 vs 实际哈希)+ 顶部提示条;**EXTERNAL 真实 HTTP skill 端到端验收**:test_trace_external_http.py(uvicorn stub + HTTP skill → flex.schedule.external 真实 POST → trace 可复算);`test_trace_break_visual.py` 6 项 + `test_trace_external_http.py` | 无(本轮验收完成) | 开发负责协议/存储;算法负责复算信息 |
| 未登记动作默认最高风险 | Done | `server/agent_core/harness.py` 的 `power_of` 默认 P3;`test_unregistered_action_defaults_to_p3` | 保持白名单测试;新增动作必须同步门禁文档 | 开发负责;测试/审计把关 |
| 审计 append-only 且可检测篡改 | Partial | 链式哈希+独立锚定根+镜像+跨进程锁+清理归档+**WORM 归档模拟与篡改检测**(round-42)+**归档加密模拟**(round-43:AES-256-GCM+scrypt 派生+双层哈希校验+密钥缺失/错误显式失败);`test_audit_encryption.py` 12 项 + `test_audit_worm.py` 7 项 + `test_audit_appendonly_guard.py` 4 项 + `test_audit_merkle.py` 6 项 | WORM 物理介质与归档密钥管理(HSM/KMS)需外部设施 | 安全/开发负责 |
| 跨层契约和文档同轮同步 | Done | `docs/README.md`、`docs/CHANGELOG.md` 有规则;`test_contract_sync.py` schema 清单门禁与不兼容消费者测试;`test_doc_drift.py` 文档技术选型漂移门禁;`scripts/generate_contract_types.py` 契约 TS 类型生成器 + `test_contract_generation.py` 5 项(确定性、必填/枚举/手写关键面与 schema 一致) | 无(本轮验收完成;pydantic 端自动生成与复杂嵌套类型完整支持留待扩展) | 开发维护;测试/审计拥有门禁 |
| 黄金测试是发布门禁 | Partial | Round 66 focused **80 passed**;最终全量黄金 **1029 passed / 2 warnings**;Node **60 passed**;Web build、ruff/compile/node check/diff-check 通过;8003/5173 与浏览器真实健康。响应 `responseDigest`、条目完整排列/业务字段、pipeline/status 与可行槽位覆盖均失败关闭;源码 Sidecar `--solver-child -I` 已通过 | 重建冻结 Sidecar 后验收真实 EXE `--solver-child`;继续完成干净离线机、Defender for Endpoint、签名/provenance 和真实 GitHub CI | 测试主责;开发/算法修复各自失败 |
| 世界状态与真实外部系统提交具有明确补偿语义 | Done | `saga.py` 每个 P3 动作定义:幂等键(mes.dispatch step 用 `dispatch:{eventId}`)、超时(timeoutSec)、重试(maxRetries 指数退避简化)、补偿(`mes.cancel_dispatch` 外部工单撤销 + 回执 VOIDED + 世界回滚;重排 step 用 `flex.reschedule.rollback` 写前快照回滚)、人工接管(补偿失败→MANUAL_TAKEOVER + takeover 端点)、审计事件(saga.step.*);`mes_stub.cancel_work_order` + `mes.cancel_dispatch` 幂等;`test_saga_wms_closure.py` 6 项(全链成功/重排失败回滚/下发失败撤销/补偿失败人工接管/gate 暂停/入口幂等) | 无(本轮验收完成;P3 自动代行身份与真实操作员授权接线留待生产) | 产品定义业务补偿;开发/集成实现 |
## 11. 建议交付顺序
| 批次 | 用户价值 | 范围 | 批次验收出口 |
| --- | --- | --- | --- |
| P0 可信试点基线 | 计划员敢发布,企业敢留痕 | 全量 P2/P3 evidence 封套、审批迁移可靠性、审计前置快照;Windows 求解器 fatal 已本地隔离关闭但冻结产物待构建后验收 | P0/P1/P2/P3 权力路径自动化验收;证据全动作一致;审批迁移故障关闭;冻结 solver-child/契约三方一致 |
| P1 安全探索与长期会话 | 试排不污染、后悔可恢复、多人可协作 | 分支树、真正成对状态、时间线 diff/回放、会话引用、四层上下文 | 任意分支切换/回滚后对话和世界逐字段一致;跨租户隔离 |
| P2 真实现场闭环 | 从“能演示”升级为“能接厂” | WMS 事件、MES MCP、房间自动化、幂等重试、Saga | 缺料 -> 重排 -> 审批 -> 下发 -> 回执全链通过;断网可恢复且不重复下单 |
| P3 算法可信升级 | 提升可行率、交期和方案质量 | 工序级 CP、GA/NSGA-II、LNS、Pareto、调参、Monte Carlo、主控约束 | 每个引擎是真实现;固定基准集报告可行率/KPI/gap/性能/稳定性 |
| P4 私有化产品化 | 可在客户内网稳定安装、升级和审计 | 桌面技术路线决策、自包含 Sidecar、信创、CI/CD、签名/SBOM、多模态、WORM | 干净离线机器可运行;签名产物可追溯;升级失败回滚;合规审计可导出 |
最高优先级未完成项仍是 **P0 可信试点基线**。Windows 求解器 fatal 已在源码/运行层形成子进程隔离与 fail-closed,本地风险收口;冻结产物仍需构建后验收。审批迁移可靠性(R67-B1:digest 归一化、损坏 fail-closed、单事务回滚、quiesced 停写门禁)已于 2026-08-20 本地收口。下一本地优先级调整为:把当前强证据扩展为**全量 P2/P3 evidence 封套**(SAP outbound、folder.schedule TOCTOU、Automation G4 payload 绑定),以及 **R67-B2 数据库权威时钟与 TTL/CAS 跨主机语义**。若这些项未闭环,继续扩展真实外部集成会放大不可审计、不可恢复与错误授权风险。
## Round 65 专项:闭环 APS 排产内核
| 能力 | 状态 | 直接证据 | 剩余边界 |
|---|---|---|---|
| 跨订单 BOM/库存/供应净算 | Done (local) | `closed_loop_problem.py`、`supply_netting.py`、`test_closed_loop_scheduling.py`、`test_closed_loop_provenance.py` | 真实供应承诺仍需 SAP/WMS |
| 制造需求准入与原子排产 | Done (local) | `closed_loop_runtime.py`、`test_flex_atomic_schedule.py`;MOM 新版本 0 VL/WO | 39 缺工艺、67 模板/缺能力待现场补齐 |
| `SchedulingProblemV2` + 多资源 Validator | Done (local) | `test_closed_loop_resource_adapter.py`:设备/班组/模具/组织层级/容量/寿命正负用例 | 外部 V1 Skill 必须补多资源分配,否则按设计拒绝 |
| P2/P3 完整证据绑定 | Done (local) | `test_mes_evidence_v2.py`、`test_flex_publish_mes_gate.py`;身份/维保/能力/班组/模具漂移失败关闭 | 真实 MES 仍需凭据/白名单 |
| WMS 重排精确新版本 | Done (local) | `test_saga_wms_closure.py`:确认拒绝且已有旧 DRAFT 时不发布 | 真实 WMS 事件源待接入 |
| 6 阶段持久状态与真实浏览器 E2E | Done (local) | `OrderPanel.tsx`、`closed-loop-scheduling.spec.ts`;MOM 刷新恢复 + 正向 P2/P3/MES | 现场操作仍受真实主数据和厂商系统约束 |
| 订单 GET 读语义与现场保护 | Done (local) | `sync_flex_orders_to_sales` 幂等、MRP 预览深拷贝、`test_orders_flex_pool.py`、`test_closed_loop_gateway.py` | 历史 world 漂移未擅自回退 |
验收:2026-08-03 全量黄金测试 979 passed、Node 60 passed、Web build passed、真实隔离 Playwright 2 passed。首轮审计 7 个阻断已修复,最终独立复审 `AUDIT: PASS`;本项不改变原有 P0 跨主机审批存储、真实厂商对接、签名/离线机等剩余优先级。
## Round 66 专项:Windows 求解器进程隔离
| 能力 | 状态 | 直接证据 | 剩余边界 |
|---|---|---|---|
| 子进程协议与父进程校验 | Done (local) | `solver_process.py`、`solver_worker.py`、`test_solver_subprocess.py`;协议 `aps.solver-process.v1` | 干净 Windows/真实 CI 仍需重复 |
| CP/HYBRID 失败关闭 | Done (local) | fatal/timeout 均 `UNAVAILABLE`、0 PO/WO、不可发布、父存活、业务原子、无 RULE 降级 | 外部监控/告警接线可继续扩展 |
| 健康正向求解 | Done (local) | 复制 `world.json`:CP/HYBRID `OPTIMAL`、1 PO/10 WO/10 slots、`runtimeSafe=true` | MOM 真实主数据仍阻断,不得外推 |
| 进程树回收 | Done (local) | timeout 15.633s,grandchild 随进程树回收 | 真实压力/长期 soak 待 CI/客户机 |
| 运行时来源门禁 | Done (local) | 默认 Anaconda 稳定拒绝;`.venv`/`.sidecar-venv` 各 5 次通过 | 发布 runtime 仍需冻结产物与签名 |
| 源码 Sidecar `--solver-child -I` | Done (source) | trusted source root bootstrap + `test_source_solver_child_entry_runs_under_isolated_python`;实际 CP 1/10/10 | 不等同于冻结 EXE 打包通过 |
| 冻结 Sidecar 新入口 | Partial | 现有 EXE 仅旧 `--probe-child` 通过 | 重建后验证新 `--solver-child`,并做干净机/Defender/签名验收 |
| Round 66 最终全量黄金 | Done (local) | focused 80;full golden 1029 passed / 2 warnings;首轮假绿复现现被 `SOLVER_RESPONSE_INVALID` 拒绝 | 冻结 EXE 重建与干净机仍属构建后验收 |
| 成功响应完整性与业务语义 | Done (local) | 临时文件 stdin 消除 Windows pipe 写入背压并恢复有界 timeout; 父进程重算 `responseDigest`;条目缺失/多余/重复/字段改写、错误 pipeline/status/slots 均拒绝 | 无;真实冻结制品仍需重建后复验同一协议 |
数据边界:`world.json` 与 MOM JSON 业务源 hash 验收前后不变,`server/data/**`/`.env` 无 Git 状态;`master.db` 的 license `last_seen` 等运行元数据允许变化,不能拿文件 hash 冒充业务不变。矩阵总计仍为 **50 Done / 10 Partial / 0 Missing**。

View File

@ -0,0 +1,51 @@
# Round 67 多智能体并行实施计划
日期:2026-08-03
## 1. 边界
- W67-MIGRATE 独占:`server/agent_core/approval_migrate.py`。
- W67-TEST 独占:`tests/golden/test_approval_migration.py`。
- 主 agent:GitNexus/Serena 门禁、契约协调、全量回归、真实运行、文档和最终审计。
- worker 不修改彼此文件,不提交,不触碰现场数据。
## 2. 协议约束
- canonical digest 复用 `server.agent_core.approval_store.token_digest`。
- 迁移执行分两阶段:持有文件后端同一 `.lock` 完成全量 load/validate/normalize -> 单 SQLAlchemy Session 事务写入/legacy 修复 -> source digest 复核 -> commit。
- grant 缺 pending request 时从 grant/history 重建 APPROVED request。
- 冲突判断比较不可变身份字段;允许目标状态单调前进,禁止回退覆盖。
- 同时检查 digest/raw-token/confirm_id 三路遗留,canonical event digest 对完整 payload 去重。
- 缺失源文件和损坏源文件必须具有不同结果;未设置 quiesced 必须拒绝。
## 3. Worker 验收
### W67-MIGRATE
- token digest 正确;原 token 不落 DB。
- JSON/schema/记录/引用预检失败关闭。
- 目标冲突失败;一致记录幂等 skipped;合法状态前进不误报。
- legacy raw-token 一致行原子改主键,不一致需人工处置。
- 任一预检或后段故障均整体回滚。
### W67-TEST
- 迁移后 consume once。
- raw token 不落库。
- 损坏 JSON、顶层类型、三类记录非法、引用错误。
- 目标记录不可变字段冲突、状态前进/回退。
- 终态 request 重建、canonical event digest 近似事件、legacy raw-token 修复/冲突。
- quiesced/锁/source digest 与后段故障整体回滚。
- dry-run、缺失文件、幂等旧测试不回归。
## 4. 集成顺序
1. 主 agent 锁定源/目标契约和错误语义。
2. W67-MIGRATE 与 W67-TEST 分离写入并通信对齐。
3. focused -> approval 相关 suites -> full golden。
4. 临时 SQLite 真实迁移/消费证据与现场 hash 保护。
5. detect_changes、文档、独立只读审计。
## 5. Pre-target-merge
仅形成未提交工作区与验收证据;未经授权不 commit/push/merge/release/cleanup。

View File

@ -0,0 +1,46 @@
# Round 67 状态:审批迁移 Fail-Closed 与 Grant Digest 归一化(R67-B1)
日期:2026-08-20
关联:`docs/round-67-approval-migration-work-plan.md`、`docs/round-67-approval-migration-parallel-implementation-plan.md`
## 1. 结论
R67-B1 九项目标全部在本地 SQLite 闭环,最终状态 **DONE (local)**。真实跨主机停写编排与 MySQL 多主机仍属部署验收,不在本地冒充完成。
| 目标 | 状态 | 证据 |
|---|---|---|
| 1. grant 只存 `token_digest(raw)` | Done | `test_db_persists_only_digests`、`test_migrated_p3_grant_consumable_once` |
| 2. 损坏 JSON/schema/记录 fail-closed | Done | `test_corrupt_json_fails_closed`、`test_invalid_schema_and_records_fail_closed`;文件缺失空源语义保持(`test_migrate_missing_file_empty_source`) |
| 3. 规范化预检 + 终态 APPROVED request 重建 | Done | `test_migrated_p3_grant_consumable_once`(`rebuilt=1`、`migratedTerminalRequest=True`) |
| 4. 单 Session 事务 flush/commit,后段异常整体回滚 | Done | `test_late_failure_rolls_back_all`(故障注入后三表零写入) |
| 5. 不可变身份 + 单调状态幂等 | Done | `test_identity_conflict_fails_closed`、`test_monotonic_status_no_regression`、`test_source_ahead_advances_db` |
| 6. expected digest 预检、legacy raw-token 原子修复/冲突、禁二次哈希 | Done | `test_legacy_raw_token_row_repaired`、`test_legacy_raw_token_row_conflict_fails_closed`、`test_known_digest_key_never_rehashed` |
| 7. history 完整 canonical event digest 去重 | Done | `test_history_canonical_digest_preserves_near_duplicates`(近似事件保留、完全重复去重、重跑幂等) |
| 8. `APS_APPROVAL_MIGRATION_QUIESCED=1` + 同源 `.lock` + 源 SHA-256 复核 | Done | `test_migrate_requires_quiesced`、`test_source_drift_rolls_back` |
| 9. 迁移后 active P3 grant 消费一次、重放失败 | Done | `test_migrated_p3_grant_consumable_once`(第一次 True、第二次 False) |
## 2. 验证记录
- focused:`tests/golden/test_approval_migration.py` **19 passed**(原 5 项适配 quiesced 前置 + 新增 14 项)。
- 审批相关套件:migration / database_store / store / delegate / transfer / role_policies / gov_api / harness_p3 合计 **88 passed**。
- 全量黄金(154 文件分三批):**1065 passed / 1 failed**;唯一失败 `test_preference_features.py::test_extract_features_from_world` 为 DXF-01 轮已记录的既有失败(偏好特征口径,与本轮零交集)。
- ruff:本轮两文件全绿(import 排序已 `--fix`);`git diff --check` passed。
## 3. 影响分析
GitNexus MCP 工具本运行环境不可用;采用工作计划既有门禁结论(`load_file_store` LOW、`migrate_file_to_db` MEDIUM)+ 人工复核:两函数仅被迁移自身与测试调用,无网关/生产端接线;`ApprovalStore`、`DatabaseApprovalStore`、`harness` 零改动。爆炸半径 LOW。
## 4. 数据保护
全部测试使用 `tmp_path` 临时审批文件与临时 SQLite;fixture 显式 `APS_APPROVAL_MIGRATION_QUIESCED=1`。验收后 `git status -- server/data .env` 干净,现场 `approvals.json` / `master.db` 未读取后写回。
## 5. 遗留 / 边界
- R67-B2:数据库权威时钟、TTL/CAS 跨主机语义。
- 真实 MySQL 多主机、断连/死锁故障注入。
- SAP outbound P3 evidence envelope、folder.schedule TOCTOU、Automation G4 payload 绑定(下一 P0:全量 P2/P3 evidence 封套)。
- 真实跨主机停写编排属部署验收,需现场运维流程配合。
## 6. 发布动作
本地 commit(用户授权);无 push / merge / publish。

View File

@ -0,0 +1,66 @@
# Round 67 工作计划:审批迁移 Fail-Closed 与 Grant Digest 归一化
日期:2026-08-03
## 1. 方向选择
Round 66 已完成并通过独立复审。并行候选审计发现:
- 审批文件迁移把原始 grant token 直接写入数据库 token_hash,迁移后 P3 grant 无法被数据库后端消费;
- 损坏 JSON/非法结构会被当成空审批源并报告迁移成功;
- 数据库 TTL 仍依赖应用主机时钟,留到后续 R67-B2;
- SAP outbound P3 evidence envelope、folder.schedule TOCTOU 与 Automation G4 payload 绑定列入后续 P0。
本轮选择 R67-B1,因为两个 CRITICAL 缺陷可在本地 SQLite 完整复现和闭环,且写边界最小。
## 2. GitNexus 门禁
- `load_file_store`:LOW,1 个直接调用、6 个总影响,无执行流。
- `migrate_file_to_db`:MEDIUM,5 个直接测试调用,无执行流。
- 不修改审批 API、Harness、数据库 consume/TTL 或真实 MySQL 部署路径。
## 3. 目标
1. 迁移 grant 时只存 `token_digest(raw_token)`,不持久化原始 token。
2. 文件不存在维持空源语义;文件存在但 JSON/schema/记录损坏必须明确失败。
3. 所有源记录先完成规范化与交叉引用预检;grant 缺 pending request 时依据 grant/history 重建终态 APPROVED request,满足数据库外键。
4. 全部请求、grant 修复和事件在一个 SQLAlchemy Session 事务中 flush/commit;后段异常整体回滚。
5. 幂等比较拆分为不可变身份字段与单调状态:允许数据库端合法前进到 APPROVED/CONSUMED/EXPIRED,禁止状态回退或覆盖。
6. 同时预检 expected digest、历史 raw-token 主键与 confirm_id;一致 legacy 行在事务内原子修复为 digest,冲突则 fail-closed,禁止对 DB 中已知 digest 再哈希。
7. history 使用完整 canonical event digest 去重;同秒同状态但 approver/note/payload 不同的事件必须保留。
8. 迁移要求 `APS_APPROVAL_MIGRATION_QUIESCED=1`,并持有与文件后端相同的 `.lock` 到事务提交;读取、flush 后复核 source SHA-256 一致,再 commit。
9. 迁移后 active P3 grant 可成功消费一次,重放失败。
## 4. 写边界
- 业务实现:`server/agent_core/approval_migrate.py`
- 黄金测试:`tests/golden/test_approval_migration.py`
- 文档:本计划、并行计划、状态/运行证据、plan/matrix/changelog
## 5. Out of scope
- 数据库权威时钟、TTL/CAS 跨主机语义(R67-B2)。
- 真实 MySQL 多主机、网络断连/死锁恢复。
- SAP outbound、folder.schedule、Automation G4 evidence envelope。
- 提交、推送、合并、发布或修改现场审批文件。
## 6. 验收
- 文件后端 active P3 grant -> 迁移 -> 数据库后端 consume 一次成功、第二次失败。
- DB 仅包含 64 位 SHA-256 digest,不包含 raw token。
- 损坏 JSON、错误顶层 schema、非法 pending/grant/history 均失败且三张审批表不变。
- 同键不同不可变身份字段报冲突;数据库端合法状态前进不误报冲突,状态回退不覆盖。
- legacy raw-token 行一致时原子修复,不一致时 fail-closed。
- history canonical digest 保留合法近似事件并保证重跑幂等。
- 后段事件构造/flush 故障注入后,三表及迁移前目标数据整体不变。
- 未设置 quiesced 明确拒绝;锁持有期间 source digest 漂移则回滚。
- dry-run、幂等迁移、缺失文件原有行为保持。
- focused、相关 approval suites、全量黄金、Node/Web(如受影响)、真实临时数据库运行、diff-check、detect_changes 和独立审计通过。
## 7. 数据保护
所有测试使用临时审批文件和临时 SQLite;fixture 显式设置 quiesced。不得读取后写回 `server/data/approvals.json` 或现场 `master.db`。验收前后记录现场审批文件 hash 与 `server/data/.env` Git 状态。
## 8. 首轮 Plan Audit 修订
首轮 `PLAN AUDIT: FAIL` 的六项阻断已纳入:终态 request 重建、单调状态幂等、legacy raw-token 修复/冲突、canonical event digest、单事务后段回滚、quiesced + 同锁稳定快照。真实跨主机停写编排仍是部署验收,不在本地 SQLite 冒充完成。

View File

@ -0,0 +1,447 @@
# ============================================================
# 审批文件后端 → database 后端存量迁移(moduleId: approval-migrate, 可重生 ✅)
# 矩阵 117 行剩余项:文件→DB 存量迁移可靠性(round-67 R67-B1)。
# 安全不变量:
# 1. grant 只持久化 token_digest(raw_token),绝不落 raw token;
# 2. 文件缺失维持空源语义;文件存在但 JSON/schema/记录损坏一律 fail closed;
# 3. 全部写入在单个 SQLAlchemy Session 事务内 flush/commit,后段异常整体回滚;
# 4. 幂等按「不可变身份字段冲突即拒绝 + 单调状态只前进不回退」判定;
# 5. 迁移要求 APS_APPROVAL_MIGRATION_QUIESCED=1,并持有与文件后端相同的
# `{source}.lock` 进程锁直到事务提交;flush 后复核源文件 SHA-256,
# 漂移即回滚。
# dry_run 只预检与报告,不写库。
# ============================================================
from __future__ import annotations
import copy
import hashlib
import json
import math
import os
import time
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from server.agent_core.approval_db_store import DatabaseApprovalStore
from server.agent_core.approval_store import (
_SCHEMA_VERSION,
default_approval_path,
token_digest,
)
from server.db.database import get_session
from server.db.models import (
ApprovalEventRecord,
ApprovalGrantRecord,
ApprovalRequestRecord,
)
_QUIESCED_ENV = "APS_APPROVAL_MIGRATION_QUIESCED"
_HEX_DIGEST_LEN = 64
_REQUEST_IDENTITY_FIELDS = ("action", "power", "paramsHash", "tenantUuid", "projectId", "ownerUserId")
_GRANT_IDENTITY_FIELDS = ("confirmId", "action", "paramsHash", "tenantUuid", "projectId", "ownerUserId")
_GRANT_STATUS_RANK = {"ACTIVE": 0, "CONSUMED": 1, "EXPIRED": 1}
class MigrationError(RuntimeError):
"""迁移 fail-closed:源损坏、身份冲突、状态回退、未停写或源漂移。"""
def _optional_int(value: Any) -> int | None:
try:
parsed = int(value)
except (TypeError, ValueError):
return None
return parsed or None
def _optional_float(value: Any) -> float | None:
try:
return float(value)
except (TypeError, ValueError):
return None
def _optional_str(value: Any) -> str | None:
return str(value) if value is not None else None
def _sha256_path(path: str) -> str:
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
digest.update(chunk)
return digest.hexdigest()
@contextmanager
def _source_lock(path: str) -> Iterator[None]:
"""与文件后端 ApprovalStore 相同的 `{path}.lock` 跨进程锁(持有到事务提交)。"""
lock_path = f"{path}.lock"
parent = os.path.dirname(lock_path) or "."
os.makedirs(parent, exist_ok=True)
with open(lock_path, "a+b") as handle:
handle.seek(0, os.SEEK_END)
if handle.tell() == 0:
handle.write(b"\0")
handle.flush()
handle.seek(0)
if os.name == "nt":
import msvcrt
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
try:
yield
finally:
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def _canonical_event_digest(event: dict[str, Any]) -> str:
"""完整事件 canonical digest:同秒同状态但 approver/note/payload 不同的事件必须区分。"""
blob = json.dumps(event, sort_keys=True, ensure_ascii=False, separators=(",", ":"), default=str)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
def _validate_store_record(kind: str, key: Any, record: Any) -> None:
if not isinstance(key, str) or not isinstance(record, dict):
raise MigrationError(f"审批源损坏:{kind} 记录键/值类型非法(key={key!r})")
try:
expires_at = float(record.get("expiresAtEpoch"))
except (TypeError, ValueError):
raise MigrationError(f"审批源损坏:{kind} 记录 {key} 缺少合法 expiresAtEpoch") from None
if not math.isfinite(expires_at) or expires_at <= 0:
raise MigrationError(f"审批源损坏:{kind} 记录 {key} expiresAtEpoch 非法")
if not isinstance(record.get("action"), str) or not record.get("action"):
raise MigrationError(f"审批源损坏:{kind} 记录 {key} 缺少 action")
if not isinstance(record.get("tenantUuid"), str):
raise MigrationError(f"审批源损坏:{kind} 记录 {key} 缺少 tenantUuid")
def _validate_history(events: Any) -> None:
if not isinstance(events, list):
raise MigrationError("审批源损坏:history 不是数组")
for index, event in enumerate(events):
if not isinstance(event, dict):
raise MigrationError(f"审批源损坏:history[{index}] 不是对象")
if not isinstance(event.get("action"), str) or not isinstance(event.get("status"), str):
raise MigrationError(f"审批源损坏:history[{index}] 缺少 action/status")
if _optional_float(event.get("decidedAtEpoch")) is None:
raise MigrationError(f"审批源损坏:history[{index}] 缺少合法 decidedAtEpoch")
def load_file_store(file_path: str | None = None) -> dict[str, Any]:
"""读取并校验文件后端审批仓。
文件不存在 → 空源语义(不抛错);文件存在但 JSON/schema/记录损坏 → fail closed。
"""
path = file_path or default_approval_path()
try:
with open(path, "r", encoding="utf-8") as handle:
raw = json.load(handle)
except FileNotFoundError:
return {"schemaVersion": _SCHEMA_VERSION, "pending": {}, "grants": {}, "history": []}
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as exc:
raise MigrationError(f"审批源损坏:{path} 无法解析({type(exc).__name__}),已拒绝迁移") from exc
if not isinstance(raw, dict) or raw.get("schemaVersion") != _SCHEMA_VERSION:
raise MigrationError(f"审批源损坏:{path} 顶层 schema 非法,已拒绝迁移")
pending, grants, history = raw.get("pending"), raw.get("grants"), raw.get("history")
if not isinstance(pending, dict) or not isinstance(grants, dict):
raise MigrationError(f"审批源损坏:{path} pending/grants 结构非法,已拒绝迁移")
for confirm_id, record in pending.items():
_validate_store_record("pending", confirm_id, record)
for key, record in grants.items():
_validate_store_record("grants", key, record)
_validate_history(history)
return {"schemaVersion": _SCHEMA_VERSION, "pending": pending, "grants": grants, "history": history}
def _identity_of(record: dict[str, Any], fields: tuple[str, ...]) -> tuple[Any, ...]:
normalized: list[Any] = []
for field in fields:
value = record.get(field)
normalized.append(_optional_int(value) if field == "ownerUserId" else str(value or ""))
return tuple(normalized)
def _request_identity_matches(row: ApprovalRequestRecord, record: dict[str, Any]) -> bool:
db_identity = (
row.action,
row.power,
row.params_hash,
row.tenant_uuid,
row.project_id,
row.owner_user_id,
)
return db_identity == _identity_of(record, _REQUEST_IDENTITY_FIELDS)
def _grant_identity_matches(row: ApprovalGrantRecord, record: dict[str, Any], confirm_id: str) -> bool:
db_identity = (
row.confirm_id,
row.action,
row.params_hash,
row.tenant_uuid,
row.project_id,
row.owner_user_id,
)
expected = list(_identity_of(record, _GRANT_IDENTITY_FIELDS))
expected[0] = confirm_id
return db_identity == tuple(expected)
def _grant_row(digest: str, confirm_id: str, record: dict[str, Any], now_epoch: float) -> ApprovalGrantRecord:
return ApprovalGrantRecord(
token_hash=digest,
confirm_id=confirm_id,
tenant_uuid=str(record.get("tenantUuid") or "platform"),
project_id=str(record.get("projectId") or "default"),
owner_user_id=_optional_int(record.get("ownerUserId")),
action=str(record.get("action") or ""),
params_hash=str(record.get("paramsHash") or ""),
status=str(record.get("status") or "ACTIVE"),
created_at_epoch=float(record.get("createdAtEpoch") or now_epoch),
expires_at_epoch=float(record.get("expiresAtEpoch") or now_epoch),
consumed_at_epoch=_optional_float(record.get("consumedAtEpoch")),
revision=int(record.get("revision") or 0),
payload=copy.deepcopy(record),
)
def _terminal_request_row(confirm_id: str, grant: dict[str, Any], now_epoch: float) -> ApprovalRequestRecord:
"""grant 缺 pending request 时,依据 grant 重建终态 APPROVED request 以满足外键。"""
approvals = list(grant.get("approvals") or [])
power = str(grant.get("power") or "")
payload = copy.deepcopy(grant)
payload["confirmId"] = confirm_id
payload["status"] = "APPROVED"
payload["migratedTerminalRequest"] = True # 诚实标注:迁移重建,非现场决策
return ApprovalRequestRecord(
confirm_id=confirm_id,
tenant_uuid=str(grant.get("tenantUuid") or "platform"),
project_id=str(grant.get("projectId") or "default"),
owner_user_id=_optional_int(grant.get("ownerUserId")),
action=str(grant.get("action") or ""),
power=power,
params_hash=str(grant.get("paramsHash") or ""),
status="APPROVED",
approval_step=len(approvals),
required_approvals=max(len(approvals), 2 if power == "P3" else 1),
created_at_epoch=float(grant.get("createdAtEpoch") or now_epoch),
expires_at_epoch=float(grant.get("expiresAtEpoch") or now_epoch),
revision=0,
payload=payload,
)
def migrate_file_to_db(file_path: str | None = None, *, dry_run: bool = False) -> dict[str, Any]:
"""把文件后端审批存量迁移到 database 后端(幂等、单事务、fail-closed)。
前置:APS_APPROVAL_BACKEND=database、DB schema 就绪、
APS_APPROVAL_MIGRATION_QUIESCED=1(确认文件后端已停写)。
Returns: {"pending": {migrated, skipped, rebuilt}, "grants": {migrated, skipped, repaired},
"events": {migrated, skipped}, "dryRun": bool, "source": str,
"sourceDigest": str}
"""
backend = (os.environ.get("APS_APPROVAL_BACKEND") or "file").strip().lower()
if backend != "database":
raise MigrationError(
"approval migration requires APS_APPROVAL_BACKEND=database(请先配置 database 后端)")
if (os.environ.get(_QUIESCED_ENV) or "").strip() != "1":
raise MigrationError(
f"approval migration requires {_QUIESCED_ENV}=1"
"(请先停止文件后端写入并显式确认停写)")
source = file_path or default_approval_path()
result: dict[str, Any] = {
"pending": {"migrated": 0, "skipped": 0, "rebuilt": 0},
"grants": {"migrated": 0, "skipped": 0, "repaired": 0},
"events": {"migrated": 0, "skipped": 0},
"dryRun": bool(dry_run),
"source": str(source),
}
if not os.path.exists(source):
result["sourceDigest"] = None # 文件缺失维持空源语义
return result
DatabaseApprovalStore() # DB 可用性/schema 门禁
with _source_lock(str(source)):
source_digest = _sha256_path(str(source))
result["sourceDigest"] = source_digest
data = load_file_store(str(source))
now_epoch = time.time()
try:
with get_session() as session:
_migrate_pending(session, data, result, dry_run=dry_run)
_migrate_grants(session, data, result, now_epoch=now_epoch, dry_run=dry_run)
_migrate_history(session, data, result, dry_run=dry_run)
if dry_run:
session.rollback()
return result
session.flush()
if _sha256_path(str(source)) != source_digest:
session.rollback()
raise MigrationError("迁移期间审批源文件发生变化,已整体回滚(请在停写后重试)")
session.commit()
except MigrationError:
raise
except SQLAlchemyError as exc:
raise MigrationError(f"approval database write failed(已整体回滚):{type(exc).__name__}") from exc
return result
def _migrate_pending(
session: Any, data: dict[str, Any], result: dict[str, Any], *, dry_run: bool
) -> None:
"""pending 请求:不可变身份字段冲突即拒绝;DB 端已合法前进到终态则不覆盖。"""
pending = data.get("pending") or {}
for confirm_id, record in pending.items():
existing = session.get(ApprovalRequestRecord, confirm_id)
if existing is not None:
if not _request_identity_matches(existing, record):
raise MigrationError(
f"审批迁移冲突:请求 {confirm_id} 与数据库已有记录的身份字段不一致,已拒绝迁移")
# 已迁移过,或 DB 端已合法前进到终态(APPROVED/REJECTED/EXPIRED):不覆盖、不回退
result["pending"]["skipped"] += 1
continue
result["pending"]["migrated"] += 1
if dry_run:
continue
payload = copy.deepcopy(record)
payload.setdefault("confirmId", confirm_id)
session.add(ApprovalRequestRecord(
confirm_id=confirm_id,
tenant_uuid=str(record.get("tenantUuid") or "platform"),
project_id=str(record.get("projectId") or "default"),
owner_user_id=_optional_int(record.get("ownerUserId")),
action=str(record.get("action") or ""),
power=str(record.get("power") or ""),
params_hash=str(record.get("paramsHash") or ""),
status="PENDING",
approval_step=int(record.get("approvalStep") or 0),
required_approvals=int(record.get("requiredApprovals") or 1),
created_at_epoch=float(record.get("createdAtEpoch") or 0),
expires_at_epoch=float(record.get("expiresAtEpoch") or 0),
revision=0,
payload=payload,
))
def _migrate_grants(
session: Any, data: dict[str, Any], result: dict[str, Any], *, now_epoch: float, dry_run: bool
) -> None:
"""grants:只落 digest;legacy raw-token 行一致时原子修复,冲突 fail-closed。"""
grants = data.get("grants") or {}
pending = data.get("pending") or {}
planned_confirm_ids: set[str] = set()
for raw_key, record in grants.items():
confirm_id = str(record.get("confirmId") or "")
if not confirm_id:
raise MigrationError(f"审批源损坏:grant {raw_key} 缺少 confirmId,已拒绝迁移")
# 守卫:文件键本身已是 DB 已知 digest 时按已迁移处理,禁止再哈希
key_is_digest = len(raw_key) == _HEX_DIGEST_LEN and all(
ch in "0123456789abcdef" for ch in raw_key.lower())
existing_by_key = session.get(ApprovalGrantRecord, raw_key)
if key_is_digest and existing_by_key is not None:
if not _grant_identity_matches(existing_by_key, record, confirm_id):
raise MigrationError(
f"审批迁移冲突:grant {raw_key[:12]}… 与数据库已有记录的身份字段不一致,已拒绝迁移")
result["grants"]["skipped"] += 1
continue
digest = token_digest(raw_key)
existing_digest = session.get(ApprovalGrantRecord, digest)
if existing_digest is not None and existing_by_key is not None:
raise MigrationError(
f"审批迁移冲突:grant {raw_key[:12]}… 在数据库中同时存在 raw/digest 两行,已拒绝迁移")
if existing_digest is not None:
if not _grant_identity_matches(existing_digest, record, confirm_id):
raise MigrationError(
f"审批迁移冲突:grant {raw_key[:12]}… 与数据库已有记录的身份字段不一致,已拒绝迁移")
db_rank = _GRANT_STATUS_RANK.get(existing_digest.status, 0)
src_rank = _GRANT_STATUS_RANK.get(str(record.get("status") or "ACTIVE"), 0)
if db_rank >= src_rank:
result["grants"]["skipped"] += 1 # 合法前进或一致:不覆盖、不回退
continue
result["grants"]["migrated"] += 1 # 源更新:单调前进 DB 状态
if not dry_run:
existing_digest.status = str(record.get("status") or "ACTIVE")
existing_digest.consumed_at_epoch = _optional_float(record.get("consumedAtEpoch"))
existing_digest.revision = int(existing_digest.revision) + 1
merged = copy.deepcopy(existing_digest.payload)
merged.update(copy.deepcopy(record))
existing_digest.payload = merged
continue
if existing_by_key is not None:
# legacy raw-token 主键行:一致 → 事务内原子修复为 digest;不一致 → fail-closed
if not _grant_identity_matches(existing_by_key, record, confirm_id):
raise MigrationError(
f"审批迁移冲突:legacy grant {raw_key[:12]}… 与源记录身份字段不一致,已拒绝迁移")
result["grants"]["repaired"] += 1
if not dry_run:
repaired = _grant_row(digest, confirm_id, record, now_epoch)
repaired.status = existing_by_key.status # 保留 DB 端合法前进后的状态
repaired.consumed_at_epoch = existing_by_key.consumed_at_epoch
repaired.revision = int(existing_by_key.revision)
session.delete(existing_by_key)
session.flush()
session.add(repaired)
continue
result["grants"]["migrated"] += 1
if confirm_id in planned_confirm_ids:
raise MigrationError(
f"审批迁移冲突:confirmId {confirm_id} 对应多个待插入 grant(唯一约束),已拒绝迁移")
planned_confirm_ids.add(confirm_id)
# grant 缺 pending request 且 DB 无该 request → 重建终态 APPROVED request 满足外键
needs_rebuild = (
confirm_id not in pending
and session.get(ApprovalRequestRecord, confirm_id) is None
)
if needs_rebuild:
result["pending"]["rebuilt"] += 1
if dry_run:
continue
if needs_rebuild:
session.add(_terminal_request_row(confirm_id, record, now_epoch))
session.add(_grant_row(digest, confirm_id, record, now_epoch))
def _migrate_history(
session: Any, data: dict[str, Any], result: dict[str, Any], *, dry_run: bool
) -> None:
"""history:完整 canonical event digest 去重;近似但不相同的事件全部保留。"""
events = data.get("history") or []
existing_digests = {
_canonical_event_digest(payload)
for payload in session.execute(select(ApprovalEventRecord.payload)).scalars()
}
batch_digests: set[str] = set()
for event in events:
digest = _canonical_event_digest(event)
if digest in existing_digests or digest in batch_digests:
result["events"]["skipped"] += 1
continue
batch_digests.add(digest)
result["events"]["migrated"] += 1
if dry_run:
continue
session.add(ApprovalEventRecord(
confirm_id=_optional_str(event.get("confirmId")),
tenant_uuid=str(event.get("tenantUuid") or "platform"),
project_id=str(event.get("projectId") or "default"),
owner_user_id=_optional_int(event.get("ownerUserId")),
action=str(event.get("action") or ""),
power=str(event.get("power") or ""),
status=str(event.get("status") or ""),
decided_at_epoch=float(event.get("decidedAtEpoch") or 0),
payload=copy.deepcopy(event),
))

View File

@ -0,0 +1,444 @@
# ============================================================
# 审批文件→DB 存量迁移黄金测试(矩阵 117 行剩余项 / round-67 R67-B1)
# 覆盖:幂等迁移、dry_run、缺文件空源、后端/停写前置、digest 归一化、
# 损坏 fail-closed、身份冲突、单调状态、legacy raw-token 修复、
# canonical event digest、源漂移/后段故障整体回滚、迁移后 P3 grant 消费一次。
# ============================================================
from __future__ import annotations
import json
from pathlib import Path
import pytest
from sqlalchemy import func, select
from server.agent_core import approval_migrate
from server.agent_core.approval_migrate import MigrationError, migrate_file_to_db
from server.agent_core.approval_store import token_digest
from server.db.database import get_session, reset_engine
from server.db.models import (
ApprovalEventRecord,
ApprovalGrantRecord,
ApprovalRequestRecord,
)
_RAW_TOKEN_1 = "c0ffee00" * 4 # 32 位 raw token(文件后端 grant 键形态)
_RAW_TOKEN_2 = "deadbee5" * 4
@pytest.fixture
def database_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("APS_APPROVAL_BACKEND", "database")
monkeypatch.setenv("APS_APPROVAL_MIGRATION_QUIESCED", "1")
monkeypatch.delenv("APS_APPROVAL_PATH", raising=False)
monkeypatch.setenv("APS_DATABASE_URL", f"sqlite:///{(tmp_path / 'approval.db').as_posix()}")
monkeypatch.setenv("APS_APPROVAL_DATABASE_ALLOW_SQLITE", "1")
reset_engine()
from server.agent_core import harness
store = harness.configure_approval_store()
yield store
store.clear()
reset_engine()
def _file_store() -> dict:
"""合成文件后端数据:2 pending + 2 grants + 3 history 事件。"""
pending = {
"abc1111111": {
"confirmId": "abc1111111", "sessionId": "s1", "action": "mes.dispatch",
"power": "P3", "params": {"versionId": 1}, "paramsHash": "h1",
"tenantUuid": "platform", "ownerUserId": 1001, "projectId": "p1",
"worldKey": "w1", "evidenceRefs": ["schedule-version:1"],
"createdAtEpoch": 1750000000.0, "expiresAtEpoch": 1750003600.0,
},
"abc2222222": {
"confirmId": "abc2222222", "sessionId": "s1", "action": "schedule.publish",
"power": "P2", "params": {}, "paramsHash": "h2",
"tenantUuid": "platform", "ownerUserId": 1002, "projectId": "p1",
"createdAtEpoch": 1750000100.0, "expiresAtEpoch": 1750003700.0,
},
}
grants = {
_RAW_TOKEN_1: {
"confirmId": "abc1111111", "action": "mes.dispatch", "power": "P3",
"tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1001,
"paramsHash": "h1", "status": "ACTIVE",
"approvals": [{"userId": 1001}, {"userId": 1002}],
"createdAtEpoch": 1750000000.0, "expiresAtEpoch": 1750003600.0,
},
_RAW_TOKEN_2: {
"confirmId": "abc2222222", "action": "schedule.publish", "power": "P2",
"tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1002,
"paramsHash": "h2", "status": "CONSUMED",
"createdAtEpoch": 1750000100.0, "expiresAtEpoch": 1750003700.0,
"consumedAtEpoch": 1750000200.0,
},
}
history = [
{"confirmId": "abc1111111", "action": "mes.dispatch", "power": "P3",
"status": "APPROVED", "tenantUuid": "platform", "projectId": "p1",
"ownerUserId": 1001, "decidedAtEpoch": 1750000100.0},
{"confirmId": "abc2222222", "action": "schedule.publish", "power": "P2",
"status": "APPROVED", "tenantUuid": "platform", "projectId": "p1",
"ownerUserId": 1002, "decidedAtEpoch": 1750000200.0},
{"confirmId": "abc2222222", "action": "schedule.publish", "power": "P2",
"status": "CONSUMED", "tenantUuid": "platform", "projectId": "p1",
"ownerUserId": 1002, "decidedAtEpoch": 1750000300.0},
]
return {"schemaVersion": 1, "pending": pending, "grants": grants, "history": history}
def _write(tmp_path: Path, data: dict) -> Path:
path = tmp_path / "approvals.json"
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return path
def _write_file_store(tmp_path: Path) -> Path:
return _write(tmp_path, _file_store())
def _counts() -> dict:
with get_session() as session:
return {
"pending": session.execute(select(func.count()).select_from(ApprovalRequestRecord)).scalar(),
"grants": session.execute(select(func.count()).select_from(ApprovalGrantRecord)).scalar(),
"events": session.execute(select(func.count()).select_from(ApprovalEventRecord)).scalar(),
}
def test_migrate_file_to_db_idempotent(database_backend, tmp_path: Path):
"""迁移全量 + 幂等:二次运行全部 skipped,不重复插入。"""
path = _write_file_store(tmp_path)
first = migrate_file_to_db(str(path))
assert first["pending"] == {"migrated": 2, "skipped": 0, "rebuilt": 0}
assert first["grants"] == {"migrated": 2, "skipped": 0, "repaired": 0}
assert first["events"] == {"migrated": 3, "skipped": 0}
assert first["dryRun"] is False
assert first["sourceDigest"]
assert _counts() == {"pending": 2, "grants": 2, "events": 3}
second = migrate_file_to_db(str(path))
assert second["pending"] == {"migrated": 0, "skipped": 2, "rebuilt": 0}
assert second["grants"] == {"migrated": 0, "skipped": 2, "repaired": 0}
assert second["events"] == {"migrated": 0, "skipped": 3}
assert _counts() == {"pending": 2, "grants": 2, "events": 3}
def test_migrate_dry_run_does_not_touch_db(database_backend, tmp_path: Path):
"""dry_run:报告数量但库内零写入。"""
path = _write_file_store(tmp_path)
report = migrate_file_to_db(str(path), dry_run=True)
assert report["dryRun"] is True
assert report["pending"]["migrated"] == 2
assert report["grants"]["migrated"] == 2
assert report["events"]["migrated"] == 3
assert _counts() == {"pending": 0, "grants": 0, "events": 0}
def test_migrate_missing_file_empty_source(database_backend, tmp_path: Path):
"""缺文件 → 空源,全部 0,不抛错。"""
report = migrate_file_to_db(str(tmp_path / "missing.json"))
assert report["pending"] == {"migrated": 0, "skipped": 0, "rebuilt": 0}
assert report["grants"] == {"migrated": 0, "skipped": 0, "repaired": 0}
assert report["events"] == {"migrated": 0, "skipped": 0}
assert report["sourceDigest"] is None
def test_migrate_requires_database_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""未配置 database 后端 → 显式失败(fail closed)。"""
monkeypatch.delenv("APS_APPROVAL_BACKEND", raising=False)
monkeypatch.delenv("APS_APPROVAL_PATH", raising=False)
with pytest.raises(MigrationError, match="APS_APPROVAL_BACKEND=database"):
migrate_file_to_db(str(tmp_path / "approvals.json"))
def test_migrate_requires_quiesced(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""未显式停写(QUIESCED!=1)→ 明确拒绝,库表不变。"""
monkeypatch.delenv("APS_APPROVAL_MIGRATION_QUIESCED")
path = _write_file_store(tmp_path)
with pytest.raises(MigrationError, match="QUIESCED"):
migrate_file_to_db(str(path))
assert _counts() == {"pending": 0, "grants": 0, "events": 0}
def test_migrated_data_readable_via_db_store(database_backend, tmp_path: Path):
"""迁移后 pending/历史可通过 database 后端读回(数据真实生效)。"""
path = _write_file_store(tmp_path)
migrate_file_to_db(str(path))
now = 1749990000.0 # 合成数据时间窗口内(expiresAt 1750003600 之前)
pending_items = database_backend.pending_items(
tenant_uuid="platform", project_id="p1", allowed=lambda _payload: True, now_epoch=now)
assert len(pending_items) == 2
assert any(item["confirmId"] == "abc1111111" for item in pending_items)
history = database_backend.history_items(
tenant_uuid="platform", project_id="p1", allowed=lambda _payload: True,
limit=50, now_epoch=now)
assert len(history) == 3
assert all(item["status"] in ("APPROVED", "CONSUMED") for item in history)
def test_db_persists_only_digests(database_backend, tmp_path: Path):
"""DB 仅含 64 位 SHA-256 digest,不包含 raw token。"""
path = _write_file_store(tmp_path)
migrate_file_to_db(str(path))
with get_session() as session:
rows = list(session.execute(select(ApprovalGrantRecord)).scalars())
assert len(rows) == 2
hashes = {row.token_hash for row in rows}
assert hashes == {token_digest(_RAW_TOKEN_1), token_digest(_RAW_TOKEN_2)}
assert all(len(row.token_hash) == 64 for row in rows)
assert _RAW_TOKEN_1 not in hashes and _RAW_TOKEN_2 not in hashes
def test_migrated_p3_grant_consumable_once(database_backend, tmp_path: Path):
"""grant 无 pending 源记录 → 重建终态 APPROVED request;迁移后消费一次成功、重放失败。"""
store = _file_store()
grant = store["grants"][_RAW_TOKEN_1]
store["pending"].pop("abc1111111") # 真实场景:批准后 pending 已弹出,grant 仍在
store["history"] = [e for e in store["history"] if e["confirmId"] != "abc1111111"]
path = _write(tmp_path, store)
report = migrate_file_to_db(str(path))
assert report["pending"]["rebuilt"] == 1
assert report["grants"]["migrated"] == 2
with get_session() as session:
rebuilt = session.get(ApprovalRequestRecord, "abc1111111")
assert rebuilt is not None and rebuilt.status == "APPROVED"
assert rebuilt.payload.get("migratedTerminalRequest") is True
now = 1750001000.0
ok = database_backend.consume_grant(
_RAW_TOKEN_1, confirm_id=grant["confirmId"], action=grant["action"],
params_hash=grant["paramsHash"], allowed=lambda _r: True,
decided_by={"userId": 1001}, now_epoch=now)
assert ok is True
replay = database_backend.consume_grant(
_RAW_TOKEN_1, confirm_id=grant["confirmId"], action=grant["action"],
params_hash=grant["paramsHash"], allowed=lambda _r: True,
decided_by={"userId": 1001}, now_epoch=now)
assert replay is False
def test_corrupt_json_fails_closed(database_backend, tmp_path: Path):
"""损坏 JSON → 明确失败,三张审批表不变。"""
path = tmp_path / "approvals.json"
path.write_text("{not-json", encoding="utf-8")
with pytest.raises(MigrationError, match="审批源损坏"):
migrate_file_to_db(str(path))
assert _counts() == {"pending": 0, "grants": 0, "events": 0}
def test_invalid_schema_and_records_fail_closed(database_backend, tmp_path: Path):
"""错误顶层 schema / 非法 pending / 非法 history → 全部 fail closed。"""
bad_schema = _write(tmp_path, {"schemaVersion": 2, "pending": {}, "grants": {}, "history": []})
with pytest.raises(MigrationError, match="schema"):
migrate_file_to_db(str(bad_schema))
bad_record = _file_store()
bad_record["pending"]["abc1111111"].pop("expiresAtEpoch")
with pytest.raises(MigrationError, match="expiresAtEpoch"):
migrate_file_to_db(str(_write(tmp_path, bad_record)))
bad_history = _file_store()
bad_history["history"] = [{"action": "mes.dispatch"}] # 缺 status/decidedAtEpoch
with pytest.raises(MigrationError, match="history"):
migrate_file_to_db(str(_write(tmp_path, bad_history)))
assert _counts() == {"pending": 0, "grants": 0, "events": 0}
def test_identity_conflict_fails_closed(database_backend, tmp_path: Path):
"""同 confirmId 不同不可变身份字段 → 冲突拒绝,已迁移数据不被覆盖。"""
path = _write_file_store(tmp_path)
migrate_file_to_db(str(path))
tampered = _file_store()
tampered["pending"]["abc1111111"]["paramsHash"] = "hX"
with pytest.raises(MigrationError, match="身份字段不一致"):
migrate_file_to_db(str(_write(tmp_path, tampered)))
assert _counts() == {"pending": 2, "grants": 2, "events": 3}
with get_session() as session:
row = session.get(ApprovalRequestRecord, "abc1111111")
assert row.params_hash == "h1" # 未被覆盖
def test_monotonic_status_no_regression(database_backend, tmp_path: Path):
"""DB 端已前进到 CONSUMED 的 grant 不被源端 ACTIVE 回退。"""
path = _write_file_store(tmp_path)
migrate_file_to_db(str(path))
now = 1750001000.0
assert database_backend.consume_grant(
_RAW_TOKEN_1, confirm_id="abc1111111", action="mes.dispatch",
params_hash="h1", allowed=lambda _r: True,
decided_by={"userId": 1001}, now_epoch=now) is True
second = migrate_file_to_db(str(path)) # 源文件仍是 ACTIVE
assert second["grants"]["skipped"] == 2
with get_session() as session:
row = session.get(ApprovalGrantRecord, token_digest(_RAW_TOKEN_1))
assert row.status == "CONSUMED" # 未回退
def test_source_ahead_advances_db(database_backend, tmp_path: Path):
"""源端 CONSUMED、DB 端 ACTIVE → 迁移单调前进 DB 状态。"""
store = _file_store()
store["grants"].pop(_RAW_TOKEN_2)
path = _write(tmp_path, store)
migrate_file_to_db(str(path))
advanced = _file_store()
advanced["grants"].pop(_RAW_TOKEN_2)
advanced["grants"][_RAW_TOKEN_1]["status"] = "CONSUMED"
advanced["grants"][_RAW_TOKEN_1]["consumedAtEpoch"] = 1750000500.0
advanced["pending"].pop("abc2222222") # 避免无关变更
advanced["grants"].pop("unused", None)
report = migrate_file_to_db(str(_write(tmp_path, advanced)))
assert report["grants"]["migrated"] == 1
with get_session() as session:
row = session.get(ApprovalGrantRecord, token_digest(_RAW_TOKEN_1))
assert row.status == "CONSUMED"
assert row.consumed_at_epoch == 1750000500.0
def _insert_legacy_raw_token_row(record: dict, raw_token: str) -> None:
"""模拟旧版缺陷迁移产物:token_hash 直接落 raw token。"""
with get_session() as session:
if session.get(ApprovalRequestRecord, record["confirmId"]) is None:
session.add(ApprovalRequestRecord(
confirm_id=record["confirmId"], tenant_uuid=record["tenantUuid"],
project_id=record["projectId"], owner_user_id=record["ownerUserId"],
action=record["action"], power=record["power"],
params_hash=record["paramsHash"], status="APPROVED",
approval_step=2, required_approvals=2,
created_at_epoch=record["createdAtEpoch"],
expires_at_epoch=record["expiresAtEpoch"], revision=0, payload=dict(record),
))
session.add(ApprovalGrantRecord(
token_hash=raw_token, confirm_id=record["confirmId"],
tenant_uuid=record["tenantUuid"], project_id=record["projectId"],
owner_user_id=record["ownerUserId"], action=record["action"],
params_hash=record["paramsHash"], status="ACTIVE",
created_at_epoch=record["createdAtEpoch"],
expires_at_epoch=record["expiresAtEpoch"], consumed_at_epoch=None,
revision=0, payload=dict(record),
))
session.commit()
def test_legacy_raw_token_row_repaired(database_backend, tmp_path: Path):
"""一致的 legacy raw-token 行在事务内原子修复为 digest,修复后可消费。"""
store = _file_store()
grant = store["grants"][_RAW_TOKEN_1]
store["pending"].pop("abc1111111") # 批准后 pending 已弹出,grant 仍在源中
store["history"] = [e for e in store["history"] if e["confirmId"] != "abc1111111"]
_insert_legacy_raw_token_row(dict(grant), _RAW_TOKEN_1)
report = migrate_file_to_db(str(_write(tmp_path, store)))
assert report["grants"]["repaired"] == 1
with get_session() as session:
assert session.get(ApprovalGrantRecord, _RAW_TOKEN_1) is None
repaired = session.get(ApprovalGrantRecord, token_digest(_RAW_TOKEN_1))
assert repaired is not None and repaired.status == "ACTIVE"
assert database_backend.consume_grant(
_RAW_TOKEN_1, confirm_id=grant["confirmId"], action=grant["action"],
params_hash=grant["paramsHash"], allowed=lambda _r: True,
decided_by={"userId": 1001}, now_epoch=1750001000.0) is True
def test_legacy_raw_token_row_conflict_fails_closed(database_backend, tmp_path: Path):
"""不一致的 legacy raw-token 行 → fail-closed,legacy 行不被改写。"""
store = _file_store()
store["pending"].pop("abc1111111") # 聚焦 grant 冲突路径
grant = dict(store["grants"][_RAW_TOKEN_1])
_insert_legacy_raw_token_row(grant, _RAW_TOKEN_1) # request 行身份一致
with get_session() as session: # legacy grant 行身份不一致
row = session.get(ApprovalGrantRecord, _RAW_TOKEN_1)
row.action = "mes.dispatch.v2"
session.commit()
with pytest.raises(MigrationError, match="legacy"):
migrate_file_to_db(str(_write(tmp_path, store)))
with get_session() as session:
row = session.get(ApprovalGrantRecord, _RAW_TOKEN_1)
assert row is not None and row.action == "mes.dispatch.v2" # 原样保留
assert session.get(ApprovalGrantRecord, token_digest(_RAW_TOKEN_1)) is None
def test_known_digest_key_never_rehashed(database_backend, tmp_path: Path):
"""文件键本身已是 DB 已知 digest(64 位 hex)→ 按已迁移跳过,禁止再哈希。"""
raw = "f" * 32
digest = token_digest(raw) # 64 位 hex
store = _file_store()
store["pending"] = {}
store["history"] = []
grant = {
"confirmId": "digestkey01", "action": "mes.dispatch", "power": "P3",
"tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1001,
"paramsHash": "hd", "status": "ACTIVE",
"approvals": [{"userId": 1001}, {"userId": 1002}],
"createdAtEpoch": 1750000000.0, "expiresAtEpoch": 1750003600.0,
}
store["grants"] = {digest: grant} # 源键已是 digest 形态
with get_session() as session: # DB 已有该 digest 行
session.add(ApprovalRequestRecord(
confirm_id="digestkey01", tenant_uuid="platform", project_id="p1",
owner_user_id=1001, action="mes.dispatch", power="P3",
params_hash="hd", status="APPROVED", approval_step=2, required_approvals=2,
created_at_epoch=1750000000.0, expires_at_epoch=1750003600.0,
revision=0, payload=dict(grant),
))
session.add(ApprovalGrantRecord(
token_hash=digest, confirm_id="digestkey01", tenant_uuid="platform",
project_id="p1", owner_user_id=1001, action="mes.dispatch",
params_hash="hd", status="ACTIVE", created_at_epoch=1750000000.0,
expires_at_epoch=1750003600.0, consumed_at_epoch=None,
revision=0, payload=dict(grant),
))
session.commit()
report = migrate_file_to_db(str(_write(tmp_path, store)))
assert report["grants"] == {"migrated": 0, "skipped": 1, "repaired": 0}
with get_session() as session:
assert session.get(ApprovalGrantRecord, token_digest(digest)) is None # 未再哈希
assert session.get(ApprovalGrantRecord, digest) is not None
def test_history_canonical_digest_preserves_near_duplicates(database_backend, tmp_path: Path):
"""同秒同状态但 note 不同的事件全部保留;完全重复事件幂等去重。"""
store = _file_store()
base = dict(store["history"][0])
twin = dict(base, note="second approver comment") # 同秒同状态不同 note
store["history"] = [base, twin, dict(base)] # 第 3 条与第 1 条完全重复
path = _write(tmp_path, store)
first = migrate_file_to_db(str(path))
assert first["events"] == {"migrated": 2, "skipped": 1}
second = migrate_file_to_db(str(path))
assert second["events"] == {"migrated": 0, "skipped": 3}
assert _counts()["events"] == 2
def test_source_drift_rolls_back(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""锁持有期间源文件 digest 漂移 → 整体回滚,三表不变。"""
path = _write_file_store(tmp_path)
original = approval_migrate._sha256_path
calls = {"n": 0}
def drifting(p: str) -> str:
calls["n"] += 1
return original(p) if calls["n"] == 1 else "0" * 64
monkeypatch.setattr(approval_migrate, "_sha256_path", drifting)
with pytest.raises(MigrationError, match="源文件发生变化"):
migrate_file_to_db(str(path))
assert _counts() == {"pending": 0, "grants": 0, "events": 0}
def test_late_failure_rolls_back_all(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""后段事件构造故障注入 → 请求/grant/事件三表整体回滚。"""
path = _write_file_store(tmp_path)
original = approval_migrate._canonical_event_digest
calls = {"n": 0}
def failing(event: dict) -> str:
calls["n"] += 1
if calls["n"] == 2:
raise RuntimeError("injected digest failure")
return original(event)
monkeypatch.setattr(approval_migrate, "_canonical_event_digest", failing)
with pytest.raises(RuntimeError, match="injected"):
migrate_file_to_db(str(path))
assert _counts() == {"pending": 0, "grants": 0, "events": 0}