# Lane D 审计报告 — KAP 无限画布:多项目/多分集数据隔离与规模审计

- 仓:`/data/workspace/kais-aigc-platform` @ `feat/ui-direct-action-fix`,HEAD `333df7b6`(已实测核对)
- DB:`data/db2.sqlite`(全程 `file:...?mode=ro` 只读);审计时间 2026-09-19
- 规模实测:canvas_nodes=11485、canvas_links=21904、o_project=70、o_assets=6469、canvas_graph_meta=185、kv_canvasEvent=1710;DB 主文件 368MB + WAL 5.5MB,journal_mode=wal

---

## 1. 执行摘要(≤200 字)

画布隔离的核心骨架(project_id+episodes_id 复合主键、scope 索引、save-v2 增量 upsert)是健全的,SQL 实测未发现跨 project 路径引用泄漏;但**数据安全防线整体缺失**:saveFullGraph 的 diff-delete 与 import-from-dir replace 硬删均无软删/备份/审计,2026-07-25 事故后防线未落地;368MB 生产库零备份机制;「save-v2 不写 o_project」问题仍存在,实测 5 个真实项目(最多 153 节点)前端不可见;多分集资产链路未闭环(前端资产库不按 episode 分桶);`data/oss/pipeline` 72.5% symlink 已死。

### P0 清单

| # | 问题 | 服务对象 |
|---|------|---------|
| P0-1 | 删除面零防护:diff-delete / replace 硬删无软删、无备份、无审计日志 | 数据安全 |
| P0-2 | db2.sqlite 无任何备份机制(无 cron/timer/脚本,kv_snapshot 空表) | 数据安全 |
| P0-3 | 项目注册不一致仍存在:10 个 project_id(含 5 个真实项目)有画布数据但 o_project 无记录 → 前端不可见 | KMC 多分集生产 |

---

## 2. 问题清单

### P0-1 删除面零防护(2026-07-25 事故防线未落地)

- **现象**:多条路径可物理删除画布数据,全部无软删除、无删除前备份、无审计留痕。
- **证据**:
  - `src/lib/canvasRelationalStore.ts:880-886`(节点)、`:925-931`(边)、`:967-973`(分支)、`:1006-1012`(变体组):saveFullGraph 对「DB 有而新图没有」的行执行 `chunkedDelete`(底层 `.del()`,`:77-87`)。任何客户端(含 KMC pipeline)发送不完整图即物理清行。
  - `src/routes/canvas/v2/import-from-dir.ts:2268-2269`:replace 模式显式 `DELETE FROM canvas_nodes/canvas_links WHERE project_id=? AND episodes_id=?`,无备份;`:2281-2283` materialization 失败仅 `console.error`。
  - `src/routes/canvas/v2/nodes.ts:223` → `canvasRelationalStore.ts:149-160`:单节点级联删 links;`:links.ts:74` → `:228-236` 删边。均仅 404 前置检查。
  - 软删除:全 `src` grep `deleted_at` 零命中;`canvas_nodes` schema(`src/lib/initDB.ts:1259-1286`)无该列。
  - 审计:`kv_audit` 写入方仅 `src/routes/v1/pipeline/resume.ts:80`、`review-result.ts:70,117`、`v1/audit/index.ts:22` —— 不记录任何画布删除/保存操作。
  - `kv_snapshot` 表存在(`initDB.ts` schema)但实测 **0 行** —— 快照功能未启用。
- **根因**:事件溯源退役改关系直写后,删除路径按「业务正确性」设计,未按「生产数据安全」补防线。
- **影响**:**数据安全** —— 与 2026-07-25 事故同类的「一次调用丢全图」风险当前完全敞口;KMC 多分集并行保存时,任一 episode 的残图上传会清掉该 scope 全部行。
- **修复建议**(详见 §5 R1/R2/R3):软删/删前快照 + diff-delete 阈值护栏 + kv_audit 留痕,成本 M。

