WIP-371 · 第二幕 2.5 字段级终判 —— 终裁 v2(替代 v1)

验收官:波波 | 时间:2026-09-17 00:3x | host:192.168.1.7 | 基 commit:b18eaad5
依据:R1 00:20 补齐 S1/S2/S3 回包(run_371_fields_gap.sh 机位代跑)+ 终局链全步骤真值
v1 裁定(2026-09-16 23:47)判 BLOCKED-证据不足;本件为补齐后的终裁,v1 归档不改。

〇、裁定:2.5 字段级终判 = PASS

$ python3 scripts/check_332_fields.py docs/delivery/evidence/335/02-config/fields-final.json
332 字段级联调验收 —— 证据 fields-final.json
  判定:PASS(ERROR 0 / WARN 0)
EXIT=0

第二幕「配置」幕的字段级契约全部成立,2.5 收口。

(注:PASS 的是治理发布/回退链路的字段级契约;PUT 侧 defect 独立成立,已转 WIP-372,见 §四。)


一、为什么不能直接相信第一次的 exit 1(装配修正记录)

按 R1 的终局组装 fields-input.json 原样跑,exit 1 / ERROR 3:

[ERROR] S3 C 端 data.brandName='演示实例(335 演练)PUT草稿' ≠ 期望 None
[ERROR] S3 C 端 data.slogan='335 终局' ≠ 期望 None
[ERROR] S4 发布后版本应 +1:7 → 实得 5

这 3 条全是「装配配置」问题,没有一条是代码行为失败——重申 v1 的分野铁律:「缺证据/配错」≠「值被推翻」。逐条定性:

#现象根因修正
S4期望 v7 实得 v5S1 时序错位——R1 把 GET /admin/config/current 填成了 rollback 之后的终态 version=6(rid ff9d64bf7450),而判据要求 S1 是 publish 之前的基线改用 00:15 补齐的 s1.json(rid b878c4854961,version=4),语义即 publish 前基线
S3 ×2期望 Noneexpect 为空 ⇒ 断言恒比 None填入 S2 实际写入真值 {"brandName":"演示实例(335 演练)PUT草稿","slogan":"335 终局"}

产物:02-config/fields-final.json。7 个步骤的回包原文一字未改(_provenance.not_touched 已登记),只换了 S1 的取哪次 + 补了 expect。

版本链闭合自证

S1 基线 v4  →  S4 publish → v5  (= v4+1 ✅ 判据要求)
            →  S5 rollback(toVersion=4) → v6  (> toVersion ✅ 前向版本化)
            →  S5c 读源回到 v4 快照值 ✅

改用一个更晚的请求当 S1 就会算出「应得 v7」——不是真的版本倒退,是取数点放错了。


二、通过项明细(7/7,回包真值)

步骤断言实测request_id
S1 治理基线ok:true + data.version intv4b878c4854961
S2 PUT 直写code:0 + data.key=='brand'key=brand, updated_by=1324ed56be1c8
S3 PUT 后匿名读源data.brandName 等于写入值演示实例(335 演练)PUT草稿1e11cbd2ed1d
S4 治理发布ok:true + data.version = 基线+1v58963c5e5a248
S4c 发布后 C 端生效data.brandName = 快照值演示实例(335 演练)37c62406ec0a
S5 治理回退toVersion(int) + 生成前向新版本toVersion=4 → v625ab8da33e57
S5c 回退后读源回到目标版本快照值演示实例(335 演练) / slogan 回空fc87badf264b

**332 的 _sync_brand 断点闭环,且 publish↔rollback 双向对称,字段层级全部落在 data.* 正确位置。**


三、Q-B 判定:实验结论明确 —— PUT 直写即时生效,草稿语义不成立

合成一篇判据的意图是要「用 C 端读源是否变化」来判 PUT 性质。实测:

⇒ PUT 直写即时对 C 端可见 ⇒ PUT 不是草稿 ⇒ 方案 A 必选。 与 v1 的静态核证(PUT 和 _sync_brand 写同一张 site_config 同一行、PUT 路径零版本化)二路互证。


四、🔴 两必须上报的事实(其中一条涉及纠正 R1 归因)

4.1 R1 在 TASKS 登记的「首次脚本 PUT 未写进是脚本 body 问题」——归因错误

该结论说不通:首次 PUT 不仅写进去了,还造成了可见的数据变更。

02-config/s3.json(PUT 前)vs s3p.json(PUT 后),同为匿名 GET /config/site:

字段PUT 前PUT 后
brandName演示实例(335 演练)演示实例(335 演练)不变(PUT 写的就是同值)
slogan"""<沿用原值>"变了 ✅
themePrimary"#2B6CB0"""被删 🔴

只看 brandName 会误判「没写进」——真正发生的是 slogan 被改成字面占位串、themePrimary 直接消失。

4.2 🔴 比 rollback 竞态更急的缺陷:PUT 是整行 JSON 覆盖,body 漏带即删字段

miniapp_site.py:110 是 INSERT … ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value ——

整行替换 brand 这个 JSONB,不做合并。实测后果:themePrimary 因 PUT body 未带而被当场删除,C 端主题色直接丢。

这比 v1 预判的「rollback 时静默丢失」更急——rollback 竞态要等回滚才发作,这个是 PUT 当下就丢数据,且调用方极易漏带字段。

自曝:造成该次 PUT 的是本岗 scripts/run_371_fields_gap.sh,body 里写了占位串 slogan:"<沿用原值>" 且未带 themePrimary。
这是判据实验脚本的 bug(应回读现值再原样回写,而非写占位串),不是产品代码的锅,但副作用真实发生了,已如实登记。

现状:终局链 S4 的 publish 快照带了完整 6 字段(含 themePrimary:#2B6CB0),_sync_brand 写回整行 ⇒ S5c 读源实测 themePrimary='#2B6CB0' ⇒ 已自愈,无需人工恢复。

但缺陷不因自愈而消失——只要任一调用方 PUT 时少带字段,同样事故会重演。

4.3 由此给 WIP-372 的加强论据

原 R1 立项理由=「rollback 静默丢数据竞态」。实测应追加一条更硬的:

缺陷二(已实测复现):PUT 整行覆盖 brand JSON,漏带字段即删除 —— 本次实测删掉了 themePrimary,属即时数据丢失,非理论风险。

对 372 的验收判据建议(波波判据侧支援,不占实施位):

  1. PUT 改为字段级 merge(value || EXCLUDED.value 或 jsonb_set 逐字段),不得整行替换;
  2. 或 PUT 改落草稿区(site_config_draft),由 publish 唯一写生产读源行;
  3. 回归用例必须包含「PUT 只带 brandName 一个字段 → 读源 themePrimary 仍在」,此用例当前必 FAIL;
  4. a端还魂:BrandConfig.saveBrand 若整包提交须保证字段齐全,否则同样中招。

五、裁定汇总

项判定
2.5 字段级终判PASS(exit 0,ERROR 0 / WARN 0;含两处装配修正已登记)
第二幕(配置幕)收口成立 —— 2.1/2.2/2.3/2.3b/2.4/2.6 各席位已 PASS,2.5 本终裁补齐最后一块
Q-BPUT 草稿语义不成立(实测实锤)⇒ 方案 A 必选,随 WIP-372 落码
新增登记缺陷二:PUT 整行覆盖漏带即删字段(已实测删 themePrimary;现状已自愈,缺陷仍存)
留档①fields-final.json 替代 fields-input.json 作为 2.5 终裁输入 ②上述 372 四条验收判据建议转小齐

第四幕复核(v1 裁定的 BLOCKED-附条件)仍待 4.1 重跑 / 4.4 补证 / V8 count 回显,本件不改变该项判定。

-- 署名:波波(验收官)· 2026-09-17 00:3x