Преглед на файлове

用户令选①: 向振动线取料单 + 缺料检查器 chain_gap_check + 缺脚本报错改成可行动

把"要什么料、拿到后怎么验收"做成可交付、可执行的件(而不是一句口头请求):

· 新增 docs/向振动线取料单_v0.1.md —— 一页说清:
  要的四件(四个步骤脚本: rudong_tcm_oem_scan / rudong_line_energy_share / rudong_model_run /
  rudong_fusion_run) + 两件标准答案(model_run_l6.parquet / fusion_38.csv 任一份历史产出);
  逐条写明"现状 / 解锁什么"; 接口不用重新讨论(壳里已写死调用约定); 附照抄即可的验收命令与判据
  (chain_gap_check → windcms.py analyze --steps ... → rudong_fusion_run.py --verify →
  products_reverse_audit) 与我方承诺(不改壳、件到位才转正、当期把验收结果写回规格书)。
  注: fusion 那件本包已自研(scripts/rudong_fusion_run.py 可重出 fleet_scalar_z), 所以若源码一时给不出,
  只给一件 fusion_38.csv 也能让行集疑问结案。
· 新增 scripts/chain_gap_check.py(带 --check, 缺料 rc=5): 把"还缺什么"变成一条命令 ——
  A 四步脚本在位? B 两件标准答案在位? C 提示已自研部分怎么对拍。实测 rc=5(三缺一有)。
· src/windcms/pipeline.py: 四步调用前先判脚本在位, 缺了就抛指名到料的错(原先现场只看到 python 的
  can't open file 'scripts/rudong_model_run.py', 没法判断是包缺件还是操作有误); 失败时把原因打到控制台
  (原先只在 logs/audit 的 JSON 里)。实测控制台现在会直接说:
  六层链的 model_run 步脚本未随包 … 取料单见 docs/向振动线取料单_v0.1.md … 自查 python
  scripts/chain_gap_check.py。

自检: 入口引用闭合/编码守则 无问题; guanlan.py check 仅剩本机未装 Ollama 的预期 FAIL。
zhouyang.xie преди 3 седмици
родител
ревизия
4d1c9626cc
променени са 3 файла, в които са добавени 180 реда и са изтрити 4 реда
  1. 64 0
      docs/向振动线取料单_v0.1.md
  2. 91 0
      scripts/chain_gap_check.py
  3. 25 4
      src/windcms/pipeline.py

+ 64 - 0
docs/向振动线取料单_v0.1.md

@@ -0,0 +1,64 @@
+# 向振动线 / 研发取料单(v0.1,2026-09-17)
+
+> **要什么**:六层链那四步脚本的源码,以及两件"标准答案"产物(任一份历史产出即可)。
+> **为什么**:观澜包内**只有编排壳、没有实现** —— `scripts/windcms.py analyze` 会去调
+> `scripts/rudong_tcm_oem_scan.py` / `rudong_line_energy_share.py` / `rudong_model_run.py` /
+> `rudong_fusion_run.py`,这四个脚本没随包,所以六层链在目标机上**一步也跑不动**
+> (实测:`can't open file 'scripts/rudong_model_run.py'`)。
+> **接口不用重新讨论**:壳里已经写死了每步怎么调、吃哪个环境变量(见
+> `docs/振动六层链_接口规格与缺口_v0.1.md` §1),**照现在的调用约定实现即可**,我们不改壳。
+
+---
+
+## 一、要的四件(脚本)
+
+| # | 要的东西 | 现状 | 拿到后解锁 |
+|---|---|---|---|
+| 1 | `scripts/rudong_tcm_oem_scan.py` | 缺 | 轴承/齿轮/保持架/叶片 1P 的**特征频率扫描**(约 23 件扫描件可从"✗ 无生成端"转成"✓ 已在重算链上") |
+| 2 | `scripts/rudong_line_energy_share.py` | 缺 | 线能量占比(对应产物名待你确认,包内未见同名件) |
+| 3 | `scripts/rudong_model_run.py` | 缺 | `model_run_l6.parquet` + `model_run_summary.json`(六层模型的第六层出件) |
+| 4 | `scripts/rudong_fusion_run.py` | 缺(**本包已逆向实现了一件**,见下) | `fusion_38.csv` + `fleet_scalar_z.parquet` 等融合面产物 |
+
+★ 第 4 件**部分已自研**:`fleet_scalar_z.parquet` 的口径已反推并在包内实现
+(`scripts/rudong_fusion_run.py`)——`val` 逐值 100% 一致、基线公式(`median` + `1.4826×MAD`)
+在正确行集上 100% 一致。**还差的只是"样件行集为什么少 276 行"**,有了原件(或下面第 6 件)
+一眼就能判定。所以第 4 件若一时给不出源码,**只给一件 `fusion_38.csv` 也行**。
+
+## 二、要的两件(标准答案)
+
+| # | 要的东西 | 为什么非要它 | 拿到后怎么验收 |
+|---|---|---|---|
+| 5 | `model_run_l6.parquet` 任一份历史产出 | 这是 `model_run` 步的**标准答案**。没有它,反推出来的实现**无法证明是"复现"还是"另一个算法"**(本项目的硬规矩:允许响亮降级,不许造数) | 放进 `outputs/<场>/m5_cms_tcm/` 后跑 `python scripts/rudong_fusion_run.py --verify`(同口径的对拍器)或按 §三 逐值对拍 |
+| 6 | `fusion_38.csv` 任一份历史产出 | 同上,且它能让 `fleet_scalar_z` 的行集疑问当场结案(同一次融合的产物,行集应当一致) | 同上 |
+
+## 三、接到料之后怎么验收(照抄即可)
+
+```bash
+# ① 四个脚本是否就位(本包自带检查器)
+python scripts/chain_gap_check.py --check
+
+# ② 六层链能不能跑(先只跑扫描/建模/融合,不碰报告)
+python scripts/windcms.py analyze --window w0316 --steps oem_scan energy_share model_run fusion
+
+# ③ 融合面逐值对拍(口径已反推,只查行集)
+python scripts/rudong_fusion_run.py --verify
+
+# ④ 反查台账:哪些件从"✗ 无生成端"转成"✓ 呼应成立"
+python scripts/products_reverse_audit.py --check
+python scripts/products_restore_missing.py --refresh     # 台账账实相符
+```
+
+**通过判据**(不含糊):
+
+- `chain_gap_check --check` 退出码 0(四件脚本 + 两件样件都在位);
+- ② 各步 `rc=0`,且 `outputs/<场>/m5_cms_tcm/` 出现 `model_run_l6.parquet` / `fusion_38.csv`;
+- ③ **逐值 100% 一致**(`val`/`fleet_med`/`z` 三列都不许有 1e-4 以上的偏差);对不上就**不许**替换样件;
+- ④ `products_reverse_audit` 里 `vib_handoff_and_scans` 的 ✗ 件数应从 36 降到个位数。
+
+## 四、我们这边的承诺
+
+1. **不改壳、不改接口**:四步脚本放回 `scripts/` 即可被 `windcms.py analyze` 调用(参数与环境变量见规格书 §1)。
+2. **件到位才转正**:对拍未通过前,相关件一律留在"✗ 无生成端"并如实显示缺件;
+   绝不用"看起来对"的近似产物顶替(这条已经在 `rudong_fusion_run.py --verify` 里做成机器判据)。
+3. **收到料的当期**:把验收结果(对拍一致率、行集差异、剩余未知)写回
+   `docs/振动六层链_接口规格与缺口_v0.1.md` 与 docs §13.6 的自动块,便于下次交接。

+ 91 - 0
scripts/chain_gap_check.py

@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+r"""六层链"缺料"检查器(2026-09-17 用户令 #2 的收口件)。
+
+作用只有一个:**把"还缺什么"变成一条命令**,而不是靠人记着哪四个脚本没随包。
+
+    python scripts/chain_gap_check.py            # 人看
+    python scripts/chain_gap_check.py --check    # 只出结论(缺料 → 退出码 5)
+
+判据(全部是"文件在不在"这种可核事实,不含猜测):
+
+  A. 四步脚本是否在位 —— 壳 `src/windcms/pipeline.py::analyze` 会调它们;
+  B. 两件"标准答案"是否在位 —— `model_run_l6.parquet` / `fusion_38.csv`(逐值对拍的前提);
+  C. 台账里这一族还剩多少"✗ 无生成端"(接料后应显著下降)。
+
+取料单:`docs/向振动线取料单_v0.1.md`;接口规格:`docs/振动六层链_接口规格与缺口_v0.1.md`。
+"""
+from __future__ import annotations
+
+import argparse
+import pathlib
+import sys
+
+ROOT = pathlib.Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+sys.path.insert(0, str(ROOT / 'scripts'))
+from src import paths as P                                              # noqa: E402
+
+# 壳里写死的四步脚本(改壳才会变;这里只是把"缺什么"列清楚)
+STEPS = [
+    ('oem_scan', 'scripts/rudong_tcm_oem_scan.py', '特征频率扫描(轴承/齿轮/保持架/叶片 1P)'),
+    ('energy_share', 'scripts/rudong_line_energy_share.py', '线能量占比'),
+    ('model_run', 'scripts/rudong_model_run.py', '六层模型出件(model_run_l6.parquet + 摘要)'),
+    ('fusion', 'scripts/rudong_fusion_run.py', '融合面出件(fusion_38.csv 等)'),
+]
+# 逐值对拍用的"标准答案"(随包件里有,但两件关键件从未随过包)
+SAMPLES = [
+    ('model_run_l6.parquet', 'model_run 步的标准答案'),
+    ('fusion_38.csv', 'fusion 步的标准答案'),
+]
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser(description='六层链缺料检查')
+    ap.add_argument('--check', action='store_true', help='只出结论(缺料 → rc=5)')
+    ap.add_argument('--farm', default=None)
+    a = ap.parse_args()
+    farm = a.farm or P.farm()
+    m5 = P.m5(farm)
+
+    missing_scripts = [s for s in STEPS if not (ROOT / s[1]).is_file()]
+    have_fusion_impl = (ROOT / 'scripts/rudong_fusion_run.py').is_file()
+    missing_samples = [s for s in SAMPLES if not (m5 / s[0]).is_file()]
+
+    print(f'== 六层链缺料检查 · 场站 {farm} ==')
+    print(f'   壳: src/windcms/pipeline.py::analyze(它按固定路径调这四步)')
+    print(f'   规格: docs/振动六层链_接口规格与缺口_v0.1.md | 取料单: docs/向振动线取料单_v0.1.md')
+    print()
+    print('A. 四步脚本:')
+    for name, rel, what in STEPS:
+        ok = (ROOT / rel).is_file()
+        tag = 'OK ' if ok else '缺 '
+        extra = ''
+        if not ok and name == 'fusion' and have_fusion_impl:
+            extra = '(本包已逆向实现其中一件产物: fleet_scalar_z;源码仍缺)'
+        print(f'   [{tag}] {name:12s} {rel:44s} {what}{extra}')
+    print()
+    print('B. 逐值对拍的标准答案:')
+    for nm, why in SAMPLES:
+        ok = (m5 / nm).is_file()
+        print(f'   [{"OK " if ok else "缺 "}] {nm:28s} {why}  ({P.rel(m5 / nm)})')
+    # 已完成的那件: 用本器自带 verify 报一句现状(不写盘)
+    fr = ROOT / 'scripts/rudong_fusion_run.py'
+    if fr.is_file():
+        print()
+        print('C. 已自研部分(fleet_scalar_z): 跑 python scripts/rudong_fusion_run.py --verify 看逐值对拍')
+    print()
+    rc = 5 if missing_scripts else 0
+    print(f'   结论: {"四步脚本齐全 ⇒ 六层链可跑, 接着做逐值对拍" if not missing_scripts else str(len(missing_scripts)) + " 个步骤脚本未随包 ⇒ 六层链跑不动(取料单见上)"}')
+    if a.check:
+        print(f'[{"OK" if rc == 0 else "X"}] chain_gap_check rc={rc}')
+    return rc
+
+
+if __name__ == '__main__':
+    for _s in (sys.stdout, sys.stderr):
+        try:
+            _s.reconfigure(errors='replace')
+        except Exception:
+            pass
+    sys.exit(main())

+ 25 - 4
src/windcms/pipeline.py

@@ -125,14 +125,27 @@ def analyze(cfg, input_path=None, window=None, steps=None, bands='ALL'):
         env['M5_WINDOWS'] = ','.join(wins)
         env['SCAN_BANDS'] = bands
         want = set(steps or ['oem_scan', 'energy_share', 'model_run', 'fusion', 'report', 'kb'])
+        # ★ 2026-09-17: 缺脚本要给"人看得懂、且知道去哪儿要料"的错, 而不是让 python 抛一句
+        #   `can't open file 'scripts/rudong_model_run.py'`(实测就是这句 —— 现场看到它没法判断
+        #   这是"包缺件"还是"我敲错了")。四步脚本属振动线分支产物, 未随包; 缺了就指名要说清楚。
+        def _step(name: str, script: str):
+            p = ROOT / 'scripts' / script
+            if not p.is_file():
+                raise RuntimeError(
+                    f'六层链的 {name} 步脚本未随包: {p.relative_to(ROOT).as_posix()}(振动线分支产物)。'
+                    f'调用约定见 docs/振动六层链_接口规格与缺口_v0.1.md §1, 取料单见 docs/向振动线取料单_v0.1.md; '
+                    f'自查: python scripts/chain_gap_check.py。'
+                    f'(本包已逆向实现 fusion 步的一件产物: scripts/rudong_fusion_run.py 可重出 fleet_scalar_z)')
+            return [PY, str(p)]
+
         if 'oem_scan' in want:
-            _run('oem_scan', [PY, str(ROOT / 'scripts/rudong_tcm_oem_scan.py')], env, log)
+            _run('oem_scan', _step('oem_scan', 'rudong_tcm_oem_scan.py'), env, log)
         if 'energy_share' in want:
-            _run('energy_share', [PY, str(ROOT / 'scripts/rudong_line_energy_share.py')], env, log)
+            _run('energy_share', _step('energy_share', 'rudong_line_energy_share.py'), env, log)
         if 'model_run' in want:
-            _run('model_run', [PY, str(ROOT / 'scripts/rudong_model_run.py')], env, log)
+            _run('model_run', _step('model_run', 'rudong_model_run.py'), env, log)
         if 'fusion' in want:
-            _run('fusion', [PY, str(ROOT / 'scripts/rudong_fusion_run.py')], env, log)
+            _run('fusion', _step('fusion', 'rudong_fusion_run.py'), env, log)
         if 'report' in want:
             # 标量缓存按窗集合命名, 新窗自动失效; 谱元数据进程内缓存由新进程重建
             _run('report', [PY, str(ROOT / 'scripts/windcms.py'), 'report', '--farm', cfg.get('key', 'rudong')], env, log)
@@ -153,4 +166,12 @@ def analyze(cfg, input_path=None, window=None, steps=None, bands='ALL'):
     with open(logp, 'w', encoding='utf-8', newline='\n') as f:
         json.dump(summary, f, ensure_ascii=False, indent=1)
     print(f'analyze {status}: {summary["seconds"]}s → {logp}', flush=True)
+    if status != 'ok':
+        # ★ 2026-09-17: 失败时把**原因**打到控制台, 而不是只留一句 "analyze failed" 让人去翻 JSON。
+        #   实测场景: 四步脚本未随包时, 原来只在日志里能看到 python 的 FileNotFoundError,
+        #   现场看到的是"重算失败"四个字, 无从判断是包缺件还是操作有误。
+        bad = [s for s in log if s.get('step') == 'FAILED'] + [s for s in log if s.get('rc') not in (0, None)]
+        for s in bad[:2]:
+            print('   [X] ' + str(s.get('step')) + ': ' + str(s.get('tail', '')).strip()[:400], flush=True)
+        print(f'   详情: {logp}', flush=True)
     return summary