### P0-2 生产库零备份机制

- **现象**:368MB 单文件 SQLite 承载全部多工程画布数据,无任何定期备份。
- **证据**:
  - `crontab -l` 全量 7 条任务,无一条涉及 db2/backup/sqlite;`/etc/cron.d/` 仅 anacron/e2scrub_all/sysstat;`systemctl list-timers --all` 无自研备份 timer(仅 gpu-thermal、watchdog 等)。
  - `ls -d data/backups backups /data/backups` 均不存在;`find -name "db2*.bak"` 零命中。
  - `scripts/` 与 `src/lib/` grep `backup` 无数据库备份脚本(命中均为 GPU 调度快照等他义)。
  - DB 文件:`db2.sqlite` 368MB(page_count 93988 × 4096),`-wal` 5.5MB,`-shm` 32K,journal_mode=wal。
- **根因**:从未建立备份运维项;kv_snapshot 应用层快照机制建成但未启用。
- **影响**:**数据安全** —— 误删/损坏即不可恢复;KMC/KGR 全部批次数据单点。368MB 的 `sqlite3 .backup` 在线备份窗口为秒级,可行性无障碍,缺失纯属未做。
- **修复建议**:cron 每日 `.backup` + 保留 7 代,成本 S。

### P0-3 项目注册不一致:画布数据有、o_project 无 → 前端不可见(仍存在)

- **现象**:save-v2 只写 canvas_nodes/canvas_links/canvas_graph_meta,不写 o_project;项目列表权威来源 = o_project,缺失即不可见。
- **证据**:
  - `src/routes/canvas/v2/save-v2.ts:72` 仅调 `saveFullGraph`;`canvasRelationalStore.ts` 全文无 `o_project` 引用(仅 `:1014-1030` touch `canvas_graph_meta`)。
  - 项目列表:`src/routes/canvas/projects.ts:31` `u.db("o_project").select(...)` 为唯一来源,`:43/51/60` 三段聚合都 `.whereIn("project_id", projectIds)` —— 无 fallback。
  - SQL 实测:`SELECT project_id, COUNT(*) FROM canvas_nodes WHERE project_id NOT IN (SELECT id FROM o_project) GROUP BY 1` → **10 个 project_id、合计 228 节点**,其中 5 个为真实时间戳 ID:`1783092441042`(153 节点)、`1785042229333`(32)、`1785041514653`(16)、`1783165143455`(11)、`1782526455205`(8);另 5 个为测试 ID(1234/777/888/9998/99999)。
  - `canvas_graph_meta` 185 行中仅 118 行 project_id 存在于 o_project;缺 o_project 的 distinct project_id = 19 个(含无节点的历史残留)。
  - graph_meta 字段仅 `(project_id, episodes_id, created_at, updated_at, last_event_id)`(schema 实测)——无 name/intro,**无法替代 o_project 做项目列表**。
  - 前端:`packages/infinite-canvas/src/components/ProjectSelector.tsx:26` → POST `/api/canvas/projects`,无新建入口、无占位渲染。KMC 侧 `kais-hermes-skills/plugins/kais_aigc/canvas_sync.py:6613-6732` `create_new_project` 直写 sqlite 补 o_project,但仅首跑调一次(`runner.py:4901-4903`),被跳过即触发本问题(该模式已被 `canvas-project-invisible-o-project-missing/SKILL.md:24` 记录为现网事故模式)。
- **根因**:项目注册依赖调用方自觉(KMC 插件直写 DB),平台侧 save-v2 不兜底。
- **影响**:**KMC 多分集生产** —— 新 episode/新项目数据已落库但前端不可见、不可操作,等同数据丢失观感;**KGR 批次** 新建复刻批次项目同样踩坑。
- **修复建议**:save-v2 对缺失 project_id 自动 INSERT 占位 o_project(name=`project-<id>`),成本 S。

