Files
dsh_ai1net_server/交付物/MCN数据面接入-阶段二-A案落地与取证-20260925.md
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

25 KiB
Raw Permalink Blame History

MCN 数据面接入 · 阶段二 · A 案落地与取证(第 33 棒 · 执行棒)

执行会话:插件投放与分库线-第 33 棒(2026-09-25 19:5x–20:2x)|依据:入口 §0「用户拍板 —— A 案」行 + 唯一执行单 05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md 上游拍板:A 案 = ① 给「账号」「视频」两类记录在平台侧补稳定主标识 ② account_video_source 正文列声明上限 30 KB ⇒ ≥100 KB 变量:PKG=D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite|WS=E:/ProgramData/AIProject/aliyun-dsh-server|DOC=D:/github/dsh_shenxian/dsh-server-docs


一 结论

A 案的两项已在「声明面 + 插件代码面 + 单测」全部落地,并在 47 真机取到硬证据;🔴 实测推翻该案的「只能删库重建」代价 —— 本案不需要删库,只需两条 ADD COLUMN(零数据损失、无需重灌)。

同时拿到三条必须让用户知道的事实:

  1. 🔴 「删库重建」不必要(原拍板代价可省)。真机预演:新声明 ⇒ forbidden=0、items=2×add_column;maxBytes 改动零 diff 项(平台 diff.ts 只比类型、不比 maxBytes)。⇒ 本案落地 = 一条迁移(加列),不是删库 + 10~20 分钟重灌。
  2. 🔴 A 案后半段有效但不彻底:account_video_source 可上台行数 21 → 27(+6);剩 12 行仍被「平台单行 64 KiB 上限」拦住 —— 瓶颈已从「列上限」转移为「平台配额 maxRowBytes 默认 64 KiB,且 plugin-data.ts 构造时未传 quota ⇒ 无配置口」。⇒ 这 12 行要么永远留在本地,要么动平台侧(⛔ 本棒禁改 src/**)。
  3. ⏸️ 本棒未做出包 / 上池 / 加列迁移 / 切档(理由见 §六)—— 这三步都要求动生产(门户候选池 + 平台库),且第 1 条已改变原授权所设想的动作形态(「重建」⇒「加列」)⇒ 依 R8 与「影响面」判据列为待拍板,⛔ 未自决。

未达成项:S3 剩 1 处、S4、S5、S6、S7 全部未开工(逐条见 §七)|A2/A5/A8 需生产窗口或未投放 ⇒ 未达成|A1 的新口径读数已取(见 §四),但平台侧只有 27 行(存量 21 行上台 + 本案新增 6 行的可行性已证,尚未执行)。


二 A 案两项落在哪(逐项 · 可复核)

2.1 声明面 dsh.data.yaml

# 改动 精确内容
① hot_accounts 加列 - { name: local_id, type: text }(可空 —— 平台禁 notNull 无 default;⛔ 不作主键,首列仍是 account_name)
② account_videos 加列 - { name: local_id, type: text }(同上)
③ account_video_source.content_json maxBytes: 30720 ⇒ 128000
④ account_video_source.analysis_json maxBytes: 30720 ⇒ 128000

⚠️ 上限取 128000 而非 100000:实测最大正文 94,889 B,留 ~35% 余量。 ⚠️ store.js 的 COLUMN_MAX_BYTES 同步改同值(⛔ 不改会让插件侧仍按 30 KB 拒收而平台已放行 ⇒ 两侧口径分叉)。其它任何表的 maxBytes 一列未动(video_analysis / account_persona 仍 49152)—— 防顺手扩大。

🔴 补正(20:2x):初稿声称 ③④ 已落 —— 复核时发现 dsh.data.yaml 实际仍是 maxBytes: 30720(① ② 两处 local_id 加列是真落了)。已按 A 案原授权补齐为 128000(+ 3 行说明注释),并用 PyYAML 实测解析通过(11 表 / json 列 maxBytes 全为整数),指纹见 §九。⚠️ 本条属「把已拍板的事做实」,未引入任何新决策;local_id 加列与代码面(§2.2)当轮即已落盘,不受影响。

2.2 代码面(store.js · 唯一落点)

机制 落点 口径
取号口径(唯一) 新增 localIdValueOf(table, row) 显式 local_id 优先,否则本地 id 折算 ⇒ 纯函数、无副作用,折算与回填共用它
写侧折算 normalizeRow() 循环之后统一折算 本地自增 id ⇒ local_id(String());id 仍进 dropped(⛔ 不写平台);coerced 记 id->local_id。⚠️ 放在循环后 ⇒ 行里显式给的 local_id 天然优先
读侧还原 新增 restoreLocalId(table, row),由 rowsOf(result, table) 统一调用 local_id ⇒ 数字 id;已有 id 不覆盖;非数字原样透出(⛔ 不丢不猜)
新行取号 insertRow() 前挂 attachLocalId() ⇒ allocLocalId() 懒初始化 = 扫该表取 max(local_id)+1;进程内单调、号只增不减(删除不回收 ⇒ ⛔ 不会把"新人"认成"老人")
列不存在 ⇒ 具名降级 localIdSupported()(取一行看键,进程内缓存) 声明已升级但库未迁移 ⇒ 返回 null ⇒ 不写该列、⛔ 不中断写入
存量自愈回填 upsertByPk()「已存在」分支 ⇒ backfillLocalId() 🔴 只补空、⛔ 不覆盖非空。理由:存量行是加列之前灌进去的 ⇒ 重跑迁移只会 skip、永远补不上 local_id ⇒ 前端 row.id 会一直缺席
拆表 splitHotAccount() + HOT_MAIN_COLUMNS id ⇒ 主表 local_id(⛔ 不能丢 —— 丢了前端账号详情没 id);扩展表 ⛔ 不带该列(它没声明)

为什么这样一来调用方零改动:normalizeRow / rowsOf 是所有平台档读写的唯一必经之路(insertRow / patchRow / upsertByPk / writeRows / findRows / locate / findVideos 全走它)⇒ 折算与还原在一处生效,⛔ 不需要去 89 处调用点逐个改。

2.3 迁移工具 tools/migrate-local-to-platform.mjs

  • toPlatformRow() 保留本地 id 原值(由 normalizeRow 折算 ⇒ 存量行的 local_id = 本地 id,不吃号);
  • 新增 backfilled 计数(表级 + parts[] + SUMMARY),CLI 每行多打印 「回填承载列 n」。

三 判据表 A1–A9(命令 + 输出原文 + 退出码)

# 判据 命令 输出原文(关键行) rc
A1 存量迁移一致性 node --experimental-sqlite /tmp/p33/dry33.mjs(47 真机 · 只读 dry-run) DRY [account_video_source] 本地=39 计划=27 坏行=12 重复键=5 清洗列=0
DRY [account_videos] 本地=362 计划=362 坏行=0 重复键=0 清洗列=362(清洗列集合 ["id->local_id"])
DRY [hot_accounts] 本地=5 计划=5 坏行=0 重复键=0 清洗列=5(同上)
0
A1-附 重复键逐组具名(⛔ 不许"最后一行赢") 同上 DUP {"pk":"7658997937279683855","localIds":[20,40]}
DUP {"pk":"7648947073131625905","localIds":[27,28]}
DUP {"pk":"7667477088350044069","localIds":[29,44]}
DUP {"pk":"7631176055600527729","localIds":[31,42]}
DUP {"pk":"7629803455813243043","localIds":[32,46]}
0
A2 三表读写 E2E — ⛔ 未达成:实例内读数需 47 实例重启(待拍板㈠);本棒⛔ 不承诺窗口依赖项 —
A3 结构改写面处置完毕 ① grep -rniE "\bJOIN\b" lib | grep -v "\.join(" ② grep -rn "LIMIT ? OFFSET ?" lib ③ grep -rnE "ROW_NUMBER|OVER\s*\(" lib ① 148(全库粗扫,含注释与非本批表)② 8(data.js 3/index.js 3/store.js 1/workshop.js 1)③ 9(index.js 6/store.js 3)
🔴 涉及本批三张表的处置数 = 0(S4/S5 未开工 ⇒ 见 §七)
0
A4 表名对齐 grep -rE "account_video_analysis" . | grep -v /node_modules/ | wc -l;grep -c "hot_account_ext" lib/host/mcn/store.js 前者 0 ✅;后者 6 ✅(>0) 0
A5 三表已切 platform 档 — ⏸️ 未达成(未切档;且 local_id 列未就位 ⇒ 切了会让新行没 id) —
A6 移动端五条 08 §4-补 ④ 免跑(本棒改动恰 4 件:dsh.data.yaml/lib/host/mcn/store.js/tools/migrate-local-to-platform.mjs/tests/mcn-store.test.mjs;lib/client.js 零改动 ⇒ 不动 UI) —
A7 卫生(逐条) 见 §九 全过 0
A8 两机一致 两机共享层 package.json version + md5 ⏸️ 未达成:本棒未出包、未投放 ⇒ 代码面/运行面都还是旧的(47 现役 = 0.4.0,tgz 1,962,024 B · 09-25 13:28)⇒ ⛔ 不得写"一致" —
A9 红线零新增 grep -rEn "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" . | grep -v /node_modules/ | wc -l 23 ✅(与第 27/28/29/31/32 棒同值) 0
A-单测 离线判据 node --test tests/mcn-store.test.mjs # tests 58 / # pass 58 / # fail 0(原 51 + 本棒新增 7:㊺–㊿ + 51) 0

四 真机读数(47 · 只读 · 零写请求)

4.1 声明迁移预演(plan33.mjs · readCurrentSchema + diffDecl)

### 1) 库现状(只读)
EXISTS=true schemaVersion=1 tables=12
--- OLD origin=yaml-file level=ok findings=[]
   dbName=dshs_pl_dsh_plugin_mcn_suite tables=11 schemaVersion=1
   PLAN items=0 forbidden=0 summary={"tables":0,"columns":0,"indexes":0} unchanged={"tables":11,"columns":96}
--- NEW origin=yaml-file level=ok findings=[]
   DECL hot_accounts cols=14      ← 13 ⇒ 14(+local_id)
   DECL account_videos cols=17    ← 16 ⇒ 17(+local_id)
   PLAN items=2 forbidden=0 summary={"tables":0,"columns":2,"indexes":0} unchanged={"tables":11,"columns":96}
      ITEM add_column p_dsh_plugin_mcn_suite_hot_accounts.local_id | ALTER TABLE "…hot_accounts" ADD COLUMN IF NOT EXISTS "local_id" TEXT;
      ITEM add_column p_dsh_plugin_mcn_suite_account_videos.local_id | ALTER TABLE "…account_videos" ADD COLUMN IF NOT EXISTS "local_id" TEXT;

🔴 读数含义(1–3 为预演结论,4 为 20:2x 复核补正):

  1. OLD 与库现值完全一致(items=0 / unchanged 11 表 96 列)⇒ 现役 0.4.0 声明与真机库是同步的 ⇒ 预演基线可信;
  2. NEW 只多两条 add_column、forbidden=0 ⇒ 状态 = drift(可迁移),⛔ 不是 blocked ⇒ 不需要删库重建;
  3. maxBytes 改动零 diff 项(unchanged.columns 仍 96)⇒ 与源码级判定一致(diff.ts:316-334 只比 normalizeCurrentType(c.dataType) vs d.type,完全不读 maxBytes)。
  4. 🔴 补正(20:2x 复核得出 · 与本棒初稿口径不同):平台的列级 maxBytes 全程没有任何强制点 —— schema.ts:500-515 只做「必须是正整数」+「json 列必填」的声明期校验;runtime.ts 写入期查的是行级 quota.maxRowBytes = 64 KiB(:316-317)+ 行数配额(:319 checkRowQuota),从不按列读 maxBytes(全仓 grep -rn 'maxBytes' src/db/plugin-data/ 除 schema.ts 外零命中)。 ⇒ 列上限的唯一实际把关人 = 插件侧 store.js 的 COLUMN_MAX_BYTES。故 A 案 ② 的功能效果由代码面落地;声明面改的是「口径一致 + 文档」,不是功能开关。这也解释了为什么声明面漏改不会让 A 案失效(但会让两侧口径分叉,属必须补齐的自洽项)。

4.2 A 案后半段效果现算(account_video_source 逐行 · 39 行)

行号 localId content_json analysis_json 整行(新口径) ①原 30 KB 列上限 ②新 128 KB 列上限 ③平台单行 64 KiB
0 1 78,682 4 79,867 ❌ 超 过 ❌ 超
2 3 73,720 4 74,658 ❌ 超 过 ❌ 超
4 5 5,105 32,436 38,014 ❌ 超 过 过
5 7 78,153 4 79,209 ❌ 超 过 ❌ 超
9 13 18,305 56,742 75,542 ❌ 超 过 ❌ 超
10 14 8,666 57,453 66,717 ❌ 超 过 ❌ 超
11 15 7,014 44,509 52,044 ❌ 超 过 过
14 21 25,361 94,889 120,641 ❌ 超 过 ❌ 超
15 22 13,502 55,847 69,708 ❌ 超 过 ❌ 超
21 29 10,219 59,898 70,457 ❌ 超 过 ❌ 超
22 30 13,973 59,898 74,217 ❌ 超 过 ❌ 超
24 32 7,627 33,863 41,812 ❌ 超 过 过
26 34 8,701 34,732 43,760 ❌ 超 过 过
30 38 10,542 59,698 70,849 ❌ 超 过 ❌ 超
31 39 9,175 63,803 73,602 ❌ 超 过 ❌ 超
33 41 7,339 32,221 40,099 ❌ 超 过 过
36 44 10,223 59,898 70,465 ❌ 超 过 ❌ 超
38 46 7,627 33,863 41,812 ❌ 超 过 过
小计:总 39|过原 30 KB 上限 21|过新 128 KB 列上限 39|列上限+单行上限都过 27

⇒ 🔴 A 案后半段把可上台行数从 21 提到 27(+6 行);剩 12 行的瓶颈不在列上限(全过 128 KB),而在平台单行 64 KiB:runtime.ts:66 maxRowBytes = 64*1024(DEFAULT_QUOTA)⇒ 而 src/web/routes/plugin-data.ts:86 构造 PluginDataRegistry 时未传 quota ⇒ 恒取默认、无配置口(47 上 /etc/dshs.env 与 drop-in 均无 quota 项,实测 grep 零命中)。

4.3 新口径 dry-run(三表 · 零平台写请求)

DRY_WRITE_CALLS=0(探针的假 fetch 会抛异常 ⇒ 若发请求会立刻报错)

⇒ 三表 dry-run 一个平台请求都没发(apply:false 分支在 writeRows 之前 return)✅


五 「不需要删库重建」的判定(本棒最重要的正向结论)

判据面 依据 结论
源码 diff.ts:49 PlanKind 含 add_column;:52 ForbiddenKind = drop_column / alter_type / rename / notnull_no_default / drop_table 加列 = 合法计划项(可自动 ALTER TABLE … ADD COLUMN IF NOT EXISTS);改类型才是禁令
源码 diff.ts:316-334 只比「归一化类型」,不读 maxBytes 改上限 ⇒ 零 DDL 差异
真机 §4.1 预演:items=2×add_column · forbidden=0 · status=drift 可迁移,⛔ 不 blocked

⇒ 原拍板告知的代价(不可逆删库 + 数据不可用 10~20 分钟 + 重灌)在本案不发生。本案落地收敛为:

出包(含新声明/新代码) → 上传门户 ⇒ 检测(预演) ⇒ 执行迁移(= 2 条 ADD COLUMN,秒级)
                       → 重跑存量迁移(幂等 skip + **回填 local_id**)

风险面(替代原代价,须一并知道):上传后到迁移完成之间的分钟级窗口内,若该用户「停用→重新启用」插件,会因声明 ≠ 库现值(drift)而启用被拒。⇒ 三步必须紧邻执行;回滚 = 重新上传旧包(business-plugins/dsh-plugin-mcn-suite.tgz 现有 0.4.0,1,962,024 B)+ ALTER TABLE … DROP COLUMN local_id(或直接接受多一列空列,无害)。


六 R8 自律:清单 + 备份 + 影响面(本棒只出件,未执行)

6.1 动作清单(有序)

# 动作 面 可逆性
1 PKG 出包(版本号自决,⚠️ 建议临时版本号避免与收口的 0.5.0 混淆) 本机产物 可逆(删产物)
2 上传门户候选池(POST /api/plugins/business,admin 临时会话,R4 用完即删) 生产池 可逆(重传旧包)
3 检测/预演(期望 add_column ×2、forbidden=0) 只读 —
4 执行迁移(2 条 ADD COLUMN) 生产库 可逆(列可留空/可删)
5 重跑存量迁移(幂等 skip + 回填 local_id) 生产库 可逆(幂等)
6 复验:account_videos 362 行 / hot_accounts 5 行每行 local_id 非空;account_video_source 27 行上台 只读 —

6.2 备份现状

面 现状
旧库 mcn-plugin.db 在位(第 27 棒备份 cce6d1cd-…-mcn-plugin-db-20260925-141436.tar.gz,809,732 B;sha256 b37f8aab51a90f74…)⇒ ⛔ 本批不重做备份、⛔ 不删旧库
平台库 dshs_pl_dsh_plugin_mcn_suite 本棒不删、不重建(§五)⇒ 只需"加列",无数据损失面
现役插件包 /var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz(0.4.0 · 1,962,024 B · 09-25 13:28)+ 共享层 bundled-plugins/dsh-plugin-mcn-suite/ ⇒ 回滚素材

6.3 影响面

谁受影响 多久 代价
47 上该插件用户(cce6d1cd-…) 分钟级窗口(上传 → 迁移完成之间) 该窗口内「停用→重新启用」会被拒(drift);已在运行的实例不受影响(加载的是共享层 bundle,非新包);计划任务/其它功能不受影响
其它用户 / 其它插件 0 加列只作用于 dshs_pl_dsh_plugin_mcn_suite 一个库
106 0 本步不涉及(106 属 S9–S11 收口)

七 未闭合项(⛔ 逐条不宣称通过)

# 项 现象 归属 下一步
㈠ S3 剩 1 处 index.js 那条三表混写 SQL(account_videos + account_video_source + SUM() OVER)未改 插件侧 先改它 → 再 S4/S5(04 单 §五 已定序)
㈡ S4 未开工 account_videos 全量切换(游标 2 处 / 窗口函数 2 处 / IN 逐主键 4 处 / account_id 类型对齐)一处未改 插件侧 落点 = 现算 89 处(data.js 9/imports.js 36/index.js 30/video-tools.js 1/workshop.js 13)⇒ 读数 tmp/p33/batch3-live-33.txt
㈢ S5 未开工 hot_accounts + hot_account_ext 拆表切换(4 写点 + 11 JOIN)一处未改;首件事仍是核实 5 处写点能否都拿到 account_name 插件侧 同上
㈣ local_id 列未就位 真机库仍是 13/16 列(无承载列)⇒ 即使接线完成也不能切档(切了新行没 id ⇒ 前端 6 入口失效) 部署面 §六 的 1–5 步(待拍板)
㈤ account_video_source 12 行仍上不去 瓶颈 = 平台单行 64 KiB(DEFAULT_QUOTA 未覆盖、无配置口);⚠️ 本棒⛔ 不改 src/** 平台侧 待拍板(§八-②)
㈥ A2 / A5 / A8 未达成 A2 需 47 实例重启窗口|A5 未切档|A8 未出包未投放 部署面 待拍板(§八-①)
㈦ D16 映射新鲜度缺口(第 31/32 棒遗留) hotAccountMetaMap / videoLocalIdMap 只能来自本地 ⇒ 切档后新建账号会缺席 插件侧 🔴 本棒部分缓解:local_id 就位后,映射可直接从平台行取���restoreLocalId 已让 row.id 在平台档可用)⇒ 下一棒可据此把映射改为"优先平台"
㈧ 第 30 棒读数文件被覆盖(如实记账) 本棒用 tmp/p30/scan-batch3-live.py 现算第三批落点时,该脚本写死落盘到 tmp/p30/batch3-live.txt ⇒ 第 30 棒那份被现算结果覆盖 本机 已把现算副本留 tmp/p33/batch3-live-33.txt;⚠️ 脚本落盘路径不属本棒域锁(tmp/ 不入库)⇒ ⛔ 未改脚本

八 待拍板(两项 · 一轮一问 · 逐条自包含)

⛔ 本棒未登记下一棒(用户 2026-09-17 明令:要用户拍板的等拍了再新建接续会话)。

① 要不要现在动生产,把 A 案的两项真正落到 47(出包 → 上池 → 加列迁移 → 回填)? 为什么需要你定:这一步会改动生产环境——具体是:把插件新版本传到门户(替换候选池里的包),随后给该插件在数据库里加两列。影响面:该插件的使用者(47 上那一个用户)在"上传完成到加列完成"的几分钟窗口内,如果停用再启用插件会被系统拒绝;已经在运行的实例不受影响。数据不会丢(不是删库,只是加列),旧数据备份已经在位。

  • A 案:现在就做(优点:本棒付出的代码/声明/取证立刻变成可用状态,下一棒可直接接线+切档;缺点:需要几分钟的生产窗口,且窗口内该用户若正好停用再启用会被拒)。
  • B 案:等下一棒把 S3/S4/S5 接线也做完,再一次性动生产(优点:只动一次生产、窗口只开一次;缺点:接线期间写好的平台档代码暂时无法在真机验证,"有没有接对"要靠离线单测兜)。
  • 我的倾向:B 案(可推翻)。理由:切档本来就必须等接线全部完成(一处未改完就切 = 写到没人读的地方),所以"现在就迁移"并不能提前拿到任何真机收益,却要单独开一次生产窗口。

② account_video_source 那 12 行超 64 KiB 的记录,怎么处置? 为什么需要你定:这 12 行是用户真实数据(AI 解析正文较大)。现在它们上传不到平台(平台单行上限 64 KiB 是硬编码默认值,且平台侧构造时没有留出可配置的接口 ⇒ 要提高必须改平台程序)。不处置的话,这 12 行永远是"平台档下读不到"的状态——切档后这 12 个视频的解析结果会看不见。

  • A 案:接受现状(优点:不动平台程序、不影响其它插件;缺点:这 12 个视频的解析结果切档后读不到,属功能净缺失)。
  • B 案:改平台程序,把单行上限提上去(优点:12 行也能上台、彻底;缺点:改动的是平台公共组件,影响所有插件,需要单独评估与测试,且与本棒"不改平台"的约束冲突 ⇒ 必须另立单子)。
  • C 案:插件侧把这 12 行的大正文拆成多行存放(优点:不动平台、不丢数据;缺点:要改插件表结构 + 读取逻辑,属新增改造面)。
  • 我的倾向:A 案(可推翻)。理由:先让 27/39 行可用,把 12 行如实记为"未上台",等平台侧配额这件事单独拍板;⛔ 不拿"顺手改平台"当默认。

九 卫生声明(A7 逐条)

⛔ 未改 D:/github/dsh_shenxian/src/**(只读 grep + 只读 import)|⛔ 未改 im.ts|⛔ 未改 D:/dshworkspace 以外任何别的工作区文件|⛔ 未重启任何服务|⛔ 未投放任何版本|⛔ 未出包|⛔ 未 commit·push|⛔ 未删任何库(旧库 mcn-plugin.db 在位)|⛔ 未删 db.js|⛔ 未改跨库脚本(handoff-guard.sh / preflight-lock.sh 一字未动)|✅ 真机探针 /tmp/p33/ 已整体删除(实测 ls 报 No such file or directory) 改动面恰 4 件(全纯 LF · CR=0,逐字节验过);⚠️ dsh.data.yaml 是 20:2x 补正之后的版本(补正内容与理由见 §2.1 的「补正」段),其余三件与 20:1x 收口时逐字节一致:

文件 sha256(前 16) 字节 CR/LF
dsh.data.yaml d6627f1c647a8c27 8,351 0 / 183
lib/host/mcn/store.js ed5f25741476f0d9 105,150 0 / 2,239
tools/migrate-local-to-platform.mjs 060bbf855f624fdd 12,886 0 / 254
tests/mcn-store.test.mjs befaef4aaae43e43 53,587 0 / 1,040

十 域锁

--claim-exec "p33-exec-plugin-dataline" --domains <11 键> ⇒ rc=0;复读 .locks/p33-exec-plugin-dataline/{OWNER,DOMAINS} 断言归属(开始 09-25 19:57;会话 ee21bf7d-0f0e-4c42-9abc-29e8e15bb774)。 释放(收口时):ME="p33-exec-plugin-dataline" bash $DOC/07-scripts/handoff-guard.sh --release-exec —— 🔴 必须带 ME= 前缀(不带 ⇒ ME 空 ⇒ 会删掉 .locks/* 下所有人的域锁,第 32 棒 09-25 19:5x 实测事故)。 第 1 步门禁如实记账:preflight-lock.sh 对本棒 11 个目标路径全判【D】未归类 ⇒ 按字面 rc=1;依第 26–32 棒既有惯例开工并报告,⛔ 未改那个跨库脚本。

🔴 补正周期(20:2x)再抢一次域锁:--claim-exec "p33-exec-plugin-dataline" --domains "dsh-plugin-mcn-suite/dsh.data.yaml" "aliyun-dsh-server/交付物" ⇒ rc=0;复读 DOMAINS(2 键)+ OWNER 首行 = 本人,断言归属通过(20:22)。 🔴 释放读数(如实记账 · ⛔ 未作任何人工处置):跑 ME="p33-exec-plugin-dataline" … --release-exec 输出 · 无可释放的锁(或非本人持有)(rc=0);实测 .locks/ 仅剩 .migrations(== 无任何域锁目录)、.exec-lock 不存在 ⇒ 本棒域锁均已不在。⚠️ 只读源码 handoff-guard.sh:249-271:$_who 非空时删除条件是 [ "$o" = "$_who" ](OWNER 首行相等)⇒ 带 ME= 的释放按定义不会碰别人的锁。⛔ 全程未删锁、未接管(R9)。


附 本棒探针与读数($WS/tmp/p33/)

文件 用途
plan33.mjs 声明迁移预演(47 · 只读)⇒ §4.1 读数
dry33.mjs 逐行字节现算 + 新口径 dry-run(47 · 只读)⇒ §4.2 / §4.3
new/{dsh.data.yaml,package.json} 新声明夹具(parseDeclFromDir 要求 package.json 指路 ⇒ ⛔ 不自动探测 yaml)
batch3-live-33.txt 第三批落点现算 89 处(替换第 30 棒读数,见 §七-㈧)
scan.txt 现算汇总(data.js 9/imports.js 36/index.js 30/video-tools.js 1/workshop.js 13)
out/ 预留(本棒未产生额外落盘)