chain_gap_check.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""六层链"缺料"检查器(2026-09-17 用户令 #2 的收口件)。
  4. 作用只有一个:**把"还缺什么"变成一条命令**,而不是靠人记着哪四个脚本没随包。
  5. python scripts/chain_gap_check.py # 人看
  6. python scripts/chain_gap_check.py --check # 只出结论(缺料 → 退出码 5)
  7. 判据(全部是"文件在不在"这种可核事实,不含猜测):
  8. A. 四步脚本是否在位 —— 壳 `src/windcms/pipeline.py::analyze` 会调它们;
  9. B. 两件"标准答案"是否在位 —— `model_run_l6.parquet` / `fusion_38.csv`(逐值对拍的前提);
  10. C. 台账里这一族还剩多少"✗ 无生成端"(接料后应显著下降)。
  11. 取料单:`docs/向振动线取料单_v0.1.md`;接口规格:`docs/振动六层链_接口规格与缺口_v0.1.md`。
  12. """
  13. from __future__ import annotations
  14. try:
  15. from app_common.app_common_guanlan.api import install_root as _install_root
  16. except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
  17. from pathlib import Path as _P
  18. def _install_root(_f): return _P(_f).resolve().parents[4]
  19. import argparse
  20. import pathlib
  21. import sys
  22. ROOT = _install_root(__file__)
  23. sys.path.insert(0, str(ROOT))
  24. sys.path.insert(0, str(ROOT / 'scripts'))
  25. from src import paths as P # noqa: E402
  26. # 壳里写死的四步脚本(改壳才会变;这里只是把"缺什么"列清楚)
  27. STEPS = [
  28. ('oem_scan', 'scripts/rudong_tcm_oem_scan.py', '特征频率扫描(轴承/齿轮/保持架/叶片 1P)'),
  29. ('energy_share', 'scripts/rudong_line_energy_share.py', '线能量占比'),
  30. ('model_run', 'scripts/rudong_model_run.py', '六层模型出件(model_run_l6.parquet + 摘要)'),
  31. ('fusion', 'scripts/rudong_fusion_run.py', '融合面出件(fusion_38.csv 等)'),
  32. ]
  33. # 逐值对拍用的"标准答案"(随包件里有,但两件关键件从未随过包)
  34. # ★2026-09-19: 这两件现在**本机能自算**(scripts/rudong_model_run.py / rudong_fusion_run.py, 按口径重建)。
  35. # 于是"文件在不在"再也不能代表"有没有标准答案" —— 本机算出来的就躺在同一个路径上。
  36. # 判据改成看**台账里的来路**: builder 文本含"按口径重建" ⇒ 那是自算产物, 记 [自算] 不记 [OK]。
  37. SAMPLES = [
  38. ('model_run_l6.parquet', 'model_run 步的标准答案'),
  39. ('fusion_38.csv', 'fusion 步的标准答案'),
  40. ]
  41. def _selfmade(farm: str) -> set:
  42. """台账里标了"按口径重建"的件 —— 它们是**本机自算**, 不是随包标准答案。"""
  43. import json as _json
  44. out = set()
  45. f = P.out_root(farm) / '_provenance.json'
  46. if not f.is_file():
  47. return out
  48. try:
  49. files = (_json.loads(f.read_text(encoding='utf-8')) or {}).get('files') or {}
  50. except Exception:
  51. return out
  52. for rel, v in files.items():
  53. if '按口径重建' in str((v or {}).get('builder', '')) + str((v or {}).get('why', '')):
  54. out.add(pathlib.Path(rel).name)
  55. return out
  56. def main() -> int:
  57. ap = argparse.ArgumentParser(description='六层链缺料检查')
  58. ap.add_argument('--check', action='store_true', help='只出结论(缺料 → rc=5)')
  59. ap.add_argument('--farm', default=None)
  60. a = ap.parse_args()
  61. farm = a.farm or P.farm()
  62. m5 = P.m5(farm)
  63. missing_scripts = [s for s in STEPS if not (ROOT / s[1]).is_file()]
  64. partial = [] # 脚本在位、但自报 complete=false ⇒ 只是"部分实现"(分母就位/分子待口径)
  65. have_fusion_impl = (ROOT / 'scripts/rudong_fusion_run.py').is_file()
  66. missing_samples = [s for s in SAMPLES if not (m5 / s[0]).is_file()]
  67. print(f'== 六层链缺料检查 · 场站 {farm} ==')
  68. print(f' 壳: src/windcms/pipeline.py::analyze(它按固定路径调这四步)')
  69. print(f' 规格: docs/振动六层链_接口规格与缺口_v0.1.md | 取料单: docs/向振动线取料单_v0.1.md')
  70. print()
  71. print('A. 四步脚本:')
  72. for name, rel, what in STEPS:
  73. p = ROOT / rel
  74. ok = p.is_file()
  75. tag, extra = ('OK ', '') if ok else ('缺 ', '')
  76. if ok:
  77. # ★ 脚本在位 ≠ 这一步能出产物: 逆向实现可能是"分母就位、分子待口径"(自报 --status)。
  78. # 这里调一次 --status 把真实状态问出来, 免得自查器给出"看起来齐了"的假结论。
  79. import json as _json
  80. import subprocess as _sp
  81. try:
  82. r = _sp.run([sys.executable, str(p), '--status'], capture_output=True, text=True,
  83. errors='replace', timeout=120)
  84. st = _json.loads((r.stdout or '').strip().splitlines()[-1]) if (r.stdout or '').strip() else {}
  85. if st and st.get('complete') is False:
  86. tag = '部分'
  87. extra = '(' + st.get('denominator', '') + ';缺: ' + ';'.join(st.get('missing', []) or []) + ')'
  88. except Exception:
  89. pass
  90. if ok and tag == '部分':
  91. partial.append(name)
  92. if not ok and name == 'fusion' and have_fusion_impl:
  93. extra = '(本包已逆向实现其中一件产物: fleet_scalar_z;源码仍缺)'
  94. print(f' [{tag}] {name:12s} {rel:44s} {what}{extra}')
  95. print()
  96. print('B. 逐值对拍的标准答案:')
  97. selfmade = _selfmade(farm)
  98. for nm, why in SAMPLES:
  99. ok = (m5 / nm).is_file()
  100. if ok and nm in selfmade:
  101. print(f' [自算] {nm:28s} 本机**按口径重建**的产物在位(不是标准答案: 无样件可对拍) ({P.rel(m5 / nm)})')
  102. else:
  103. print(f' [{"OK " if ok else "缺 "}] {nm:28s} {why} ({P.rel(m5 / nm)})')
  104. # 已完成的那件: 用本器自带 verify 报一句现状(不写盘)
  105. fr = ROOT / 'scripts/rudong_fusion_run.py'
  106. if fr.is_file():
  107. print()
  108. print('C. 已自研部分(fleet_scalar_z): 跑 python scripts/rudong_fusion_run.py --verify 看逐值对拍')
  109. print()
  110. rc = 5 if (missing_scripts or partial) else 0
  111. selfmade_txt = (';两件关键产物已**按口径重建**(无标准答案可对拍: model_run_l6 / fusion_38)'
  112. if selfmade else '')
  113. tail = ('四步脚本齐全且都已落地 ⇒ 六层链可跑' if not (missing_scripts or partial)
  114. else (f'{len(missing_scripts)} 个步骤脚本未随包' if missing_scripts else '')
  115. + (';' if (missing_scripts and partial) else '')
  116. + (f'{len(partial)} 个为"部分实现"(分母就位 / 分子待口径)' if partial else '')
  117. + ' ⇒ 六层链跑不动(取料单见上)') + selfmade_txt
  118. print(f' 结论: {tail}')
  119. if a.check:
  120. print(f'[{"OK" if rc == 0 else "X"}] chain_gap_check rc={rc}')
  121. return rc
  122. if __name__ == '__main__':
  123. for _s in (sys.stdout, sys.stderr):
  124. try:
  125. _s.reconfigure(errors='replace')
  126. except Exception:
  127. pass
  128. sys.exit(main())