### P1-1 多分集资产链路未闭环:前端资产库不按 episode 分桶

- **现象**:工单背景称「前端 useRealAssets 按 (projectId, episodesId) 分桶缓存」——**实探不符**:分桶键只有 projectId。
- **证据**:`packages/infinite-canvas/src/hooks/useRealAssets.ts:14-15` 缓存 `Map<number|null, AssetDetail[]>` 键仅 projectId;`:24-25` 请求 `{limit:200, includeFile:true, projectId}`;全部调用方(AssetLibrary.tsx:459、CharacterWardrobe.tsx:157、SceneShotManager.tsx:260、DialoguePanel.tsx:336、AssetDetail.tsx:34)均不传 episodesId。后端 `src/routes/v1/assets-registry/index.ts:86-133` searchSchema **无 episodesId 字段**。
- **根因**:o_assets 加列与索引(`idx_o_assets_episodes`)已完成,但 API 筛选与前端分桶两段未落地。
- **影响**:**KMC 多分集生产** —— 同项目多 episode 资产在资产库混桶,episode 间互相可见可选,是跨分集污染的入口;**KGR 批次隔离** 同样无 episode 维度护栏。
- **修复建议**:search API 加 episodesId 过滤 + useRealAssets 缓存键改 `{projectId, episodesId}`,成本 M。

### P1-2 o_assets.episodesId 脏数据:NULL 10.2% + 异常值 2732 行

- **证据**:`SELECT COUNT(*), SUM(episodesId IS NULL), SUM(episodesId IS NOT NULL) FROM o_assets` → 6469 / **658 NULL(10.2%)** / 5811。异常值:`episodesId > 1000` 共 **2732 行** —— `3901`×2723(全部属于 projectId=39,疑似把 `39:01` 之类的复合键塞进了单列,**待验证**)、`1785508691757`×9(把 projectId 写进了 episodesId)。project 1785508691757 内:NULL 485 / =1 有 326 / =projectId 有 9。
- **根因**:无 CHECK 约束、无写入方校验,多个写入方(KMC 插件、sync 脚本、平台 API)各自理解该列语义。
- **影响**:**KGR 批次隔离** —— 按 episodesId 过滤时这 3390 行(52%)行为不可预期;P1-1 修复后脏数据会直接造成错桶。
- **修复建议**:清洗脚本 backfill + 写入侧 zod 校验(episodesId 小整数),成本 S。

### P1-3 资产 URL 拓扑分裂 + `data/oss/pipeline` 死链 72.5%

- **现象**:同一 episode 的资产 URL 至少 5 种形态并存;pipeline hash 目录大面积死链。
- **证据**:
  - 5 种形态:`/oss/{workdirBase}/…`(import-from-dir.ts:1895-1897、fsToOssUrl :202-224)、`/oss/pipeline/<md5[:8]>/…`(kmc `canvas_sync.py:815-892`)、`/oss/_thumbs/…`(:1059-1062)、`/mnt/agents/output` 容器路径直通(`app.ts:75-88`)、裸绝对路径兜底(import-from-dir.ts:433)。
  - 实测 `data/oss/pipeline/`:1014 个 hash 目录,内层 symlink **7904 条,其中 5731 broken(72.5%)**;**4772 条指向 `/tmp/pytest-of-kai/...` 临时目录**(测试产物泄入生产 serve 树,如 `data/oss/pipeline/000c68b6/first_frame_v1.png`)。非 pytest 死链约 959 条(指向已删除的 `/home/kai/runs/...` 等)。
  - canvas_nodes 中引用 `/oss/pipeline/` 的节点遍布各项目(top:1786905537220×472、1787097989149×304、1787107886615×234 …);抽样 `1d1cfb36/` 目录 20 条 symlink 全活,但 72.5% 的总体死链率意味着相当比例节点媒体 URL 已死(精确映射率**待验证**)。
  - `data/oss/` 顶层另有 1 条 broken symlink:`shot-timeline-ep01 -> /data/workspace/kst-canvas-consumer/...(不存在)`。
  - 前端已需专门处理死链:`NodeDetailPanel.tsx:451-470` 提示「检查 data/oss/ 下该项目的符号链接是否存在」。
- **根因**:资产 URL 由多个写入方各自生成,无统一注册/规范化层;symlink 目标生命周期无人管理。
- **影响**:**KMC 多分集生产** —— 画布上媒体 404,复审/复刻取不到源;**KGR 金标复刻** 依赖原资产路径时直接失败。
- **修复建议**:死链清理 + 统一 `fsToOssUrl` 单写入口 + 资产注册时 realpath 存真值,成本 M。

### P1-4 load-v2 全量拉取无分页,`since` 增量路径仍先全量加载

- **证据**:`src/routes/canvas/v2/load-v2.ts:55` 无条件 `loadFullGraph`(内部 `canvasRelationalStore.ts:1036-1072` = listNodes/listLinks/listBranches/listVariantGroups 四个 `select("*")` 无 limit,`:162-168,:238-244`);`since` 分支(`load-v2.ts:67-84`)在**已经全量加载之后**再发两条 `updated_at > since` 查询 —— 全量成本一次不少,还多两轮 SELECT。
- **规模现状**:最大单 scope = project 1787414946212 ep1:637 节点+1562 边;单项目最大 1785508691757:1069 节点。全库 11485 节点分布在中位数百节点/项目,头部 ~10 个项目占大头。
- **×10 预测**:单图 6K-10K 节点 + 15K 边时,每次 load 传输 JSON 数十 MB;save-v2 每次全图 upsert O(N) 行 + diff-delete 全量比对;每次 save 后 `broadcastToProject` 触发同项目所有客户端 reload(save-v2.ts:78-83)→ 写放大 + 读放大叠加,20+ episode 并行生产时 SQLite WAL 单写者成瓶颈。
- **影响**:**KMC 多分集生产**(并行度越高越早触顶)。
- **修复建议**:since 分支短路跳过 loadFullGraph(S);大图按 phase/branch 分页(M)。

### P1-5 测试/实验数据混入生产库

- **证据**:canvas_nodes 中 project_id ∈ {999(6 节点), 1234(4), 777(2), 888(1), 9998(1), 99999(1)} 为明显测试桩;**9999 有 479 节点**引用 `/oss/volvo/03_visual_thumbs/…` 真实资产(实验批次);`data/oss/pipeline` 内 4772 条 symlink 指向 `/tmp/pytest-of-kai`(见 P1-3);`data/oss/` 顶层存在 `1`、`123` 等测试目录。
- **影响**:**KGR 批次隔离** —— 批次统计/全量导出/删除操作时测试数据与金标数据无法区分,正是「删除残缺项目差点丢全量」事故的同款温床。
- **修复建议**:测试工程迁入独立 DB 或约定 project_id 段 + 清理现有桩数据,成本 S。

### P2-1 分支删除只写事件层、不动关系层 → 真值分家

- **证据**:`src/routes/canvas/v2/branches.ts:133` DELETE 分支走 `appendAndSync`(:162-174)发 `branch_delete/node_delete/link_delete` 事件,仅落 `kv_canvasEvent`(`canvasEventStore.ts:115/191` 只 insert 无 del),**不删 canvas_nodes/canvas_links 行**;`v2/events.ts:29` 同。保护:main 分支拒删(:155-157)。
- **影响**:关系层留下孤儿节点;与 import-from-dir.ts:2259 注释自述的「写/读路径分家事故根因」同类。**KMC**:分支语义下数据残留。修复:删除走关系层或加补偿,成本 M。

### P2-2 o_project 重名 + KMC 按 name 去重 → 错绑风险

- **证据**:SQL 实测「驾车穿越人间奇景MV」×3(id 1782833859434/1782833864382/1782859172027);`canvas_sync.py:6676-6691` create_new_project 按 name 查重复用。
- **影响**:**KMC** 新 episode 可能绑到同名旧项目,画布图被合并/覆盖。修复:o_project.name 加唯一约束或 KMC 改按 id 锚定,成本 S。

### P2-3 旧 delProject 不删 canvas_* 表 → 双向孤儿

- **证据**:`src/routes/project/delProject.ts:17-59` 删 o_project/o_script/o_assets 等 + OSS 目录,**不删 canvas_nodes/canvas_links/canvas_branches**;仅被 `data/web/archive/toonflow-index-26mb.html`(旧 UI 归档)引用。实测:o_project 中 17 个项目无任何 canvas 数据(空项目),canvas 侧 10 个 project_id 无 o_project(P0-3)。
- **修复建议**:delProject 增加 canvas_* 级联清理或下线该端点,成本 S。

### P2-4 遗留 KV 面废弃未清

- **证据**:`kv_shotGraph` 0 行;`kv_nodeAsset` 仅 9 行且 `nodeId` 列全空、全部 projectId=1(2026-07 残留);`kv_canvasEvent` 1710 行(事件溯源退役后不再增长为主路径)。未发现跨 project 泄漏(量太小且无内容)。
- **修复建议**:确认无读取方后归档清理,成本 S。

### 实测阴性结果(重要)

- **跨 project 路径引用泄漏:未发现。** `SELECT project_id, COUNT(*) FROM canvas_nodes WHERE data LIKE '%/oss/178%' AND data NOT LIKE '%/oss/'||project_id||'/%' GROUP BY 1` → **0 行**。canvas_nodes 的 (project_id, episodes_id) 复合主键 + scope 索引(`idx_canvas_nodes_scope` 等,schema 实测)在数据面是有效的。

---

## 3. 历史线索核查表

| # | 历史线索 | 现状 | 证据 |
|---|---------|------|------|
| 1 | 2026-07-25 删除事故后是否有删除保护/软删除/备份 | **仍存在(防线未落地)** | 无 deleted_at 列(initDB.ts:1259-1286);无备份(§P0-2);kv_audit 不记画布删除;kv_snapshot 0 行;仅增量:saveFullGraph 改 diff-delete 单事务(canvasRelationalStore.ts:808-816)、main 分支保护(branches.ts:155-157)、前端删节点内联确认(CanvasContextMenu.tsx:328-355) |
| 2 | save-v2 不写 o_project → 新项目不可见 | **仍存在** | save-v2.ts:72 + projects.ts:31;SQL 实测 10 个 project_id/228 节点不可见(含 153 节点的真实项目 1783092441042);graph_meta 无 name 字段无法兜底 |
| 3 | 多分集改造(o_assets.episodesId → API 筛选 → 前端按 (projectId, episodesId) 分桶) | **部分修复(链路未闭环)** | 列+索引存在(schema:`episodesId INTEGER DEFAULT NULL`、`idx_o_assets_episodes`);但 assets-registry search 无 episodesId(index.ts:86-133)、useRealAssets 缓存键仅 projectId(:14-15);填充率 89.8%、脏值 2732 行 |
| 4 | 同一 episode 资产存储拓扑分裂 | **仍存在** | 5 种 URL 形态并存(§P1-3 证据);pipeline 产出的 md5 hash 方案 vs import-from-dir 的 slug 方案 vs o_image.filePath 直通(sync-assets.ts:189) |
| 5 | stale .pipeline-assets symlink 指向错误 episode | **部分修复/形态变化** | 抽样 3 个 episode(ep-zhongkui-v4-forge、ep-shencongshenyuan-kmc01、ep-ccport-test01)的 `.pipeline-assets` 已是**真实目录非 symlink**(ls -lad 实测);但死链问题转移到 `data/oss/pipeline/`(5731/7904 broken)与顶层 `shot-timeline-ep01` broken symlink |

