|
@@ -1,319 +1,324 @@
|
|
|
-#!/usr/bin/env python3
|
|
|
|
|
-# -*- coding: utf-8 -*-
|
|
|
|
|
-r"""一条命令重算全部 (2026-09-12) —— 把散在各脚本里的重算链按依赖顺序串起来。
|
|
|
|
|
-
|
|
|
|
|
-## 为什么要有它
|
|
|
|
|
-
|
|
|
|
|
-"从零重算"原本是六七条命令、顺序还不能错 (产物齐了本体层才跑得动, 本体层跑完才能拷随包件…),
|
|
|
|
|
-手工执行容易漏步、也说不清"到底重算了什么"。本脚本把顺序、依赖、验收锚点全写死在这里。
|
|
|
|
|
-
|
|
|
|
|
-## 顺序 (为什么是这个顺序)
|
|
|
|
|
-
|
|
|
|
|
- ① 放数据 place_raw_data.py --scope full (给了 --src 才跑; 同尺寸自动跳过)
|
|
|
|
|
- ② 三门台账 rebuild_from_raw.py 报警/工单/油样
|
|
|
|
|
- ③ SCADA 侧 rebuild_from_raw.py --scada 10 个构建器, 逐台读 ~14 GB, 约 15 分钟
|
|
|
|
|
- ④ 月度派生件 windscada_monthly_build.py 能从 raw 重算的月表(与随包件逐值对齐才落盘)
|
|
|
|
|
- ④b 振动侧 vib_raw_build.py 原始导出 → 窗索引/谱/逐台页/融合 z + CMS 报告
|
|
|
|
|
- ④c 变桨面 pitch_face_build.py 10min 液压/润滑/柱塞 + 1min 零位三口径 → pitch/**
|
|
|
|
|
- ④d/④e 振动历史与基线 component_history_build.py / baseline_38_build.py 窗索引 → 在升闭环 + 三层基线
|
|
|
|
|
- ⑤a 台账维护 products_restore_missing.py --refresh 账实相符(谁在位、谁有生成端); 只记账不搬件
|
|
|
|
|
- ⑤ 反向呼应审计 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
|
|
|
|
|
- ⑧ 验收 ontology.audit + rebuild_from_raw --verify (可选) + 页面对照
|
|
|
|
|
-
|
|
|
|
|
-## 用法
|
|
|
|
|
-
|
|
|
|
|
- python scripts/rebuild_all.py # 全套 (会跑 SCADA 那 15 分钟)
|
|
|
|
|
- python scripts/rebuild_all.py --skip-scada # 只换台账时的常用档
|
|
|
|
|
- python scripts/rebuild_all.py --src <现场包目录> # 连"放数据"一起
|
|
|
|
|
- python scripts/rebuild_all.py --no-restart # 不碰服务(自己管)
|
|
|
|
|
- python scripts/rebuild_all.py --with-verify # 末尾加等价验收(需随包基线在位)
|
|
|
|
|
- python scripts/rebuild_all.py --dry-run # 只打印计划
|
|
|
|
|
-"""
|
|
|
|
|
-from __future__ import annotations
|
|
|
|
|
-
|
|
|
|
|
-try:
|
|
|
|
|
- from app_common.app_common_guanlan.api import install_root as _install_root
|
|
|
|
|
-except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
|
|
|
|
|
- from pathlib import Path as _P
|
|
|
|
|
- def _install_root(_f): return _P(_f).resolve().parents[3]
|
|
|
|
|
-
|
|
|
|
|
-import argparse
|
|
|
|
|
-import json
|
|
|
|
|
-import os
|
|
|
|
|
-import pathlib
|
|
|
|
|
-import subprocess
|
|
|
|
|
-import sys
|
|
|
|
|
-import time
|
|
|
|
|
-
|
|
|
|
|
-ROOT = _install_root(__file__) # 模块化后按标记找安装根(原 parents[1] 已不成立)
|
|
|
|
|
-PY = sys.executable
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def step_cmd(name, cmd, tolerate=(), note=''):
|
|
|
|
|
- """tolerate: 允许的退出码 (不算失败); note: 该步非零退出时打给人看的解释(按步, 别用一句通用话糊过去)。"""
|
|
|
|
|
- return (name, cmd, tuple(tolerate), note)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _scan_plan() -> tuple[list, str]:
|
|
|
|
|
- """跑一次输入数据扫描(`--json --check`)→ (该跑的步骤列表, 人读摘要)。扫不动就返回 ([], 原因)。
|
|
|
|
|
-
|
|
|
|
|
- 为什么要它 (用户令 2026-09-19「增加自动扫描识别机制, 以能发现新增数据, 并纳入重算」):
|
|
|
|
|
- `--auto` 靠它决定"人给的 --skip-* 该不该让路" —— 新数据落在被跳过的族里时自动取消跳过。
|
|
|
|
|
- """
|
|
|
|
|
- try:
|
|
|
|
|
- # ★必须显式给子进程 UTF-8 并按其解码 (2026-09-19 实逮: 子进程写 UTF-8、父进程按 GBK 解 ⇒
|
|
|
|
|
- # 步骤名一串乱码, `startswith('③')` 永远不匹配 —— 扫描明明发现新数据, 却没取消 --skip-*)
|
|
|
|
|
- env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8')
|
|
|
|
|
- r = subprocess.run([PY, 'scripts/raw_scan.py', '--json', '--check'], cwd=str(ROOT), env=env,
|
|
|
|
|
- capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=1800)
|
|
|
|
|
- d = json.loads(r.stdout or '{}')
|
|
|
|
|
- return list(d.get('steps') or []), (f'发现 {len(d.get("changes") or [])} 处新增/变化'
|
|
|
|
|
- + ('(' + ' / '.join(d.get('steps') or []) + ')' if d.get('steps') else ''))
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- return [], f'扫描没跑成({type(e).__name__}: {str(e)[:80]})—— 不影响重算, 但 --auto 无依据'
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def build_plan(a) -> list:
|
|
|
|
|
- plan = []
|
|
|
|
|
- if a.auto:
|
|
|
|
|
- # --auto: 先看"这回新来了什么", 再决定人给的 --skip-* 要不要让路 (用户令 2026-09-19)
|
|
|
|
|
- steps, why = _scan_plan()
|
|
|
|
|
- print(f' [auto] 输入数据扫描: {why}')
|
|
|
|
|
- if steps and a.skip_scada and any(s.startswith(('③', '④ ')) or s.startswith('④c') for s in steps):
|
|
|
|
|
- print(' [auto] 有新数据落在 SCADA/变桨族里 ⇒ **取消 --skip-scada**(新数据必须进重算)')
|
|
|
|
|
- a.skip_scada = False
|
|
|
|
|
- if steps and a.skip_vib and any(s.startswith('④b') for s in steps):
|
|
|
|
|
- print(' [auto] 有新数据落在振动族里 ⇒ **取消 --skip-vib**')
|
|
|
|
|
- a.skip_vib = False
|
|
|
|
|
- if a.src:
|
|
|
|
|
- plan.append(step_cmd('① 放数据', [PY, 'scripts/place_raw_data.py', '--src', a.src, '--scope', 'full']))
|
|
|
|
|
- # ①b 输入数据扫描 (用户令 2026-09-19): 逐族指纹 → 发现新增/变化 → 指明该跑哪几步, 并把本次指纹记成
|
|
|
|
|
- # 新基线(下次比的基准)。rc: 0 无变化 · 4 有新增/变化(**不是失败**) · 5 首次没有快照(顺手记一份)。
|
|
|
|
|
- plan.append(step_cmd('①b 输入数据扫描 (新增/变化 → 该跑哪些步)',
|
|
|
|
|
- [PY, 'scripts/raw_scan.py', '--check', '--write'], tolerate=(4, 5),
|
|
|
|
|
- note='rc=4 = 这次有新数据(不是失败)—— 按它列出的步骤重跑即纳入;'
|
|
|
|
|
- 'rc=5 = 还没有快照,本次顺手记一份基线'))
|
|
|
|
|
- plan.append(step_cmd('② 三门台账', [PY, 'scripts/rebuild_from_raw.py']))
|
|
|
|
|
- if not a.skip_scada:
|
|
|
|
|
- plan.append(step_cmd('③ SCADA 侧 10 个构建器 (约 15 分钟)', [PY, 'scripts/rebuild_from_raw.py', '--scada']))
|
|
|
|
|
- # ③b SCADA 10min **窄仓**(用户令 2026-09-21「判级/曲线随所选时间窗重算」的底座):
|
|
|
|
|
- # 一次全量扫(≈2 分钟)抽出约 46 列公共子集 ⇒ 之后任何窗的判级/镜头重算从"重读 15 GB"
|
|
|
|
|
- # 变成"窄仓上过滤+分组"(秒级)。数据换了必须重跑,否则按窗重算会用旧值。
|
|
|
|
|
- if not a.skip_scada:
|
|
|
|
|
- plan.append(step_cmd('③b SCADA 10min 窄仓 (按窗重算底座)', [PY, 'scripts/scada_slim_build.py']))
|
|
|
|
|
- # ④ 月度派生件: `windscada_monthly_build.py` 会拿随包基线**逐值比对**, 没有基线时它返回 5
|
|
|
|
|
- # ("无法比对 —— 这不是通过", 这是它的诚实口径, 不要改它的退出码)。
|
|
|
|
|
- # ★但"重算链"与"等价验收"是两件事: 基线只是**验收标准答案**, 缺它不该让整条链停在第④步
|
|
|
|
|
- # (2026-09-16 实逮: 基线目录被清理后, 从页面点"执行重算" 387 s 后 rc=1 断在这里, ⑦⑧ 全不执行)。
|
|
|
|
|
- # 故这里容忍 5 并写清"跳过了什么"; 要真做验收, 先把随包件恢复成暂存区/基线(见 products_state.py)。
|
|
|
|
|
- plan.append(step_cmd('④ 月度派生件', [PY, 'scripts/windscada_monthly_build.py'],
|
|
|
|
|
- tolerate=(5,),
|
|
|
|
|
- note='rc=5 = 找不到随包基线可比对 ⇒ **跳过等价验收**(不是通过)。'
|
|
|
|
|
- '要恢复验收能力: 把随包产物放回 _products_off*/<场>/ 或 '
|
|
|
|
|
- 'outputs/<场>/windscada/_pre_rebuild_20260911/'))
|
|
|
|
|
- if not a.skip_vib:
|
|
|
|
|
- # 振动侧 (2026-09-12 用户令"振动数据参与运行、重算"): 原始导出的 Brande TCM JSON
|
|
|
|
|
- # (data/raw/<场站>/windcms/**/*_decode.json) → 窗索引 + 谱库 **+ CMS 报告/逐台页**。
|
|
|
|
|
- # 消费者 (src/windcms/data.py) 自动发现新窗, 所以这一步之后 CMS 的谱图/标量/页面就带上了新数据。
|
|
|
|
|
- # ★2026-09-18 用户令(" /detail 各页面不许用旧版产出补, 必须基于输入数据重算")后**口径翻转**:
|
|
|
|
|
- # 原先默认不跑报告(`--vib-report` 显式开), 理由是"重生成会让 report.md 从 16,857 B 掉到
|
|
|
|
|
- # 467 B(融合级表空)" —— 那是拿**随包旧快照**当基准; 现在包里没有旧产物, 页面要么有自算内容
|
|
|
|
|
- # 要么是空的。实测从重算出的窗 w0316 一键出报告: 38 台判级齐(报警 3/优秀 35), 只缺
|
|
|
|
|
- # model_run/fusion 两步的**融合级列**(如实写 —), `/api/vibcms` 立即可用。
|
|
|
|
|
- # 故默认带报告, 要旧行为用 --no-vib-report。
|
|
|
|
|
- # 现场没有振动原始件时本步**空跑退出 0** (不是每台机器都有振动导出, 不该报失败)。
|
|
|
|
|
- plan.append(step_cmd('④b 振动侧摄入 + CMS 报告 + 标量 z (原始导出 → 窗索引/谱/逐台页/融合 z)',
|
|
|
|
|
- [PY, 'scripts/vib_raw_build.py'] + ([] if a.vib_report else ['--no-report']),
|
|
|
|
|
- note='没有振动原始件时空跑属正常; 若解析出错会以非零退出 (不静默)'))
|
|
|
|
|
- if not a.skip_scada:
|
|
|
|
|
- # ④c 变桨面 (用户令 2026-09-19「所有的计算 均要形成 观澜的源代码」): `pitch/**` 原来 kind=shipped、
|
|
|
|
|
- # 无生成端 ⇒ 换台机器重算后变桨面恒"缺件 ⇒ 不可判"。现在由 scripts/pitch_face_build.py 从
|
|
|
|
|
- # scada_10min(开关量 timeon/压力锯齿/运行状态) + scada_1min(三叶桨距角/压力/有功) 算出, 约 10 分钟
|
|
|
|
|
- # (逐台读 1min 全量 ≈ 14 GB)。零位两口径 (停机顺桨段 / 满发段) 按用户令分列, 口径与证据写进
|
|
|
|
|
- # pitch/pitch_face_manifest.json。
|
|
|
|
|
- # 与 ③ 同一档门槛: 现场没放 SCADA 原始件时它按缺件如实退出 4 (不假装算过) ⇒ 容忍 4 并写清原因。
|
|
|
|
|
- plan.append(step_cmd('④c 变桨面 (10min 液压/润滑 + 1min 零位三口径)',
|
|
|
|
|
- [PY, 'scripts/pitch_face_build.py'], tolerate=(4,),
|
|
|
|
|
- note='rc=4 = data/raw 下找不到 SCADA 原始件 ⇒ 变桨面按缺件如实报 (不是失败); '
|
|
|
|
|
- '放件后重跑本步即得 (scripts/place_raw_data.py)'))
|
|
|
|
|
- if not a.skip_vib:
|
|
|
|
|
- # ④d/④e 振动历史与基线 (用户令 2026-09-19「所有的计算 均要形成 观澜的源代码」): 这两件此前是振动线
|
|
|
|
|
- # 手交的随包快照(无生成端) ⇒ 新机器上 ⑦ 的 trend_ingest 报"源缺失"、CMS 报告的三层基线卡为空。
|
|
|
|
|
- # 现在由两个生成端从**窗索引**算: component_history(逐窗标量 x_self → 在升/换件闭环, 窗不足时
|
|
|
|
|
- # 如实为空并写明边界) / baseline_38(健康期分段中位 → 自基线 + fleet 基线 + ISO 绝对限)。
|
|
|
|
|
- # ★要排在 ④b 之后 (读窗索引), 且在 ⑦ trend_ingest 之前。
|
|
|
|
|
- plan.append(step_cmd('④d 振动在升/换件闭环 (窗索引 → component_history.json)',
|
|
|
|
|
- [PY, 'scripts/component_history_build.py'], tolerate=(4,),
|
|
|
|
|
- note='rc=4 = 本机没有振动窗索引 (没放 CMS 原始导出) ⇒ 本件按缺件如实报, 不是失败'))
|
|
|
|
|
- plan.append(step_cmd('④e 三层基线 (窗索引 → baseline_38.json)',
|
|
|
|
|
- [PY, 'scripts/baseline_38_build.py'], tolerate=(4,),
|
|
|
|
|
- note='rc=4 = 本机没有振动窗索引 ⇒ 本件按缺件如实报, 不是失败'))
|
|
|
|
|
- # ⑤b 事实契约的 claim 生成 (用户令 2026-09-19「要让观澜从重算台账生成 claim」):
|
|
|
|
|
- # 由重算产物(alarms/workorders/oil/temp_bins/powercurve_dev/loss_monthly/L6/fusion)算 claim,
|
|
|
|
|
- # 写成 sop/findings.json + paradigm_r1 三份底稿(与契约构建器同结构) ⇒ 契约/门户/问答三处随重算刷新。
|
|
|
|
|
- # ★必须排在 ⑤a 台账维护之前: 这两步产出的件要进同一轮台账与审计, 否则审计会按"溯源缺失"报 rc=6。
|
|
|
|
|
- plan.append(step_cmd('⑤b 事实契约: 由重算台账生成 claim', [PY, 'scripts/sop_findings_from_ledger.py'],
|
|
|
|
|
- note='从重算产物生成; 判据与缺证据逐条写在 claim 里(相对判据封顶「候选」)'))
|
|
|
|
|
- plan.append(step_cmd('⑤c 事实契约: 构建 + 渲染消费者', [PY, 'scripts/guanlan_facts_contract.py', 'build'],
|
|
|
|
|
- tolerate=(2,),
|
|
|
|
|
- note='rc=2 = 契约自检不过(sha/脱敏/枚举), 需要看输出; 门户结论段由 ⑧b 重装时刷新'))
|
|
|
|
|
- # ⑤a 台账维护 (用户令 2026-09-18: 页面只认"有来路的件" ⇒ 台账必须跟着重算一起更新)。
|
|
|
|
|
- # ★实逮: 2026-09-18 用户「清除产物」后从 raw 全量重算, `outputs/<场>/_provenance.json`
|
|
|
|
|
- # **整本丢了** —— 因为写它的唯一入口 (旧的第⑤步 products_restore_missing.py) 已按用户令
|
|
|
|
|
- # 从链上拿下, 而新的第⑤步只**读**台账不写。后果: 门户/页面的「逐件来源」、反向呼应审计的
|
|
|
|
|
- # 族匹配全部退化成"未归类", 而盘上 1,728 件其实件件有生成端。故把 `--refresh`(纯账实相符,
|
|
|
|
|
- # 不搬任何随包件) 作为**独立一步**放回链上, 位置在审计之前。
|
|
|
|
|
- plan.append(step_cmd('⑤a 逐件来源台账 (账实相符: 谁在位、谁有生成端)',
|
|
|
|
|
- [PY, 'scripts/products_restore_missing.py', '--refresh'],
|
|
|
|
|
- note='只维护台账, **不搬任何随包件**(用户令 2026-09-17: 运行期不从交付包补齐); '
|
|
|
|
|
- '盘上有而无人登记的件按族表归口, 归不上族的记 unregistered 并打出来'))
|
|
|
|
|
- # ⑤ 反向呼应审计 (用户令 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:
|
|
|
|
|
- # 只重启**组件服务**, 网关留着 —— 两个理由:
|
|
|
|
|
- # ① 控制台页面是网关提供的, 连它一起停的话, 从页面点的重算会把自己的界面(甚至自己)弄没;
|
|
|
|
|
- # ② rebuild_all 自己可能就是从控制台/网关起的任务, `guanlan.py stop` 用 taskkill /T,
|
|
|
|
|
- # 连网关会**沿父子树把正在执行的重算一起杀掉**(2026-09-12 实测: 日志停在 stop 那行, 之后全无)。
|
|
|
|
|
- # 组件停掉再 serve 就够: chain_ingest 要的是 detail(18033) 重新加载新产物, 网关不缓存产物。
|
|
|
|
|
- plan.append(step_cmd('⑥ 重启组件服务 (保留网关, stop)', [PY, 'scripts/_ops_stop_keep_gateway.py']))
|
|
|
|
|
- plan.append(step_cmd('⑥ 重启组件服务 (serve)', [PY, 'guanlan.py', 'serve'], tolerate=(1,),
|
|
|
|
|
- note='serve 在"有模块 DOWN"时故意返回 1 —— degraded 是正常状态(例如本机模型未启动)'))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 码表/手册/文档', [PY, '-m', 'src.ontology.kb_ingest']))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 铺开判级与台账', [PY, '-m', 'src.ontology.populate']))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 决策链进度', [PY, '-m', 'src.ontology.chain_ingest']))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 在升/闭环证据', [PY, '-m', 'src.ontology.trend_ingest']))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 检索索引', [PY, '-c', 'from src.ontology import retrieval as R; R.build(use_vec=False)']))
|
|
|
|
|
- plan.append(step_cmd('⑦ 本体: 实机参数表', [PY, '-c', 'from src.ontology.maintenance import refresh_params as f; print(f())']))
|
|
|
|
|
- # ⑦b 总览「全场状态」页 (`outputs/<场>/windscada/index.html`): /detail/v2 的 iframe 目标。
|
|
|
|
|
- # 用户令 2026-09-19「所有的计算均要形成观澜的源代码」——它此前是随包件(0 处写入方),
|
|
|
|
|
- # 新机器上那块只能显示"产物不在位"。现在用组件同一个烘焙器从重算产物生成。
|
|
|
|
|
- # ★必须排在 ⑦ 之后: 烤进去的响应里有本体数据(objects.json)。
|
|
|
|
|
- plan.append(step_cmd('⑦b 全场状态总览页 (自算烘焙)', [PY, 'scripts/windscada_overview_build.py'],
|
|
|
|
|
- note='烤的是组件同一套取数函数(见脚本头); 缺产物时会响亮报错, 不产出半张页'))
|
|
|
|
|
- plan.append(step_cmd('⑧ 本体审计 (期望 0 问题)', [PY, '-m', 'src.ontology.audit']))
|
|
|
|
|
- # ⑧b 重装门户: 门户正文里的"契约结论段"是**产物派生**的(由 guanlan_portal_inject_claims.py
|
|
|
|
|
- # 从 outputs/<场>/guanlan/… 渲染), 所以产物变了门户必须重装一遍, 否则页面挂着旧结论。
|
|
|
|
|
- tolerate=(1,),
|
|
|
|
|
- note='rc=1 = 缺契约产物(产物被清除时属预期) ⇒ 门户保留原样, 不是装配坏了'))
|
|
|
|
|
- # ⑧c 页面归口审计: 重算之后, 那些"把数据烘进页面"的页面件(见 configs/portal_pages.yaml)会不会陈旧。
|
|
|
|
|
- # rc=7 = 有页面陈旧(数据换了页面没换) —— 不打断整条链, 但必须报出来, 处置见 docs §10。
|
|
|
|
|
- # ★ rc=5 也容忍 (2026-09-17 实逮): rc=5 = "登记的文件/被引产物不在位", 而**包内没有生成端**的那批
|
|
|
|
|
- # 随包件(如 windscada/index.html、m5_cms_tcm/tcm_index.parquet、windcms 页面件)在
|
|
|
|
|
- # "清过产物 + 交付包不含产物"的机器上必然不在位 ⇒ 第 16 步判失败、整条链 rc=1, 而且把最后的
|
|
|
|
|
- # ⑧台账等价验收也跳过了(实测 2026-09-17: 用户在页面上点重算, 1367 s 后 rc=1 断在这里)。
|
|
|
|
|
- # 这与 ④/⑤/⑥/⑧b 的口径一致: 缺的是"随包件"这一路, 不是重算本身。缺件仍会由
|
|
|
|
|
- # `guanlan.py check` 报 FAIL(那是给人看的告警位), 本步只保证"链不白断"。
|
|
|
|
|
- plan.append(step_cmd('⑧c 页面归口审计 (页面陈旧检测)', [PY, 'scripts/pages_audit.py', '--check'],
|
|
|
|
|
- tolerate=(5, 6, 7),
|
|
|
|
|
- note='rc=7 = 有 data-derived 页面陈旧(内嵌快照的 source_sha256 与当前产物不一致): '
|
|
|
|
|
- '需要对应生成端重跑, 或在登记表里把它改成 frozen-delivery 并接受会过期; '
|
|
|
|
|
- 'rc=5 = 登记的文件/被引产物不在位(清过产物、且那批件没有生成端时属预期); '
|
|
|
|
|
- '★rc=6 也容忍 (2026-09-19 实逮): rc=6 = 溯源缺失(产物在盘上/登记里, 但台账查不到来路) '
|
|
|
|
|
- '—— 与 5 同属"缺的是随包件这一路", 例如 ④b 在 report 步崩掉时 '
|
|
|
|
|
- 'vib_raw_manifest.json 没写成, 审计就按"溯源缺失"报 6 并把整条链判失败; '
|
|
|
|
|
- '处置见 docs §7 与「数据重算」页的缺件提示; `guanlan.py check` 里仍有给人看的告警位'))
|
|
|
|
|
- # ⑧d 版本记录一致性 (2026-09-22 实逮, 远端 2.9.1 升级+重算后验收才暴露):
|
|
|
|
|
- # 那次升级的差异盘点只覆盖 src/ + scripts/ + configs/ —— **文档不在盘点面上**, 于是代码升到 2.9.1、
|
|
|
|
|
- # docs/版本记录.md 与 docs/系统设计说明.md 留在旧版; 而重算链此前只到 ⑧c, 没有版本门 ⇒
|
|
|
|
|
- # 24/24 步全 OK、控制台写"重算完成", 版本/文档漂移**没有任何自动拦截点**(只有人工 `guanlan.py check` 能逮)。
|
|
|
|
|
- # 与 ⑧c 同口径: tolerate=6 —— 缺的是"文档同步"这一路, 不是重算本身 ⇒ 报出来但不打断整条链。
|
|
|
|
|
- plan.append(step_cmd('⑧d 版本记录一致性 (docs 与 src/version.py)',
|
|
|
|
|
- [PY, 'scripts/version_log.py', '--check'], tolerate=(6,),
|
|
|
|
|
- note='rc=6 = docs/版本记录.md 的自动区块与 src/version.py 的 HISTORY 不一致 '
|
|
|
|
|
- '(升级只同步了代码、没同步文档即属此类) ⇒ 跑 python scripts/version_log.py --write '
|
|
|
|
|
- '重生成自动区块, 再把 docs/ 一并同步到目标机(升级差异盘点要含 docs/); '
|
|
|
|
|
- '`guanlan.py check` 里仍有给人看的 FAIL 位'))
|
|
|
|
|
- if a.with_verify:
|
|
|
|
|
- # ★2026-09-20 改 (远端实逮, 2.8.0 在服务器上一轮 24/24 步全 OK 却整轮 rc=1):
|
|
|
|
|
- # 原先 tolerate=(4,) —— rc=5("找不到随包基线 ⇒ 没验收") 被当成失败, 于是**交付包装出来的机器
|
|
|
|
|
- # (按用户令包内不含产物 ⇒ 基线必然不存在)每次重算都以 rc=1 结束, 门户上写"重算失败"**,
|
|
|
|
|
- # 而实质步骤全绿。这与 ④ 的口径自相矛盾(④ 容忍 5 并写明"跳过等价验收≠通过")。
|
|
|
|
|
- # 现在: rc=4(有需人工看的差异, 本包已知项: 油样 102 行源件不在现场包) 与 rc=5(没基线=没验收)
|
|
|
|
|
- # 都容忍, 但**都在汇总行里如实标 rc**、并在步说明里写清"这不是通过"; 想真做验收就让
|
|
|
|
|
- # `scripts/set_baseline.py` 建一份基线(用已核实的产物快照 + 写明来历), 之后这一项才会真比对。
|
|
|
|
|
- plan.append(step_cmd('⑧ 台账等价验收 (rc=4 = 有需人工看的差异; rc=5 = 没基线 ⇒ 没验收)',
|
|
|
|
|
- [PY, 'scripts/rebuild_from_raw.py', '--verify'], tolerate=(4, 5),
|
|
|
|
|
- note='rc=4 = 有需人工看的差异(本包已知项, 见 docs §7); '
|
|
|
|
|
- 'rc=5 = 找不到随包基线 ⇒ **没做验收**(不是失败, 也不是通过)。'
|
|
|
|
|
- '★ 交付包按用户令不含产物 ⇒ 全新机器上这一步必然 rc=5; 要恢复验收能力: '
|
|
|
|
|
- '用 python scripts/set_baseline.py 一条命令把当前已核实的产物快照登记成基线, '
|
|
|
|
|
- '再重算就能真比对'))
|
|
|
|
|
- return plan
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def main() -> int:
|
|
|
|
|
- ap = argparse.ArgumentParser()
|
|
|
|
|
- ap.add_argument('--src', default=None, help='现场数据包目录 (给了就先跑 place_raw_data --scope full)')
|
|
|
|
|
- ap.add_argument('--auto', action='store_true',
|
|
|
|
|
- help='先扫一遍输入数据(raw_scan.py),发现新数据落在被 --skip-* 跳过的族里时自动取消跳过'
|
|
|
|
|
- '(用户令 2026-09-19「发现新增数据并纳入重算」)')
|
|
|
|
|
- ap.add_argument('--skip-scada', action='store_true', help='跳过 SCADA 侧 10 个构建器 (没换 10min 数据时用)')
|
|
|
|
|
- ap.add_argument('--skip-vib', action='store_true', help='跳过振动侧摄入 (没换 CMS 原始导出时用)')
|
|
|
|
|
- ap.add_argument('--no-vib-report', dest='vib_report', action='store_false',
|
|
|
|
|
- help='振动侧只摄入(索引/谱), 不生成 CMS 报告/逐台页 —— ★默认**生成**(2026-09-18 口径'
|
|
|
|
|
- '翻转: 包里没有旧产物可参照, 页面要么有自算内容要么空白; 报告只缺 '
|
|
|
|
|
- 'model_run/fusion 的融合级列, 如实写 —)')
|
|
|
|
|
- ap.add_argument('--no-restart', action='store_true', help='不自动重启服务')
|
|
|
|
|
- ap.add_argument('--with-verify', action='store_true', help='末尾加等价验收')
|
|
|
|
|
- ap.add_argument('--dry-run', action='store_true', help='只打印计划')
|
|
|
|
|
- a = ap.parse_args()
|
|
|
|
|
-
|
|
|
|
|
- plan = build_plan(a)
|
|
|
|
|
- print(f'重算全部 · {len(plan)} 步 · 根目录 {ROOT}')
|
|
|
|
|
- print(f' 跳过 SCADA: {a.skip_scada} | 重启服务: {not a.no_restart} | 等价验收: {a.with_verify}\n')
|
|
|
|
|
- if a.dry_run:
|
|
|
|
|
- for name, cmd, tol, note in plan:
|
|
|
|
|
- print(f' {name:34s} {" ".join(cmd[1:])[:90]}')
|
|
|
|
|
- print('\n(dry-run, 未执行)')
|
|
|
|
|
- return 0
|
|
|
|
|
-
|
|
|
|
|
- # 子进程环境: 与 guanlan.py 给服务的一致 —— 中文 Windows 控制台是 GBK, 不设的话
|
|
|
|
|
- # 子脚本 print 里的 ✔/✗ 会抛 UnicodeEncodeError, 明明干成了却以退出码 1 结束。
|
|
|
|
|
- env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8')
|
|
|
|
|
- results, t_all = [], time.time()
|
|
|
|
|
- for i, (name, cmd, tol, note) in enumerate(plan, 1):
|
|
|
|
|
- print(f'\n{"=" * 78}\n[{i}/{len(plan)}] {name}\n$ {" ".join(cmd)}\n{"=" * 78}', flush=True)
|
|
|
|
|
- t0 = time.time()
|
|
|
|
|
- rc = subprocess.run(cmd, cwd=str(ROOT), env=env).returncode
|
|
|
|
|
- dt = time.time() - t0
|
|
|
|
|
- ok = (rc == 0) or (rc in tol)
|
|
|
|
|
- results.append((name, rc, dt, ok, tol))
|
|
|
|
|
- if not ok:
|
|
|
|
|
- print(f'\n[X] 第 {i} 步失败 (退出码 {rc}): {name} —— 后面的步骤依赖它, 先停下。')
|
|
|
|
|
- if note:
|
|
|
|
|
- print(f' 该步说明: {note}')
|
|
|
|
|
- break
|
|
|
|
|
- if rc != 0:
|
|
|
|
|
- print(f'\n (退出码 {rc} 属预期{(": " + note) if note else ""})')
|
|
|
|
|
-
|
|
|
|
|
- print(f'\n{"=" * 78}\n汇总 (总耗时 {time.time() - t_all:.0f}s)')
|
|
|
|
|
- for name, rc, dt, ok, tol in results:
|
|
|
|
|
- tag = 'OK' if rc == 0 else (f'OK(rc={rc})' if ok else 'FAIL')
|
|
|
|
|
- print(f' [{tag:9s}] {name:34s} {dt:7.1f}s')
|
|
|
|
|
- n_fail = sum(1 for *_, ok, _ in results if not ok)
|
|
|
|
|
- if n_fail:
|
|
|
|
|
- print(f'\n结论: {n_fail} 步失败 —— 按上面的日志处理后再重跑本命令 (各步都是幂等的)')
|
|
|
|
|
- return 1
|
|
|
|
|
- print('\n结论: 全部完成。核验: .venv\\Scripts\\python.exe -m src.ontology.maintenance '
|
|
|
|
|
- '(数据层/机理层的条数与末次更新); 浏览器 http://127.0.0.1:28084/detail/')
|
|
|
|
|
- return 0
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-if __name__ == '__main__':
|
|
|
|
|
- for _s in (sys.stdout, sys.stderr):
|
|
|
|
|
- try: _s.reconfigure(errors='replace')
|
|
|
|
|
- except Exception: pass
|
|
|
|
|
- sys.exit(main())
|
|
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+# ★P13(2026-10-01)待办:链上「⑦b 全场状态总览页(自算烘焙)」与「⑧b 重装门户」两步的脚本已随旧页退役
|
|
|
|
|
+# (scripts/windscada_overview_build.py 已不再产单文件页;scripts/portal_build.py 已删除)⇒ 需要清理这两步。
|
|
|
|
|
+# 清理务必**按 AST 列表元素整段删除**(我此前两次"按行/按语句"删都把多行调用删残,别重蹈覆辙)。
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+r"""一条命令重算全部 (2026-09-12) —— 把散在各脚本里的重算链按依赖顺序串起来。
|
|
|
|
|
+
|
|
|
|
|
+## 为什么要有它
|
|
|
|
|
+
|
|
|
|
|
+"从零重算"原本是六七条命令、顺序还不能错 (产物齐了本体层才跑得动, 本体层跑完才能拷随包件…),
|
|
|
|
|
+手工执行容易漏步、也说不清"到底重算了什么"。本脚本把顺序、依赖、验收锚点全写死在这里。
|
|
|
|
|
+
|
|
|
|
|
+## 顺序 (为什么是这个顺序)
|
|
|
|
|
+
|
|
|
|
|
+ ① 放数据 place_raw_data.py --scope full (给了 --src 才跑; 同尺寸自动跳过)
|
|
|
|
|
+ ② 三门台账 rebuild_from_raw.py 报警/工单/油样
|
|
|
|
|
+ ③ SCADA 侧 rebuild_from_raw.py --scada 10 个构建器, 逐台读 ~14 GB, 约 15 分钟
|
|
|
|
|
+ ④ 月度派生件 windscada_monthly_build.py 能从 raw 重算的月表(与随包件逐值对齐才落盘)
|
|
|
|
|
+ ④b 振动侧 vib_raw_build.py 原始导出 → 窗索引/谱/逐台页/融合 z + CMS 报告
|
|
|
|
|
+ ④c 变桨面 pitch_face_build.py 10min 液压/润滑/柱塞 + 1min 零位三口径 → pitch/**
|
|
|
|
|
+ ④d/④e 振动历史与基线 component_history_build.py / baseline_38_build.py 窗索引 → 在升闭环 + 三层基线
|
|
|
|
|
+ ⑤a 台账维护 products_restore_missing.py --refresh 账实相符(谁在位、谁有生成端); 只记账不搬件
|
|
|
|
|
+ ⑤ 反向呼应审计 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
|
|
|
|
|
+ ⑧ 验收 ontology.audit + rebuild_from_raw --verify (可选) + 页面对照
|
|
|
|
|
+
|
|
|
|
|
+## 用法
|
|
|
|
|
+
|
|
|
|
|
+ python scripts/rebuild_all.py # 全套 (会跑 SCADA 那 15 分钟)
|
|
|
|
|
+ python scripts/rebuild_all.py --skip-scada # 只换台账时的常用档
|
|
|
|
|
+ python scripts/rebuild_all.py --src <现场包目录> # 连"放数据"一起
|
|
|
|
|
+ python scripts/rebuild_all.py --no-restart # 不碰服务(自己管)
|
|
|
|
|
+ python scripts/rebuild_all.py --with-verify # 末尾加等价验收(需随包基线在位)
|
|
|
|
|
+ python scripts/rebuild_all.py --dry-run # 只打印计划
|
|
|
|
|
+"""
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+try:
|
|
|
|
|
+ from app_common.app_common_guanlan.api import install_root as _install_root
|
|
|
|
|
+except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
|
|
|
|
|
+ from pathlib import Path as _P
|
|
|
|
|
+ def _install_root(_f): return _P(_f).resolve().parents[3]
|
|
|
|
|
+
|
|
|
|
|
+import argparse
|
|
|
|
|
+import json
|
|
|
|
|
+import os
|
|
|
|
|
+import pathlib
|
|
|
|
|
+import subprocess
|
|
|
|
|
+import sys
|
|
|
|
|
+import time
|
|
|
|
|
+
|
|
|
|
|
+ROOT = _install_root(__file__) # 模块化后按标记找安装根(原 parents[1] 已不成立)
|
|
|
|
|
+PY = sys.executable
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def step_cmd(name, cmd, tolerate=(), note=''):
|
|
|
|
|
+ """tolerate: 允许的退出码 (不算失败); note: 该步非零退出时打给人看的解释(按步, 别用一句通用话糊过去)。"""
|
|
|
|
|
+ return (name, cmd, tuple(tolerate), note)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _scan_plan() -> tuple[list, str]:
|
|
|
|
|
+ """跑一次输入数据扫描(`--json --check`)→ (该跑的步骤列表, 人读摘要)。扫不动就返回 ([], 原因)。
|
|
|
|
|
+
|
|
|
|
|
+ 为什么要它 (用户令 2026-09-19「增加自动扫描识别机制, 以能发现新增数据, 并纳入重算」):
|
|
|
|
|
+ `--auto` 靠它决定"人给的 --skip-* 该不该让路" —— 新数据落在被跳过的族里时自动取消跳过。
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ # ★必须显式给子进程 UTF-8 并按其解码 (2026-09-19 实逮: 子进程写 UTF-8、父进程按 GBK 解 ⇒
|
|
|
|
|
+ # 步骤名一串乱码, `startswith('③')` 永远不匹配 —— 扫描明明发现新数据, 却没取消 --skip-*)
|
|
|
|
|
+ env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8')
|
|
|
|
|
+ r = subprocess.run([PY, 'scripts/raw_scan.py', '--json', '--check'], cwd=str(ROOT), env=env,
|
|
|
|
|
+ capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=1800)
|
|
|
|
|
+ d = json.loads(r.stdout or '{}')
|
|
|
|
|
+ return list(d.get('steps') or []), (f'发现 {len(d.get("changes") or [])} 处新增/变化'
|
|
|
|
|
+ + ('(' + ' / '.join(d.get('steps') or []) + ')' if d.get('steps') else ''))
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return [], f'扫描没跑成({type(e).__name__}: {str(e)[:80]})—— 不影响重算, 但 --auto 无依据'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def build_plan(a) -> list:
|
|
|
|
|
+ plan = []
|
|
|
|
|
+ if a.auto:
|
|
|
|
|
+ # --auto: 先看"这回新来了什么", 再决定人给的 --skip-* 要不要让路 (用户令 2026-09-19)
|
|
|
|
|
+ steps, why = _scan_plan()
|
|
|
|
|
+ print(f' [auto] 输入数据扫描: {why}')
|
|
|
|
|
+ if steps and a.skip_scada and any(s.startswith(('③', '④ ')) or s.startswith('④c') for s in steps):
|
|
|
|
|
+ print(' [auto] 有新数据落在 SCADA/变桨族里 ⇒ **取消 --skip-scada**(新数据必须进重算)')
|
|
|
|
|
+ a.skip_scada = False
|
|
|
|
|
+ if steps and a.skip_vib and any(s.startswith('④b') for s in steps):
|
|
|
|
|
+ print(' [auto] 有新数据落在振动族里 ⇒ **取消 --skip-vib**')
|
|
|
|
|
+ a.skip_vib = False
|
|
|
|
|
+ if a.src:
|
|
|
|
|
+ plan.append(step_cmd('① 放数据', [PY, 'scripts/place_raw_data.py', '--src', a.src, '--scope', 'full']))
|
|
|
|
|
+ # ①b 输入数据扫描 (用户令 2026-09-19): 逐族指纹 → 发现新增/变化 → 指明该跑哪几步, 并把本次指纹记成
|
|
|
|
|
+ # 新基线(下次比的基准)。rc: 0 无变化 · 4 有新增/变化(**不是失败**) · 5 首次没有快照(顺手记一份)。
|
|
|
|
|
+ plan.append(step_cmd('①b 输入数据扫描 (新增/变化 → 该跑哪些步)',
|
|
|
|
|
+ [PY, 'scripts/raw_scan.py', '--check', '--write'], tolerate=(4, 5),
|
|
|
|
|
+ note='rc=4 = 这次有新数据(不是失败)—— 按它列出的步骤重跑即纳入;'
|
|
|
|
|
+ 'rc=5 = 还没有快照,本次顺手记一份基线'))
|
|
|
|
|
+ plan.append(step_cmd('② 三门台账', [PY, 'scripts/rebuild_from_raw.py']))
|
|
|
|
|
+ if not a.skip_scada:
|
|
|
|
|
+ plan.append(step_cmd('③ SCADA 侧 10 个构建器 (约 15 分钟)', [PY, 'scripts/rebuild_from_raw.py', '--scada']))
|
|
|
|
|
+ # ③b SCADA 10min **窄仓**(用户令 2026-09-21「判级/曲线随所选时间窗重算」的底座):
|
|
|
|
|
+ # 一次全量扫(≈2 分钟)抽出约 46 列公共子集 ⇒ 之后任何窗的判级/镜头重算从"重读 15 GB"
|
|
|
|
|
+ # 变成"窄仓上过滤+分组"(秒级)。数据换了必须重跑,否则按窗重算会用旧值。
|
|
|
|
|
+ if not a.skip_scada:
|
|
|
|
|
+ plan.append(step_cmd('③b SCADA 10min 窄仓 (按窗重算底座)', [PY, 'scripts/scada_slim_build.py']))
|
|
|
|
|
+ # ④ 月度派生件: `windscada_monthly_build.py` 会拿随包基线**逐值比对**, 没有基线时它返回 5
|
|
|
|
|
+ # ("无法比对 —— 这不是通过", 这是它的诚实口径, 不要改它的退出码)。
|
|
|
|
|
+ # ★但"重算链"与"等价验收"是两件事: 基线只是**验收标准答案**, 缺它不该让整条链停在第④步
|
|
|
|
|
+ # (2026-09-16 实逮: 基线目录被清理后, 从页面点"执行重算" 387 s 后 rc=1 断在这里, ⑦⑧ 全不执行)。
|
|
|
|
|
+ # 故这里容忍 5 并写清"跳过了什么"; 要真做验收, 先把随包件恢复成暂存区/基线(见 products_state.py)。
|
|
|
|
|
+ plan.append(step_cmd('④ 月度派生件', [PY, 'scripts/windscada_monthly_build.py'],
|
|
|
|
|
+ tolerate=(5,),
|
|
|
|
|
+ note='rc=5 = 找不到随包基线可比对 ⇒ **跳过等价验收**(不是通过)。'
|
|
|
|
|
+ '要恢复验收能力: 把随包产物放回 _products_off*/<场>/ 或 '
|
|
|
|
|
+ 'outputs/<场>/windscada/_pre_rebuild_20260911/'))
|
|
|
|
|
+ if not a.skip_vib:
|
|
|
|
|
+ # 振动侧 (2026-09-12 用户令"振动数据参与运行、重算"): 原始导出的 Brande TCM JSON
|
|
|
|
|
+ # (data/raw/<场站>/windcms/**/*_decode.json) → 窗索引 + 谱库 **+ CMS 报告/逐台页**。
|
|
|
|
|
+ # 消费者 (src/windcms/data.py) 自动发现新窗, 所以这一步之后 CMS 的谱图/标量/页面就带上了新数据。
|
|
|
|
|
+ # ★2026-09-18 用户令(" /detail 各页面不许用旧版产出补, 必须基于输入数据重算")后**口径翻转**:
|
|
|
|
|
+ # 原先默认不跑报告(`--vib-report` 显式开), 理由是"重生成会让 report.md 从 16,857 B 掉到
|
|
|
|
|
+ # 467 B(融合级表空)" —— 那是拿**随包旧快照**当基准; 现在包里没有旧产物, 页面要么有自算内容
|
|
|
|
|
+ # 要么是空的。实测从重算出的窗 w0316 一键出报告: 38 台判级齐(报警 3/优秀 35), 只缺
|
|
|
|
|
+ # model_run/fusion 两步的**融合级列**(如实写 —), `/api/vibcms` 立即可用。
|
|
|
|
|
+ # 故默认带报告, 要旧行为用 --no-vib-report。
|
|
|
|
|
+ # 现场没有振动原始件时本步**空跑退出 0** (不是每台机器都有振动导出, 不该报失败)。
|
|
|
|
|
+ plan.append(step_cmd('④b 振动侧摄入 + CMS 报告 + 标量 z (原始导出 → 窗索引/谱/逐台页/融合 z)',
|
|
|
|
|
+ [PY, 'scripts/vib_raw_build.py'] + ([] if a.vib_report else ['--no-report']),
|
|
|
|
|
+ note='没有振动原始件时空跑属正常; 若解析出错会以非零退出 (不静默)'))
|
|
|
|
|
+ if not a.skip_scada:
|
|
|
|
|
+ # ④c 变桨面 (用户令 2026-09-19「所有的计算 均要形成 观澜的源代码」): `pitch/**` 原来 kind=shipped、
|
|
|
|
|
+ # 无生成端 ⇒ 换台机器重算后变桨面恒"缺件 ⇒ 不可判"。现在由 scripts/pitch_face_build.py 从
|
|
|
|
|
+ # scada_10min(开关量 timeon/压力锯齿/运行状态) + scada_1min(三叶桨距角/压力/有功) 算出, 约 10 分钟
|
|
|
|
|
+ # (逐台读 1min 全量 ≈ 14 GB)。零位两口径 (停机顺桨段 / 满发段) 按用户令分列, 口径与证据写进
|
|
|
|
|
+ # pitch/pitch_face_manifest.json。
|
|
|
|
|
+ # 与 ③ 同一档门槛: 现场没放 SCADA 原始件时它按缺件如实退出 4 (不假装算过) ⇒ 容忍 4 并写清原因。
|
|
|
|
|
+ plan.append(step_cmd('④c 变桨面 (10min 液压/润滑 + 1min 零位三口径)',
|
|
|
|
|
+ [PY, 'scripts/pitch_face_build.py'], tolerate=(4,),
|
|
|
|
|
+ note='rc=4 = data/raw 下找不到 SCADA 原始件 ⇒ 变桨面按缺件如实报 (不是失败); '
|
|
|
|
|
+ '放件后重跑本步即得 (scripts/place_raw_data.py)'))
|
|
|
|
|
+ if not a.skip_vib:
|
|
|
|
|
+ # ④d/④e 振动历史与基线 (用户令 2026-09-19「所有的计算 均要形成 观澜的源代码」): 这两件此前是振动线
|
|
|
|
|
+ # 手交的随包快照(无生成端) ⇒ 新机器上 ⑦ 的 trend_ingest 报"源缺失"、CMS 报告的三层基线卡为空。
|
|
|
|
|
+ # 现在由两个生成端从**窗索引**算: component_history(逐窗标量 x_self → 在升/换件闭环, 窗不足时
|
|
|
|
|
+ # 如实为空并写明边界) / baseline_38(健康期分段中位 → 自基线 + fleet 基线 + ISO 绝对限)。
|
|
|
|
|
+ # ★要排在 ④b 之后 (读窗索引), 且在 ⑦ trend_ingest 之前。
|
|
|
|
|
+ plan.append(step_cmd('④d 振动在升/换件闭环 (窗索引 → component_history.json)',
|
|
|
|
|
+ [PY, 'scripts/component_history_build.py'], tolerate=(4,),
|
|
|
|
|
+ note='rc=4 = 本机没有振动窗索引 (没放 CMS 原始导出) ⇒ 本件按缺件如实报, 不是失败'))
|
|
|
|
|
+ plan.append(step_cmd('④e 三层基线 (窗索引 → baseline_38.json)',
|
|
|
|
|
+ [PY, 'scripts/baseline_38_build.py'], tolerate=(4,),
|
|
|
|
|
+ note='rc=4 = 本机没有振动窗索引 ⇒ 本件按缺件如实报, 不是失败'))
|
|
|
|
|
+ # ⑤b 事实契约的 claim 生成 (用户令 2026-09-19「要让观澜从重算台账生成 claim」):
|
|
|
|
|
+ # 由重算产物(alarms/workorders/oil/temp_bins/powercurve_dev/loss_monthly/L6/fusion)算 claim,
|
|
|
|
|
+ # 写成 sop/findings.json + paradigm_r1 三份底稿(与契约构建器同结构) ⇒ 契约/门户/问答三处随重算刷新。
|
|
|
|
|
+ # ★必须排在 ⑤a 台账维护之前: 这两步产出的件要进同一轮台账与审计, 否则审计会按"溯源缺失"报 rc=6。
|
|
|
|
|
+ plan.append(step_cmd('⑤b 事实契约: 由重算台账生成 claim', [PY, 'scripts/sop_findings_from_ledger.py'],
|
|
|
|
|
+ note='从重算产物生成; 判据与缺证据逐条写在 claim 里(相对判据封顶「候选」)'))
|
|
|
|
|
+ plan.append(step_cmd('⑤c 事实契约: 构建 + 渲染消费者', [PY, 'scripts/guanlan_facts_contract.py', 'build'],
|
|
|
|
|
+ tolerate=(2,),
|
|
|
|
|
+ note='rc=2 = 契约自检不过(sha/脱敏/枚举), 需要看输出; 门户结论段由 ⑧b 重装时刷新'))
|
|
|
|
|
+ # ⑤a 台账维护 (用户令 2026-09-18: 页面只认"有来路的件" ⇒ 台账必须跟着重算一起更新)。
|
|
|
|
|
+ # ★实逮: 2026-09-18 用户「清除产物」后从 raw 全量重算, `outputs/<场>/_provenance.json`
|
|
|
|
|
+ # **整本丢了** —— 因为写它的唯一入口 (旧的第⑤步 products_restore_missing.py) 已按用户令
|
|
|
|
|
+ # 从链上拿下, 而新的第⑤步只**读**台账不写。后果: 门户/页面的「逐件来源」、反向呼应审计的
|
|
|
|
|
+ # 族匹配全部退化成"未归类", 而盘上 1,728 件其实件件有生成端。故把 `--refresh`(纯账实相符,
|
|
|
|
|
+ # 不搬任何随包件) 作为**独立一步**放回链上, 位置在审计之前。
|
|
|
|
|
+ plan.append(step_cmd('⑤a 逐件来源台账 (账实相符: 谁在位、谁有生成端)',
|
|
|
|
|
+ [PY, 'scripts/products_restore_missing.py', '--refresh'],
|
|
|
|
|
+ note='只维护台账, **不搬任何随包件**(用户令 2026-09-17: 运行期不从交付包补齐); '
|
|
|
|
|
+ '盘上有而无人登记的件按族表归口, 归不上族的记 unregistered 并打出来'))
|
|
|
|
|
+ # ⑤ 反向呼应审计 (用户令 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:
|
|
|
|
|
+ # 只重启**组件服务**, 网关留着 —— 两个理由:
|
|
|
|
|
+ # ① 控制台页面是网关提供的, 连它一起停的话, 从页面点的重算会把自己的界面(甚至自己)弄没;
|
|
|
|
|
+ # ② rebuild_all 自己可能就是从控制台/网关起的任务, `guanlan.py stop` 用 taskkill /T,
|
|
|
|
|
+ # 连网关会**沿父子树把正在执行的重算一起杀掉**(2026-09-12 实测: 日志停在 stop 那行, 之后全无)。
|
|
|
|
|
+ # 组件停掉再 serve 就够: chain_ingest 要的是 detail(18033) 重新加载新产物, 网关不缓存产物。
|
|
|
|
|
+ plan.append(step_cmd('⑥ 重启组件服务 (保留网关, stop)', [PY, 'scripts/_ops_stop_keep_gateway.py']))
|
|
|
|
|
+ plan.append(step_cmd('⑥ 重启组件服务 (serve)', [PY, 'guanlan.py', 'serve'], tolerate=(1,),
|
|
|
|
|
+ note='serve 在"有模块 DOWN"时故意返回 1 —— degraded 是正常状态(例如本机模型未启动)'))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 码表/手册/文档', [PY, '-m', 'src.ontology.kb_ingest']))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 铺开判级与台账', [PY, '-m', 'src.ontology.populate']))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 决策链进度', [PY, '-m', 'src.ontology.chain_ingest']))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 在升/闭环证据', [PY, '-m', 'src.ontology.trend_ingest']))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 检索索引', [PY, '-c', 'from src.ontology import retrieval as R; R.build(use_vec=False)']))
|
|
|
|
|
+ plan.append(step_cmd('⑦ 本体: 实机参数表', [PY, '-c', 'from src.ontology.maintenance import refresh_params as f; print(f())']))
|
|
|
|
|
+ # ⑦b 总览「全场状态」页 (`outputs/<场>/windscada/index.html`): /detail/v2 的 iframe 目标。
|
|
|
|
|
+ # 用户令 2026-09-19「所有的计算均要形成观澜的源代码」——它此前是随包件(0 处写入方),
|
|
|
|
|
+ # 新机器上那块只能显示"产物不在位"。现在用组件同一个烘焙器从重算产物生成。
|
|
|
|
|
+ # ★必须排在 ⑦ 之后: 烤进去的响应里有本体数据(objects.json)。
|
|
|
|
|
+ plan.append(step_cmd('⑦b 全场状态总览页 (自算烘焙)', [PY, 'scripts/windscada_overview_build.py'],
|
|
|
|
|
+ note='烤的是组件同一套取数函数(见脚本头); 缺产物时会响亮报错, 不产出半张页'))
|
|
|
|
|
+ plan.append(step_cmd('⑧ 本体审计 (期望 0 问题)', [PY, '-m', 'src.ontology.audit']))
|
|
|
|
|
+ # ⑧b 重装门户: 门户正文里的"契约结论段"是**产物派生**的(由 guanlan_portal_inject_claims.py
|
|
|
|
|
+ # 从 outputs/<场>/guanlan/… 渲染), 所以产物变了门户必须重装一遍, 否则页面挂着旧结论。
|
|
|
|
|
+ # 装配是"先写临时文件再原子替换", 失败不会破坏正在被网关读的 portal.html (见 portal_build 注释)。
|
|
|
|
|
+ plan.append(step_cmd('⑧b 重装门户 (把新产物灌进门户结论段)', [PY, 'scripts/portal_build.py'],
|
|
|
|
|
+ tolerate=(1,),
|
|
|
|
|
+ note='rc=1 = 缺契约产物(产物被清除时属预期) ⇒ 门户保留原样, 不是装配坏了'))
|
|
|
|
|
+ # ⑧c 页面归口审计: 重算之后, 那些"把数据烘进页面"的页面件(见 configs/portal_pages.yaml)会不会陈旧。
|
|
|
|
|
+ # rc=7 = 有页面陈旧(数据换了页面没换) —— 不打断整条链, 但必须报出来, 处置见 docs §10。
|
|
|
|
|
+ # ★ rc=5 也容忍 (2026-09-17 实逮): rc=5 = "登记的文件/被引产物不在位", 而**包内没有生成端**的那批
|
|
|
|
|
+ # 随包件(如 windscada/index.html、m5_cms_tcm/tcm_index.parquet、windcms 页面件)在
|
|
|
|
|
+ # "清过产物 + 交付包不含产物"的机器上必然不在位 ⇒ 第 16 步判失败、整条链 rc=1, 而且把最后的
|
|
|
|
|
+ # ⑧台账等价验收也跳过了(实测 2026-09-17: 用户在页面上点重算, 1367 s 后 rc=1 断在这里)。
|
|
|
|
|
+ # 这与 ④/⑤/⑥/⑧b 的口径一致: 缺的是"随包件"这一路, 不是重算本身。缺件仍会由
|
|
|
|
|
+ # `guanlan.py check` 报 FAIL(那是给人看的告警位), 本步只保证"链不白断"。
|
|
|
|
|
+ plan.append(step_cmd('⑧c 页面归口审计 (页面陈旧检测)', [PY, 'scripts/pages_audit.py', '--check'],
|
|
|
|
|
+ tolerate=(5, 6, 7),
|
|
|
|
|
+ note='rc=7 = 有 data-derived 页面陈旧(内嵌快照的 source_sha256 与当前产物不一致): '
|
|
|
|
|
+ '需要对应生成端重跑, 或在登记表里把它改成 frozen-delivery 并接受会过期; '
|
|
|
|
|
+ 'rc=5 = 登记的文件/被引产物不在位(清过产物、且那批件没有生成端时属预期); '
|
|
|
|
|
+ '★rc=6 也容忍 (2026-09-19 实逮): rc=6 = 溯源缺失(产物在盘上/登记里, 但台账查不到来路) '
|
|
|
|
|
+ '—— 与 5 同属"缺的是随包件这一路", 例如 ④b 在 report 步崩掉时 '
|
|
|
|
|
+ 'vib_raw_manifest.json 没写成, 审计就按"溯源缺失"报 6 并把整条链判失败; '
|
|
|
|
|
+ '处置见 docs §7 与「数据重算」页的缺件提示; `guanlan.py check` 里仍有给人看的告警位'))
|
|
|
|
|
+ # ⑧d 版本记录一致性 (2026-09-22 实逮, 远端 2.9.1 升级+重算后验收才暴露):
|
|
|
|
|
+ # 那次升级的差异盘点只覆盖 src/ + scripts/ + configs/ —— **文档不在盘点面上**, 于是代码升到 2.9.1、
|
|
|
|
|
+ # docs/版本记录.md 与 docs/系统设计说明.md 留在旧版; 而重算链此前只到 ⑧c, 没有版本门 ⇒
|
|
|
|
|
+ # 24/24 步全 OK、控制台写"重算完成", 版本/文档漂移**没有任何自动拦截点**(只有人工 `guanlan.py check` 能逮)。
|
|
|
|
|
+ # 与 ⑧c 同口径: tolerate=6 —— 缺的是"文档同步"这一路, 不是重算本身 ⇒ 报出来但不打断整条链。
|
|
|
|
|
+ plan.append(step_cmd('⑧d 版本记录一致性 (docs 与 src/version.py)',
|
|
|
|
|
+ [PY, 'scripts/version_log.py', '--check'], tolerate=(6,),
|
|
|
|
|
+ note='rc=6 = docs/版本记录.md 的自动区块与 src/version.py 的 HISTORY 不一致 '
|
|
|
|
|
+ '(升级只同步了代码、没同步文档即属此类) ⇒ 跑 python scripts/version_log.py --write '
|
|
|
|
|
+ '重生成自动区块, 再把 docs/ 一并同步到目标机(升级差异盘点要含 docs/); '
|
|
|
|
|
+ '`guanlan.py check` 里仍有给人看的 FAIL 位'))
|
|
|
|
|
+ if a.with_verify:
|
|
|
|
|
+ # ★2026-09-20 改 (远端实逮, 2.8.0 在服务器上一轮 24/24 步全 OK 却整轮 rc=1):
|
|
|
|
|
+ # 原先 tolerate=(4,) —— rc=5("找不到随包基线 ⇒ 没验收") 被当成失败, 于是**交付包装出来的机器
|
|
|
|
|
+ # (按用户令包内不含产物 ⇒ 基线必然不存在)每次重算都以 rc=1 结束, 门户上写"重算失败"**,
|
|
|
|
|
+ # 而实质步骤全绿。这与 ④ 的口径自相矛盾(④ 容忍 5 并写明"跳过等价验收≠通过")。
|
|
|
|
|
+ # 现在: rc=4(有需人工看的差异, 本包已知项: 油样 102 行源件不在现场包) 与 rc=5(没基线=没验收)
|
|
|
|
|
+ # 都容忍, 但**都在汇总行里如实标 rc**、并在步说明里写清"这不是通过"; 想真做验收就让
|
|
|
|
|
+ # `scripts/set_baseline.py` 建一份基线(用已核实的产物快照 + 写明来历), 之后这一项才会真比对。
|
|
|
|
|
+ plan.append(step_cmd('⑧ 台账等价验收 (rc=4 = 有需人工看的差异; rc=5 = 没基线 ⇒ 没验收)',
|
|
|
|
|
+ [PY, 'scripts/rebuild_from_raw.py', '--verify'], tolerate=(4, 5),
|
|
|
|
|
+ note='rc=4 = 有需人工看的差异(本包已知项, 见 docs §7); '
|
|
|
|
|
+ 'rc=5 = 找不到随包基线 ⇒ **没做验收**(不是失败, 也不是通过)。'
|
|
|
|
|
+ '★ 交付包按用户令不含产物 ⇒ 全新机器上这一步必然 rc=5; 要恢复验收能力: '
|
|
|
|
|
+ '用 python scripts/set_baseline.py 一条命令把当前已核实的产物快照登记成基线, '
|
|
|
|
|
+ '再重算就能真比对'))
|
|
|
|
|
+ return plan
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main() -> int:
|
|
|
|
|
+ ap = argparse.ArgumentParser()
|
|
|
|
|
+ ap.add_argument('--src', default=None, help='现场数据包目录 (给了就先跑 place_raw_data --scope full)')
|
|
|
|
|
+ ap.add_argument('--auto', action='store_true',
|
|
|
|
|
+ help='先扫一遍输入数据(raw_scan.py),发现新数据落在被 --skip-* 跳过的族里时自动取消跳过'
|
|
|
|
|
+ '(用户令 2026-09-19「发现新增数据并纳入重算」)')
|
|
|
|
|
+ ap.add_argument('--skip-scada', action='store_true', help='跳过 SCADA 侧 10 个构建器 (没换 10min 数据时用)')
|
|
|
|
|
+ ap.add_argument('--skip-vib', action='store_true', help='跳过振动侧摄入 (没换 CMS 原始导出时用)')
|
|
|
|
|
+ ap.add_argument('--no-vib-report', dest='vib_report', action='store_false',
|
|
|
|
|
+ help='振动侧只摄入(索引/谱), 不生成 CMS 报告/逐台页 —— ★默认**生成**(2026-09-18 口径'
|
|
|
|
|
+ '翻转: 包里没有旧产物可参照, 页面要么有自算内容要么空白; 报告只缺 '
|
|
|
|
|
+ 'model_run/fusion 的融合级列, 如实写 —)')
|
|
|
|
|
+ ap.add_argument('--no-restart', action='store_true', help='不自动重启服务')
|
|
|
|
|
+ ap.add_argument('--with-verify', action='store_true', help='末尾加等价验收')
|
|
|
|
|
+ ap.add_argument('--dry-run', action='store_true', help='只打印计划')
|
|
|
|
|
+ a = ap.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ plan = build_plan(a)
|
|
|
|
|
+ print(f'重算全部 · {len(plan)} 步 · 根目录 {ROOT}')
|
|
|
|
|
+ print(f' 跳过 SCADA: {a.skip_scada} | 重启服务: {not a.no_restart} | 等价验收: {a.with_verify}\n')
|
|
|
|
|
+ if a.dry_run:
|
|
|
|
|
+ for name, cmd, tol, note in plan:
|
|
|
|
|
+ print(f' {name:34s} {" ".join(cmd[1:])[:90]}')
|
|
|
|
|
+ print('\n(dry-run, 未执行)')
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ # 子进程环境: 与 guanlan.py 给服务的一致 —— 中文 Windows 控制台是 GBK, 不设的话
|
|
|
|
|
+ # 子脚本 print 里的 ✔/✗ 会抛 UnicodeEncodeError, 明明干成了却以退出码 1 结束。
|
|
|
|
|
+ env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8')
|
|
|
|
|
+ results, t_all = [], time.time()
|
|
|
|
|
+ for i, (name, cmd, tol, note) in enumerate(plan, 1):
|
|
|
|
|
+ print(f'\n{"=" * 78}\n[{i}/{len(plan)}] {name}\n$ {" ".join(cmd)}\n{"=" * 78}', flush=True)
|
|
|
|
|
+ t0 = time.time()
|
|
|
|
|
+ rc = subprocess.run(cmd, cwd=str(ROOT), env=env).returncode
|
|
|
|
|
+ dt = time.time() - t0
|
|
|
|
|
+ ok = (rc == 0) or (rc in tol)
|
|
|
|
|
+ results.append((name, rc, dt, ok, tol))
|
|
|
|
|
+ if not ok:
|
|
|
|
|
+ print(f'\n[X] 第 {i} 步失败 (退出码 {rc}): {name} —— 后面的步骤依赖它, 先停下。')
|
|
|
|
|
+ if note:
|
|
|
|
|
+ print(f' 该步说明: {note}')
|
|
|
|
|
+ break
|
|
|
|
|
+ if rc != 0:
|
|
|
|
|
+ print(f'\n (退出码 {rc} 属预期{(": " + note) if note else ""})')
|
|
|
|
|
+
|
|
|
|
|
+ print(f'\n{"=" * 78}\n汇总 (总耗时 {time.time() - t_all:.0f}s)')
|
|
|
|
|
+ for name, rc, dt, ok, tol in results:
|
|
|
|
|
+ tag = 'OK' if rc == 0 else (f'OK(rc={rc})' if ok else 'FAIL')
|
|
|
|
|
+ print(f' [{tag:9s}] {name:34s} {dt:7.1f}s')
|
|
|
|
|
+ n_fail = sum(1 for *_, ok, _ in results if not ok)
|
|
|
|
|
+ if n_fail:
|
|
|
|
|
+ print(f'\n结论: {n_fail} 步失败 —— 按上面的日志处理后再重跑本命令 (各步都是幂等的)')
|
|
|
|
|
+ return 1
|
|
|
|
|
+ print('\n结论: 全部完成。核验: .venv\\Scripts\\python.exe -m src.ontology.maintenance '
|
|
|
|
|
+ '(数据层/机理层的条数与末次更新); 浏览器 http://127.0.0.1:28084/detail/')
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ for _s in (sys.stdout, sys.stderr):
|
|
|
|
|
+ try: _s.reconfigure(errors='replace')
|
|
|
|
|
+ except Exception: pass
|
|
|
|
|
+ sys.exit(main())
|