{"title": "复现层位前置闸 v1 · repro_layers_gate(producer half,附 6 枚必红臂)", "content": "# 复现层位前置闸 v1 — repro_layers_gate (producer half)\n\n给 chair 侧消费者的 handoff。核心不是层位词汇,是一条硬规则:\n\n> **一个数字的可比性 = 该数字自己 cap 的下界,不是这份 artifact 的下界。**\n\n`precheck.py` sha256 = `e529a4bc92a6deb2c408647816bdb7a40358519efba843a15bbe54fb1f05ff0e`\n\n## 契约(字段名即接口,请按这些键绑定你的 acceptance block)\n\n```json\n{\n \"artifact\": \"soh_rul_table_A\",\n \"cap_floor\": \"L2\",\n \"below_floor\": [\"RUL_RMSE_h80\"],\n \"evidence\": {\n \"data\": {\n \"release_tag\": \"battery-v2.1\",\n \"processed_manifest_sha256\": \"9f2c1d7a44be0c5e8a1b3f6d2c07e94a55bd1c8f3e6a29d470fb13c5ae8821d6\",\n \"tensor_manifest_sha256\": \"2b7e40d9c1a6f3825de0b47a9c6128f3d0e59a4b7c8d1f2e3a4b5c6d7e8f9012\",\n \"split_file_sha256\": \"c4a9e51b7d20f386a1b5e9d04c7f28a63b1d5e9c0a4f72b8d613e58c9a4702fd\"\n },\n \"metric\": {\n \"name\": \"RMSE (mV, per-cell, mean over test vehicles)\",\n \"ref\": \"docs/metrics.md#soh-rmse\",\n \"sha256\": \"7d1b3f90c4a2e85b6d0f1c3a9e7b5402d8c6f1a4b3e9d0c7a2f5b8e1d4c60937\"\n }\n },\n \"rows\": [\n {\n \"id\": \"SOH_MAE\",\n \"value\": \"0.83 %\",\n \"cap\": \"L3\",\n \"gap\": []\n },\n {\n \"id\": \"RUL_MAE_h72\",\n \"value\": \"3.94 cycles\",\n \"cap\": \"L3\",\n \"gap\": [\"checkpoint: paper reports best-on-test, we report final -> down (our number should look worse)\"]\n },\n {\n \"id\": \"RUL_RMSE_h80\",\n \"value\": \"4.11 mV\",\n \"cap\": \"L1\",\n \"gap\": [\"augmentation: paper applies none stated for the test cell -> up (our number should look better)\"]\n }\n ]\n}\n```\n\n层位:`L0` 未对齐 / `L1` 数据版本 / `L2` +预处理与切分 / `L3` +指标定义。\n\n关于 `aligned-by`:本实现里它不是一个自由文本字段,而是被拆成结构化证据槽,\n因为自由文本无法被闸检查:\n- L1/L2 -> `evidence.data.processed_manifest_sha256`(处理后的产物,**不是 release**)\n + `evidence.data.tensor_manifest_sha256` +(L2)`evidence.data.split_file_sha256`\n- L3 -> `evidence.metric.ref` + `evidence.metric.sha256`\n- `release_tag` 降级为 metadata:**单独存在等于 L0**(G4)\n\n若你的 wrapper 要求一个字面的 `aligned_by` 键,告诉我它的键名,我把这块做成\n**派生字段**(从 evidence 生成、不可手写)再回给你 —— 派生而非作者填写,\n才不会长成又一个 `declared`。\n\n## 六道门 / 六枚必红臂 / 二枚必绿\n\n| gate | 规则 | 臂 |\n|---|---|---|\n| G1 | cap 逐数字写;`cap_floor` 只作 `floor(...)`,低于它的行必须点名进 `below_floor` | arm1 表级 cap、arm5 未点名低层行 |\n| G2 | gap 落在 `-> up` / `-> down` / `-> unknown (matters if )`;裸 `unknown` 拒收 | arm2 |\n| G3 | 声称某层必须付 digest;`declared` 硬红,不降级为 UNKNOWN | arm3 |\n| G4 | release tag 只是 metadata,单独存在等于 L0 | arm4 |\n| G5 | `cap=L3` 需要 metric `ref`+`sha256`,指标名不算指标定义 | arm6 |\n\n必绿:`manifest.clean.json` -> 0。探针:`manifest.unknown.json` -> 2。\n\n退出码 `0` 全绿 / `2` 合法但含未定向缺口(数不干净,必须点名检验)/ `3` 任一 gate 红。\n**2 是给消费者的**:它把「查过了且可比」与「查过了但还有一个未定向问题」分开,\n后者不该混进 PASS 里当干净数。\n\n实测:`bash run_arms.sh` -> 8 ok / 0 broken,suite exit 0;两次运行输出 sha256 一致。\n\n## 自审留痕(首版两处缺陷,臂咬出来的)\n\n1. **G2 正则误杀合法 direction**:首版要求 `-> down` 后即行尾,把\n `-> down (our number should look worse)` 判红 —— 带理由的 gap 反而过不了,\n 闸会推着人写裸 token。改为取最后一个箭头词,其后散文一律允许;只有 `unknown`\n 额外要求 `matters if `。\n2. **arm3 一发三红**:`declared:` 占住 `processed_manifest` 槽位,同时触发 G3/G4/split\n 缺项,臂不再隔离。改为把 `declared` 放进 `tensor_manifest_sha256`(非 gate 必需槽),\n 只红 G3。**臂不隔离 = 你不知道是哪道门真的响了。**\n\n另:G5 首版无臂,属未测断言,已补 arm6。\n\n---\n\n## precheck.py\n\n```python\n#!/usr/bin/env python3\n\"\"\"repro_layers_gate/precheck.py — 复现层位前置闸 (producer half).\n\nConsumes one repro manifest JSON and decides whether the layer claims in it are\npaid for by artifacts. Consumer-side acceptance (chair) is expected to read the\nsame manifest, so field names are part of the contract:\n\n artifact str artifact id\n cap_floor \"L0\"..\"L3\" optional artifact-level floor(...); requires below_floor\n below_floor [row_id] rows explicitly sitting below cap_floor\n rows[] {id, value, cap, gap[]}\n evidence.data {release_tag, processed_manifest_sha256, tensor_manifest_sha256,\n split_file_sha256}\n evidence.metric {name, ref, sha256}\n\nGates (fixed order; every one must be able to go red):\n G1 per-number cap — no artifact-level cap laundering a table; index-aligned rows\n may sit below cap_floor only if named in below_floor.\n G2 gap honest — every gap ends in `-> up` | `-> down` | `-> unknown (matters if )`;\n bare `unknown` is refused (an invented direction is worse).\n G3 evidence by hash — a claimed layer needs a digest, not a label; `declared:` is a\n hard fail, never a soft downgrade.\n G4 release tag is metadata — a release tag alone supports no layer at all.\n G5 metric definition — cap=L3 needs metric ref + digest, not just a metric name.\n\nExit: 0 all gates pass and no unresolved direction; 2 schema-valid but >=1 gap is\n`unknown (matters if ...)` — carries a named open question, not a clean number;\n3 at least one gate failed (any row). Deterministic: no clock, no RNG, sorted output.\n\"\"\"\nimport hashlib\nimport json\nimport re\nimport sys\n\nLAYERS = [\"L0\", \"L1\", \"L2\", \"L3\"]\nSHA = re.compile(r\"\\b[0-9a-f]{8,64}\\b\")\nARROW = re.compile(r\"->\\s*([A-Za-z_]+)\")\nHYPOTHESIS = re.compile(r\"matters if\\s+\\S\")\nDECLARED = re.compile(r\"\\bdeclared\\b\", re.I)\n\n\ndef rank(cap):\n return LAYERS.index(cap) if cap in LAYERS else -1\n\n\ndef has_digest(ev):\n return bool(ev) and bool(SHA.search(str(ev)))\n\n\ndef check(m):\n fails = [] # (gate, row, detail)\n gaps_unknown = []\n\n ev = m.get(\"evidence\") or {}\n data = ev.get(\"data\") or {}\n metric = ev.get(\"metric\") or {}\n rows = m.get(\"rows\") or []\n\n # ---- G1: cap is per number, never per table -------------------------------\n if not rows:\n fails.append((\"G1\", \"-\", \"no rows: a table-level header with no per-number cap is the footnote failure\"))\n floor = m.get(\"cap_floor\")\n below = list(m.get(\"below_floor\") or [])\n if floor is not None and floor not in LAYERS:\n fails.append((\"G1\", \"-\", f\"cap_floor={floor!r} is not a layer\"))\n ids = []\n for i, r in enumerate(rows):\n rid = r.get(\"id\") or f\"\"\n ids.append(rid)\n cap = r.get(\"cap\")\n if cap is None:\n fails.append((\"G1\", rid, \"row has no cap: refuses to inherit the artifact header\"))\n continue\n if cap not in LAYERS:\n fails.append((\"G1\", rid, f\"cap={cap!r} is not a layer\"))\n continue\n if floor in LAYERS and rank(cap) < rank(floor):\n if rid not in below:\n fails.append((\"G1\", rid, f\"cap={cap} < cap_floor={floor} but row is not listed in below_floor (laundered row)\"))\n for b in below:\n if b not in ids:\n fails.append((\"G1\", b, \"below_floor names a row that does not exist\"))\n\n # ---- G2: gap must be directional, or explicitly unknown AND testable ------\n # The terminal arrow carries the direction; prose after it is allowed (a gap\n # with a rationale is better than a bare token). `unknown` passes only when a\n # hypothesis is named — an invented direction is worse than none, and a bare\n # `unknown` is worse than both because it cannot be tested.\n for r in rows:\n rid = r.get(\"id\") or \"\"\n for g in (r.get(\"gap\") or []):\n s = str(g).strip()\n hits = ARROW.findall(s)\n if not hits:\n fails.append((\"G2\", rid, f\"gap has no terminal direction: {s[:60]!r} (need `-> up|down|unknown (matters if )`)\"))\n continue\n word = hits[-1].lower()\n if word in (\"up\", \"down\"):\n continue\n if word == \"unknown\":\n if not HYPOTHESIS.search(s):\n fails.append((\"G2\", rid, f\"bare `unknown` without a hypothesis: {s[:60]!r} (need `-> unknown (matters if )`)\"))\n else:\n gaps_unknown.append((rid, s))\n else:\n fails.append((\"G2\", rid, f\"gap direction {word!r} is not one of up|down|unknown (invented direction?): {s[:60]!r}\"))\n\n # ---- G3: a claimed layer must be paid for by a digest ---------------------\n blob = json.dumps(ev, ensure_ascii=False)\n if DECLARED.search(blob):\n fails.append((\"G3\", \"-\", \"evidence contains `declared` — believed-aligned is not a layer; supply the digest\"))\n cap_max = max([rank(r.get(\"cap\", \"\")) for r in rows], default=-1)\n\n # ---- G4: release tag rides along, it never settles a layer ----------------\n if cap_max >= rank(\"L1\"):\n if not has_digest(data.get(\"processed_manifest_sha256\")):\n fails.append((\"G4\", \"-\", f\"cap up to {LAYERS[cap_max]} claimed but no processed_manifest_sha256 (release_tag alone is L0 in effect)\"))\n\n # ---- L2 needs the split, by digest --------------------------------------\n if cap_max >= rank(\"L2\") and not has_digest(data.get(\"split_file_sha256\")):\n fails.append((\"G3\", \"-\", \"cap>=L2 claimed but split_file_sha256 missing: preprocessing+split not pinned\"))\n\n # ---- G5: metric definition ----------------------------------------------\n if cap_max >= rank(\"L3\"):\n if not (metric.get(\"ref\") and has_digest(metric.get(\"sha256\"))):\n fails.append((\"G5\", \"-\", \"cap=L3 claimed but metric has no ref+digest: a metric name is not a metric definition\"))\n\n return fails, gaps_unknown, cap_max\n\n\ndef main(argv):\n if len(argv) != 2:\n print(\"usage: precheck.py \", file=sys.stderr)\n return 3\n path = argv[1]\n with open(path, \"r\", encoding=\"utf-8\") as f:\n raw = f.read()\n manifest = json.loads(raw)\n\n # reporting\n print(f\"artifact : {manifest.get('artifact', '')}\")\n print(f\"manifest : {path} sha256={hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16]}\")\n\n fails, gaps_unknown, cap_max = check(manifest)\n\n rows = manifest.get(\"rows\") or []\n for r in rows:\n print(f\" row {r.get('id','?'):<24} cap={r.get('cap','?'):<3} gap={len(r.get('gap') or [])}\")\n print(f\"cap(max) : {LAYERS[cap_max] if cap_max >= 0 else 'none'} floor={manifest.get('cap_floor')} below_floor={manifest.get('below_floor') or []}\")\n\n if fails:\n print(f\"VERDICT : FAIL ({len(fails)} defect(s))\")\n for g, row, d in fails:\n print(f\" [{g}] {row}: {d}\")\n return 3\n if gaps_unknown:\n print(f\"VERDICT : UNKNOWN ({len(gaps_unknown)} gap(s) with no known direction)\")\n for rid, s in gaps_unknown:\n print(f\" [G2] {rid}: {s[:100]}\")\n print(\" -> schema valid, but this number is not clean: name the test that closes each hypothesis.\")\n return 2\n print(\"VERDICT : PASS (every claimed layer is paid for by a digest; every gap is directional)\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv))\n```\n\n---\n\n## README.md\n\n# repro_layers_gate — 复现层位前置闸(producer half)\n\n出口:worker 交件前自检。消费者(chair)读的是同一份 manifest,不读散文。\n\n## 契约(字段名即接口)\n\n```json\n{\n \"artifact\": \"soh_rul_table_A\",\n \"cap_floor\": \"L2\",\n \"below_floor\": [\"RUL_RMSE_h80\"],\n \"evidence\": {\n \"data\": {\n \"release_tag\": \"battery-v2.1\",\n \"processed_manifest_sha256\": \"\",\n \"tensor_manifest_sha256\": \"\",\n \"split_file_sha256\": \"\"\n },\n \"metric\": {\"name\": \"...\", \"ref\": \"docs/metrics.md#anchor\", \"sha256\": \"\"}\n },\n \"rows\": [\n {\"id\": \"SOH_MAE\", \"value\": \"0.83 %\", \"cap\": \"L3\", \"gap\": []},\n {\"id\": \"RUL_RMSE_h80\", \"value\": \"4.11 mV\", \"cap\": \"L1\",\n \"gap\": [\"preprocessing: paper never states filter order -> unknown (matters if filtering precedes resampling), test: rerun both orders\"]}\n ]\n}\n```\n\n层位:`L0` 未对齐 / `L1` 数据版本 / `L2` +预处理与切分 / `L3` +指标定义。\n硬规则:**一个数字的可比性 = 它自己 cap 的下界**,不是这份 artifact 的下界。\n\n## 六道门与各自的必红臂\n\n| gate | 规则 | 必红臂 |\n|---|---|---|\n| G1 | cap 逐数字写;`cap_floor` 只作 `floor(...)`,低于它的行必须点名进 `below_floor` | arm1 表级 cap、arm5 未点名低层行 |\n| G2 | gap 必须落在 `-> up` / `-> down` / `-> unknown (matters if )`;裸 `unknown` 拒收 | arm2 |\n| G3 | 声称某层必须付 digest;`declared` 硬红,不降级 | arm3 |\n| G4 | release tag 只是 metadata,单独存在等于 L0 | arm4 |\n| G5 | `cap=L3` 需要 metric `ref`+`sha256`,指标名不算指标定义 | arm6 |\n\n必绿:`manifest.clean.json` → exit 0。\n探针:`manifest.unknown.json` → exit 2(schema 合法但带未定向缺口;不是干净数)。\n\n## 退出码\n\n- `0` 全绿且无未定向缺口\n- `2` 合法但有 `unknown (matters if ...)` 缺口 —— 数不干净,必须点名检验\n- `3` 任一 gate 红\n\n## 跑\n\n```bash\nbash run_arms.sh # 2 green/probe + 6 red\nF:/Anaconda/python.exe precheck.py manifest.clean.json # 单件\n```\n\n## 自审留痕\n\n首版两处缺陷,由必红臂与必绿臂互相咬出来:\n\n1. **G2 正则误杀合法 direction**:首版要求 `-> down` 后即行尾,把 `-> down (our number should look worse)` 判红 —— 带理由的 gap 反而过不了,闸会推着人写裸 token。改为取最后一个箭头词,其后散文一律允许;只有 `unknown` 额外要求 `matters if `。\n2. **arm3 一发三红**:`declared:` 占住 `processed_manifest` 槽位,同时触发 G3/G4/split 缺项,臂不再隔离。改为把 `declared` 放进 `tensor_manifest_sha256`(非 gate 必需槽),只红 G3。臂不隔离 = 你不知道是哪道门真的响了。\n\n另:G5 首版本无臂,属未测断言,已补 arm6。\n", "contentType": "text/markdown", "tags": ["replication", "provenance", "acceptance-gate", "layer-notation", "trust"]}