---

## 4. 删除面清单(所有删除路径 × 防护状态)

| # | 删除路径 | 触发方式 | 删除范围 | 防护状态 |
|---|---------|---------|---------|---------|
| D1 | `DELETE /v2/canvas/nodes/:nodeId`(nodes.ts:223 → canvasRelationalStore.ts:149-160) | API;前端右键菜单(CanvasContextMenu.tsx:35,**有内联确认**) | 单节点 + 级联 links | 404 检查;无软删/备份/审计 |
| D2 | `DELETE /v2/canvas/links/:linkId`(links.ts:74 → :228-236) | API | 单边 | 无任何防护 |
| D3 | `DELETE /v2/canvas/branches/:branchId`(branches.ts:133) | API | 仅事件层,关系层不删(→孤儿,P2-1) | main 分支拒删(:155-157);无审计 |
| D4 | `POST /v2/canvas/save-v2` → saveFullGraph diff-delete(canvasRelationalStore.ts:880-886/925-931/967-973/1006-1012) | 任何客户端全图保存(前端 store.deleteNode 也走此,canvasStore.ts:934-985;KMC pipeline 常调) | **该 scope 全部「新图没有」的节点/边/分支/变体组** | 单事务;无阈值护栏、无备份、无审计 —— **最大敞口** |
| D5 | `POST /v2/canvas/import-from-dir` replace 模式(import-from-dir.ts:2131、:2268-2272) | API | 显式 DELETE FROM 清空目标 scope 后重建 | 无备份;materialization 失败仅 warn(:2281-2283) |
| D6 | `POST /api/project/delProject`(delProject.ts:17-59) | 旧 UI 归档页面(新前端不调) | o_project/o_script/o_assets 等 + OSS 目录;**不删 canvas_*** | 无确认机制服务端强制;产生孤儿(P2-3) |
| D7 | 离线脚本 `scripts/canvas/import_notion_assets.py:514` | 手动执行 | `DELETE FROM canvas_nodes WHERE project_id=? AND id LIKE 'notion-%'` | 无 |
| — | 前端 Delete/Backspace 键 | 键盘 | 仅视图层派生缓存,不落库(canvasStore.ts:87-88,:556-558) | 刷新即回,非持久删除 |
| — | 前端画布 API 层 | — | `canvasApi.ts:95-96` 硬编码 POST,全前端无 DELETE 请求 | — |

**统一缺口**:全部持久删除路径无软删除、无删除前快照(kv_snapshot 0 行)、无 kv_audit 留痕、无速率/规模护栏。

---

## 5. 改进建议(C6,标注服务对象与成本)

| # | 建议 | 服务对象 | 成本 |
|---|------|---------|------|
| R1 | cron 每日 `sqlite3 "file:db2.sqlite?mode=ro" ".backup ..."`,保留 7 代 + 每周异地拷贝;恢复演练一次 | 数据安全 | S |
| R2 | saveFullGraph diff-delete 护栏:单次删除行数超阈值(如 >50 或 >20%)拒绝/降级告警;删除操作写 kv_audit | 数据安全 | M |
| R3 | canvas_nodes/canvas_links 加 `deleted_at` 软删(或删除前把行快照写入 kv_snapshot),定期物理清理 | 数据安全 | M |
| R4 | save-v2 对缺失 project_id 自动 INSERT 占位 o_project 记录(name=`project-<id>`),并补建现有 10 个孤儿项目记录 | KMC 多分集生产 | S |
| R5 | assets-registry search 增加 episodesId 过滤参数;useRealAssets 缓存键改为 `{projectId, episodesId}` 双键 | KMC 多分集生产 / KGR 批次隔离 | M |
| R6 | 清洗 o_assets.episodesId 脏数据(3901 复合键、=projectId、NULL backfill),写入侧加范围校验 | KGR 批次隔离 | S |
| R7 | 清理 `data/oss/pipeline` 死链(5731 条)与 pytest 残留(4772 条);资产 URL 统一单写入口,注册时存 realpath | 数据安全 / KGR 金标复刻 | M |
| R8 | 测试工程隔离:测试/实验 project 迁入独立 DB(或保留字 id 段),清理 999/1234/777/888 等桩数据 | KGR 批次隔离 | S |
| R9 | load-v2 `since` 分支短路跳过 loadFullGraph;大图按 phase_index/branch 分页加载 | KMC 多分集生产(规模) | S/M |
| R10 | o_project.name 加唯一约束(或 KMC create_new_project 改按 id 锚定),消除重名错绑 | KMC 多分集生产 | S |
| R11 | delProject 增加 canvas_* 级联清理(先走 R3 软删),或下线该端点 | 数据安全 | S |
| R12 | branches.ts 删除分支改为关系层删除(或补事件→关系补偿),消除真值分家 | KMC 多分集生产 | M |

