diff --git a/docs/plan/01-w1-review.md b/docs/plan/01-w1-review.md index 5025ff5f..30304038 100644 --- a/docs/plan/01-w1-review.md +++ b/docs/plan/01-w1-review.md @@ -19,20 +19,20 @@ |---|---|---|---|---| | B1 | OutboxRelay 只看 `published_at`,没有用 db 0002 新增的 `status/next_attempt_at/attempts/idempotency_key/last_error`。结果是:投递后状态一直停在 pending;失败后每个 tick 都立即重试,没有退避;行上的幂等键也没用上。测试建的表和 db 的表结构不同 | `packages/tasks/src/outbox-relay.ts` | 按 ADR-002 §2.1 对齐:认领 pending 且到期的行;成功置 dispatched;失败按指数退避重试,达到上限置 dead | **已修** 31729f2 | | B2 | contracts 没有 `release.resolved` 事件。W2-B 负责发出、W2-C 负责消费,两边并行开发会各自发明一种 payload | `packages/contracts/src/events.ts` | 新增 `ReleaseResolvedPayload`,字段为 sourceReleaseId、observationId、mediaEntityId、contentUnitIds、evidenceLevel | **已修** 9310814 | -| B3 | 能力描述有三套定义,且互相冲突:agent 的 `CapabilityDescriptor`(scope 为 read/preview/propose/execute,risk 为 low…critical);contracts 的 `CapabilityDefinition`(risk 为 read/preview/write/destructive);contracts 的 `CapabilityDescriptor`(kind 含 approve,scope 用 ApiScope)。agent 与 contracts 还有同名的 `AgentRun`/`AgentUsage` | `packages/agent/src/capability/types.ts:11-33`、`packages/contracts/src/capability.ts:28-96`、`packages/agent/src/runtime/port.ts` | 以 contracts 的 `CAPABILITY_CATALOG` + `CAPABILITY_IO` 为唯一来源,删除 `CapabilityDefinition`;agent 改为派生描述,重名类型改名(如 `AgentRunHandle`) | 未修(涉及设计取舍) | -| B4 | 14 个能力一个都没注册,连 stub 都没有。README 示例里的 `registerAllCapabilities` 并不存在;测试用的是手写 schema | `packages/agent/src/`、`packages/agent/README.md:56`、`packages/agent/test/helpers.ts:36-70` | 在 W2-D 开工前(或 W2-A 期间)补上 `registerAllCapabilities(registry, handlers)`,schema 取自 `CAPABILITY_IO`;加测试断言注册数等于 `CAPABILITY_IDS` | 未修 | -| B5 | domain 与 contracts 的枚举和字段不一致,server 必须写映射。具体有:下载状态(domain 的 `paused_by_threshold/paused_by_user/error/missing_files` 对 contracts 的 `paused+PauseReason/stalled/failed/missing/submitting/importing/imported`);证据等级(`needs_confirm` 对 `needs_review/manual`,且两边都叫 `EvidenceLevel`);身份结果(`auto/ambiguous/no_match` 对 `resolved/proposed/ambiguous/unidentifiable`);PathBinding 字段名(entityId/scope/season/templateVersion:number/active 对 mediaEntityId/bindingKind/seasonNumber/templateVersion:string/status);媒体类型(`movie/tv` 对 `movie/series/variety/…`) | `packages/domain/src/download-state/index.ts:2`、`domain/src/release-parser/types.ts:48`、`domain/src/identity/match.ts:7,19,158`、`domain/src/path/binding.ts:10-19`、`contracts/src/execution.ts:131`、`contracts/src/identity.ts:12`、`contracts/src/assets.ts:152` | 下载状态以 contracts 为准,domain 改为输出 `status + pauseReason`;domain 的 `EvidenceLevel` 改名为 `MatchLevel`;在 contracts 测试里加一条 domain↔contracts 枚举对照测试;字段映射集中写在 `apps/server/src/mappers/` | 未修(需要定方向) | -| B6 | `apps/server/src/modules/` 骨架和模块列表还没有。`http/app.ts` 的路由是硬编码;`worker/index.ts` 只有心跳,没有接入 tasks 引擎,注释"W1 接入"已过时。A/B/C 并行开发时会在 app.ts/worker 上发生冲突 | `apps/server/src/http/app.ts:98-113`、`apps/server/src/worker/index.ts:12` | W2 开工第一件事:先定好 `ServerModule { registerRoutes(app, deps); registerTasks(registry, deps) }`,在 app.ts 和 worker 里各留一个模块数组,并把 TaskEngine 和 OutboxRelay 接进 worker | 未修(接口签名属于设计) | -| B7 | qB `delete(…, { deleteFiles: true })` 没有任何防护。注释里说的 `assertSafeToDeleteData` 在全仓库都找不到。接管任务的 save_path 如果落在媒体库内,会连带删掉库文件,违背 REQ §2.1 第 5 条 | `packages/adapters/src/qbittorrent/client.ts:164,589-593` | 实现 `assertSafeToDeleteData`:检查 content_path 不在任何 library_root 下,已验证入库,且没有被 asset_location 引用。W2-C 的所有删除都只经过一个 `safeDeleteTorrent` 入口 | 未修 | -| B8 | mock qB 的 `complete()` 只改进度,不在磁盘上生成文件;带 `deleteFiles` 删除时也不删文件。这样"下载完成→Copy 入库"的 e2e 没有源文件可用 | `packages/testkit/src/qbittorrent/mock-qb.ts:91,161-171,402-405` | 增加 `materializeTo(root)` 或 `complete(hash, { writeFiles: true })`,按 save_path/files 写出临时文件;deleteFiles 为 true 时同步删除 | 未修(W2-D 前补) | -| B9 | agent_run 事件没有落库路径。README 提到的 `agent_run_event` 表不存在,`events()` 只在内存里回放;agent 的事件类型(tool_call_start/end/rejected)与 contracts 的 `AgentToolCallPayload.phase`(started/succeeded/failed/denied)对不上 | `packages/agent/src/pi/runtime.ts:38`、`packages/agent/src/runtime/port.ts:16-45`、`packages/contracts/src/events.ts` | W2-D 新增迁移 `agent_run_event(run_id, seq, type, payload)`,唯一键为 (run_id, seq),并统一 phase 映射 | 未修 | +| B3 | 能力描述有三套定义,且互相冲突:agent 的 `CapabilityDescriptor`(scope 为 read/preview/propose/execute,risk 为 low…critical);contracts 的 `CapabilityDefinition`(risk 为 read/preview/write/destructive);contracts 的 `CapabilityDescriptor`(kind 含 approve,scope 用 ApiScope)。agent 与 contracts 还有同名的 `AgentRun`/`AgentUsage` | `packages/agent/src/capability/types.ts:11-33`、`packages/contracts/src/capability.ts:28-96`、`packages/agent/src/runtime/port.ts` | 以 contracts 的 `CAPABILITY_CATALOG` + `CAPABILITY_IO` 为唯一来源,删除 `CapabilityDefinition`;agent 改为派生描述,重名类型改名(如 `AgentRunHandle`) | **已修** db5352e | +| B4 | 14 个能力一个都没注册,连 stub 都没有。README 示例里的 `registerAllCapabilities` 并不存在;测试用的是手写 schema | `packages/agent/src/`、`packages/agent/README.md:56`、`packages/agent/test/helpers.ts:36-70` | 在 W2-D 开工前(或 W2-A 期间)补上 `registerAllCapabilities(registry, handlers)`,schema 取自 `CAPABILITY_IO`;加测试断言注册数等于 `CAPABILITY_IDS` | 部分:W2-0 提供 `capabilitySpec(id)`,各模块在 `registerCapabilities` 里注册;注册数断言留给 W2-D | +| B5 | domain 与 contracts 的枚举和字段不一致,server 必须写映射。具体有:下载状态(domain 的 `paused_by_threshold/paused_by_user/error/missing_files` 对 contracts 的 `paused+PauseReason/stalled/failed/missing/submitting/importing/imported`);证据等级(`needs_confirm` 对 `needs_review/manual`,且两边都叫 `EvidenceLevel`);身份结果(`auto/ambiguous/no_match` 对 `resolved/proposed/ambiguous/unidentifiable`);PathBinding 字段名(entityId/scope/season/templateVersion:number/active 对 mediaEntityId/bindingKind/seasonNumber/templateVersion:string/status);媒体类型(`movie/tv` 对 `movie/series/variety/…`) | `packages/domain/src/download-state/index.ts:2`、`domain/src/release-parser/types.ts:48`、`domain/src/identity/match.ts:7,19,158`、`domain/src/path/binding.ts:10-19`、`contracts/src/execution.ts:131`、`contracts/src/identity.ts:12`、`contracts/src/assets.ts:152` | 下载状态以 contracts 为准,domain 改为输出 `status + pauseReason`;domain 的 `EvidenceLevel` 改名为 `MatchLevel`;在 contracts 测试里加一条 domain↔contracts 枚举对照测试;字段映射集中写在 `apps/server/src/mappers/` | **已修** 42ecc93(映射集中在新包 `@umt/mapping`,domain 未改名) | +| B6 | `apps/server/src/modules/` 骨架和模块列表还没有。`http/app.ts` 的路由是硬编码;`worker/index.ts` 只有心跳,没有接入 tasks 引擎,注释"W1 接入"已过时。A/B/C 并行开发时会在 app.ts/worker 上发生冲突 | `apps/server/src/http/app.ts:98-113`、`apps/server/src/worker/index.ts:12` | W2 开工第一件事:先定好 `ServerModule { registerRoutes(app, deps); registerTasks(registry, deps) }`,在 app.ts 和 worker 里各留一个模块数组,并把 TaskEngine 和 OutboxRelay 接进 worker | **已修** 9858b84 | +| B7 | qB `delete(…, { deleteFiles: true })` 没有任何防护。注释里说的 `assertSafeToDeleteData` 在全仓库都找不到。接管任务的 save_path 如果落在媒体库内,会连带删掉库文件,违背 REQ §2.1 第 5 条 | `packages/adapters/src/qbittorrent/client.ts:164,589-593` | 实现 `assertSafeToDeleteData`:检查 content_path 不在任何 library_root 下,已验证入库,且没有被 asset_location 引用。W2-C 的所有删除都只经过一个 `safeDeleteTorrent` 入口 | **已修** fa92210 | +| B8 | mock qB 的 `complete()` 只改进度,不在磁盘上生成文件;带 `deleteFiles` 删除时也不删文件。这样"下载完成→Copy 入库"的 e2e 没有源文件可用 | `packages/testkit/src/qbittorrent/mock-qb.ts:91,161-171,402-405` | 增加 `materializeTo(root)` 或 `complete(hash, { writeFiles: true })`,按 save_path/files 写出临时文件;deleteFiles 为 true 时同步删除 | **已修** fa92210 | +| B9 | agent_run 事件没有落库路径。README 提到的 `agent_run_event` 表不存在,`events()` 只在内存里回放;agent 的事件类型(tool_call_start/end/rejected)与 contracts 的 `AgentToolCallPayload.phase`(started/succeeded/failed/denied)对不上 | `packages/agent/src/pi/runtime.ts:38`、`packages/agent/src/runtime/port.ts:16-45`、`packages/contracts/src/events.ts` | W2-D 新增迁移 `agent_run_event(run_id, seq, type, payload)`,唯一键为 (run_id, seq),并统一 phase 映射 | **已修** db5352e、da8e53b | ## 2. 应修 | # | 问题 | 位置 | 修复建议 | |---|---|---|---| | S1 | 跨卷移动时,已存在的目标只按大小比较就判为一致,随后删源(同尺寸、不同内容会丢数据) | `packages/adapters/src/fs/move.ts` | **已修** c33034a:预先存在的目标强制用 sha256 比较,并补了测试 | -| S2 | tasks 自带的三张表(`tasks_run_meta/tasks_fact/tasks_op_ledger`)仍走独立迁移器,没有并入 db,`types.ts` 里也没有这些表 | `packages/tasks/migrations/0001_tasks_tables.ts` | W2-A 在 db 新增 0013 迁移并入这三张表,`migrateTasksTables` 只保留给测试用 | +| S2 | tasks 自带的三张表(`tasks_run_meta/tasks_fact/tasks_op_ledger`)仍走独立迁移器,没有并入 db,`types.ts` 里也没有这些表 | `packages/tasks/migrations/0001_tasks_tables.ts` | **已修** da8e53b:并入 db `0013_tasks_tables`,`migrateTasksTables` 已删除,tasks 测试用 db 迁移器建表 | | S3 | `tasks_op_ledger` 与 `asset_operation` 职责重叠,两者都有 attempt_token 和 attempts | `packages/db/migrations/0009_execution.ts`、`packages/tasks/src/op-ledger.ts` | 建议不合并表,按层分工:`tasks_op_ledger` 只做通用 fencing,op_key 等于 `asset_operation.operation_id`(或 `download_task::submit`);`asset_operation` 是业务时间线和结果的事实来源,在 `withAttemptToken` 的 `commit` 回调里同事务更新。W2-C 开工前写进 ADR-002 | | S4 | domain 下载状态机的 `ALLOWED` 允许除 removed 外任意两个状态互转,等于没有约束 | `packages/domain/src/download-state/index.ts:97` | 随 B5 一起收紧迁移表 | | S5 | `organize/plan` 在调用方没传 `existingTargets` 时,按"目标不存在"处理(违背"失败≠不存在") | `packages/domain/src/organize/plan.ts:39,162` | 改为必填,或者不传时输出 hold | @@ -87,3 +87,25 @@ ## 6. 收尾项说明 任务里说的"sites/mp-sync 有 9 处相对 import 缺扩展名",在基线里已经不存在了:`no-sloppy-imports` 零命中,`deno lint` 报的 9 条都是 `require-await`。所以这次收尾只做了以下几件事:删除 biome override;删除 `--unstable-sloppy-imports`;mp-sync 改为 deno shebang 并加可执行位;把 YAML 改成静态 import;更新 README 里的调用命令。`check.mjs --to e6820a8` 结果为 49 个未变,`movieclaw-import --check` 结果为 0 差异。 + +## 7. W2-0 处理结果(2026-09-27) + +W2 打底,目标是让 W2-A/B/C 在互不冲突的前提下并行开工。`deno task ci` 全绿。 + +| # | 处理 | 提交 | +|---|---|---| +| B3 | contracts 为唯一定义:`CapabilityDescriptor` 增加 `replay`;`CapabilitySpec`(描述符 + 真实 zod schema)与 `capabilitySpec(id)`(CAPABILITY_CATALOG + CAPABILITY_IO 组装);删除 `CapabilityDefinition`/`CapabilityRisk`。agent 删除自己的描述类型,注册中心直接收 `CapabilitySpec`,作用域改称 `kind`/`allowedKinds`,approve 类同样必须授权;agent 的 `AgentRun` 端口改名 `AgentRunHandle`,用量改用 contracts `AgentUsageTotals` | db5352e | +| B4 | 未做 `registerAllCapabilities`;改由各模块 `registerCapabilities` 用 `capabilitySpec(id)` 注册,注册数断言留给 W2-D | — | +| B5 | 新包 `@umt/mapping`(只依赖 domain + contracts):下载状态 + 暂停原因、证据等级、媒体类型、身份解析状态、Slot 状态/规则列/评估结果、ParsedRelease ↔ `source_release.parsed`、SiteRelease ↔ source_release、PathBinding、OrganizePlan → 整理记录/文件结果/organize.plan 输出。映射表为 `Record<联合类型, …>`,任一侧增值都会让 typecheck 或测试失败。domain 的 `EvidenceLevel` 没有改名,mapping 里以 `DomainMatchLevel` 称呼 | 42ecc93 | +| B6 | `apps/server/src/modules/` 骨架:`ServerModule`、`ServerContext`、`SERVER_MODULES`(四个占位模块);`bootstrap()` 装配上下文并连接引擎;worker 启动引擎 worker + OutboxRelay + 模块 `start()`;分工规则写在 apps/server/README.md | 9858b84 | +| B7 | `assertSafeToDeleteData({ contentPath, libraryRoots, importVerified })`;`delete({ deleteFiles: true, safety })` 必须逐 hash 附检查输入,不通过抛 `DownloaderError('unsafe_delete')`,请求不发出 | fa92210 | +| B8 | mock qB `complete(hash, { files, writeFiles })` 在 save_path 下写随机内容文件(默认关闭、有总量上限、防路径越界);`deleteFiles=true` 同步删除 | fa92210 | +| B9 | 迁移 `0014_agent_run_event`(主键 (run_id, seq),删 run 级联);contracts `AgentRunEvent`/`AgentRunEventRow` + 行转换 + SSE 映射(`agentRunEventToBusEvent`,tool phase 统一为 started/succeeded/failed/denied);agent `onEvent` 收 contracts 事件、按 seq 串行投递,`firstSeq` 续接序号 | db5352e、da8e53b | +| S2 | tasks 三张表并入 db `0013_tasks_tables` | da8e53b | + +注意事项: + +- `apps/server` 启动时连接任务引擎,数据库不可达会直接退出;`--role migrate` 顺带迁移 openworkflow schema,`api`/`worker` 分进程时不再各自迁移。 +- `ctx.secrets` 仍是占位(调用即抛错),W2-A 在 `modules/auth-settings/provided.ts` 实现;`ctx.capabilities` 暂无 authorize,propose/approve/execute 一律拒绝,W2-D 在 `modules/agent/capability-registry.ts` 接入。 +- 引入 @umt/db 后 pg 的 int8 全局解析为 number,依赖 `count(*)` 字符串结果的测试需改用 `Number()`。 +