|
|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
> 文档定位:基于当前 `agent` 通用框架,把上一代固定 Workflow 脚本构建迁移为由主 Agent 动态规划、专业 Worker 执行、独立 Validator 验收的业务 Host 技术方案
|
|
|
> 对应产品方案:`智能创作构建系统产品方案.md`
|
|
|
-> 事实基线:2026-07-19 的当前仓库 `main@eff9d8f`、上一代 `script_build_refactor_0709@3b2db592079a`、只读数据库样本和线上只读接口
|
|
|
+> 事实基线:2026-07-19 的当前仓库 `main@530b6a5`、上一代 `script_build_refactor_0709@3b2db592079a`、只读数据库样本和线上只读接口
|
|
|
> 实施边界:本文规定后端、数据、工具、Agent 角色、验证、发布与恢复;HTML 仅作语义参考,不实施前端
|
|
|
> 代码位置:阶段一、阶段二已落地在独立业务包 `script_build_host/`;三个业务无关的运行时前置能力落在 `agent/`;阶段三继续在 Host 扩展,不得把脚本领域对象写进 `agent/agent/orchestration`
|
|
|
> 状态标记:阶段一及其生产加固、阶段二功能里程碑为“已实现”;阶段三为“待实现”。阶段二只产生已验证候选组合,Root 停在 `PHASE_TWO_CANDIDATE_PORTFOLIO_READY`,不代表已正式发布。
|
|
|
@@ -336,6 +336,18 @@ class RootDeliveryManifestV1:
|
|
|
|
|
|
Root Worker 只能读取 accepted Direction/Portfolio/StructuredScript、执行只读 legacy projection dry-run、保存 Manifest 和 `submit_attempt`。Root Validator 验证 Manifest 与同一 StructuredScript;Publisher 从被 Root ACCEPT 绑定的 Manifest 解析唯一正文引用。
|
|
|
|
|
|
+`legacy_projection_digest` 不对旧 HTTP JSON 或数据库行直接做 hash,而是对 `LegacyProjectionCanonicalV1` 做 canonical SHA-256。该 canonical 对象把脚本转换为与物理 ID 无关的业务图:
|
|
|
+
|
|
|
+- canonical 只包含旧正式详情可见的 active Paragraph、Element 和 Link;先校验 parent/Link 引用闭合、parent 无环。候选本地 ID 或 branch0 数据库 ID 只在本次转换中用于解引用,转换完成后全部丢弃;
|
|
|
+- 延续当前 StructuredScript 已有的 active `paragraph_index` 唯一约束;V3 canonical/Root 确定性预检新增 active Element 自然键 `(dimension_primary, dimension_secondary, name)` 唯一和 active Link `(paragraph logical key, element logical key)` 唯一约束。现有 workspace 通常会按 Element 自然键收敛,但 V2 Artifact schema 尚未把它声明为硬约束,因此 Root 必须重新验证;任何重复返回 `LEGACY_CANONICAL_IDENTITY_CONFLICT`,不靠插入顺序或 ordinal 消歧;
|
|
|
+- Paragraph logical key 固定为 canonical `paragraph_index`;payload 包含 `level`、四列文本、四组原子点、content range、描述和 parent 的 paragraph logical key;Element logical key是上述自然键的 canonical hash,payload 包含 commonality/topic support/weight/support-elements;Link logical key 是 Paragraph/Element logical key 对,payload 包含 Link 自身稳定业务字段;
|
|
|
+- Paragraph、Element、Link 三组记录分别按 logical key 字典序排序。因为每个 active node/edge 都有唯一稳定 key,父关系和 Link 归属不会因物理 ID 或插入顺序丢失;
|
|
|
+- 包含方向、摘要、正式计数和旧详情中会影响业务读取的稳定字段;计数从 canonical active records 重新计算,不信任可变数据库计数字段;
|
|
|
+- 不包含 Paragraph/Element/Link 数据库自增 ID、`legacy_branch_id`、publication revision、Trace/Task/Attempt ID、创建/更新时间和数据库插入顺序;
|
|
|
+- object key 排序、业务数组排序、UTF-8、null、空 object/数组和浮点格式继续使用全项目 canonical JSON 规则。
|
|
|
+
|
|
|
+Root dry-run 从 frozen StructuredScript 转成该 canonical 对象;Publisher 插入时可以使用候选本地 ID 建立一次性的 candidate ID→branch0 新 ID 映射,但该映射不进入 Manifest/digest。回读时再把 branch0 物理 ID/parent/link 引用解成同一业务图计算 digest。候选 `branch_id>0` 永远保持不可变,不允许通过原地修改 `branch_id=0` 冒充正式发布。旧 HTTP API 仍返回 branch0 的真实物理 ID;“API 展示 ID”与“内容验收 digest”是两个明确分离的合同。
|
|
|
+
|
|
|
## 4. 总体架构
|
|
|
|
|
|
```text
|
|
|
@@ -486,15 +498,28 @@ Repository 的领域错误统一映射为稳定代码,例如 `BUILD_NOT_FOUND`
|
|
|
|
|
|
```python
|
|
|
class LegacyDetailProjectionService:
|
|
|
- async def project(
|
|
|
+ async def project_authorized(
|
|
|
self,
|
|
|
script_build_id: int,
|
|
|
*,
|
|
|
- session: AsyncSession,
|
|
|
principal: Principal,
|
|
|
) -> LegacyScriptBuildDetail: ...
|
|
|
+
|
|
|
+ async def project_in_session(
|
|
|
+ self,
|
|
|
+ script_build_id: int,
|
|
|
+ *,
|
|
|
+ session: AsyncSession,
|
|
|
+ ) -> LegacyScriptBuildDetail: ...
|
|
|
+
|
|
|
+ def to_canonical(
|
|
|
+ self,
|
|
|
+ detail: LegacyScriptBuildDetail,
|
|
|
+ ) -> LegacyProjectionCanonicalV1: ...
|
|
|
```
|
|
|
|
|
|
+HTTP 层只调用 `project_authorized()`,先做 build ownership,再由 service 自己开只读 Session;Publisher 只调用 `project_in_session()`,复用 Final UoW Session,不伪造 Principal。二者最终复用同一字段映射与 `to_canonical()`。
|
|
|
+
|
|
|
字段映射:
|
|
|
|
|
|
| 旧响应字段 | 新系统来源 | 规则 |
|
|
|
@@ -512,6 +537,68 @@ class LegacyDetailProjectionService:
|
|
|
|
|
|
所有 object/数组的空值、ID 类型、展示顺序和嵌套结构由旧 Run 详情 fixture 固化。Publisher 事务内调用同一 Projection Service 生成 canonical readback;普通 GET 也调用它,避免发布校验和线上响应使用两套拼装逻辑。
|
|
|
|
|
|
+阶段三第 0 步先建立版本化金标目录,代码实现不得先于 fixture 合同:
|
|
|
+
|
|
|
+```text
|
|
|
+script_build_host/tests/fixtures/legacy_api/v1/
|
|
|
+├── manifest.json
|
|
|
+├── start_success.json
|
|
|
+├── parse_topic_json_success.json
|
|
|
+├── from_topic_json_success.json
|
|
|
+├── list_success.json
|
|
|
+├── detail_run_454.json
|
|
|
+├── detail_empty_relations.json
|
|
|
+├── detail_nested_paragraphs.json
|
|
|
+├── log_running.json
|
|
|
+├── log_partial.json
|
|
|
+├── log_stopping.json
|
|
|
+├── log_stopped.json
|
|
|
+├── log_failed.json
|
|
|
+├── log_success.json
|
|
|
+├── prefill_success.json
|
|
|
+├── retry_success.json
|
|
|
+├── favorite_success.json
|
|
|
+├── overview_success.json
|
|
|
+├── delete_success.json
|
|
|
+├── trace_messages_success.json
|
|
|
+├── stop_running_success.json
|
|
|
+├── stop_terminal_response.json
|
|
|
+└── errors/
|
|
|
+ ├── not_found_404.json
|
|
|
+ ├── validation_422.json
|
|
|
+ └── business_conflict.json
|
|
|
+
|
|
|
+script_build_host/tests/fixtures/security_overlay/v3/
|
|
|
+├── unauthorized_401.json
|
|
|
+├── cross_build_hidden_404.json
|
|
|
+├── websocket_unauthorized_4404.json
|
|
|
+└── validation_422.json
|
|
|
+```
|
|
|
+
|
|
|
+`legacy_api/v1/manifest.json` 至少记录旧仓库 commit、旧路由、采集时间、脱敏规则、fixture 文件 SHA-256、样本 build/run ID 和响应状态码。fixture 来源以旧版 `pattern_server.py` 的真实 HTTP 响应为准:先在隔离数据副本中采集,再仅脱敏 token、账号隐私和不可提交的外部 URL。旧版没有统一认证 middleware,因此不能伪造“旧版真实 401”;401、跨 build 隐藏 404 和 WebSocket 4404 属于新系统安全叠加合同,单独存入 `security_overlay/v3`,安全合同优先于旧版无认证行为。
|
|
|
+
|
|
|
+允许在测试 harness 中归一化 request host、非业务时间戳和每次运行必然变化的 Trace ID;不得归一化业务 ID 的 JSON 类型、字段存在性、null 与缺字段、空 object/数组、Paragraph/Element/Link 关系、状态、错误码或错误体。物理 ID 的具体数值按下述分层断言处理,不能既要求新自增 ID 等于旧 Run ID,又要求 Publisher 生成全新 branch0 行。
|
|
|
+
|
|
|
+必须覆盖上一版真实路由的以下兼容面:
|
|
|
+
|
|
|
+- start、parse_topic_json、from_topic_json:成功体、输入校验错误和创建出的 build 字段;
|
|
|
+- list:分页、总数、构建状态、方向/摘要、favorite、时间字段和稳定排序;
|
|
|
+- detail:`topic/build/paragraphs/elements/decision_traces/post_datas/script_datas`,包括 Paragraph 四列/原子点、`linked_element_ids`、`linked_elements`、`sub_paragraphs`、parent 引用和空关系;
|
|
|
+- log:running/partial/stopping/stopped/failed/success 的字段和空值;
|
|
|
+- prefill、favorite、delete、overview、`trace_messages`、stop 各关键状态的成功体;
|
|
|
+- retry:保持“复制旧配置并创建新 build”的产品语义;
|
|
|
+- 旧版真实 404/422/业务冲突,以及安全 overlay 的 401/隐藏 404/4404/422 的 HTTP status、content type 和 JSON body。
|
|
|
+
|
|
|
+旧版 `resume` 通过 rewind/改写 Trace 消息继续旧 Agent 流程,破坏不可变因果链,不列为响应语义兼容项;新 `resume` 只实现第 6.5 节的安全续跑/恢复。若仍需保留同一路由名,必须返回新合同并在 API 版本说明中明确行为差异,不能暗中复刻 rewind。
|
|
|
+
|
|
|
+兼容测试采用三层断言:
|
|
|
+
|
|
|
+1. 固定种子/固定自增起点的 serializer 测试可以与 golden 做完整 JSON 字面比较;
|
|
|
+2. 普通 Publisher/E2E 比较字段、null/空集合、物理 ID 的 JSON 类型与唯一性、parent/link 引用闭合和确定性排序,不要求新物理 ID 数值等于旧 Run;
|
|
|
+3. 内容一致性始终把 branch0 回读转换成 `LegacyProjectionCanonicalV1` 并与 Manifest digest 比较。
|
|
|
+
|
|
|
+上一版 Paragraph 明确按 `level, paragraph_index` 排序;Element/Link 没有可靠显式排序。V3 在保持旧主排序的同时,以 branch0 物理主键升序作为 API 展示 tie-breaker,并把这一点标记为“确定性收紧”,不是声称旧实现已有保证。该 tie-breaker 只影响展示顺序,不进入内容 digest。Run 454 的 5 Paragraph、11 Element、13 Link 只作为关系容量 fixture 的一个样本,不能替代上述完整响应、空值、ID、排序和错误合同。
|
|
|
+
|
|
|
## 6. Mission 生命周期与主要函数
|
|
|
|
|
|
### 6.1 服务接口
|
|
|
@@ -548,13 +635,32 @@ class ScriptMissionService:
|
|
|
|
|
|
```python
|
|
|
class ScriptMissionService:
|
|
|
- async def resume(self, script_build_id: int) -> ResumeResult: ...
|
|
|
+ async def advance_to_phase_three(
|
|
|
+ self, script_build_id: int, principal: Principal
|
|
|
+ ) -> PhaseAdvanceResult: ...
|
|
|
+
|
|
|
+ async def run_phase_three(
|
|
|
+ self,
|
|
|
+ script_build_id: int,
|
|
|
+ *,
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
+ ) -> None: ...
|
|
|
+
|
|
|
+ async def resume(
|
|
|
+ self, script_build_id: int, principal: Principal
|
|
|
+ ) -> ResumeResult: ...
|
|
|
|
|
|
async def finalize_accepted_root(
|
|
|
- self, script_build_id: int, root_trace_id: str
|
|
|
+ self,
|
|
|
+ script_build_id: int,
|
|
|
+ root_trace_id: str,
|
|
|
+ *,
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
) -> PublicationResult: ...
|
|
|
```
|
|
|
|
|
|
+`advance_to_phase_three()` 只完成阶段检查、policy migration、Root UNBLOCK 和 active-run 注册;`run_phase_three()` 驱动同一 Planner Trace 与 Root Worker/Validator;`finalize_accepted_root()` 是不调用模型的确定性发布路径。`resume()` 根据持久状态选择安全继续、Operation recovery 或只重试 finalize,不把四种动作混成一次“重跑”。
|
|
|
+
|
|
|
### 6.2 `start()`
|
|
|
|
|
|
事务外读取和校验:
|
|
|
@@ -639,7 +745,7 @@ Planner 框架 Role Contract 只要求使用 preset 实际暴露的计划、检
|
|
|
|
|
|
### 6.5 stop/resume/recovery
|
|
|
|
|
|
-阶段一只实现同进程 cooperative stop:旧业务状态 `stopping` 就是持久 stop intent,Host 以同一进程内 `BuildTransitionGate` 串行化 Direction publication 与 stop intent,停止 active Operation 并通知当前 Planner runtime;全部终止后才投影 `stopped`,超时继续保持 `stopping`。跨进程 OwnerLease、resume/recovery 与进程崩溃后的 publication 竞态恢复仍属于阶段三。
|
|
|
+阶段一、二当前只实现同进程 cooperative stop:旧业务状态 `stopping` 就是持久 stop intent,Host 以同一进程内 `BuildTransitionGate` 串行化阶段投影与 stop intent,停止 active Operation 并通知当前 Planner runtime;全部终止后才投影 `stopped`,超时继续保持 `stopping`。跨进程 OwnerLease、resume/recovery 与进程崩溃后的 final publication 竞态恢复仍属于阶段三。
|
|
|
|
|
|
`stop()`:
|
|
|
|
|
|
@@ -671,18 +777,74 @@ Planner 框架 Role Contract 只要求使用 preset 实际暴露的计划、检
|
|
|
|
|
|
### 6.6 单机单活所有权(阶段三计划)
|
|
|
|
|
|
-FileStore 自带的 Ledger 文件锁只保护单次 commit,不等于 Mission 运行所有权。Host 另设以 `script_build_id + root_trace_id` 为键的 `OwnerLease`(进程锁文件 + owner token):
|
|
|
+FileStore 自带的 Ledger 文件锁只保护单次 commit,不等于 Mission 运行所有权;框架 Operation 的 `execution_epoch` 只表示单个 Operation 的执行世代,也不能作为整个 Mission 的发布 fencing token。Host 必须同时使用进程 `OwnerLease` 和 binding 中的持久化 epoch:
|
|
|
+
|
|
|
+```python
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class MissionOwnerToken:
|
|
|
+ script_build_id: int
|
|
|
+ root_trace_id: str
|
|
|
+ owner_epoch: int
|
|
|
+ owner_instance_id: str
|
|
|
+```
|
|
|
+
|
|
|
+binding 由 `0003` 增加 `owner_epoch`、`stop_epoch`、`owner_instance_id` 和 `owner_acquired_at`。获取顺序固定为:
|
|
|
+
|
|
|
+1. 先对 `script_build_id + root_trace_id` 获取并持续持有进程文件描述符锁;锁文件只记录 PID、Host instance、root 和获取时间,不写 secret;
|
|
|
+2. 再开启短数据库事务,以 `SELECT ... FOR UPDATE` 锁 binding;校验 build/root,CAS 将 `owner_epoch=owner_epoch+1`、写入 `owner_instance_id/owner_acquired_at`;
|
|
|
+3. commit 后形成 `MissionOwnerToken`。顶层 `run/resume` 在任何调度前获取一次;`start` 创建 binding 后把 token 直接交给后台 `run`;
|
|
|
+4. `run()` 内调用 `finalize_accepted_root()` 时复用同一 token,禁止嵌套再次加锁;独立 publication recovery 才获取新 lease,并因此获得更大的 `owner_epoch`;
|
|
|
+5. 正常释放时,只有 binding 的 epoch+instance 仍匹配当前 token 才清空 `owner_instance_id/owner_acquired_at`,永不回退 `owner_epoch`。进程崩溃由 OS 释放文件锁,下一 owner 重新 CAS 增长 epoch。
|
|
|
+
|
|
|
+MySQL 发布事务使用 `READ COMMITTED`,所有竞态关键读取都显式 `FOR UPDATE`,并使用固定锁顺序:
|
|
|
+
|
|
|
+```text
|
|
|
+script_build_mission_binding
|
|
|
+→ script_build_record
|
|
|
+→ script_build_publication
|
|
|
+→ branch0 paragraph-element links
|
|
|
+→ branch0 paragraphs
|
|
|
+→ branch0 elements
|
|
|
+→ script_build_artifact_version
|
|
|
+```
|
|
|
+
|
|
|
+SQLite 只用于行为测试;阶段三生产最低支持 MySQL `8.0.16`,启动/迁移门禁必须实测 CHECK constraint 已 enforce、`READ COMMITTED`、`FOR UPDATE`、事务回滚、DATETIME 精度和约束名称与 SQLAlchemy metadata 一致。锁等待超时映射为稳定 `PUBLICATION_LOCK_TIMEOUT`,deadlock victim 映射为 `PUBLICATION_DEADLOCK_RETRYABLE`,均先退出当前事务并按同一 final identity 回读,不在 Repository 内无限重试或改变锁顺序。
|
|
|
+
|
|
|
+同一表内多行一律按主键升序 `ORDER BY ... FOR UPDATE`;Artifact 按 version ID 升序。final reservation 在进入大事务前已经由唯一键短事务创建,因此 UoW 不承担“锁不存在行”的 gap-lock 猜测。DirectionReconciler、候选 workspace 和 final Publisher 不得在同一 build/phase 并行进入互相冲突的锁序。
|
|
|
|
|
|
-- 顶层 `run`/`resume` 在任何调度前获取一次;`start` 创建 binding 后把已获取 token 原子交接给后台 `run`,不存在两个 owner 的窗口;
|
|
|
-- `run()` 内调用 `finalize_accepted_root()` 时传递并复用同一个 owner token,禁止嵌套二次加锁;独立 publication recovery 才新获取 OwnerLease;
|
|
|
-- `stop()` 不争抢正在持有的独占 OwnerLease,只持久化 stop intent 并通过 runtime signal 通知当前 owner,且不能执行发布;
|
|
|
-- 锁文件写 owner PID、Host instance ID、root、获取时间,不写 secret;
|
|
|
-- 持有进程以文件描述符锁持续占有,不能仅靠可过期文本时间戳;
|
|
|
-- 正常结束或异常退出由 OS 释放,进程启动时核对 binding 和 Ledger 决定恢复;
|
|
|
-- 获取失败返回 `409 MISSION_ALREADY_OWNED`,不得启动第二个 Planner;
|
|
|
-- 发布过程与 Planner 运行使用同一所有权域和 token,避免 stop/resume/finalize 交叉执行。
|
|
|
+行锁只约束遵守协议的 writer,不能阻止上一版进程或人工账号绕过 binding 锁直接插入 branch0。阶段三上线门禁必须保证:旧脚本写进程下线;旧 branch0 写账号撤权或轮换;只有 Final UoW 专用写账号能写正式 Paragraph/Element/Link;任何保留的运维 writer 必须先按 binding→build 锁序并留下审计。未完成该门禁不得开放 final publication。
|
|
|
|
|
|
-该机制只保证同一主机。跨机部署不在本方案范围。
|
|
|
+`stop()` 不获取进程 OwnerLease,但在一个短事务中按相同前缀顺序锁 binding→build,令 `stop_epoch=stop_epoch+1` 并将 build 写为 `stopping`;随后从共享 durable Ledger 定位并停止 active Operation,本机 IPC/runtime signal 只是降低延迟的补充,不能假设 HTTP stop 一定落到 owner 进程。Owner 在每次 dispatch、受控 tool submit、Validation submit、Planner decision 和进入 UoW 前都重读 binding status/stop epoch;晚于 stop epoch 到达的候选写入、submit、decision 和 publication 一律返回 `MISSION_STOP_REQUESTED`。发布 UoW 在 preflight、取得数据库锁后和 commit 前都比较 token 的 `owner_epoch/owner_instance_id` 与进入发布时冻结的 `stop_epoch`:
|
|
|
+
|
|
|
+- stop 先取得 binding 锁:`stop_epoch` 和状态先改变,Publisher 获锁后必须 rollback/退出;
|
|
|
+- Publisher 先取得 binding 锁并完成整个原子提交:stop 等待,随后读到 `success`,幂等返回终态,不得再覆盖为 `stopping`;
|
|
|
+- 同进程 signal 只能帮助 Publisher 在进入 UoW 前更快退出,不能替代数据库行锁的线性化规则。
|
|
|
+
|
|
|
+获取文件锁失败返回 `409 MISSION_ALREADY_OWNED`;token 不匹配返回 `MISSION_FENCING_TOKEN_STALE`。该机制只保证当前方案限定的单机多进程部署;跨主机租约与分布式共识不在阶段三范围。
|
|
|
+
|
|
|
+### 6.7 Phase2→Phase3 continuation 与崩溃矩阵(阶段三计划)
|
|
|
+
|
|
|
+Phase3 continuation 固定写入:`policy_version=script-build-phase-three/v1`、`policy_digest`、`toolset_digest`、`input_snapshot_id`、system/user message ID/sequence 和唯一 `planner_policy_migrated` event。policy digest 和 toolset digest 由 Host 对 canonical 内容签名;当前 Root Trace 中同版本记录不一致时返回 `PLANNER_POLICY_MIGRATION_REQUIRED`。
|
|
|
+
|
|
|
+| 持久状态 | 唯一允许动作 | 禁止动作 |
|
|
|
+|---|---|---|
|
|
|
+| Phase2 boundary,尚无 Phase3 policy/message/event | 校验 Portfolio 闭包后按父链追加一次 system policy + continuation user,再写 migration event | 先 UNBLOCK、重复消息、新建 Root |
|
|
|
+| 唯一 system+user 已完整落盘,无 migration event | 校验内容、sequence、parent chain、digest/toolset/snapshot 后只补 event | 再追加消息、调用模型 |
|
|
|
+| 只有一条消息、存在重复消息,或 event 与消息父链不闭合 | 返回 `PLANNER_POLICY_MIGRATION_REQUIRED` | 猜测补另一条、删除历史消息 |
|
|
|
+| policy/message/event 已持久化,尚未 UNBLOCK | 复核 digest/toolset/snapshot,补一次幂等 UNBLOCK | 再追加 policy/user |
|
|
|
+| 已 UNBLOCK,尚无 Phase3 Planner 模型输出/tool call/Root Operation | `runner.run(messages=[], trace_id=root_trace_id)` 从当前主路径继续 | rewind、创建新 Planner Trace |
|
|
|
+| 已有 Planner 模型输出或 tool call,但尚无 Root Operation | 返回 `MISSION_RECOVERY_REQUIRED`,等待显式恢复对齐 | 重放 Planner 输出、猜测是否已执行 tool |
|
|
|
+| Root Operation 已创建、尚未 reserve Attempt | `recover_operations()` 对齐后,框架返回 `resume_safe` 才恢复该 Operation | 创建重复 Attempt/Operation |
|
|
|
+| Attempt 已 reserve 且 `started_at is None` | 复用同一 Operation 安全启动 Worker | 新建 sibling 猜测替代 |
|
|
|
+| Worker Submission 已持久化、Validation 尚未启动 | 复用同一 Operation 只运行 Validator,断言 Worker 调用次数不增加 | 重跑 Worker、改写 Submission |
|
|
|
+| Worker 已启动但未提交,或 Validator 已启动未提交 | 将遗留 stage 归一为 stopped/expired/failed,由 Planner 基于 durable 状态新建 Attempt/replan | 原地重跑同一已启动 stage、猜测模型输出 |
|
|
|
+| Root 已 ACCEPT,尚无 final publication | 不再运行 Planner/模型;对同一 Manifest 调 `finalize_accepted_root()` | 生成另一 Manifest/StructuredScript |
|
|
|
+| publication 为 pending/failed | 复核同一 Root ACCEPT/Manifest/StructuredScript 与 fencing 后重试同一 UoW | 换 idempotency identity |
|
|
|
+| publication 已 published 且 build=`success` | 独立 readback 校验后幂等返回 | UNBLOCK Root 或重开模型 |
|
|
|
+| publication published 但 build 非 success,或 success 无 published publication | fail-closed,记录 `PUBLICATION_STATE_INCONSISTENT`,只允许人工一致性恢复 | 猜测补字段、自动重发模型 |
|
|
|
+| 发现 branch0 改变但无同事务 publication 证据 | 记录 `PUBLICATION_ATOMICITY_VIOLATION` 并停止自动恢复 | 把可见 branch0 当作成功 |
|
|
|
+
|
|
|
+最后一类在正确 `FinalPublicationUnitOfWork` 下理论上不可发生;保留检测是为了发现旧代码、人工 SQL 或迁移错误。Phase3 已产生实际模型输出或 Operation、但状态不足以落入上述安全分支时统一返回 `MISSION_RECOVERY_REQUIRED`。
|
|
|
|
|
|
## 7. Agent 角色与权限
|
|
|
|
|
|
@@ -1122,6 +1284,10 @@ CREATE TABLE script_build_mission_binding (
|
|
|
input_snapshot_id BIGINT NOT NULL,
|
|
|
active_direction_artifact_version_id BIGINT NULL,
|
|
|
accepted_root_artifact_version_id BIGINT NULL,
|
|
|
+ owner_epoch BIGINT NOT NULL DEFAULT 0,
|
|
|
+ stop_epoch BIGINT NOT NULL DEFAULT 0,
|
|
|
+ owner_instance_id VARCHAR(128) NULL,
|
|
|
+ owner_acquired_at DATETIME(6) NULL,
|
|
|
engine_version VARCHAR(64) NOT NULL,
|
|
|
schema_version VARCHAR(32) NOT NULL,
|
|
|
created_at DATETIME(6) NOT NULL,
|
|
|
@@ -1131,7 +1297,7 @@ CREATE TABLE script_build_mission_binding (
|
|
|
);
|
|
|
```
|
|
|
|
|
|
-该表只保存业务关联和当前正式指针,不保存 root_status、focused_task、pending count 或 Decision 副本。
|
|
|
+该表只保存业务关联、当前正式指针和 Mission 发布栅栏,不保存 root_status、focused_task、pending count 或 Decision 副本。`accepted_root_artifact_version_id` 只能由 `FinalPublicationUnitOfWork` 在 final publication 成功的同一事务写入;普通 Binding Repository 不提供脱离发布事务的 setter。`owner_epoch/stop_epoch` 只增不减,语义见第 6.6 节,不能用 Operation `execution_epoch` 填充。
|
|
|
|
|
|
explicit 模式中的 Planner 主 Trace ID 就是 `root_trace_id`;不另存 `planner_trace_id`,避免制造双身份。Worker/Validator 子 Trace 通过框架 parent/root 关联查询。
|
|
|
|
|
|
@@ -1179,7 +1345,7 @@ CREATE TABLE script_build_artifact_version (
|
|
|
|
|
|
状态仅允许 `draft/frozen/published/discarded`。`canonical_json` 在 draft 可为空,freeze 后不可变。它保存候选正文版本,不复制框架 Task 状态。
|
|
|
|
|
|
-阶段二通过 `0002` migration 扩展 `artifact_type` 检查,至少加入:`structure`、`paragraph`、`element_set`、`comparison`、`structured_script`、`candidate_portfolio`。阶段三通过 `0003` 加入 `root_delivery_manifest`。仍复用同一张 append-only version 表,不新增“active frontier”表;TaskContract 单独进入 content-addressed immutable store,Portfolio 通过不可变引用保存候选闭包。
|
|
|
+阶段二通过 `0002` migration 扩展 `artifact_type` 检查,至少加入:`structure`、`paragraph`、`element_set`、`comparison`、`structured_script`、`candidate_portfolio`。阶段三通过 `0003_phase_three_publication_fencing` 加入 `root_delivery_manifest`,同时增加 binding fencing 列和 final publication 唯一约束。仍复用同一张 append-only version 表,不新增“active frontier”表;TaskContract 单独进入 content-addressed immutable store,Portfolio 通过不可变引用保存候选闭包。
|
|
|
|
|
|
### 10.5 `script_build_publication`
|
|
|
|
|
|
@@ -1200,11 +1366,14 @@ CREATE TABLE script_build_publication (
|
|
|
updated_at DATETIME(6) NOT NULL,
|
|
|
published_at DATETIME(6) NULL,
|
|
|
UNIQUE KEY uq_publication_decision (accept_decision_id),
|
|
|
+ UNIQUE KEY uq_publication_build_type (script_build_id, publication_type),
|
|
|
KEY ix_publication_build (script_build_id, publication_type, state)
|
|
|
);
|
|
|
```
|
|
|
|
|
|
-`publication_type` 为 `direction` 或 `final`。Direction publication 的 `artifact_version_id/expected_sha256` 指向 Direction;final publication 则指向 Root ACCEPT 直接绑定的 RootDeliveryManifest 版本及 digest,StructuredScript 引用和 `legacy_projection_digest` 从该 Manifest 闭合解析。`last_error_summary` 必须脱敏。数据库幂等唯一键是 `accept_decision_id`;ManifestRef 是该 Decision 不可变的闭合校验项。同一 Decision 重试携带不同 ManifestRef 必须 fail-closed,不能变成另一套可变幂等身份。
|
|
|
+`publication_type` 为 `direction` 或 `final`。Direction publication 的 `artifact_version_id/expected_sha256` 指向 Direction;final publication 则指向 Root ACCEPT 直接绑定的 RootDeliveryManifest 版本及 digest,StructuredScript 引用和 `legacy_projection_digest` 从该 Manifest 闭合解析。`last_error_summary` 必须脱敏。`accept_decision_id` 防止一个 Decision 多次发布,`(script_build_id, publication_type)` 防止同一 build 出现两条 final;ManifestRef 是该 Decision 不可变的闭合校验项。同一 Decision 重试携带不同 ManifestRef 必须 fail-closed,不能变成另一套可变幂等身份。
|
|
|
+
|
|
|
+`0003` upgrade 前必须先扫描重复 `(script_build_id, publication_type)`、非法 final、非零 accepted Root 指针及不一致 Artifact;存在冲突则停止迁移并输出只读校验报告,不自动选择“最新”记录。upgrade 在 SQLite 使用 batch recreate,在 MySQL 明确 drop/add check/unique,并提供 offline SQL;生产 MySQL 另跑真实事务、行锁、时区和约束集成测试。downgrade 前若存在 `root_delivery_manifest`、final publication、`accepted_root_artifact_version_id` 或非零 owner/stop epoch,则明确拒绝降级,不能丢失 fencing/publication 事实。
|
|
|
|
|
|
## 11. 验证设计
|
|
|
|
|
|
@@ -1273,6 +1442,8 @@ class ScriptPublisher:
|
|
|
root_trace_id: str,
|
|
|
accept_decision_id: str,
|
|
|
root_delivery_manifest_ref: ArtifactRef,
|
|
|
+ *,
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
) -> PublicationResult: ...
|
|
|
|
|
|
async def readback_and_verify(
|
|
|
@@ -1282,24 +1453,87 @@ class ScriptPublisher:
|
|
|
) -> bool: ...
|
|
|
```
|
|
|
|
|
|
+当前 `SqlAlchemyPublicationRepository` 只支持 Direction,并且各 Repository 方法自行创建 Session/事务;它不能直接承担 final publication。阶段三先定义唯一事务边界:
|
|
|
+
|
|
|
+```python
|
|
|
+class FinalPublicationReservationRepository(Protocol):
|
|
|
+ async def reserve(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ script_build_id: int,
|
|
|
+ accept_decision_id: str,
|
|
|
+ manifest_version_id: int,
|
|
|
+ manifest_digest: str,
|
|
|
+ ) -> Publication: ...
|
|
|
+
|
|
|
+ async def record_failure_if_uncommitted(...) -> Publication: ...
|
|
|
+```
|
|
|
+
|
|
|
+该 Repository 使用独立短事务,只预留/回读不可变 final identity 和记录脱敏错误,不写 branch0、Artifact/pointer 或 success。reservation 必须使用 dialect-safe upsert/no-op insert;若依赖 duplicate-key,必须先 rollback/关闭失败 Session,再用新 Session 回读,禁止在 PostgreSQL/MySQL 已失败事务中继续查询。这样进程可以在正式 UoW 前后崩溃而留下可判定的 pending/failed 状态。
|
|
|
+
|
|
|
+```python
|
|
|
+class FinalPublicationUnitOfWork(Protocol):
|
|
|
+ session: AsyncSession
|
|
|
+
|
|
|
+ async def __aenter__(self) -> "FinalPublicationUnitOfWork": ...
|
|
|
+ async def __aexit__(self, exc_type, exc, tb) -> None: ...
|
|
|
+ async def commit(self) -> None: ...
|
|
|
+ async def rollback(self) -> None: ...
|
|
|
+
|
|
|
+ bindings: FinalBindingRepository
|
|
|
+ builds: FinalBuildRepository
|
|
|
+ publications: FinalPublicationRepository
|
|
|
+ legacy_projection: TransactionalLegacyProjectionRepository
|
|
|
+ artifacts: FinalArtifactRepository
|
|
|
+```
|
|
|
+
|
|
|
+上述五个 final Repository 的每个读写方法都必须接收/使用 UoW 暴露的同一个 `AsyncSession`,禁止内部调用 session factory、`begin()`、`commit()` 或创建嵌套事务。DirectionReconciler 的既有 Repository 可保持原实现;final 路径使用独立 adapter,避免为了阶段三改变已通过验证的阶段一语义。Application 层的 `ScriptPublisher` 只依赖 UoW port,不直接依赖 SQLAlchemy 实现。
|
|
|
+
|
|
|
+UoW 至少提供以下原子动作:
|
|
|
+
|
|
|
+```text
|
|
|
+lock_binding_and_verify_fence
|
|
|
+lock_build_and_verify_stop_epoch
|
|
|
+lock_reserved_final_publication
|
|
|
+replace_branch_zero_with_id_map
|
|
|
+project_legacy_detail_canonical
|
|
|
+set_accepted_root_artifact_version
|
|
|
+mark_manifest_and_structured_script_published
|
|
|
+mark_publication_published
|
|
|
+mark_build_success
|
|
|
+```
|
|
|
+
|
|
|
+reservation 依靠 `(script_build_id, publication_type)` 和 `accept_decision_id` 双唯一约束收敛;正式 UoW 只锁已经 committed 的 reservation 并比较 Manifest identity,不能在大事务内临时创建一个失败后必然消失的 pending 记录,也不能换一套 identity 重试。
|
|
|
+
|
|
|
### 12.2 发布算法
|
|
|
|
|
|
+事务外只做不可变闭包 preflight:
|
|
|
+
|
|
|
1. 一次读取 Mission Snapshot,定位 Root 最终 ACCEPT;
|
|
|
2. 验证 Root Decision→Root Attempt→Root Validation passed→ArtifactSnapshot 闭合;
|
|
|
-3. 要求 Snapshot 恰有一个 `kind=root_delivery_manifest` ArtifactRef,且与入参 `root_delivery_manifest_ref` 完全相同;
|
|
|
-4. 从业务表读取 RootDeliveryManifest frozen version,验证 digest、build/root/task/attempt/snapshot owner,并要求 `script_build_publication.artifact_version_id/expected_sha256` 指向该 Manifest;
|
|
|
-5. 从 Manifest 解析唯一 `structured_script_ref`,验证 accepted Portfolio/Direction 闭合、StructuredScript frozen version/owner/digest 及 `legacy_projection_digest`;
|
|
|
-6. 检查 durable stop intent/execution epoch;已请求停止则不进入 publication;
|
|
|
-7. 插入或读取 `script_build_publication`;数据库以 `accept_decision_id` 唯一,已有记录的 Manifest version/digest 与本次不同则 fail-closed;
|
|
|
-8. 若已 published 且同一 Manifest/同一 StructuredScript 闭合仍成立,直接返回成功;
|
|
|
-9. 复用 Mission OwnerLease,在 MySQL 事务中 `SELECT ... FOR UPDATE` 锁定 build/publication;
|
|
|
-10. 锁定后再次读取 stop intent/execution epoch;命中则 rollback,publication 保持 pending/failed,build 保持 stopping;
|
|
|
-11. 只把 Manifest 绑定的 StructuredScript 替换到该 `script_build_id + branch_id=0` 的 Paragraph/Element/Link,并更新计数、正式指针和 publication revision;
|
|
|
-12. **在同一事务、同一 Session 中**复用旧详情 repository 做 canonical readback,与 `Manifest.legacy_projection_digest` 比对;
|
|
|
-13. mismatch 或读取异常立即 rollback,未验证正文不得对旧 GET 可见;
|
|
|
-14. digest 一致后、commit 前最后重检 stop intent/execution epoch;若发生变化,rollback 全部 branch0、digest、pointer 和 revision 写入;
|
|
|
-15. 无 stop 时在同一事务将 publication、RootDeliveryManifest 和其绑定的 StructuredScript 标为 published,build 标为 success,然后 commit;
|
|
|
-16. commit 后再通过正常旧 HTTP/独立只读 Session 做健康读回;若此处异常,告警并进入一致性修复,不能生成新正文或新 ACCEPT。后续重试永远使用同一 Root ACCEPT 绑定的同一 Manifest,不接受另一 frozen version。
|
|
|
+3. 要求 Snapshot 恰有一个 `kind=root_delivery_manifest` ArtifactRef,且与入参完全相同;
|
|
|
+4. 读取 frozen RootDeliveryManifest,验证 build/root/task/attempt/snapshot owner 和 digest;
|
|
|
+5. 从 Manifest 解析唯一 StructuredScript、accepted Portfolio/Direction 和 `legacy_projection_digest`,验证所有版本 frozen、owner/digest/输入闭包一致;
|
|
|
+6. 比较当前 durable build 状态、OwnerLease 和 binding fencing 快照;若已 stopping/stopped 或 token 明显过期,不进入 UoW;
|
|
|
+7. 通过 `FinalPublicationReservationRepository.reserve()` 短事务创建/回读唯一 pending/failed/published identity;identity 与本次 Root ACCEPT/Manifest 不同则 fail-closed;
|
|
|
+8. 若 reservation 已 published 且同一 Manifest/StructuredScript 闭包仍成立,执行独立只读健康回读后幂等返回。
|
|
|
+
|
|
|
+随后只开启一个 `FinalPublicationUnitOfWork`,按第 6.6 节锁顺序执行:
|
|
|
+
|
|
|
+1. 锁 binding,校验 build/root、`owner_epoch/owner_instance_id` 与 owner token,并冻结本事务看到的 `stop_epoch`;
|
|
|
+2. 锁 build,确认不在 stopping/stopped/success 冲突状态;再次校验 stop epoch;
|
|
|
+3. 锁定已 committed 的唯一 final reservation;校验 `accept_decision_id + Manifest version/digest` 不变,将 pending/failed 转为 publishing 并增加 attempt count/revision;
|
|
|
+4. 精确锁定本 build 的 branch0 行;先删除 ParagraphElement Link,再删除 Paragraph/Element。所有 SQL 都显式包含 `script_build_id` 与 `branch_id=0`;
|
|
|
+5. 不修改候选 workspace。按 Manifest 指向的 StructuredScript 顺序插入新的顶层 Paragraph→子 Paragraph→Element→Link,建立 `paragraph_logical_key→new_id`、`element_logical_key→new_id` 和 `link_logical_key→new_id` 映射;parent/link 只从该映射解析,找不到或重复立即 rollback;
|
|
|
+6. 同 Session 更新方向、摘要和 Paragraph/Element/Link 计数;
|
|
|
+7. 同 Session 调 `LegacyDetailProjectionService` 回读真实 branch0,再转换为 `LegacyProjectionCanonicalV1`;与 Manifest digest 不同返回 `PUBLICATION_READBACK_MISMATCH` 并 rollback;
|
|
|
+8. 第三次校验 owner token、binding `owner_epoch/owner_instance_id`、冻结的 `stop_epoch` 和 build status;任何变化都 rollback;
|
|
|
+9. 同 Session 设置 `binding.accepted_root_artifact_version_id`,将 RootDeliveryManifest 与其 StructuredScript 标为 published,将 publication 标为 published,并把 build checkpoint 清空、结束时间/计数写全、status 写为 `success`;
|
|
|
+10. 只调用一次 UoW `commit()`。任何异常由 `__aexit__` rollback,外部不得补提交其中某一步;rollback 后 reservation 仍是进入 UoW 前的 pending/failed,不会留下 publishing 或部分 branch0。
|
|
|
+
|
|
|
+明确 rollback 后,Publisher 可在新的短控制事务调用 `record_failure_if_uncommitted()`:它仍按 binding→build→publication 固定锁序并做条件更新,先回读 publication、binding、build 和 canonical branch0;只有确认 final 未 published、build 未 success、accepted pointer 未提交,且 owner token/冻结的 stop epoch 仍匹配、build 仍为本次 finalize 预期的 `running` 时,才把 reservation 标为 failed并 CAS 投影 build=`failed`/脱敏错误。若 build 已 stopping/stopped,只更新 reservation 的脱敏失败原因和审计,绝不覆盖 durable stop intent;owner token 已过期则不改 build,记录 stale-fence 审计。若出现 commit-unknown(数据库可能已 commit但客户端断线),必须先按唯一 final identity 回读:published+success+digest 一致则按成功返回;全部仍为未提交状态才记录失败/重试;任意半闭合组合转 `PUBLICATION_STATE_INCONSISTENT`,不得盲目补写。
|
|
|
+
|
|
|
+commit 后通过正常旧 HTTP 或独立只读 Session 再做健康读回。该检查用于告警和检测数据库/缓存异常,不再改变已原子提交的成功事实;若失败,记录 `PUBLICATION_POST_COMMIT_READBACK_FAILED` 并进入人工一致性修复,绝不生成新正文、新 Root ACCEPT 或第二条 final publication。
|
|
|
|
|
|
本方案锁定“事务内 branch0 投影 + 同 Session 回读 + digest 门禁”实现,不在首版引入 shadow/active 双表方案。替换前精确锁定单个 build;任何删除都必须带显式 `script_build_id` 和 `branch_id=0` 条件,禁止 unresolved 变量、宽范围条件和跨 build 操作。
|
|
|
|
|
|
@@ -1597,44 +1831,108 @@ Host 再次受控 UNBLOCK Root。Root Worker 只读 accepted Portfolio 和其唯
|
|
|
|
|
|
### 阶段三(待实现):Root、发布与恢复
|
|
|
|
|
|
-#### 15.3.1 Root 与最终验证
|
|
|
+当前 `agent/` 已具备同 Root 前向 continuation、UNBLOCK、durable Operation、`recover_operations()`、`resume_operation()`、独立 Worker/Validator 和不可变 ACCEPT 输入;阶段三计划不修改 Coordinator、Ledger、单父 Task 树或 ACCEPT 语义。RootDeliveryManifest、Publication UoW、Mission fencing、旧 API DTO 和恢复分类全部属于 `script_build_host/`。如果实施中证明必须新增“原子终止已启动遗留 Attempt”这类通用框架命令,应先单列业务无关 RFC 和兼容测试,经确认后再改 `agent/`,不得在阶段三 Host 提交中顺手改变框架状态机。
|
|
|
+
|
|
|
+#### 15.3.0 阶段三实施前置固化
|
|
|
+
|
|
|
+本步骤先完成,不与 Root Worker 或 Publisher 并行开发:
|
|
|
+
|
|
|
+1. **冻结 V2 Git 基线**:确认 `HEAD=origin/main=530b6a5`、工作区无遗留 V2 修改;保存 18 个 V2 提交清单。重跑 Agent `255 passed`、Host `156 passed, 1 skipped`、ruff、format、严格 mypy、`git diff --check` 和禁止项搜索,结果作为阶段三 before baseline。
|
|
|
+2. **冻结旧 API 金标**:依据第 5.2 节从上一版本真实 HTTP 保存版本化 fixture 和 checksum manifest;覆盖 start/upload/list/detail/log/prefill/retry/delete/favorite/overview/stop/`trace_messages`、Run 454、空关系/嵌套段落和旧错误;新 401/隐藏404/4404/422 进入独立 security overlay。fixture 尚未审查通过前不得实现 Legacy Detail DTO。
|
|
|
+3. **冻结 canonical 与 ID 算法**:以同一 StructuredScript 生成两套不同物理自增 ID 的投影,证明 `LegacyProjectionCanonicalV1` 和 digest 相同;再改变任一正文/关系字段,证明 digest 必须变化。规则落在共享 domain canonicalizer,Root dry-run 与事务 readback 只能调用同一实现。
|
|
|
+4. **冻结原子发布 port**:先定义第 12.1 节短事务 FinalPublicationReservationRepository、`FinalPublicationUnitOfWork`、所有 session-bound Repository port、禁止 nested transaction 的静态/单元测试,以及每一步故障注入点;final Publisher 不复用当前会自行开 Session 的 Direction Repository。
|
|
|
+5. **冻结 fencing、锁与写账号协议**:确认 `0003` 字段、MySQL 最低版本/`READ COMMITTED`、固定表序与同行主键序、OwnerLease 获取/释放/CAS、stop/finalize 线性化和错误码;冻结旧 writer 下线/撤权清单;用迁移 offline SQL 与并发测试骨架证明设计可执行。
|
|
|
+6. **冻结 continuation 崩溃矩阵**:把第 6.7 节每个持久状态写成 table-driven 测试骨架,明确唯一动作、是否允许模型调用、是否允许 finalize 和预期错误。
|
|
|
+
|
|
|
+退出门禁:金标 fixture 有来源与 checksum;canonical/ID 算法无歧义;UoW、fencing 和 continuation 都有接口及失败测试骨架;产品方案和技术方案字段、状态、错误语义一致。任一项未满足,不进入 `15.3.1`。
|
|
|
+
|
|
|
+#### 15.3.1 迁移、领域合同与 Composition Root
|
|
|
+
|
|
|
+- 新增 `0003_phase_three_publication_fencing`:Artifact type 加 `root_delivery_manifest`,binding 加 `owner_epoch/stop_epoch/owner_instance_id/owner_acquired_at`,publication 加 `(script_build_id, publication_type)` 唯一约束;upgrade 先做冲突 preflight,downgrade 按第 10 节 fail-closed;
|
|
|
+- 增加 `root-delivery` TaskKind、RootDeliveryManifestV1、LegacyProjectionCanonicalV1、MissionOwnerToken、FinalPublication 状态/错误合同;所有 canonical schema 都有显式 `schema_version`、最大大小和 strict validation;
|
|
|
+- 增加 `script_root_worker`、`script_root_validator` 和 Phase3 Planner policy/preset;Root Worker 不获得候选写工具、Shell、文件写或网络工具,Validator 只读;
|
|
|
+- Composition Root 注入 OwnerLease、FinalPublicationUnitOfWork factory、LegacyDetailProjectionService、Phase3 policy resolver 和 recovery service;生产启动校验 MySQL 能力与 durable TraceStore,测试显式注入 fake/SQLite adapter;
|
|
|
+- 保持现有 Direction publication、Phase1/Phase2 preset 和 Repository 行为不变,新增合同必须通过向后兼容回归。
|
|
|
+
|
|
|
+#### 15.3.2 Phase2→Phase3 continuation
|
|
|
+
|
|
|
+- `advance_to_phase_three()` 校验 build=`partial` 且 checkpoint 精确为 `PHASE_TWO_CANDIDATE_PORTFOLIO_READY`;binding/snapshot/root 同 build;唯一 CandidatePortfolio Task completed+ACCEPT;Decision→Attempt→passed Validation→ArtifactSnapshot→Portfolio Artifact 闭合;唯一 adopted StructuredScript frozen 且 digest/owner 一致;子树终态、无 active/stopping Operation、build 不在 stopping/stopped/failed/success;
|
|
|
+- 生成 Host 签名的 `script-build-phase-three/v1` policy、policy digest 和 toolset digest;第一次仅追加一组 system policy+continuation user,并持久化唯一 migration event;
|
|
|
+- 使用 `RunConfig.trace_id=root_trace_id`,不传新 Root、不 rewind。system+user 已完整落盘但 event 缺失时只补 event;消息不完整/重复或 event 不闭合时拒绝迁移;event 已落盘未 UNBLOCK时补 UNBLOCK;UNBLOCK 后无 Planner 模型输出/tool call 时以 `messages=[]` 继续;
|
|
|
+- Root 从 blocked→needs_replan 后,Planner 通过受控 REVISE 写入 execution-ready root-delivery TaskContract;`dispatch_script_tasks` 仅在 `task_id==root_task_id`、phase=3、TaskKind/preset 精确匹配时允许 Root Worker;
|
|
|
+- 严格实现第 6.7 节全部崩溃分支。任何无法证明“尚未产生副作用”的状态返回 `MISSION_RECOVERY_REQUIRED`,不自动创建新 Attempt;
|
|
|
+- 新 build 可从 Phase2 自动进入 Phase3;历史 Phase2 partial 只通过鉴权 advance/resume 单个 build,不做启动时全库扫描恢复。
|
|
|
+
|
|
|
+#### 15.3.3 Root Worker 与最终验证
|
|
|
+
|
|
|
+- Root Attempt 自动冻结 Root 的直接子 Direction + CandidatePortfolio ACCEPT Decisions;Portfolio 的唯一 `adopted_structured_script_ref` 是唯一正文来源,不扫描更深历史、候选 workspace 或 latest;
|
|
|
+- Root Worker 读取冻结 InputSnapshot、Direction、Portfolio、StructuredScript,调用共享 `legacy_projection_dry_run`,只 freeze 一个 RootDeliveryManifestV1;不创建或修改 Paragraph/Element/Link;
|
|
|
+- dry-run 将 StructuredScript 转成稳定逻辑身份,生成 `LegacyProjectionCanonicalV1` 和 digest;物理数据库 ID 既未知也不应伪造;
|
|
|
+- Root Validator 校验 Manifest owner/snapshot/input closure、Direction/Portfolio/StructuredScript refs、schema/digest、选题/人设/策略、叙事/证据/结构、placeholder/meta-description,以及 dry-run canonical 可读性;
|
|
|
+- `submit_attempt` 和 `submit_validation` 继续由 Host 生成 bounded summary;Root ACCEPT 只能绑定该 Attempt 的唯一 Manifest ArtifactSnapshot,不触发通用 Coordinator 自动发布;
|
|
|
+- Validator failed 后 Planner 可创建新的 Root Attempt/修订交付合同;若正文缺陷需要回到阶段二,必须显式 BLOCK 并进入人工/受控阶段回退设计,阶段三首版不得私自改 completed Portfolio 或 StructuredScript。
|
|
|
+
|
|
|
+#### 15.3.4 Legacy Projection 与完整旧 API
|
|
|
|
|
|
-- 新增幂等 `advance_to_phase_three()`;校验唯一 accepted Portfolio 后受控 UNBLOCK Root,并以同一 `trace_id` 前向续跑 Planner;
|
|
|
-- Root 从 blocked→needs_replan 后,Planner 必须通过受控 REVISE 产生新 Root TaskSpec,增加 `script-build://task-kinds/root-delivery` 和 execution-ready `ScriptTaskContractV1` URI;`dispatch_script_tasks` 只在 `task_id==root_task_id` 且 phase=3 时允许 `root-delivery → script_root_worker`,不从 objective 猜角色;
|
|
|
-- Root Attempt 自动绑定直接子 Direction + CandidatePortfolio;Portfolio 的 `adopted_structured_script_ref` 是唯一正文候选,Root 不扫描更深历史或 latest;
|
|
|
-- `script_root_worker` 只读上述固定引用,运行 legacy projection dry-run,在 Root Attempt 中只 freeze 一个 `RootDeliveryManifestV1`;不改 Paragraph/Element/Link;
|
|
|
-- Root Validator 同时核验 Manifest 与它指向的同一 StructuredScript,执行完整 schema、输入闭包、叙事、证据、人设、策略、placeholder 和 legacy projection digest 检查;
|
|
|
-- Planner ACCEPT Root 绑定同一 Snapshot/RootDeliveryManifest Artifact,不自动发布。
|
|
|
+- 实现一个 `LegacyDetailProjectionService`,普通 GET 和 Publisher 同 Session readback 复用同一字段映射;授权、candidate/official 选择和 canonical 转换位于不同明确方法;
|
|
|
+- branch0 API 返回真实新物理 ID,parent/link 通过 Publisher 生成的 ID map 闭合;排序、null、空 object/数组、ID 类型、`post_datas/script_datas`、`linked_elements/sub_paragraphs` 完全按第 5.2 节金标;
|
|
|
+- 逐个补齐 start/upload/list/detail/log/prefill/retry/delete/favorite/overview/stop/`trace_messages` 路由及旧错误,并单独补安全 overlay 的 401/隐藏404/4404/422;旧 retry 创建新 build,新 resume 继续原 build,两者不得共用模糊“重试”服务;
|
|
|
+- 金标比较必须走真实 ASGI/HTTP 序列化层,不能只测 Pydantic model;响应 content type、status code、字段缺失与 null 都参与断言;
|
|
|
+- HTML compatibility 只以现有旧接口字段为后端合同,不在本阶段改前端;若旧页面依赖未记录字段,先补 fixture/文档再实现,禁止测试里临时忽略。
|
|
|
|
|
|
-#### 15.3.2 Publisher 与兼容输出
|
|
|
+#### 15.3.5 FinalPublicationUnitOfWork 与 Publisher
|
|
|
|
|
|
-- 实现 ScriptPublisher、LegacyDetailProjectionService、final publication 状态机;
|
|
|
-- 以 Root ACCEPT Decision 为数据库唯一幂等键,将 RootDeliveryManifestRef 作为必须恒定的闭合校验项;由 Manifest 解析唯一 frozen StructuredScriptRef,事务内锁单 build,三次检查 stop intent/fencing epoch;
|
|
|
-- 只投影该 build 的 `branch_id=0` Paragraph/Element/Link、方向/摘要/计数;
|
|
|
-- 同事务旧详情 canonical readback 与 digest mismatch rollback;
|
|
|
-- Root ACCEPT + publication published + readback verified 才 status=success;失败重试同一 RootDeliveryManifest 绑定的 StructuredScript,不生成新正文;
|
|
|
-- 补旧 list/detail/log/retry/prefill/delete/favorite/overview/HTML compatibility 和辅助字段。
|
|
|
+- 实现第 12 节 reservation Repository、UoW factory 和五类 session-bound Repository;加入检测,final transaction 内任何 Repository 若调用独立 session factory 或 commit 即失败;
|
|
|
+- Publisher 先做不可变闭包 preflight并短事务 reserve 唯一 final identity,再携带 MissionOwnerToken 进入 UoW;严格按 binding→build→publication→links→paragraphs→elements→artifact locks、同表主键升序获取行锁;
|
|
|
+- branch0 替换先删 Link,再删 Paragraph/Element;按父先子后插 Paragraph,再插 Element/Link,生成稳定逻辑 key→物理 ID map;不修改 branch>0,不使用 `MAX(id)+1`,不跨 build 引用;
|
|
|
+- 同 Session 执行 canonical readback 和 digest 对账,再检查 fencing/stop epoch,最后同事务写 publication published、Manifest/StructuredScript published、accepted Root 指针、build 摘要/计数/end time/status=`success`;
|
|
|
+- 在 reservation 后、删除后、Paragraph 后、Element 后、Link 后、readback 后、pointer 后、published 后、success 后分别注入异常,逐一证明所有正式可见写入整体 rollback,reservation 仅保持 pending/failed identity;
|
|
|
+- 重放同一 Root Decision/Manifest 幂等;不同 Manifest、第二条 final、digest mismatch、stale owner token、stop epoch 改变全部 fail-closed。
|
|
|
|
|
|
-#### 15.3.3 恢复、所有权与观测
|
|
|
+#### 15.3.6 OwnerLease、stop、resume 与 recovery
|
|
|
|
|
|
-- 实现 resume/recover、OwnerLease/fencing token、start→run token handoff、嵌套 finalize token 复用;
|
|
|
-- 分类遗留 Operation/Attempt:未启动可安全推进,已启动模型 stage 不猜测重跑,`accepted_child_decision_ids=None` fail-closed;
|
|
|
-- reconcile orphan framework Snapshot/business version;完整保护 stop/resume/finalize/publication 竞态;
|
|
|
-- 建立稳定 MissionSnapshotView、完整授权 V2/Trace/WS DTO、权限续验、统一错误和审计。
|
|
|
+- 实现单机持续文件描述符 OwnerLease + binding CAS epoch;start→run token handoff和 run→finalize token 复用不得出现二次 owner 窗口;
|
|
|
+- stop 事务按 binding→build 写 durable stop epoch/status,再通过共享 Ledger 停止 durable Operation;本机 IPC/signal 只作加速。dispatch/submit/validation/decision/finalize 每个边界都用 owner token 重读 stop epoch;与 finalize 的胜负严格采用第 6.6 节数据库线性化规则;
|
|
|
+- `resume()` 先获取新 owner token,再分类:success 只校验;Root ACCEPT+pending/failed publication 只 finalize;Phase3 未完成按第 6.7 节恢复;阶段一/二旧 checkpoint 走各自 advance;
|
|
|
+- `recover_operations()` 只对齐 durable state:Operation 未 reserve 和 Attempt `started_at=None` 可安全续 Worker;Worker Submission 已持久化且 Validation 未启动时只续 Validator、不得重跑 Worker;Worker 未提交前已启动或 Validator 已启动后崩溃则归一化 stage,再由 Planner 新建 Attempt/replan。无 Operation 且 `started_at` 已存在、或 `accepted_child_decision_ids=None` 一律 fail-closed;
|
|
|
+- reconcile framework orphan Snapshot 和业务 orphan version 仍按 owner/attempt/digest,不绑定 latest;需要人工处理的 inconsistent publication/branch0 状态写稳定错误和审计,不启动模型;
|
|
|
+- 实现双 owner 409、stale token、文件锁进程崩溃释放、owner instance 清理 CAS、stop timeout、重复 stop/resume/finalize 的幂等测试。
|
|
|
|
|
|
-#### 15.3.4 阶段三验收
|
|
|
+#### 15.3.7 API、观测与人工恢复
|
|
|
+
|
|
|
+- 挂载鉴权的 Phase3 advance、safe resume、finalize/reconcile 和 publication detail;所有入口先由 build 反查 root/owner,不接受客户端伪造 Trace/Artifact identity;
|
|
|
+- Mission/Task/Artifact/Trace DTO 增加 Phase3 bounded manifest、publication state、owner epoch 和恢复原因,但不返回 protected context、完整正文、工具参数、锁文件路径或 instance secret;
|
|
|
+- 补 HTTP `Idempotency-Key`、统一 stable error middleware 和 WebSocket 长连接权限续验;HTTP 幂等记录不得代替 Coordinator tool-call 幂等或 publication 数据库唯一约束;
|
|
|
+- 审计至少记录 policy migration、owner acquire/release/fence failure、Root ACCEPT、publication attempt/rollback/commit、post-commit readback 和人工 reconcile;
|
|
|
+- 人工恢复只允许“检查一致性、重试同一 finalize、确认不可恢复并标记失败”,不提供修改已 ACCEPT Artifact、改 digest 或绕过 Validator 的接口。
|
|
|
+
|
|
|
+#### 15.3.8 阶段三收口与验收
|
|
|
+
|
|
|
+阶段三完成门禁固定为:
|
|
|
+
|
|
|
+```text
|
|
|
+唯一 RootDeliveryManifest frozen
|
|
|
+→ Root Validation passed
|
|
|
+→ Root Decision=ACCEPT
|
|
|
+→ FinalPublicationUnitOfWork committed
|
|
|
+→ publication=published
|
|
|
+→ Manifest/StructuredScript=published
|
|
|
+→ binding.accepted_root_artifact_version_id=Manifest version
|
|
|
+→ branch0 canonical digest=Manifest.legacy_projection_digest
|
|
|
+→ build=success
|
|
|
+→ 独立旧 HTTP readback health check passed 或已产生明确 post-commit 告警
|
|
|
+```
|
|
|
|
|
|
- 首次 publication/readback 故障不 success;重试只发布同一 RootDeliveryManifest 绑定的 StructuredScript;
|
|
|
-- branch0 投影/readback mismatch 整事务回滚;
|
|
|
-- OwnerLease 竞争、进程重启、遗留 RUNNING Attempt、stop/finalize 竞态均不重复模型或发布;
|
|
|
-- Decision→Root Attempt→RootDeliveryManifest→Validation→StructuredScript→Publication→Trace 全闭合;
|
|
|
-- 旧消费者无需理解 Agent 对象即可读取唯一正式脚本。
|
|
|
+- OwnerLease 竞争、进程重启、遗留 RUNNING Attempt、stop/finalize 竞态均不重复已启动模型或产生第二条 final;
|
|
|
+- Decision→Root Attempt→Validation→RootDeliveryManifest→StructuredScript→Publication→branch0→Trace 全闭合;
|
|
|
+- 旧消费者无需理解 Agent 对象即可读取唯一正式脚本;阶段一/二全量门禁继续通过,且无旧 Round/multipath/branch merge/第二套调度器回归。
|
|
|
|
|
|
## 16. 测试方案
|
|
|
|
|
|
### 16.1 当前全量门禁(2026-07-19)
|
|
|
|
|
|
-当前 Agent 全量为 `255 passed`;Host 全量为 `156 passed, 1 skipped`(skip 为需显式 DSN 的 MySQL marker)。Host `ruff check`、`ruff format --check`、严格 mypy 和 Agent 的 V2 框架文件定向 ruff/format 均通过;额外清理的旧飞书凭据文件通过定向 E/F lint,并新增 AST 回归,禁止凭据环境变量重新出现非空默认值。覆盖包括:
|
|
|
+V2 已拆成 18 个细粒度提交并推送为 `HEAD=origin/main=530b6a5`;阶段三 before baseline 固定为 Agent 全量 `255 passed`、Host 全量 `156 passed, 1 skipped`(skip 为需显式 DSN 的 MySQL marker)。Host `ruff check`、`ruff format --check`、严格 mypy 和 Agent 的 V2 框架文件定向 ruff/format 均通过;额外清理的旧飞书凭据文件通过定向 E/F lint,并新增 AST 回归,禁止凭据环境变量重新出现非空默认值。覆盖包括:
|
|
|
|
|
|
- canonical JSON/digest、空值/顺序、Evidence/Direction schema 与 ArtifactRef wire/DB 转换;
|
|
|
- Topic graph 归属、Persona alias、Prompt DB/file 来源、strategy/model/datasource manifest;
|
|
|
@@ -1679,23 +1977,49 @@ safe MissionSnapshot DTO、raw Ledger 字段过滤、InputSnapshot read digest m
|
|
|
13. SQL statement monitor 捕获实际 INSERT/UPDATE/DELETE 目标表,只允许四张新控制表、branch>0 Paragraph/Element/Link 和自动 TaskPlan;Run 454 的 5 Paragraph、11 Element、13 Link 可完整 freeze/reload;
|
|
|
14. Contract、Ledger、Trace、Snapshot、Artifact 可在 FileStore/Repository 重开后读取,但测试明确不猜测并重跑模型。
|
|
|
15. 历史 Phase1 partial 从鉴权 ASGI HTTP advance 进入:SQL 只追加 Snapshot v2 并 CAS 更新 Binding,Direction 继续钉死 v1;在 policy 落盘且 Root UNBLOCK 后模拟进程中断,重建 Host/FileStore 后仍以同一 Root 安全重入,policy、continuation user message 和 migration event 各只有一条。
|
|
|
-15. 历史 Snapshot v2 continuation 在 FileSystemTraceStore 重开后覆盖 policy 已写未 UNBLOCK、已 UNBLOCK无模型输出和已有输出需 `MISSION_RECOVERY_REQUIRED` 三条分支;跨 build/伪造 Root WebSocket 均在握手前 4404;CandidatePortfolio command replay 复用同一个 frozen version/ref。
|
|
|
-16. 真实 Coordinator durable Operation 在 Worker 阻塞期间执行 `ScriptMissionService.stop()`,验证 Operation→STOPPED、Task→needs_replan、build→stopped、重复 stop 幂等及 stop intent 后禁止新 dispatch;timeout/stopping 矩阵继续由可控时钟单元测试覆盖。
|
|
|
+16. 历史 Snapshot v2 continuation 在 FileSystemTraceStore 重开后覆盖 policy 已写未 UNBLOCK、已 UNBLOCK无模型输出和已有输出需 `MISSION_RECOVERY_REQUIRED` 三条分支;跨 build/伪造 Root WebSocket 均在握手前 4404;CandidatePortfolio command replay 复用同一个 frozen version/ref。
|
|
|
+17. 真实 Coordinator durable Operation 在 Worker 阻塞期间执行 `ScriptMissionService.stop()`,验证 Operation→STOPPED、Task→needs_replan、build→stopped、重复 stop 幂等及 stop intent 后禁止新 dispatch;timeout/stopping 矩阵继续由可控时钟单元测试覆盖。
|
|
|
|
|
|
### 16.3 阶段三测试
|
|
|
|
|
|
-- Root Worker 只消费 accepted Portfolio/唯一 StructuredScript 并冻结 RootDeliveryManifest;Root Validator 核验同一 Manifest/正文后才 ACCEPT;
|
|
|
-- Publisher 的 Root Decision→Root Attempt→RootDeliveryManifest→Validation→StructuredScript 闭合和 idempotency;
|
|
|
-- 首次 publication failed、重试同一 Manifest 绑定的 StructuredScript、事务内 readback mismatch rollback、成功后三者门禁;
|
|
|
-- stop intent/fencing epoch 在锁前、锁后、commit 前三处竞态;
|
|
|
-- OwnerLease token handoff、嵌套 finalize、双 owner 409、进程崩溃释放;
|
|
|
-- 未启动/已启动/无 Operation/unknown child binding 等遗留 Attempt 恢复矩阵;
|
|
|
-- 旧 list/detail/log/retry/prefill/delete/favorite/overview、辅助字段和 HTTP stable errors;
|
|
|
-- build-scoped DTO/V2/Trace/WS 授权、长连接撤权和审计。
|
|
|
+第 0 步合同测试:
|
|
|
+
|
|
|
+- 校验 `legacy_api/v1/manifest.json` 中每个 fixture 的 SHA-256、旧 commit/路由/status/provenance;fixture 脱敏后仍保持字段、null、ID 类型、排序和关系;
|
|
|
+- 同一 StructuredScript 分别从起始自增 ID=1 和任意更大 ID 投影,旧 HTTP 物理 ID 不同,但 `LegacyProjectionCanonicalV1` 和 digest 完全相同;正文、parent、Link 或 Element 任一业务变化必须改变 digest;active Paragraph index、Element 自然键或 Link pair 重复必须返回 `LEGACY_CANONICAL_IDENTITY_CONFLICT`;
|
|
|
+- 用 spy session factory 证明 Final UoW 内只有一个 AsyncSession、一次 begin/commit;任一 Repository 尝试新开 Session/commit 都失败;
|
|
|
+- `0003` SQLite upgrade/downgrade、MySQL offline SQL、重复 publication preflight、非零 epoch/final Artifact downgrade 拒绝;显式 DSN marker 在 MySQL>=8.0.16 验证版本能力、CHECK 真正 enforce、`READ COMMITTED`、`FOR UPDATE`、唯一约束、锁超时、DATETIME 精度,以及 Alembic 约束名与 `tables.py` metadata 完全一致;
|
|
|
+- 生产权限门禁证明旧脚本写进程/账号已下线或撤销 branch0 写权限,Final UoW 专用账号具备最小权限;绕过 binding 锁的测试 writer 必须被权限拒绝。
|
|
|
+
|
|
|
+Root/continuation:
|
|
|
+
|
|
|
+- Root Worker 只消费 accepted Direction、Portfolio 和唯一 StructuredScript,并冻结唯一 RootDeliveryManifest;Root Validator 核验同一 Manifest/正文后才允许 Planner ACCEPT;
|
|
|
+- table-driven 覆盖第 6.7 节全部状态:policy 未写、两条消息已写但 event 未写、单条/重复/父链错误、event 已写未 UNBLOCK、已 UNBLOCK无 Planner 输出、已有 Planner 输出无 Operation、Operation 未 reserve、Attempt 未启动、Worker submitted而 Validator 未启动、Worker/Validator 已启动崩溃、Root ACCEPT 未 publication、publication pending/failed、published+success、持久状态不一致;
|
|
|
+- 每个分支断言 policy/user/event 数量、Root BLOCK/UNBLOCK 数量、Operation/Attempt 数量、模型调用次数和稳定错误码;FileStore 重开后结果相同;
|
|
|
+- Phase3 policy/toolset/snapshot digest 不一致 fail-closed,阶段三不重开 Direction/Portfolio、不扫描 latest、不改 StructuredScript。
|
|
|
+
|
|
|
+Publication UoW:
|
|
|
+
|
|
|
+- Publisher 验证 Root Decision→Root Attempt→passed Validation→RootDeliveryManifest→Portfolio/Direction/StructuredScript 的完整 owner/digest/输入闭包和 idempotency;
|
|
|
+- 在 reservation 后、删 Link、删 Paragraph/Element、插 Paragraph、插 Element、插 Link、canonical readback、accepted pointer、Artifact published、publication published、build success 每处故障注入,断言正式 UoW rollback 后 branch0/pointer/Artifact/build 全部回到进入前状态,reservation 只能保持同一 pending/failed identity;
|
|
|
+- readback mismatch、parent/link ID map 缺失、stale fencing token、第二 Manifest、第二 final publication 全部 rollback;同一 Manifest replay 只保留一个 final 与一套 branch0;
|
|
|
+- stop 在 Publisher 前获得 binding 锁时 finalize 退出且 build 保持 stopping;Publisher 先获得锁并 commit 时 stop 等待后只能看到 success;并发循环测试不出现 published+非success、success+非published 或部分 branch0;
|
|
|
+- stop 与 `record_failure_if_uncommitted()` 并发时,failure recorder 必须按 frozen stop epoch/owner token CAS;stopping/stopped 永不被 failed 覆盖,stale owner 不得改 build;
|
|
|
+- OwnerLease token handoff、嵌套 finalize token 复用、双 owner 409、进程崩溃释放、owner 清理 CAS、旧 token fencing、锁超时/deadlock 映射;
|
|
|
+- 真实 MySQL 覆盖事务中连接断开、deadlock victim、lock timeout、UoW 进程 kill 和 commit-unknown(服务端可能已提交、客户端断线);重入先按唯一 final identity 回读,闭合则按成功返回,确认未提交才 failed/retry,半闭合只进入人工恢复;
|
|
|
+- stop 请求落到非 owner Host 进程时,仍通过 durable Ledger stop Operation;stop epoch 后到达的 workspace write、submit_attempt、submit_validation、Planner decision 和 finalize 全部被 fencing 拒绝;
|
|
|
+- commit 后独立旧 HTTP readback 故障只产生告警/人工恢复状态,不重复模型或另写 final。
|
|
|
+
|
|
|
+Recovery/API/E2E:
|
|
|
+
|
|
|
+- 未 reserve/Attempt 未启动/Worker 已提交而 Validator 未启动/Worker 或 Validator 已启动/无 Operation/unknown child binding 等恢复矩阵;Worker 已提交后只续 Validator,已启动未提交的模型 stage 永不原地重跑;Root ACCEPT 后只走 finalize;
|
|
|
+- 旧 start/upload/list/detail/log/retry/prefill/delete/favorite/overview/stop/`trace_messages` 与完整 `legacy_api/v1` golden 比较,覆盖 `post_datas/script_datas`、`linked_elements/sub_paragraphs`、空值、ID 类型与引用、排序和旧错误体;新 401/隐藏404/4404/422 与 `security_overlay/v3` 单独比较;
|
|
|
+- retry 创建新 build,resume 继续同 build且不 rewind;重复 HTTP Idempotency-Key、finalize 和 reconcile 收敛;
|
|
|
+- build-scoped DTO/V2/Trace/WS 授权、跨 build 隐藏、长连接撤权、人工恢复权限和审计;
|
|
|
+- 真实 Runner→Root Worker→Root Validator→Planner ACCEPT→MySQL/SQLite UoW→旧 HTTP detail 全链路,最终仅一次 success、branch0>0 行存在、candidate branch 保持 frozen,SQL monitor 证明没有旧 Round/multipath/branch merge 或阶段二候选覆写。
|
|
|
|
|
|
### 16.4 真实基线与质量回归
|
|
|
|
|
|
-以 Run 454 的 5 Paragraph、11 Element、13 Link 作为“旧详情可表达性”样本,而不是新模型必须逐字复现的金标。回归比较:
|
|
|
+以 Run 454 的 5 Paragraph、11 Element、13 Link 作为“旧详情可表达性”样本,而不是新模型必须逐字复现的内容金标,也不是旧 HTTP 兼容金标。旧 API 字段、空值、ID、排序和错误合同由第 5.2/16.3 节的真实响应 fixture 单独固化。质量回归比较:
|
|
|
|
|
|
- 输入字段覆盖率;
|
|
|
- 输出 schema 和关系可读性;
|