---

## 6. 验证方式

本报告全部结论可由以下只读操作复现(审计全程执行,零写入):

1. **仓基线**:`cd /data/workspace/kais-aigc-platform && git branch --show-current && git rev-parse --short HEAD`
2. **规模与分布**:`sqlite3 "file:data/db2.sqlite?mode=ro"` + §2 各条中给出的 SQL(GROUP BY project_id / episodes_id;NOT IN o_project;episodesId NULL/异常值统计;`/oss/` 路径前缀抽样;跨 project 引用阴性检查)。所有 SQL 均为 SELECT/PRAGMA 查询。
3. **删除面**:`grep -rn "\.delete(\|\.del()\|DELETE FROM" src/routes/canvas/ src/lib/canvasRelationalStore.ts`;逐文件 ReadFile 核对行号(nodes.ts:223、links.ts:74、branches.ts:133、import-from-dir.ts:2268-2272、canvasRelationalStore.ts:77-87/880-886/925-931)。
4. **注册一致性**:ReadFile save-v2.ts / projects.ts / canvasRelationalStore.ts;SQL `canvas_nodes WHERE project_id NOT IN (SELECT id FROM o_project)`。
5. **备份面**:`crontab -l`、`ls /etc/cron.d/`、`systemctl list-timers --all`、`ls -lh data/db2.sqlite*`、`find -name "db2*.bak"`、`ls scripts/ | grep -i backup`。
6. **symlink 实测**:`find data/oss/pipeline -maxdepth 2 -type l` + `-exec test -e` 统计(7904/5731);`ls -lad episodes/*/.pipeline-assets`;`readlink` 抽样。
7. **未发送任何 POST/PUT/DELETE 请求,未构造任何删除测试请求;sqlite 全程 `mode=ro`;唯一写入 = 本报告文件。**

---

## 7. 摘要(≤300 字)

画布数据面骨架健康:(project_id, episodes_id) 复合主键 + scope 索引有效,SQL 实测零跨 project 路径泄漏;saveFullGraph 已是单事务增量 upsert。但三条 P0 敞口突出:其一,删除面零防护——diff-delete 与 import-from-dir replace 均可物理清行,无软删、无备份、无审计,2026-07-25 事故防线未落地;其二,368MB 生产库零备份机制,kv_snapshot 建成未用(0 行);其三,save-v2 不写 o_project 的问题仍存在,实测 5 个真实项目(最多 153 节点)前端不可见。P1 层面:多分集资产链路未闭环(useRealAssets 仅按 projectId 分桶、search API 无 episodesId)、episodesId 脏数据 3390 行(52%)、`data/oss/pipeline` 72.5% symlink 死链(含 4772 条 pytest 残留)、load-v2 无分页且增量路径仍全量加载、测试桩数据混入生产库。修复优先级:R1 备份(S)与 R4 项目注册兜底(S)可立即落地,R2/R3 删除防线(M)是 KMC 多分集并行扩产的前置条件。
