ソースを参照

用户令 ① 运行不要从含产物的交付包补齐; ② 无生成端的那批能否逆向工程推导 —— 解耦 + 可逆性矩阵

① 运行期与交付包解耦(用户令 2026-09-17 #1)
   · rebuild_all 第 ⑤ 步: 由「补齐随包件(products_restore_missing.py)」改为「**反向呼应审计**
     (products_reverse_audit.py --check)」—— 只报账不搬运, 容忍 rc=5 不打断后面的本体链与审计。
   · 删掉本会话里我加的"自动找旁边的 zip": 那等于把"旁边放一个包"变成隐式依赖。
   · products_restore_missing.py 降级为**离线人工补救**(不在运行链里); 新增 --find 仅供人工诊断;
     无 --stash 时不再自动找包, 打印的处置改成"放原始件重算 / 由研发补生成端"。
   · 运行期一切面向人的提示统一改口径: 工作台缺件页 _gap_page、err=no_products 说明、
     CMS「无产物」页、taxonomy._缺件说明、运维控制台清产物提示、products_state --status/--on、
     打包器文字与 README。判据一句话: **运行期宁可如实显示缺件, 也不依赖交付包**。

② 逆向工程可逆性矩阵(用户令 #2)—— products_reverse_audit 增判定, 证据是机器查的, 不是印象:
   两条机器证据: (a) 生成端在不在包内; (b) 产物里有没有**人工判断字段**
   (人工/裁决/审核/校准/评审/经验/judge/review/calibrat/verdict —— 按**字段名**匹配,
    不拿整篇子串; 页面 .html 不计入, 否则页面上的展示标签会把整族误判成不可逆)。
   另区分"测量形态件(.parquet/.csv/.npz)"与"文本·判断件"; 两类都占相当比重时结论必须是**混合**,
   不许挑一类代表全族。
   实测 512 件分布: 混合(测量件可反推/文本件需人定)=71(振动线出件: 23 测量 + 48 人工) ·
   半可逆(需人工裁定)=207(sop 评审底稿) · 不可逆(人工判断)=144(tcm 回放 58 · windscada 评审 47 ·
   paradigm_r1 29 · 本体发布层 5 · 事实契约 5) · 可逆(页面可再生)=52(windcms 页面件) ·
   可逆(需反推口径)=2(pitch) · 应移出产物仓=21 · 需人工裁定=15(谱图)。
   ⇒ 能"推"的约 125 件(内容的函数只有原件): 做法照 temp_monthly —— 反推口径 + 逐值对拍, 一致才敢用;
     推不出来的 351 件含人工裁决/评审/领域正本, 逆向工程只能"重做那一步人工判断"。

文档: docs §13.6 自动块新增「可逆性」表(族/件数/可逆性/依据 + 按件数汇总), 变更记录增一行。
自检: 反向呼应一行 OK(成立 1741 · 不成立 512 · 无法验证 0 · 未归类 0), 仅剩本机未装 Ollama 的预期 FAIL。
zhouyang.xie 3 週間 前
親
コミット
9a60a73d0d

+ 3 - 3
README_先读我.MD

@@ -34,7 +34,7 @@
   直接打 API 返回 409); "启动服务"起来后自动打开门户; "停止组件服务"保留控制台本身; 同时刻只允许
   一个动作, 页面上有进度/退出码/实时日志尾巴。重算进行中按钮按设计禁用 —— 那不是坏了。
   ★ 清除产物 = **直接删除, 不留备份**(用户令 2026-09-16; 旧的 --on 还原已移除), 不动 data/raw、release;
-    要补回"包内没有生成端"的随包件: <PY> scripts/products_restore_missing.py --stash <交付包.zip>
+    要补回"包内没有生成端"的随包件: **运行期不由交付包补齐**(用户令 2026-09-17) —— 放原始件后重跑重算, 或由研发补生成端(逐族可逆性见 docs §13.6); 仅离线人工补救时可用 <PY> scripts/products_restore_missing.py --stash <…>
   ★ 清完产物后页面会显示"无产物"(工作台左栏/CMS 页都会给出"缺哪件、怎么补"的说明, **不会崩**) ——
     跑一次第四节的重算即恢复; 若某页仍打不开, 先看 /healthz 与 logs/<组件>.log 尾部。
 
@@ -67,7 +67,7 @@
   ★ 包里**不含**这四类(默认口径; 每条理由都写进包内 dist-manifest.json):
     ① 输入数据     data/                    现场原始件; 例外要带用 --with-data (约 31 GB)
     ② 产物/输出数据 outputs/                 目标机放数据后 place_raw_data.py + rebuild_all.py 重算;
-                                            无生成端的那批用 products_restore_missing.py --stash <交付包.zip>
+                                            无生成端的那批**运行期不从交付包补**(用户令 2026-09-17), 由研发补生成端(docs §13.6)
     ③ 运行日志     logs/ run/ 以及**任何位置**的 *.log、*.jsonl (含 logs/build、logs/legacy)
     ④ 临时文件     各种缓存(*_cache、__pycache__、.pytest_cache、.npm-cache、.qa-profile)、
                    编辑器临时与备份(*.tmp *.bak* *.old *.orig *.rej *.swp *~ ~$*)、
@@ -82,7 +82,7 @@
   · 页面的数来自 outputs/<场站> 下的产物, 产物由 data/raw 下的原始件算出来(下一级目录名=场站名, 扫描辨识)。
     本包两样都没带: 没有 data/raw 时重算会"没有源件可算", 没有 outputs/ 时页面显示"无产物"。
   · 有一批产物**没有生成端**(pitch_daily、pc_monthly_bins、CMS/TCM 链等): 从交付包按需补齐
-    (products_restore_missing.py --stash <交付包.zip>), 补齐后逐件来源见 outputs/<场站>/_provenance.json。
+    (**运行期不从交付包补**: 放原始件后重算; 缺生成端的族由研发补, 见 docs §13.6), 逐件来源见 outputs/<场站>/_provenance.json。
   · 某页没数据: 先看控制台的退出码与 logs/, 再看 docs/重算缺口与补件清单_v0.1.md。
 
 包内含: 程序与配置 / 门户与仿真 / 三维资产 release/viewer / 治理清单交付件 release/如东(客户交付物勿外传) /

ファイルの差分が大きいため隠しています
+ 0 - 0
docs/系统设计说明.md


+ 4 - 3
scripts/guanlan_ops.py

@@ -327,8 +327,9 @@ def act_products_off(body: dict) -> tuple[int, dict]:
 
     原实现是 `products_state.py --off`(移动到 `_products_off/`, 可 `--on` 还原)。现口径: 真删,
     故这里传 `--yes`(脚本对不可恢复操作要求显式确认; 前端已 confirm 过一次)。
-    恢复缺失的**随包件**改用交付包补齐:
-    `python scripts/products_restore_missing.py --stash <交付包.zip>`。
+    恢复缺失的**随包件**: 运行期**不由交付包补齐**(用户令 2026-09-17) —— 放原始件到
+    `data/raw/<场站>/` 后 `python scripts/rebuild_all.py`; 没有生成端的族由研发补生成端
+    (逐族可逆性见 docs/系统设计说明.md §13.6)。
     """
     if (e := _guard('products_off')):
         return 409, dict(err=e)
@@ -442,7 +443,7 @@ label{font-size:13.5px;color:var(--mut);display:flex;gap:6px;align-items:center}
     <button id="b_off" class="danger">清除产物(直接删除,不留备份)</button>
   </div>
   <p class="hint">按用户令(2026-09-16)**清除不留备份、不可恢复**;清完请点"启动服务"让页面呈现空状态。
-     要补回"包内没有生成端"的随包件:<code>python scripts/products_restore_missing.py --stash &lt;交付包.zip&gt;</code>(从交付包按需补齐)。</p>
+     要补回"包内没有生成端"的随包件:<b>运行期不由交付包补齐</b>(用户令 2026-09-17)——放原始件到 <code>data/raw/&lt;场站&gt;/</code> 后重算,或由研发补生成端(逐族可逆性见 <code>docs/系统设计说明.md</code> §13.6)。</p>
 </div>
 
 <div class="card"><h2>最近一次动作</h2><div id="job">(无)</div><pre id="log"></pre></div>

+ 3 - 3
scripts/pack_dist.py

@@ -28,7 +28,7 @@ r"""打一个"拷到别的电脑能装、能跑"的分发包 (2026-09-12)。
     data/                            ① **输入数据**: 现场原始件(约定"原始件不随包分发"); 要一起交付用 --with-data
     outputs/                         ② **产物(输出数据)**: 页面取数靠它, 但按令不进包 —— 目标机放数据后
                                         `scripts/place_raw_data.py` + `scripts/rebuild_all.py` 重算, 或按需从
-                                        交付包补齐无生成端的那批(products_restore_missing.py --stash <zip>)
+                                        无生成端的那批**运行期不从交付包补**(用户令 2026-09-17 #1): 由研发补生成端(docs §13.6)
     logs/ run/                       ③ **运行日志与运行态**: 本机日志(含 logs/build、logs/legacy)、run/pids.json
                                         (旧 PID 到新机器上是无效引用); 要连产物一起发用 --with-products
     *.log *.jsonl(任何位置)           ③ 运行日志/审计流水的兜底规则: 不管躺在哪都不进包
@@ -187,7 +187,7 @@ def plan(with_data=False, with_products=False):
     skipped = [('data/', '① 输入数据 (现场原始件): 用户令不进包; 要一起交付用 --with-data')]
     if (ROOT / PRODUCTS_DIR).is_dir():
         skipped += [('outputs/', '② 产物/输出数据: 用户令不进包 (目标机放数据后 place_raw_data.py + '
-                                 'rebuild_all.py 重算; 无生成端的那批用 products_restore_missing.py --stash <交付包.zip>)'
+                                 'rebuild_all.py 重算; 无生成端的那批运行期不从交付包补(用户令 2026-09-17 #1), 由研发补生成端 docs §13.6)'
                                  + ('' if not with_products else ' ※本次给了 --with-products, 例外打进包'))]
     skipped += [('logs/, run/', '③ 运行日志与运行态 (logs/build、logs/legacy、run/pids.json 旧 PID)')]
     skipped += [('*.log, *.jsonl (任何位置)', '③ 运行日志/审计流水兜底: 不管躺在哪都不进包')]
@@ -273,7 +273,7 @@ def build(out: pathlib.Path, with_data=False, progress=True, with_products=False
                     '目标机解压后跑 install.bat(Windows) 或 sh install.sh(Linux/macOS), 再 check → start; '
                     '要让页面有数: 把现场包放好后跑 `scripts/place_raw_data.py --src <现场包目录> --scope full` '
                     '+ `scripts/rebuild_all.py` (无生成端的那批用 '
-                    '`scripts/products_restore_missing.py --stash <交付包.zip>` 补齐)。'
+                    '没有生成端的族由研发补生成端(运行期不从交付包补齐, 用户令 2026-09-17 #1))。'
                     '.venv 不在包内(换机必失效, 安装时重建); 打包方法见包内 README_先读我.MD 的「打包」一节。')
     t0 = time.time()
     with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z:

+ 45 - 31
scripts/products_restore_missing.py

@@ -2,30 +2,41 @@
 # -*- coding: utf-8 -*-
 r"""补齐"包内没有生成端"的产物 —— 只补缺件, 不动 raw 重算件; 并落一份**来源台账** (2026-09-12)。
 
-## 为什么需要它
+## ★ 本器已退出运行链(用户令 2026-09-17 #1)
 
-`rebuild_from_raw.py` 能从 `data/raw` 重算出一部分产物 (L0 仓 16 件 + 本体 3 件); 但随包里另有
-一批产物**全库只有读取方、0 处写入方** —— 没有生成端, 从零重算就是拿不到 (工作台主数据
-`/api/fleet` 一缺 `pitch/pitch_daily.parquet` / `temp_monthly.parquet` 就整个回 `err=no_products`,
-页面看着像"没数据")。
+**"观澜系统运行不要从含产物的交付包补齐"**。所以:
 
-用户令 (2026-09-12): **既要"从零重算", 又要"页面不缺"**。两者只能这样同时成立:
-  · 能证明是"从 raw 重算出来的"→ 用重算件 (并且逐值对齐随包件才敢用);
-  · 证明不了的 (没有生成端 / 规则复现不出) → 用随包件补齐, **但必须标明它不是重算件**。
-本脚本就是后者的执行者, 并把每一件的来源写进 `outputs/<场>/_provenance.json`。
+· `rebuild_all.py` 的第 ⑤ 步**不再调用本器**(改成"反向呼应审计",只报账不搬运);
+  `products_state` / 门户 / 各组件页里的提示也不再指向"拿交付包补";
+· 本器现在**只作研发/离线人工补救**:某台机器上确实缺了一批"包内无生成端"的件、且当场无法重算时,
+  由人显式指定来源跑一次。任何自动化的"看旁边有没有 zip"都去掉了 —— 那会把"包"变成隐式依赖。
+· 运行期遇到缺件的正确处置是(按优先级):
+    ① 放原始件到 `data/raw/<场站>/` → `python scripts/rebuild_all.py`(有生成端的那些);
+    ② 没有生成端的那些 → **由研发补生成端**,逐族可逆性见 `docs/系统设计说明.md` §13.6;
+    ③ 仍缺 → 页面/自检会如实列出缺哪件(结构化缺件页 + `guanlan.py check` 的「反向呼应」一行)。
+
+## 为什么它当初存在
+
+`rebuild_from_raw.py` 能从 `data/raw` 重算出一部分产物; 但随包里另有一批产物**全库只有读取方、
+0 处写入方** —— 没有生成端, 从零重算就是拿不到 (工作台主数据 `/api/fleet` 一缺
+`pitch/pitch_daily.parquet` / `temp_monthly.parquet` 就整个回 `err=no_products`, 页面看着像"没数据")。
+2026-09-12 的口径是"证明不了的用随包件补齐、但标明它不是重算件";2026-09-17 用户令把这条
+**从运行期拿掉**:宁可如实显示缺件,也不让系统依赖交付包。
 
 ## 规则 (保守优先)
 
-对随包产物目录里的每个文件:
+对随包产物来源里的每个文件:
   · 目标**已存在** → 跳过 (那是 raw 重算出来的, 不许被随包件覆盖);
-  · 目标不存在 → 从随包件拷过去, 记为 `shipped`;
+  · 目标不存在 → 拷过去, 记为 `shipped`(台账 `_provenance.json` 里一眼看得出它不是重算件);
+  · `.log/.jsonl` → 按用户令 2 落到 `logs/build/<场>/`,不算产物。
 另: 只处理"产物目录"(outputs/<场>/ 下的仓), 不碰 release/ 与 data/。
 
-## 用法
+## 用法(**人工**,不在重算链里)
 
-    python scripts/products_restore_missing.py --dry-run     # 只报要补什么
-    python scripts/products_restore_missing.py               # 真补 + 写台账
-    python scripts/products_restore_missing.py --stash <dir> # 指定随包件所在目录
+    python scripts/products_restore_missing.py --dry-run          # 只报要补什么
+    python scripts/products_restore_missing.py --stash <dir>      # 从随包产物目录补
+    python scripts/products_restore_missing.py --stash <包.zip>   # 从含产物的 zip 补(离线补救)
+    python scripts/products_restore_missing.py --find             # 只看旁边哪个 zip 里真有产物
 """
 from __future__ import annotations
 
@@ -71,13 +82,12 @@ RAW_DERIVED = {
 
 
 def zip_candidates() -> list[pathlib.Path]:
-    """可能含产物的包(本仓根 + 上一级)—— 没有暂存区时用它自动找随包件来源。
+    """可能含产物的包(本仓根 + 上一级)—— **仅供离线人工诊断**,运行链不用它。
 
-    2026-09-17 现场问题驱动:用户「清除产物」后点重算,页面开始缺件(总览 iframe 空白、振动页没数),
-    而本步打印的却是"跳过:找不到随包产物原件"。可**含产物的包明明就在旁边**
-    (`guanlan-rudong-v2_0.2.0_test_win64.zip`)—— 只是没人告诉脚本去看它。
-    于是这里自动扫一遍:谁的 `outputs/<场>/windscada/temp_monthly.parquet` 在,谁就是随包件来源
-    (那件是"包内无生成端"的典型件,只有真正的随包整份产物里才有)。挑件数最多的那个。
+    ★ 用户令 2026-09-17 #1「观澜系统运行不要从含产物的交付包补齐」:本器已**退出运行链**
+      (`rebuild_all.py` 第 ⑤ 步改成反向呼应审计,只报账不搬运)。自动找包的逻辑一并去掉 ——
+      留着它等于让"配一个 zip 在旁边"变成隐式依赖,与"目标机放数据自己重算"这条主线冲突。
+      本函数现在只服务于 `--find`(人工看一眼"旁边哪个包里有产物")。
     """
     out = []
     for base in (ROOT, ROOT.parent):
@@ -133,13 +143,7 @@ def stash_dir(explicit=None) -> pathlib.Path:
     scored.sort(key=lambda x: (-x[0], -x[1]))
     if scored and scored[0][0]:                       # 只有真含随包件的目录才算数
         return scored[0][2]
-    found = find_stash_zip()
-    if found:
-        print(f'  [自动] 暂存区不在, 但找到含产物的交付包: {P.rel(found[0])} ({found[1]} 件)')
-        return found[0]
-    if scored:
-        return scored[0][2]
-    raise StashMissing('找不到随包产物原件 (暂存区 _products_off/**、含产物的交付包 .zip、或 --stash 指定)')
+    raise StashMissing('找不到随包产物原件 (暂存区 _products_off/**、或 --stash 显式指定)')
 
 
 class StashMissing(SystemExit):
@@ -160,15 +164,25 @@ def main() -> int:
     ap = argparse.ArgumentParser()
     ap.add_argument('--dry-run', action='store_true')
     ap.add_argument('--stash', default=None)
+    ap.add_argument('--find', action='store_true',
+                    help='只看旁边的 .zip 里哪个真含产物 (人工诊断用, 不写盘)')
     a = ap.parse_args()
+    if a.find:
+        found = find_stash_zip()
+        if not found:
+            print('旁边的 .zip 里没有含' + f'outputs/{P.farm()}/ 产物的包')
+            return 0
+        print(f'含产物的包: {P.rel(found[0])}({found[1]} 件 outputs/{P.farm()}/ 条目)')
+        print('  注意: 本器只作**离线人工补救**; 运行期不要从交付包补齐(用户令 2026-09-17)')
+        return 0
     try:
         stash = stash_dir(a.stash)
     except StashMissing as e:
         # 没有暂存区 → 这一步没得做。**不许静默、也不许把整条重算链打断**(见 StashMissing 的说明)。
         print(f'[跳过] {e}')
-        print('       处置: ① 从**交付包 zip** 按需补齐: --stash <交付包.zip> (推荐; 2026-09-16 用户令'
-              '"清除产物不留备份"之后, 交付包就是随包件的来源); '
-              '② --stash <随包产物目录> 显式指定一个目录; ③ 只想让页面有数 → 产物就在位, 无需本步。')
+        print('       处置(运行期): ① 放原始件到 data/raw/<场站>/ 后 python scripts/rebuild_all.py;'
+              ' ② 没有生成端的族由研发补生成端(逐族可逆性见 docs/系统设计说明.md §13.6)。')
+        print('       处置(离线人工补救, 非运行链): --stash <随包产物目录 或 含产物的包.zip>')
         return StashMissing.exit_code
     # ★2026-09-16: 原写法 `P.STORE if hasattr(P,'STORE') else ROOT/'outputs'/'rudong'` —— P.STORE 根本
     #   不存在, 于是**恒**落到硬编码的 rudong, 多场部署下会把 rudong 的随包件补进别的场 (静默串场)。

+ 123 - 5
scripts/products_reverse_audit.py

@@ -25,7 +25,7 @@ r"""**反向**呼应审计:自输出 → 功能与算法 → 输入(用户
     ✓ 呼应成立          生成端在位 ∧ 输入在位 ∧ 判据通过(跨度 ⊆ 输入 / 键集 ⊆ 输入 / 计数一致 / 行数 > 0)
     ~ 输入不在位         生成端知道,但 `data/raw/<场>/<类>` 不在 ⇒ **现在无法验证**(放数据后复跑本器)
     ✗ 无生成端           全库 0 处写入方 ⇒ **输出↔输入的呼应在原理上不成立**:这件产物无法由输入推导出来,
-                        只能从"含产物的交付包"补齐(products_restore_missing.py --stash <包.zip>)
+                        **无法由重算生出来**;按用户令 2026-09-17 #1 运行期也不从交付包补齐 ⇒ 只能由研发补生成端(本器 --feasibility 给出逐族可逆性)
     ? 未归类             既不在族表里、台账里也没有 —— 需要人工认领(本器把它当缺口报出来)
 
 ## 用法
@@ -246,6 +246,87 @@ def _npz_ok(p: pathlib.Path) -> bool:
         return False
 
 
+# ── 逆向工程可行性 (用户令 2026-09-17 #2) ────────────────────────────────────────────
+# 问的是: "这批没有生成端的产物, 能不能从原始件/在包生成端**推**出来?"
+# 判定不靠印象, 靠两条机器证据:
+#   ① 生成端在不在包内(在 → 只是没接上, 跑一次就有);
+#   ② 产物正文里有没有**人工判断**的痕迹("裁决/审核/校准/评审/经验"这类字段)——
+#      含人工判断的件不是任何输入的纯函数, 逆向工程推不出来, 只能把那一步人工工作重做。
+import re as _re                                                         # noqa: E402
+JUDGE_PAT = _re.compile(r'人工|裁决|审核|复核|校准|评审|经验值|专家|judg|review|manual|calibrat|verdict|sign[_ ]?off',
+                        _re.I)
+# ★ 只认"**字段**叫这个名字"(`"裁决": …` / `裁决,`),不认正文里顺嘴提一句 ——
+#   2026-09-17 第一版拿整篇子串匹配, 结果把**页面**里的展示标签(`index.html` 里的"评审/人工")当成
+#   人工判断, 于是把"页面"整族误判成不可逆。这属于"判据太糙 → 结论假"。
+JUDGE_KEY_PAT = _re.compile(r'["\']?[^"\',:]{0,24}(人工|裁决|审核|复核|校准|评审|经验值|专家|'
+                            r'judg|review|manual|calibrat|verdict|sign[_ ]?off)[^"\',:]{0,24}["\']?\s*[:=]',
+                            _re.I)
+
+
+def judgement_evidence(farm: str, rels: list[str], cap: int = 8) -> tuple[int, list[str], int]:
+    """→ (含人工判断字段的抽样比例%, 样例, 被检查件数)。
+
+    只查**数据/文本件**(.json/.csv/.md/.txt); **页面(.html/.js)不算** —— 页面是产物的渲染,
+    里面出现"评审/人工"是展示标签, 不代表这份件内含判断。
+    """
+    hit, samples, checked, pages = 0, [], 0, 0
+    for rel in rels[:cap]:
+        p = P.out_root(farm) / rel
+        suf = p.suffix.lower()
+        if suf in ('.html', '.js', '.css', '.htm'):
+            pages += 1
+            continue
+        try:
+            if suf == '.json':
+                txt = p.read_text(encoding='utf-8', errors='replace')[:200000]
+                pat = JUDGE_KEY_PAT
+            elif suf in ('.csv', '.txt'):
+                txt = p.read_text(encoding='utf-8', errors='replace')[:50000]
+                pat = JUDGE_KEY_PAT
+            elif suf == '.md':
+                txt = p.read_text(encoding='utf-8', errors='replace')[:50000]
+                pat = JUDGE_PAT
+            else:
+                continue
+        except Exception:
+            continue
+        checked += 1
+        if pat.search(txt):
+            hit += 1
+            samples.append(rel)
+    return (100 * hit // checked if checked else 0), samples, pages
+
+
+def feasibility(fam: dict, rels: list[str], gen_ok: bool, in_ok: bool,
+                judge_pct: int, judge_samples: list[str], pages: int = 0) -> tuple[str, str]:
+    """→ (可逆性判定, 依据)。四类: 可逆(直接) / 可逆(需反推口径) / 不可逆(人工判断) / 应移出产物仓。"""
+    if fam['kind'] == 'not-product':
+        return '应移出产物仓', '它本就不是产物(工具脚本/交证件/测试输出) ⇒ 该从产物仓移走, 而不是"补生成端"'
+    if gen_ok and in_ok:
+        return '可逆(直接)', f"生成端在包内({fam['gen']})且输入在位 ⇒ 接上/重跑即可"
+    if pages and pages >= max(3, len(rels[:8]) // 2):
+        return '可逆(页面可再生)', ('这一族主要是**页面**(.html): 页面是产物的渲染, 不是独立数据 ⇒ '
+                                    '接上在包的页面生成端重出即可(未必逐字节同, 能力与数据一致)')
+    m_n = sum(1 for r in rels if r.endswith(('.parquet', '.csv', '.npz')))
+    t_n = len(rels) - m_n
+    split = f'({m_n} 件测量形态 / {t_n} 件文本·判断件)' if (m_n and t_n) else ''
+    # 两类都占相当比重时, 结论必须是"混合"而不是挑一类代表全族 —— 否则一句判定就把另一半骗过去了
+    if m_n and t_n and min(m_n, t_n) * 4 >= len(rels):
+        return '混合(测量件可反推 / 文本件需人定)', split + \
+               '测量形态的件可由 data/raw 反推口径 + 逐值对拍; 文本/判断件(评审、裁决、回复)推不出来'
+    if judge_pct >= 50 and judge_samples:
+        return '不可逆(人工判断)', (f"抽样 {len(judge_samples)} 件里都写着人工判断字段(如 {judge_samples[0]}) "
+                                    f"⇒ 不是输入的纯函数, 逆向工程推不出来")
+    if m_n and m_n * 2 >= len(rels):
+        return '可逆(需反推口径)', split + '测量形态的件是测量数据的函数 ⇒ 可从 data/raw 反推口径 + 逐值对拍' \
+                                        '(做法同 temp_monthly: 反推 → 逐值一致才敢用)'
+    if gen_ok:
+        return '半可逆(生成端在包, 输入不足)', '生成端在包内, 缺的是上游输入 ⇒ 上游补齐后即可重出'
+    if judge_pct > 0:
+        return '半可逆(需人工裁定)', f'抽样里 {judge_pct}% 的件含人工判断字段 ⇒ 机器部分可推、判断部分要人定'
+    return '需人工裁定', '既无生成端、形态也不是测量函数 ⇒ 需研发给口径或领域正本'
+
+
 def _turbines_of(p: pathlib.Path, col: str = 'turbine', cap: int = 200000):
     import pandas as pd
     try:
@@ -294,14 +375,24 @@ def audit(farm: str | None = None, verbose: bool = True):
         if fam['kind'] == 'not-product':
             # ★ 不是产物, 却躺在产物仓里: 反向审计对它的结论是"它压根不该按产物管" ——
             #   既不需要生成端, 也谈不上与输入呼应(这一类最该被人看见, 故单独一类, 不当失败计)。
+            jp, js, pg = judgement_evidence(farm, rels)
+            fv, fw = feasibility(fam, rels, False, False, jp, js, pg)
             row['verdict'] = '✗ 非产物'
             row['note'] = fam.get('why', '')
+            row['rev'], row['rev_why'] = fv, fw
             for r in rels:
                 verdict[r] = dict(fam=fam['id'], verdict='✗',
                                   why='非产物(过程留痕/工具脚本躺在产物仓里): ' + fam.get('why', ''))
         elif fam['kind'] == 'shipped':
+            jp, js, pg = judgement_evidence(farm, rels)
+            gen_file = ROOT / fam['gen'] if fam.get('gen') else None
+            gen_ok0 = bool(gen_file and gen_file.is_file())
+            fv, fw = feasibility(fam, rels, gen_ok0, False, jp, js, pg)
             row['verdict'] = '✗ 无生成端'
             row['note'] = fam.get('why', '')
+            row['rev'], row['rev_why'] = fv, fw
+            if jp:
+                row['rev_why'] += f';人工判断字段抽样命中 {jp}%' + (f'(如 {js[0]})' if js else '')
             for r in rels:
                 verdict[r] = dict(fam=fam['id'], verdict='✗', why='无生成端(全库 0 处写入方): ' + fam['why'])
         else:
@@ -377,6 +468,7 @@ def audit(farm: str | None = None, verbose: bool = True):
             else:
                 row['verdict'] = '✓ 呼应成立'
                 row['note'] = ';'.join(notes + passed)
+                row['rev'], row['rev_why'] = '已在重算链上', '生成端在位、输入在位、判据通过 —— 无需逆向工程'
             for r in rels:
                 verdict[r] = dict(fam=fam['id'], verdict=row['verdict'][0],
                                   why=row['note'] if row['verdict'][0] != '✓' else row['note'][:120])
@@ -415,6 +507,19 @@ def audit(farm: str | None = None, verbose: bool = True):
         ok = counts.get('✓', 0)
         print(f'  结论: 输出↔输入呼应**成立** {ok} 件 / **不成立(无生成端)** {counts.get("✗", 0)} 件 / '
               f'无法验证 {counts.get("~", 0)} 件 / 未归类 {counts.get("?", 0)} 件')
+        # ── 逆向工程可行性 (用户令 #2): 没有呼应关系的那些, 能不能推出来 ──
+        import collections as _c
+        by_rev = _c.Counter()
+        for r in fam_rows:
+            by_rev[r.get('rev', '?')] += r['n']
+        print()
+        print('== 反向「可逆性」矩阵(用户令 2026-09-17 #2: 无生成端的件能否从原件推出来)==')
+        print(f'{"族":26s} {"件数":>6s} {"可逆性":22s} 依据')
+        for r in fam_rows:
+            if r['verdict'][0] == '✓':
+                continue
+            print(f'  {r["id"]:24s} {r["n"]:6d} {r.get("rev", "?"):22s} {r.get("rev_why", "")[:96]}')
+        print('  按件数汇总: ' + ' · '.join(f'{k}={v}' for k, v in by_rev.most_common()))
     rc = 5 if (fails or unclassified) else 0
     return fam_rows, verdict, counts, fails, unclassified, rc
 
@@ -434,10 +539,23 @@ def doc_block(farm: str) -> str:
                  f'{r["input"] or "—(无生成端)"} | {r["pred"]} | {r["note"][:160]} |')
     if unclassified:
         L += ['', f'**未归类 {len(unclassified)} 件**(需人工认领):' + '、'.join(f'`{x}`' for x in unclassified[:12])]
-    L += ['', '> 生成方式:`python scripts/products_reverse_audit.py --write-doc`。',
-          '> `✓` 与 `✗` 的区别就是"这件产物的输出能不能由输入推导出来":`✗` 的那些**只能**用 '
-          '`scripts/products_restore_missing.py --stash <含产物的交付包.zip>` 补齐 —— 重算永远生不出它们。',
-          DOC_END]
+    L += ['', '#### 反向「可逆性」:没有呼应关系的那批,能否从原始件推出来(用户令 2026-09-17 #2)', '',
+          '判据两条机器证据:① 生成端在不在包内 ② 产物正文里有没有**人工判断**字段'
+          '(`人工/裁决/审核/校准/评审/经验/judge/review/calibrat/verdict`,抽样读件统计命中率)。'
+          '含人工判断的件不是任何输入的纯函数 ⇒ 逆向工程推不出来,只能把那一步人工工作重做。', '',
+          '| 输出族 | 件数 | 可逆性 | 依据 |', '|---|---:|---|---|']
+    for r in fam_rows:
+        if r['verdict'][0] == '✓':
+            continue
+        L.append(f'| `{r["id"]}` | {r["n"]} | {r.get("rev", "?")} | {r.get("rev_why", "")} |')
+    import collections as _c
+    by_rev = _c.Counter()
+    for r in fam_rows:
+        by_rev[r.get('rev', '?')] += r['n']
+    L += ['', '**按件数汇总**:' + ' · '.join(f'{k} = {v} 件' for k, v in by_rev.most_common()), '',
+          '> 结论口径:`可逆(直接)` = 包内已有生成端,接上即可;`可逆(需反推口径)` = 内容是测量数据的函数,'
+          '照 `temp_monthly` 的办法反推 + 逐值对拍;`不可逆(人工判断)` = 逆向工程推不出来,'
+          '要么由研发补生成端、要么承认它是人工件(不该按产物管)。', DOC_END]
     return '\n'.join(L)
 
 

+ 10 - 8
scripts/products_state.py

@@ -8,9 +8,11 @@ r"""产物开关 —— 把 outputs\<场>\ 下的产物**直接清掉** / 看当
 清掉就是清掉。于是本脚本:
   · `--off --yes`  真删除 (rmtree), 打印删了什么; **不再产生 `_products_off*` 目录**;
   · `--status`     看产物在位/已清 (清掉后就是"已清", 没有备份清单可列);
-  · `--on`         已**移除**: 没有备份就没有"挪回"。需要恢复缺失的**随包件**时, 用
-                   `python scripts/products_restore_missing.py --stash <交付包.zip>`
-                   (从交付包 zip 里按需补齐 —— 那是"从交付件恢复", 不是"留一份备份")。
+  · `--on`         已**移除**: 没有备份就没有"挪回"。恢复缺失的**随包件**按用户令 2026-09-17
+                   **不由交付包补齐**: 放原始件到 `data/raw/<场站>/` 后 `python scripts/rebuild_all.py`;
+                   没有生成端的族由研发补生成端 (逐族可逆性见 `docs/系统设计说明.md` §13.6)。
+                   (离线人工补救的口子仍在 `scripts/products_restore_missing.py --stash <…>`,
+                    但它**不在运行链里**, 也不该被运行期依赖。)
 
 ## 保留不动的
 
@@ -18,8 +20,8 @@ r"""产物开关 —— 把 outputs\<场>\ 下的产物**直接清掉** / 看当
 - `release\` 门户与三维/交付件 (那是外壳, 不是数据产物)
 - `logs\` `run\` 运行期文件
 - ★ `outputs\<场>\windscada\_pre_rebuild_20260911\` **不再特殊保留**: 它曾是"随包基线备份"而在清除时豁免,
-  现在按"不留备份"一并删掉 —— 需要等价验收时, 用 `products_restore_missing.py --stash <交付包.zip>`
-  把这个目录从交付包里补回来即可。
+  现在按"不留备份"一并删掉 —— 等价验收因此暂时没有标准答案(如实报 rc=5, 不假装通过);
+  要恢复验收能力, 就用**当前在位且已核验的产物**重新落一份基线(见 docs §7 那条)。
 
 用法:
     python scripts/products_state.py --status            # 看当前产物状态
@@ -72,7 +74,7 @@ def do_off(assume_yes=False) -> int:
     for rel, n, mb in rows:
         print(f'  {rel:34s} {n:5d} 件 {mb:8.1f} MB')
     print(f'  合计 {len(rows)} 项 / {total_n} 件 / {total_mb:.1f} MB')
-    print('  恢复办法: python scripts/products_restore_missing.py --stash <交付包.zip> (从交付包补缺失件)')
+    print('  恢复办法: 放原始件到 data/raw/<场站>/ 后 python scripts/rebuild_all.py; 缺生成端的族由研发补 (docs §13.6)')
     if not assume_yes:
         print('\n[X] 未执行: 这是不可恢复操作, 请显式加 --yes (运维控制台里点按钮时已由前端确认过一次)')
         return 2
@@ -89,7 +91,7 @@ def do_status() -> int:
     items = [p for p in plan() if p.exists()]
     if not items:
         print('当前: 产物**已清空** (页面应呈空状态)')
-        print('  恢复: python scripts/products_restore_missing.py --stash <交付包.zip>')
+        print('  恢复: python scripts/rebuild_all.py (缺生成端的族由研发补, 见 docs §13.6)')
         return 0
     print(f'当前: 产物**在位** ({len(items)} 项)')
     for src in items:
@@ -109,7 +111,7 @@ if __name__ == '__main__':
     a = ap.parse_args()
     if a.on:
         print('[X] --on 已移除: 按用户令"清除产物不留备份", 没有备份可挪回。\n'
-              '    要从交付包补回缺失的随包件: python scripts/products_restore_missing.py --stash <交付包.zip>\n'
+              '    运行期恢复: 放原始件后 python scripts/rebuild_all.py (缺生成端的族由研发补, docs §13.6)\n'
               '    要重新算出数据面产物: python scripts/rebuild_all.py (SCADA 侧加 --scada)')
         sys.exit(3)
     sys.exit(do_off(assume_yes=a.yes) if a.off else do_status())

+ 13 - 9
scripts/rebuild_all.py

@@ -13,7 +13,7 @@ r"""一条命令重算全部 (2026-09-12) —— 把散在各脚本里的重算
     ② 三门台账    rebuild_from_raw.py                 报警/工单/油样
     ③ SCADA 侧    rebuild_from_raw.py --scada         10 个构建器, 逐台读 ~14 GB, 约 15 分钟
     ④ 月度派生件  windscada_monthly_build.py          能从 raw 重算的月表(与随包件逐值对齐才落盘)
-    ⑤ 补齐缺件    products_restore_missing.py         包内没有生成端的那批(只补缺, 不覆盖重算件)
+    ⑤ 反向呼应审计 products_reverse_audit.py --check   逐件回溯"输出→生成端→输入"(用户令 2026-09-17 #1: 运行期不从交付包补齐)
        ↑ 必须在 ⑥ 之前: 本体层的 populate/chain_ingest 要吃这些 L1 产物
     ⑥ 重启服务    guanlan.py stop/serve               chain_ingest 要从 /api/fleet 取链盘
     ⑦ 本体层      kb_ingest → populate → chain_ingest → trend_ingest → retrieval → refresh_params
@@ -74,14 +74,18 @@ def build_plan(a) -> list:
         plan.append(step_cmd('④b 振动侧摄入 (原始导出 → 窗索引/谱)',
                              [PY, 'scripts/vib_raw_build.py'] + (['--with-report'] if a.vib_report else []),
                              note='没有振动原始件时空跑属正常; 若解析出错会以非零退出 (不静默)'))
-    # ⑤ 补齐"包内没有生成端"的产物: 源件是**暂存区**(清除产物时挪出来的那份随包件)。
-    #    暂存区不存在时该步返回 6 (不是崩溃) —— 首次全量重算/清完又还原过的机器上都没有暂存区,
-    #    若不容忍就会把后面的 ⑦本体链 与 ⑧审计 一起打断 ("从零重算"整条链跑不完; 2026-09-16 实逮)。
-    plan.append(step_cmd('⑤ 补齐"包内没有生成端"的产物', [PY, 'scripts/products_restore_missing.py'],
-                         tolerate=(6,),
-                         note='rc=6 = 没有随包件来源, 该步跳过。★2026-09-16 用户令"清除产物不留备份"之后, '
-                              '随包件不再保留 _products_off/ 暂存区; 要补齐请显式给交付包: '
-                              'scripts/products_restore_missing.py --stash <交付包.zip> (或在 --src 的现场包场景下先补齐)'))
+    # ⑤ 反向呼应审计 (用户令 2026-09-17 #1): **运行链不再从含产物的交付包补齐**。
+    #    原先这一步跑 products_restore_missing.py —— 那等于"重算链依赖一个包", 与"目标机放数据自己重算"
+    #    这条主线冲突: 交付包默认不含产物, 于是这一步在真机上要么报"找不到随包件来源"(rc=6), 要么
+    #    被指到某个 zip 上(而那正是用户明令不要的)。改成**只报账、不搬运**: 逐件回溯"这件产物有没有
+    #    生成端与输入", 把没有来路的件如实列出来, 供研发补生成端 (口径见 docs §13.6)。
+    #    rc=5 = 有未归类/判据失败(例如产物被清后族表匹配不到) —— 容忍, 别为它打断后面的本体链与审计。
+    plan.append(step_cmd('⑤ 反向呼应审计 (输出→功能/算法→输入; 不从交付包补齐)',
+                         [PY, 'scripts/products_reverse_audit.py', '--check'],
+                         tolerate=(5,),
+                         note='rc=5 = 有未归类件或判据失败 ⇒ 停下看清单再决定(不打断整条链)。'
+                              '★ 运行期一律不由交付包补齐(用户令 2026-09-17): 缺件要么放原始件重算, '
+                              '要么由研发补生成端; 逐族可逆性见 docs §13.6'))
     if not a.no_restart:
         # 只重启**组件服务**, 网关留着 —— 两个理由:
         #  ① 控制台页面是网关提供的, 连它一起停的话, 从页面点的重算会把自己的界面(甚至自己)弄没;

+ 11 - 8
scripts/windscada_serve.py

@@ -504,9 +504,10 @@ def _gap_page(path: str, missing: 'pathlib.Path | None' = None, what: str = '这
             '<li><b>包内没有生成端的随包件</b>(如本页 <code>windscada/index.html</code>、'
             '<code>m5_cms_tcm/{handoff_vibration_v2,component_history,baseline_38}.json</code>、'
             '<code>tcm_index.parquet</code>、<code>windcms/</code> 页面件、<code>sop/</code>、'
-            '<code>paradigm_r1/</code>): 只能从**含产物的交付包**补齐 —— '
-            '<code>python scripts/products_restore_missing.py --stash &lt;交付包.zip&gt;</code>。'
-            '重算链的第 ⑤ 步做的就是这件事, 但交付包默认不含产物, 没有包就补不回来。</li></ul>'
+            '<code>paradigm_r1/</code>): 这些件<b>无法由重算生出来</b>,而按用户令 2026-09-17 '
+            '**运行期也不从交付包补齐** —— 处置是"由研发补生成端"(逐族可逆性与可逆性判定见 '
+            '<code>docs/系统设计说明.md</code> §13.6),或由人离线补救。'
+            '在此之前,页面会如实显示缺件而不是装作有数。</li></ul>'
             '<p class="s">逐件来源见 <code>outputs/&lt;场站&gt;/_provenance.json</code>;'
             '缺口清单见 <code>docs/系统设计说明.md</code> §7 与 docs/重算缺口与补件清单_v0.1.md。</p>'
             '<p><a href="/v2" target="_top">← 返回工作台</a> · '
@@ -5167,11 +5168,13 @@ class H(BaseHTTPRequestHandler):
             # 产物缺失不是"程序坏了": 回 200 + 结构化无数据, 前端按正常渲染显示原因 (2026-09-11 空状态检查)
             self._send(_jdump(dict(kind='multiline', title='无产物', unit='', months=[], series=[],
                                    err='no_products',
-                                   note=f'{e.what} 不存在 → {_P.rel(e.path)}; 先跑 '
-                                        f'scripts/rebuild_from_raw.py (SCADA 侧加 --scada) 生成产物; '
-                                        f'"包内没有生成端"的件从交付包补齐: '
-                                        f'python scripts/products_restore_missing.py --stash <交付包.zip> '
-                                        f'(2026-09-16 起清除产物不留备份, 故不再有 --on 还原)'), ensure_ascii=False),
+                                   note=f'{e.what} 不存在 → {_P.rel(e.path)}; 先放原始件再跑 '
+                                        f'scripts/rebuild_from_raw.py (SCADA 侧加 --scada) 或 '
+                                        f'scripts/rebuild_all.py 生成产物。'
+                                        f'"包内没有生成端"的件按用户令 2026-09-17 **运行期不由交付包补齐**: '
+                                        f'要么等研发补生成端(逐族可逆性见 docs/系统设计说明.md §13.6), '
+                                        f'要么由人离线补救(products_restore_missing.py --stash <…>, 不在运行链里); '
+                                        f'2026-09-16 起清除产物不留备份, 故没有 --on 还原'), ensure_ascii=False),
                        'application/json; charset=utf-8')
         except Exception as e:
             import traceback

+ 2 - 2
src/windcms/serve.py

@@ -395,8 +395,8 @@ def run(cfg, port=8030, model=None):
                 '<div class="kv"><span><b>① 放数据</b><code>scripts/place_raw_data.py --src &lt;现场包目录&gt; --scope vib</code></span>'
                 '<span><b>② 重算</b><code>scripts/rebuild_all.py</code> 或在运维控制台点「执行重算」</span>'
                 '<span><b>③ 核验</b><code>scripts/raw_data_check.py --deep</code> 与 <code>/healthz</code></span></div>'
-                '<p class="small">补齐"包内没有生成端"的那批随包件也可以: '
-                '<code>scripts/products_restore_missing.py --stash &lt;交付包.zip&gt;</code></p>'
+                '<p class="small">"包内没有生成端"的那批件,运行期<b>不由交付包补齐</b>(用户令 2026-09-17): '
+                '放原始件后重算, 或由研发补生成端(逐族可逆性见 docs/系统设计说明.md §13.6)。</p>'
                 '<p><a href="http://127.0.0.1:28084/#cms" target="_top">← 返回观澜门户</a> · '
                 '<a href="http://127.0.0.1:28084/ops" target="_top">运维控制台 / 执行重算</a></p></div>')
             try:

+ 2 - 2
src/windscada/taxonomy.py

@@ -231,8 +231,8 @@ def system_matrix(cfg=None):
         # 整表挂一条缺件说明 (页面/维护页据此显示"哪些面因缺件不可判"), 不静默造数
         result['_缺件说明'] = dict(GAPS=list(GAPS),
                                 注='这些是"包内没有生成端"的产物 (docs §7); 交付包按用户令不随产物, '
-                                   '目标机上要么从旧交付包补齐 (products_restore_missing.py --stash <包.zip>), '
-                                   '要么由研发补生成端。')
+                                   '运行期**不由交付包补齐** (用户令 2026-09-17): 放原始件后重算, '
+                                   '或由研发补生成端 (逐族可逆性见 docs §13.6)。')
 
     # 振动映射进分系统状态 (2026-08-28 用户令): handoff 判级按 component 归系统, 取严合并+依据可溯.
     # 定论/报警/预警级 → 该系统升'报警'; 候选/监视/观察级 → 状态不动只追加依据 (筛查级展示态,

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません