验收官:波波 | 时间: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 归档不改。
$ 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,见 §四。)
按 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 实得 v5 | S1 时序错位——R1 把 GET /admin/config/current 填成了 rollback 之后的终态 version=6(rid ff9d64bf7450),而判据要求 S1 是 publish 之前的基线 | 改用 00:15 补齐的 s1.json(rid b878c4854961,version=4),语义即 publish 前基线 |
| S3 ×2 | 期望 None | expect 为空 ⇒ 断言恒比 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」——不是真的版本倒退,是取数点放错了。
| 步骤 | 断言 | 实测 | request_id |
|---|---|---|---|
| S1 治理基线 | ok:true + data.version int | v4 | b878c4854961 |
| S2 PUT 直写 | code:0 + data.key=='brand' | key=brand, updated_by=1 | 324ed56be1c8 |
| S3 PUT 后匿名读源 | data.brandName 等于写入值 | 演示实例(335 演练)PUT草稿 | 1e11cbd2ed1d |
| S4 治理发布 | ok:true + data.version = 基线+1 | v5 | 8963c5e5a248 |
| S4c 发布后 C 端生效 | data.brandName = 快照值 | 演示实例(335 演练) | 37c62406ec0a |
| S5 治理回退 | toVersion(int) + 生成前向新版本 | toVersion=4 → v6 | 25ab8da33e57 |
| S5c 回退后读源 | 回到目标版本快照值 | 演示实例(335 演练) / slogan 回空 | fc87badf264b |
**332 的 _sync_brand 断点闭环,且 publish↔rollback 双向对称,字段层级全部落在 data.* 正确位置。**
合成一篇判据的意图是要「用 C 端读源是否变化」来判 PUT 性质。实测:
演示实例(335 演练)PUT草稿 —— 就是 S2 刚 PUT 进去的值。⇒ PUT 直写即时对 C 端可见 ⇒ PUT 不是草稿 ⇒ 方案 A 必选。 与 v1 的静态核证(PUT 和 _sync_brand 写同一张 site_config 同一行、PUT 路径零版本化)二路互证。
该结论说不通:首次 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 直接消失。
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 时少带字段,同样事故会重演。
原 R1 立项理由=「rollback 静默丢数据竞态」。实测应追加一条更硬的:
缺陷二(已实测复现):PUT 整行覆盖 brand JSON,漏带字段即删除 —— 本次实测删掉了 themePrimary,属即时数据丢失,非理论风险。
对 372 的验收判据建议(波波判据侧支援,不占实施位):
value || EXCLUDED.value 或 jsonb_set 逐字段),不得整行替换;site_config_draft),由 publish 唯一写生产读源行;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-B | PUT 草稿语义不成立(实测实锤)⇒ 方案 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