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:
parent
3a73fdd766
commit
e200074612
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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**。
|
||||
|
|
@ -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。
|
||||
|
|
@ -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。
|
||||
|
|
@ -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 冒充完成。
|
||||
|
|
@ -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),
|
||||
))
|
||||
|
|
@ -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}
|
||||
Loading…
Reference in New Issue