Bläddra i källkod

2.10.0 交付文档三件(需求分析/设计说明/数据要求说明)+源代码化生成器(渲染器/图表器)+可移植性与台账消缺

zhouyang.xie 2 veckor sedan
förälder
incheckning
c48ef1d84b

+ 2 - 2
docs/detail_页面依赖与重算台账_v0.1.md

@@ -6,7 +6,7 @@
 <!-- DETAIL-DEPS:BEGIN -->
 # /detail 页面依赖与重算台账 (自动生成, 禁手改)
 
-> 生成端: `python scripts/detail_deps.py --write`|场站 `rudong`|产物仓 `outputs/rudong`|盘上 3,548 件
+> 生成端: `python scripts/detail_deps.py --write`|场站 `rudong`|产物仓 `outputs/rudong`|盘上 3,549 件
 >
 > 用户令 (2026-09-18): **/detail 各页面不许用旧版产出补**, 只能参照旧版产物的呈现样式与内容, 数值必须由 `data/raw` 重算。本表把每页的取数接口与产物件逐条列清, 缺的写明怎么补。
 
@@ -48,7 +48,7 @@
 |  | `windscada/loss_monthly.parquet` ✅必需 | 1 | 0.1 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
 |  | `windscada/powercurve_bins.parquet` ✅必需 | 1 | 0.0 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
 |  | `windscada/powercurve_dev.parquet` ✅必需 | 1 | 0.0 MB | raw-derived | src/windscada/perf/powercurve.py (scada_derived) | 在位 |
-| /api/fleet | `windscada/*.parquet` ○可选 | 55 | 577.9 MB | (未登记)·raw-derived | scripts/windscada_alarms_ingest.py (scada_alarms·scada_derived·scada_oil·scada_slim10min) | 在位 |
+| /api/fleet | `windscada/*.parquet` ○可选 | 55 | 592.1 MB | (未登记)·raw-derived | scripts/windscada_alarms_ingest.py (scada_alarms·scada_derived·scada_oil·scada_slim10min) | 在位 |
 |  | `ontology/turbine_params.parquet` ○可选 | 1 | 0.1 MB | raw-derived | src/ontology/kb_ingest.py (ontology_core) | 在位 |
 |  | `m5_cms_tcm/handoff_vibration_v2.json` ○可选 | 1 | 0.1 MB | raw-derived | scripts/rudong_fusion_handoff.py (vib_fusion_handoff) | 在位 |
 |  | `m5_cms_tcm/fleet_scalar_z.parquet` ○可选 | 1 | 0.7 MB | raw-derived | scripts/rudong_fusion_run.py (vib_fleet_scalar_z) | 在位 |

BIN
docs/figures/fig-dat-01-数据族体量.png


BIN
docs/figures/fig-dat-02-数据到产物.png


BIN
docs/figures/fig-dat-03-收资项覆盖.png


BIN
docs/figures/fig-dat-04-时间覆盖.png


BIN
docs/figures/fig-des-01-总体架构.png


BIN
docs/figures/fig-des-02-端口与进程.png


BIN
docs/figures/fig-des-03-重算链.png


BIN
docs/figures/fig-des-04-判级与融合.png


BIN
docs/figures/fig-des-05-时间窗与pending.png


BIN
docs/figures/fig-des-06-质量门闭环.png


BIN
docs/figures/fig-req-01-需求演进时间轴.png


BIN
docs/figures/fig-req-02-角色场景页面.png


BIN
docs/figures/fig-req-03-页面归口分布.png


BIN
docs/figures/fig-req-04-验收门分类.png


+ 1159 - 0
docs/src/数据要求说明_观澜_2.10.0.md

@@ -0,0 +1,1159 @@
+# 数据要求说明 · 观澜·如东样板 v2 · 版本 2.10.0
+
+## 1 文档说明
+
+### 1.1 目的
+
+本文说明「观澜·如东样板 v2」(版本 2.10.0,海上风电场智能分析离线系统)运行、判级与出件所**实际需要**的输入数据:需要哪些数据分类、每一类里需要哪些测点(字段)、每个测点是什么度量单位、以及这一类到底是**必须**还是**可替代**。
+
+本文的读者可以直接拿它去现场收资,也可以拿它逐条核对「现场已经给了什么、还缺什么、缺了会怎样」。
+
+### 1.2 读者
+
+**表 1-1 本文读者的使用方式**
+
+| 读者 | 怎么用本文 |
+|---|---|
+| 现场数据管理员 | 按第 8 章与第 11 章的清单收资、按第 7 章的流程落位、按第 10 章核对锚点 |
+| 观澜实施与运维人员 | 按第 3 章判断某一族数据有没有消费者、按第 7 章判读体检退出码 |
+| 分析人员 | 按第 5 章确认某个测点是否在契约内、单位与量纲是否正确、哪些列没有取证 |
+| 交付与验收人 | 按第 9 章确认每一条缺口是"现场补件"还是"研发补生成端",按附录 A 追到依据 |
+
+### 1.3 与《数据分析收资要求 v3》的关系
+
+现场《数据分析收资要求-v3.docx》给出 **21 类**收资项,是**现场视角的"要什么"**。本文是**观澜视角的"怎么用"**,两者是同一件事的两面:
+
+- 收资要求 v3 的原文逐字列在附录 C,一条不改、一条不删;
+- 第 8 章把 21 条逐条对照到观澜的数据族、落位目录、消费者脚本与到位状态,并给出缺口与催缴要点;
+- 收资要求 v3 没有区分"必须/可选",本文补上这一层——因为观澜对 21 类的依赖强度**并不相同**:有的缺了整个系统会响亮降级(页面该处显示"缺件"),有的缺了只影响一条辅助轴。
+
+一句话口径:**收资要求 v3 是收资面,本文是接入面与依赖面。**
+
+### 1.4 必须性分级定义
+
+**表 1-2 必须性分级定义**
+
+| 分级 | 含义 | 缺了会怎样 |
+|---|---|---|
+| 必须 | 观澜的核心分析面(判级、发电性能、可靠性、台账)直接依赖;缺则该面不可判或整步降级 | 页面该面显示"缺件/不可判",重算链对应步骤响亮报错或跳过,**不造数** |
+| 建议 | 支撑交叉校验与二次确认;缺了不阻断主线,但会削弱结论的可信度与可追溯性 | 结论仍在,但依据变少,跨源互证做不了 |
+| 可选 | 增强型输入,只在特定专题里用;不参与常规重算 | 只有对应专题不可做 |
+| 替代 | 可由观澜**自算件**或**已在位的其他数据**顶替,且替代口径必须写在产物与依据里 | 用替代件并如实标注来源,现场正本在位时正本优先 |
+
+### 1.5 口径与依据
+
+1. **单位与测点清单以机器可读真源为准**:`configs/canonical/dictionary.yaml`(**778 条** canonical 测点,字段为 `system`、`members`(契约列名)、`n_contract_columns`、`unit_majority`、`unit_variants`)与 `configs/contracts/siemens_swt40_rudong.yaml`(如东 SWT-4.0-130 字段契约)。**查不到的写"未取证",绝不凭常识补单位。**
+2. **落位与目录以** `docs/数据目录结构与落位约定_v0.2.md` 与 `docs/输入数据放置指导_v0.1.md` 为准。
+3. **体量、件数、行数均以本机 `data/raw/如东` 与 `outputs/rudong` 只读实测为准**(2026-09-22 实测)。
+4. 本文中"含窗且确指时间窗口"之处一律写**时间窗**;影响机组数量之处写**影响机组/影响机组数**;英文简写一律写"中文(英文简写)"。
+5. 全文单位用半角,温度用 `℃`,旋转速度用 `r/min`(= 机器可读真源里的 `rpm`,两者同量纲)。
+
+## 2 数据接入总览
+
+### 2.1 观澜运行所需的输入全景
+
+观澜不直接读现场原始件,而是先由**摄入链**把 `data/raw/<场站>/` 下的源件转成 `outputs/<场站>/` 下的产物,页面再读产物。因此"输入"分两层:**源件层**(现场交什么)与**产物层**(系统算什么)。
+
+源件层的落位约定只有一条判据:**摄入脚本按"源类目录 → 再往下一层就是文件"读**。多套一层包名目录(例如 `scada_10min\10分钟数据\WTG01.csv`)等于没放。
+
+如东样板当前在位 9 个族(含与场站并列的共享技术资料族),**如表 2-1 所示**。
+
+**表 2-1 在位数据族的件数与体量(依据 `data/raw/如东` 只读实测)**
+
+| 族 | 落位目录 | 当前件数 | 当前体量 |
+|---|---|---|---|
+| 10 分钟 SCADA | `data/raw/如东/scada_10min/` | 114 | 16,749,788,062 B(约 15.6 GB) |
+| 1 分钟 SCADA | `data/raw/如东/scada_1min/` | 38 | 13,709,173,574 B(约 12.8 GB) |
+| 现场年度归档库 | `data/raw/如东/scada_mdb/` | 100 | 48,247,200,121 B(约 44.9 GB) |
+| 故障报警 | `data/raw/如东/故障报警/` | 16 | 23,844,357 B(约 22.7 MB) |
+| 风机故障记录 | `data/raw/如东/风机故障记录/` | 137 | 149,795,133 B(约 142.9 MB) |
+| 油样报告 | `data/raw/如东/油样报告/` | 404 | 176,226,585 B(约 168.1 MB) |
+| CMS 振动原始导出 | `data/raw/如东/windcms/` | 25,693 | 161,219,600,707 B(约 150.1 GB) |
+| 振动线 handoff | `data/raw/如东/m5_cms_tcm/` | 1 | 21,823,545 B(约 20.8 MB) |
+| 厂商技术资料 | `data/raw/西门子4.0技术资料/` | 318 | 2,936,758,089 B(约 2.7 GB) |
+
+**如图 2-1 所示**,体量高度集中在 CMS 振动导出(约 150.1 GB,占九族合计 223.8 GB 的绝大多数)与两类 SCADA 导出上,台账类三族合计不到 0.2 GB 却承担了报警、工单、油样三条台账的**逐行对齐**。
+
+![图 2-1 输入数据族件数与体量(依据 data/raw/如东 实测)](figures/fig-dat-01-数据族体量.png)
+
+场站目录下另有 1 件散装件 `scada_10min_202607.csv`(943,520,249 B):它是**按月提取/核对件**,不是摄入源。扫描器按名字模式忽略它(不报"未归类"、不计变化),真正进产物的是 `scada_10min/WTG01.csv` 一类的逐台主件与同台补充件。
+
+### 2.2 数据 → 功能 → 产物
+
+**如图 2-2 所示**,观澜的链路是"源件 → 摄入/构建 → 产物 → 页面/交付件",每一环都能指到脚本与产物。
+
+![图 2-2 数据 → 功能 → 产物 流向](figures/fig-dat-02-数据到产物.png)
+
+### 2.3 族 → 支撑功能 → 产物
+
+**表 2-2 族 → 支撑功能 → 产物**
+
+| 观澜数据族 | 支撑的观澜功能 | 主要产物 |
+|---|---|---|
+| `scada_10min` | 判级四轴(变桨/偏航/蓄能/温度)、发电性能七镜头、功率曲线偏差、损失电量、停机事件、热链与冷却执行器、控制参数一致性 | `temp_bins`、`temp_monthly`、`powercurve_bins`、`powercurve_dev`、`loss_monthly`、`control_profile`、`control_schedule`、`stop_events`、`yaw_daily`、`hydraulic_accum`、`thermal_chain`、`system_aux`、`curve_lenses`、`curve_liveness` |
+| `scada_1min` | 变桨面零位三口径与越线计数 | `pitch/**`(日粒度液压/润滑/柱塞 + 零位三口径月表) |
+| `scada_mdb` | SCADA 侧的**上游归档**与 CSV 缺失时的回落源;按类目交付的月度通道组是"同台 10 分钟补充件"的上游 | 同 `scada_10min`(回落路径) |
+| `故障报警` | 报警集中、停机账、限电绑定、首发链 | `alarms.parquet` |
+| `风机故障记录` | 检修工单台账、重复检修与停机损失、闭环验证 | `workorders.parquet` |
+| `油样报告` | 油液化验索引、油液时效胶囊、融合面油样轴 | `oil_samples_index.parquet` |
+| `windcms` | 振动窗索引、谱库、CMS 报告与逐台页、在升与换件闭环、三层基线 | `m5_cms_tcm/windows/<窗>/index.parquet`、`spectra/*.npz`、`spectra_meta.parquet`、`windcms/报告_CMS*` |
+| `m5_cms_tcm` | 融合面判级、TCM 兼容件;现场正本优先于观澜自算件 | `handoff_vibration_v2.json`、`component_history.json`、`fusion_38.csv`、`fleet_scalar_z.parquet` |
+| `西门子4.0技术资料` | 本体对象库、检索索引、实机参数表(整定值)、故障码对译 | `ontology/objects.json`、`retrieval_index.json`、`ontology/turbine_params.parquet` |
+
+## 3 数据分类总表
+
+**如表 3-1 所示**,观澜的每一个数据族都能追到收资要求 v3 的序号、落位目录、消费者脚本、必须性与当前到位状态。
+
+**表 3-1 观澜数据族总表**
+
+| 观澜族 | 收资 v3 序号 | 落位目录 | 消费者脚本 | 必须性 | 当前到位状态 |
+|---|---|---|---|---|---|
+| `scada_10min` | 1、9 | `data/raw/如东/scada_10min/` | `scripts/rebuild_from_raw.py --scada`、`scripts/scada_slim_build.py` | 必须 | 已在位(38 台主件 + 76 件同台补充件) |
+| `scada_1min` | 2 | `data/raw/如东/scada_1min/` | `scripts/pitch_face_build.py` | 建议 | 已在位(38 件,内部台号 01E 至 38B) |
+| `scada_mdb` | 1、2、9 | `data/raw/如东/scada_mdb/` | `src/windscada/scada_source.py`、`scripts/csv_to_mdb.py` | 建议 | 部分到位(25 年 12 个月 12 类;26 年 1 至 8 月,7 月缺 10 个类目库) |
+| `故障报警` | 7 | `data/raw/如东/故障报警/` | `scripts/windscada_alarms_ingest.py` | 必须 | 已在位(16 件季度/年度导出) |
+| `风机故障记录` | 7、18 | `data/raw/如东/风机故障记录/` | `scripts/windscada_workorder_ingest.py` | 必须 | 已在位(137 件,123 件被摄入) |
+| `windcms` | 8 | `data/raw/如东/windcms/` | `scripts/rudong_tcm_index.py`、`scripts/rudong_tcm_spectra.py`、`scripts/vib_raw_build.py` | 必须 | 已在位(25,679 件 decode 导出 + 14 件厂家报告) |
+| `m5_cms_tcm` | 8 | `data/raw/如东/m5_cms_tcm/` | `scripts/rudong_fusion_handoff.py`、`scripts/component_history_build.py` | 替代 | 部分到位(现场仅有 1 份厂家 docx,handoff 正本缺失,用观澜自算件顶替) |
+| `油样报告` | 18 | `data/raw/如东/油样报告/` | `scripts/windscada_watch_channels_build.py` | 建议 | 已在位(404 件,两级目录 台号/部件) |
+| `西门子4.0技术资料` | 5、7、10、12、13、15、20、21 | `data/raw/西门子4.0技术资料/` | `python -m src.ontology.kb_ingest`、`src/ontology/maintenance.py` | 必须 | 已在位(318 件 2.7 GB,含定值表与结构树) |
+
+## 4 逐类数据要求
+
+本章逐类给出:用途、最小可用量(时间跨度、采样频率、台数)、必需测点或字段、单位、格式与编码、落位目录、到位现状、缺失影响。
+
+### 4.1 `scada_10min`(10 分钟 SCADA 数据)
+
+**用途**:观澜的**主数据层**。判级四轴(变桨、偏航、蓄能、温度)、发电性能七镜头、功率曲线偏差、损失电量、停机事件、热链与冷却执行器、控制参数一致性全部由它派生。
+
+**最小可用量**:38 台,逐台一个文件,采样周期 600 s;时间跨度**建议不少于 21 个月**(判级用的 `ref` 固定窗为 2025-07-01 至 2025-12-31,`ref_sm` 季节匹配窗为同历月早一年)。本机实测主件覆盖 2025-01-01 00:00:00 至 2026-07-07 00:00:00,同台补充件覆盖 2026-08-01 00:00:00 至 2026-09-01 00:00:00。
+
+**必需测点或字段**:见第 5 章。判级与曲线实际取用 **48 列公共子集**(第 5.2 节),其中功率、风况、转速、桨距、温度 `_mean` 27 列、偏航压力、液压压力、润滑与限电命令族是必需项。
+
+**单位**:功率 kW、风速 m/s、转速 r/min、桨距角 °、温度 ℃、压力 bar、时间 s、计数 次。
+
+**格式与编码**:`.csv`,平铺、每台一个文件,文件名 = 场配置里的机组号(`WTG01.csv` 至 `WTG38.csv`);同台补充件命名 `<台号>-<YYYYMM>.csv` 或 `<台号>-B2.csv`,取数层按**列名**对齐(新导出去掉了类型前缀,例如 `din_wtc_X` 变成 `wtc_X`)并按时间戳去重(主件优先)。编码 `utf-8-sig`。时间戳 `YYYY-MM-DD HH:MM:SS`。实测主件 598 列(3 列元信息 + 595 通道),补充件 856 列。
+
+**落位目录**:`data/raw/如东/scada_10min/`。
+
+**到位现状**:已在位。38 件主件 + 76 件同台补充件 = 114 件,逐台行数一致(WTG01 主件 77,551 行),说明全台同步断录、无单台缺失。
+
+**缺失影响**:缺台则"按台取数"为空白(页面显示空白,不报错);整族缺失则第 ③、④、④c、⑤b 四步全部无输入,判级矩阵、七镜头曲线、损失账、停机账、温度月轨迹同时不可用,**影响机组数 = 38 台**。
+
+### 4.2 `scada_1min`(1 分钟 SCADA 数据)
+
+**用途**:变桨面零位三口径与越线计数(`scripts/pitch_face_build.py`)。它是 1 分钟转发层,风速是**补偿风**(`WMET_wind_speed_compensated`,与原生 `SecAnemo` 差约 −1.35 m/s),因此**不能**拿它做功率曲线或湍流度(NTF)分析。
+
+**最小可用量**:38 台,采样周期 60 s;本机实测覆盖 2025-01-01 00:00:00 至 2026-06-30 23:59:00(01E 实测 785,250 行)。
+
+**必需测点或字段**(契约 `turbine_1min.required`):`active_power`(有功功率,kW)、`wind_velocity`(补偿风速,m/s)、`rotor_speed`(叶轮转速,r/min)、`pitch_angle_blade_1`(1 号叶片桨距角,°)。
+
+**单位**:同 4.1;桨距角 °、桨叶油压 bar。
+
+**格式与编码**:`.csv`,平铺,文件名为**内部台号**(`01E.csv` 至 `38B.csv`,字母 A 至 F 为集电线路 string);`utf-8-sig`;实测 79 列中英混合转发点表;时间戳 `YYYY-MM-DD HH:MM:SS`。
+
+**落位目录**:`data/raw/如东/scada_1min/`。
+
+**到位现状**:已在位(38 件)。但**全场级死列 16 条**已实证(含 `yaw_error1`、`cabin_position`、`twisted_cable_angle`、发电机三相绕组温度 1/2/3、1 至 3 号桨电机温度、自定义可利用率、航空灯状态 AI 等),这些列在本场 1 分钟层没有可用的时序信息。
+
+**缺失影响**:缺则变桨面退化为只用 10 分钟层,零位三口径不可判;**影响机组数 = 38 台**。
+
+### 4.3 `scada_mdb`(现场类目归档库)
+
+**用途**:现场按"月份 × 通道组"交付的 Access 归档库。它是 `scada_10min` 同台补充件的**上游**:取数层不直接读类目库,必须先合成同台 10 分钟补充件 CSV。同时,当某台没有 CSV 时,取数层回落到 `scada_mdb/<年>年/<月>月/*-10min.mdb`。
+
+**最小可用量**:按月的 10 分钟库(`<年>-<月>-10min.mdb`)优先;类目库按需。本机实测 2025 年 12 个月齐、2026 年 1 至 8 月齐。
+
+**必需测点或字段**:9 个逐台类目与主件**前缀块一一对应**:`tur` 91、`din` 93、`dot` 69、`cnt` 69、`tmp` 116、`prs` 52、`grd` 50、`flg` 42、`int` 13,合计 **595 通道,0 缺 0 多**。另有 `scd`、`std`、`sum` 三类是 farm 级与台账级,不在逐台 10 分钟布局内。
+
+**单位**:同 4.1。注意 MDB 存 double、时间写 `MM/DD/YYYY HH:MM:SS`;现场 CSV 导出是 float32、时间写 `YYYY-MM-DD HH:MM:SS+00:00`,合成补充件时按 **float32 + ISO** 对齐。
+
+**格式与编码**:`.mdb` 或 `.zip`(年度归档 `<年>年.zip`,内层 `<年>/<月>/<年-月-类>.zip` → `.mdb`)。中文类目目录名 ↔ 类目码:`风机数据`=tur、`温度数据`=tmp、`压力数据`=prs、`电网数据`=grd、`DigiIn数据`=din、`DigiOut数据`=dot、`计算数据`=cnt、`标志数据`=flg、`内部数据`=int、`统计数据`=scd、`标准数据`=std、`汇总数据`=sum。
+
+**落位目录**:`data/raw/如东/scada_mdb/<年>年/<月>月/<年>-<月>-<类>.mdb`。
+
+**到位现状**:部分到位。原始件 100 件、44.9 GB;体检器报 **24 件缺口**,集中在 `2026年/07月/` 的 10 个类目库(cnt、din、dot、flg、grd、int、prs、scd、std、sum 等)。2026-07 的 10min 库与 tur/tmp 在位。
+
+**缺失影响**:取数层会回落到 CSV,**当前不影响页面数值**;但"按类目逐列回溯到现场原始寄存器"这条链路对缺的类目不可用。另外,直接读类目库会覆盖不到 `scd`/`std`/`sum` 三类台账级数据。
+
+### 4.4 `故障报警`(报警事件导出)
+
+**用途**:报警台账(报警集中、停机账、限电绑定、首发链)。
+
+**最小可用量**:覆盖分析时间窗的年度或季度导出即可;本机实测 16 件,含 `2025Q1.xls` 至 `2025Q4.xls`、`2025年全年故障记录.xls`、`2026.1-3.xls`、`2026.4.xls`、`2026.5.xls` 等。
+
+**必需测点或字段**:每行必须带 `TimeOn`(激活时刻)与 `Alarmcode`(报警码);缺这两项的统计报表件会被摄入器**跳过**(不是报错)。「全年/年至今」累计快照件也会被自动跳过,避免同一事件算两遍。
+
+**单位**:时间戳为秒级绝对时刻;时长 s;报警码为无量纲编码(`code`)。
+
+**格式与编码**:`.xls`(实为 SpreadsheetML XML)、`.xlsx`、`.xml`;不许多套一层目录。
+
+**落位目录**:`data/raw/如东/故障报警/`。
+
+**到位现状**:已在位。产出 `alarms.parquet` 9 列、**39,211 行**(与核对锚点一致)。
+
+**缺失影响**:缺则报警台账为 0 行,停机事件的"原因链"轴、限电绑定、首发链全部不可用;`stop_events` 的报警窗计数降级。
+
+### 4.5 `风机故障记录`(检修工单台账)
+
+**用途**:检修工单台账(重复检修、停机损失、闭环验证)。
+
+**最小可用量**:覆盖分析时间窗的月度汇总表;本机实测 137 件(123 件被摄入,14 件是 `.rar` 与现场照片等附件,不摄入)。按年目录分组(2021 年至 2026 年)另加"业主统计故障"等。
+
+**必需测点或字段**(模板表表头):必须含 `机组编号`、`故障名称`、`故障代码`;表里 `风场名称` 必须属本场(集团导出件混着十来个场)。摄入器按第 3 行为列名、数据从第 4 行起读。
+
+**单位**:`发电量损失(kWh)` 为 kWh,`维修成本(元)` 为元,`故障停机时间`、`维修时间` 为时间;其余为文本。
+
+**格式与编码**:`.xls`、`.xlsx` 为摄入件;`.rar`、`.jpg`、`.png` 为附件,保留但不摄入。年目录 + 月度表,不限层级。
+
+**落位目录**:`data/raw/如东/风机故障记录/`。
+
+**到位现状**:已在位。产出 `workorders.parquet` 26 列、**5,876 行**(与核对锚点一致)。
+
+**缺失影响**:缺则检修面与闭环验证不可用,油样与工单的交叉核对做不了。
+
+### 4.6 `windcms`(CMS 振动原始导出)
+
+**用途**:振动侧的**唯一可重算源件**。索引、谱库、CMS 报告与逐台页、在升与换件闭环、三层基线都由它算。
+
+**最小可用量**:**至少一个季度**的秒级振动数据(非加密)及原始振动时域波形。本机实测只有 `CMS_RuDong_CGN_202603-04`(2026-03-16 17:27:22 至 2026-04-21 10:18:33,约 36 天),**不足一个季度**。
+
+**必需测点或字段**(9 个传感器/测点,实测):`Main_bearing_front`(主轴前)、`Main_bearing_rear`(主轴后)、`Gear_planet`(行星级)、`Gear_IMS`(中间轴)、`Gear_HS_generator_side`(高速轴发电机侧)、`Gear_HS_rotor_side`(高速轴叶轮侧)、`Generator_DE`(发电机驱动端)、`Generator_NDE`(发电机非驱动端)、`System Monitor`(系统监视)。转速类量(`rpm`、`RpmProfile`、`rawRPM`)在索引里,是叶轮 1P 与阶次跟踪的分母。
+
+**单位**:振动量纲见第 5.4 节,`y_unit` 实测取值 `1`、`m/s`、`m/s²`、`RPM`、`%`;频率轴 `x_unit` 为 `Hz`;波形时间轴 `x_unit` 为 `s`;灵敏度 mV/g(`sensitivity_mvpg`)。
+
+**格式与编码**:`*_decode.json`(Brande TCM Enterprise 导出,每件是一次 API 响应 `{body:{body:{"<时间戳>":[{"Record":{...}}]}}}`);厂家报告为 `.docx`(有文本层,可摄入)或 `.pdf`(扫描件,只归档)。`measurement/<年>/<月>/<WTGxx>/` 层级可保留,摄入按 `rglob` 找 `*_decode.json`。
+
+**落位目录**:`data/raw/如东/windcms/`;厂家报告放 `data/raw/如东/windcms/厂家报告/上海电气_月度/`。
+
+**到位现状**:已在位(25,679 件 decode 导出 + 14 件厂家报告)。产出窗 `w0316` 索引 **2,066,686 行 × 54 列**、谱 **420,742 条 / 1,701 个 npz 分片 / 3.15 GB**。
+
+**缺失影响**:缺则振动侧无源可算。**具体缺口**:2026-01 的 decode 导出(`w0127` 窗)不在本机,导致叶片 1P 那一族无法重建;`w0707`、`w0711`、`w0724`、`w0805`、`w0811` 五个窗的导出件也不在本机。
+
+### 4.7 `m5_cms_tcm`(振动线 handoff 正本)
+
+**用途**:融合面判级、TCM 兼容件。现场若给出 `handoff_vibration_v2.json` 与 `component_history.json` **正本**,放这里即被优先采用。
+
+**最小可用量**:两件正本各一份;本机实测现场只给了 1 件厂家 docx(大生科技传动链振动分析报告)。
+
+**必需测点或字段**:正本结构含 `meta`(版本/日期/来源/取代关系/原则)、`detectability_prior`、`cross_source_requests`、`baseline_三层`、`per_turbine`(人工裁决 + `claim_window`)、`field_actions_v1`、`calibration_updates_v1`、`open_items`、`tcm16_schedule`。
+
+**单位**:本件是判断与待办,不是测量量;内含的判级为枚举。
+
+**格式与编码**:`.json`、`.docx`、`.pdf`、`.md`。
+
+**落位目录**:`data/raw/如东/m5_cms_tcm/`。
+
+**到位现状**:**部分到位**。正本缺失,由观澜自算件顶替:`scripts/rudong_fusion_handoff.py`(融合面)与 `scripts/component_history_build.py`(在升与换件闭环)。本机只有 1 个导出色窗,故在升与闭环**如实为空**并把数据边界写进 `meta.★数据边界`。
+
+**缺失影响**:`/detail` 工作台的**振动融合面**(`/api/fleet` 的 `fus`)如实报"缺件";CMS 报告与评估页那一面已由按口径重建的两步喂上。
+
+### 4.8 `油样报告`(油液化验报告)
+
+**用途**:油液化验索引、油液时效胶囊、融合面油样轴。
+
+**最小可用量**:覆盖分析时间窗的逐台逐部件报告;本机实测 404 件、涉及 38 个台号目录与多个部件目录。
+
+**必需测点或字段**:摄入器从**文件名**抽字段(`DDMMYYYY_sampleid_项目_台号_部件`),不读 PDF 正文。因此文件名必须含日期、台号、部件。
+
+**单位**:无(索引为文本与日期);油液化验数值本身在 PDF 正文内,观澜不解析。
+
+**格式与编码**:`.pdf`,**两级目录** `<台号>/<部件>/*.pdf`。
+
+**落位目录**:`data/raw/如东/油样报告/`。
+
+**到位现状**:已在位(404 件)。产出 `oil_samples_index.parquet` **404 行**。**已知缺口**:华标 2026-07 批 102 行(合并报告 `BG-2026-07-YP013 中广核新能源如东海上风电场.pdf`)的源件不在现场包内,从零重算会从 506 行落到 404 行。
+
+**缺失影响**:缺则油液面不可用;油液与工单的交叉核对("换油是否紧跟检修")做不了。
+
+### 4.9 `西门子4.0技术资料`(厂商技术资料)
+
+**用途**:本体知识层(对象库、检索索引、实机参数表)与故障码对译。
+
+**最小可用量**:至少必须含 `kb_ingest.py` 点名的四个台账文件:`故障处理/故障处理手册.xlsx`、`故障处理/广核如海风电场西门子风机故障代码中英文对译表.xlsx`、`如海故障代码表.xlsx`、`维护相关/维护作业指导书.xlsx`。缺任一个都会打印 `⚠ 源缺失` 并降级,**不静默**。
+
+**必需测点或字段**:故障码(`WALM_fault_code`)、整定值(`Turbine+Parameters.*.xlsx` 的 `Number`、`Text`、`Min`、`Max`、`Default`、`Current`、`Value`、`Unit`、`Reset`)、图纸与手册的章节结构。
+
+**单位**:整定值表的 `Unit` 列实测 51 种取值,含 `℃`、`min`、`s`、`hrs`、`mHz`、`°`、`°/s`、`RPM`、`m/s`、`%`、`ms`、`Hz`、`kW`、`rpm`、`ccm`、`h`、`bar`、`V`、`A`、`m` 等。
+
+**格式与编码**:`.pdf`、`.docx`、`.doc`、`.xlsx`、`.xls`、`.pptx`;整棵树都读(白名单为全部)。
+
+**落位目录**:`data/raw/西门子4.0技术资料/`(与场站目录**并列**的共享资料,不在场站目录下)。
+
+**到位现状**:已在位(318 件、2.7 GB、63 个子目录),含 `广核如东海上风电场机组定值表.pdf`(22,614,546 B)、`上海电气4.0MW保护定值参数.pdf`(975,661 B)、`风机参数设定表.xls`、`上海电气+SWT4.0-130+结构树文件+江苏如海.doc`(2,609,152 B)。产出 `turbine_params.parquet` **1,706 条整定值**。
+
+**缺失影响**:缺则本体层降级(对象库与检索索引内容缺失),故障码对译与保护定值轴不可用。
+
+### 4.10 `机型与部件参数`
+
+**用途**:机型常量(额定功率、传动比、轮毂高度、叶轮直径)与部件参数(轴承、齿轮、齿数)是振动的分母、功率曲线的基准、控制参数的参照。
+
+**最小可用量**:一份机型级参数表 + 一份部件级参数表(主轴轴承、齿轮箱各级、发电机轴承、叶片、变桨与偏航电机与大小齿齿数)。
+
+**必需测点或字段**:见第 5.6 节。
+
+**单位**:kW(功率)、m(长度/直径/高度)、无量纲(传动比)、℃(温度)、°(角度)、m/s(风速)。
+
+**格式与编码**:`.docx`、`.pdf`、`.xlsx`,或已固化的 `.json`(`reference/rudong/oem_bearing_freqs.json` 29 个轴承条目、`reference/rudong/gear_mesh_map_measured.json`、`reference/rudong/oem_scan_plan.json` 11 个部件)。
+
+**落位目录**:随族落位(`西门子4.0技术资料/` 或场站族目录)。
+
+**到位现状**:部分到位。机型级常量已在位(额定功率 4000 kW、叶轮直径 130 m、轮毂高度 87 m、传动比 119.752);部件级只在振动侧成体系(11 个部件的理论特征频率表已验),**逐部件型号与齿数的成册件未交付**。
+
+**缺失影响**:机型常量缺失会让振动分母与功率曲线基准不可复核;部件参数缺失会让"哪个部件、哪一级"的定位停在候选级。
+
+### 4.11 `机位坐标`
+
+**用途**:邻域与尾流先验、扇区分析、台号与集电线路 string 的地理对照。
+
+**最小可用量**:38 台 + 升压站的坐标与海拔(含轮毂高度)。
+
+**必需测点或字段**:台号、经度、纬度、海拔高度(含轮毂高度)、机型。
+
+**单位**:(°)(经纬度,东经/北纬);海拔 m;机型为文本。
+
+**格式与编码**:`.xlsx` 或 `.csv`。
+
+**落位目录**:`data/raw/如东/` 下随族落位(本机实测**该件不在位**)。
+
+**到位现状**:**未取证**。契约 `materials.coordinates` 写有"风机坐标.xlsx 38 台 + 升压站;A 至 F 为 string;间距中位 6.54D;可研表 6.3-1 为逐台设计基线(2660 h / 尾流 6.8%)可提取",但该件**不在** `data/raw` 任何族内,仓库内也搜不到同名文件。故**坐标本身未交付**;"某台属于哪条集电线路(A 至 F)"这一条可由 1 分钟导出的内部台号反推(`01E` 至 `38B` 的字母)。
+
+**缺失影响**:绝对扇区分析与风玫瑰不可用(另有独立原因:风向为**伪地理**,机舱编码器基准未对北,跨台偏移 ±161° 散布,契约已明令禁用绝对扇区闸);尾流先验缺外部对照。
+
+### 4.12 `功率曲线`
+
+**用途**:发电性能偏差判别的基准(`powercurve_dev`、`powercurve_bins`)。
+
+**最小可用量**:标准空气密度功率曲线 + 合同担保功率曲线各一条,覆盖切入至切出风速段。
+
+**必需测点或字段**:风速(m/s)与对应功率(kW)的成对点;合同担保曲线的附属条件(空气密度、湍流度、风切变)与保证条款(可用率 K ≥ 95%、6 个月整改、违约金)。
+
+**单位**:风速 m/s,功率 kW,空气密度 kg/m³,湍流度无量纲。
+
+**格式与编码**:数值表(`.csv`/`.xlsx`)最佳;图片级(PDF 图)须描点提取。
+
+**落位目录**:随族落位。
+
+**到位现状**:部分到位。契约 `materials.guarantee_curve` 记录合同附件图 2-1(ρ = 1.223、湍流度 0.1、风切变 0.2)与推力图 2-3,**为图片级,须描点提取**;标准空气密度曲线本身未交付。
+
+**缺失影响**:缺则发电性能偏差只能做**机群相对**比较(同场同工况横比),不能做**对标准曲线**的绝对偏差;页面会标注口径。
+
+### 4.13 `测风塔`(未交付)
+
+**用途**:自由流风速与湍流度的场级参考,是功率曲线与尾流分析的独立对照。
+
+**最小可用量**:前期实地测风至少满一年(5 分钟或 15 分钟);运行期一整年的分钟级数据。
+
+**必需测点或字段**:温度、湿度、气压,以及不同层高的风速、风速标准差、风向;若分钟级不具备风速标准差,则需 1 秒采样。
+
+**单位**:温度 ℃、湿度 %、气压 kPa(或 Pa/hPa)、风速 m/s、风向 °、风速标准差 m/s。
+
+**格式与编码**:(未取证——该族零交付,无从约定)。
+
+**落位目录**:(未约定;若交付需新增族并配摄入器)。
+
+**到位现状**:**未到位**。契约 `materials.mast` 明确写 `FALSE`——**测风塔目录零交付**。可研内有 2009-12 史料塔记录(100 m × 6 层),但**非运行期**数据。
+
+**缺失影响**:缺则自由流风速只有机舱风速计(`SecAnemo`)与补偿风(1 分钟层),湍流度只能用代理量(契约里 `WMET_ti`、`WMET_wind_ti_std` 属**未验证**级);尾流与场级损耗只能靠场内机群互比,缺外部绝对锚。
+
+### 4.14 `生产报表`(并网至今)
+
+**用途**:日粒度与月粒度的发电账,用于与 SCADA 侧损失账交叉核对。
+
+**最小可用量**:并网至今;本机实测日粒度发电账 555 天、全寿命月账自 2016-06 起。
+
+**必需测点或字段**:风速、发电量、故障率、限电量。
+
+**单位**:风速 m/s、发电量 kWh、故障率 %、限电量 kWh。
+
+**格式与编码**:(现场报表形态;观澜侧以台账件读取)。
+
+**落位目录**:随族落位(本机实测该件以台账形式散见技术资料与故障记录目录)。
+
+**到位现状**:部分到位(日粒度发电账 + 全寿命月账在位;"并网至今"完整序列与口径说明未成册)。
+
+**缺失影响**:缺则损失账只能单源(SCADA 侧),不能与生产账对拍,限电损失的经济口径无法外部锚定。
+
+### 4.15 `运维记录`(维护与检修台账)
+
+**用途**:检修面与闭环验证;与油样、报警三源交叉。
+
+**最小可用量**:覆盖分析时间窗的维护台账与检修台账。
+
+**必需测点或字段**:时间、机组、部件、作业内容、作业类型。
+
+**单位**:时间为绝对时刻;其余为文本。
+
+**格式与编码**:`.xls`、`.xlsx`(观澜侧以工单台账形式接入)。
+
+**落位目录**:`data/raw/如东/风机故障记录/`。
+
+**到位现状**:已在位(检修记录 2025 与 2026;油样 2025)。
+
+**缺失影响**:缺则"检出 → 处置 → 复测回落"的闭环链断在处置侧。
+
+### 4.16 `大部件更换`
+
+**用途**:部件在升与换件闭环的判定输入(`component_history.json` 的换件侧)。
+
+**最小可用量**:逐次更换的时间、机组、部件、原因。
+
+**必需测点或字段**:时间、机组、部件、更换原因、新旧件标识。
+
+**单位**:时间为绝对时刻;其余为文本。
+
+**格式与编码**:`.xlsx`(观测到 `大部件维修记录.20240619143912557.xlsx` 一类的台账件)。
+
+**落位目录**:`data/raw/如东/风机故障记录/`。
+
+**到位现状**:部分到位(出保备件台账在位;**更换台账不完整**)。在升与换件闭环目前只有一个导出色窗的输入,因此**如实为空**并写明数据边界。
+
+**缺失影响**:缺则"在升/换件"两类证据无法闭环,部件可靠性表只能反映测量面,不能反映处置面。
+
+### 4.17 `图纸与技术文件`
+
+**用途**:故障码位域语义、控制逻辑、液压与电气回路的最终依据;本体层与判据的出处。
+
+**最小可用量**:风机电气控制相关图纸、液压站图纸、逻辑图或控制方式文件。
+
+**必需测点或字段**:回路图、端子与接线表、PLC 逻辑、液压原理图、保护定值表、结构树。
+
+**单位**:图纸为无量纲(标注带 V、A、bar、℃ 等)。
+
+**格式与编码**:`.pdf`、`.docx`、`.xlsx`、`.pptx`。
+
+**落位目录**:`data/raw/西门子4.0技术资料/`。
+
+**到位现状**:已在位。实测含 `4.0MW电气图纸/4.0电气原理图.pdf`、`接线表.xlsx`、`4.0MW液压图纸/液压图纸4.0.pdf`(3 份)、`4.0MW主控图纸/4.0MW-kk drawing-V304002_2014-05-28.pdf`、`4.0原理图_2014-05-28&新增设备电气接口图(1).pdf`、`4.0故障逻辑及处理.docx`、`广核如东海上风电场机组定值表.pdf` 等。
+
+**缺失影响**:缺则位域语义(例如"刹车状态 AI 的取值 61 是 0b111101,具体位含义")只能停在"须图纸"的未决态。
+
+### 4.18 `故障录波`(未交付)
+
+**用途**:故障前后机组数据的高分辨率快照,用于故障机理的秒级回溯。
+
+**最小可用量**:每次机组故障后详细记录故障前后机组数据的文件。
+
+**必需测点或字段**:(现场口径;观澜侧未接入)。
+
+**单位**:(现场口径)。
+
+**格式与编码**:契约 `materials.events` 记录 `fastlog` 为 **10 Hz 事件快照且仅 4 台**。
+
+**落位目录**:现场包里的 `fastlog数据/` **故意不落**(全库只有 2 处注释提到,无消费者)。
+
+**到位现状**:**未到位**。观澜**不消费**该类数据(无摄入器、无产物)。
+
+**缺失影响**:无(对当前分析面无影响);若将来要做秒级故障回溯,需要新增摄入器与判据。
+
+## 5 核心测点与字段明细
+
+### 5.1 10 分钟 SCADA 的通道类目与通道数
+
+现场 10 分钟导出按**类目**组织,9 个逐台类目与主件前缀块**一一对应**,合计 **595 通道,0 缺 0 多**。
+
+**表 5-1 逐台类目 → 前缀 → 通道数**
+
+| 类目码 | 中文目录名 | 契约列前缀 | 通道数 |
+|---|---|---|---|
+| `tur` | 风机数据 | `tur_wtc_` | 91 |
+| `din` | DigiIn数据 | `din_wtc_` | 93 |
+| `dot` | DigiOut数据 | `dot_wtc_` | 69 |
+| `cnt` | 计算数据 | `cnt_wtc_` | 69 |
+| `tmp` | 温度数据 | `tmp_wtc_` | 116 |
+| `prs` | 压力数据 | `prs_wtc_` | 52 |
+| `grd` | 电网数据 | `grd_wtc_` | 50 |
+| `flg` | 标志数据 | `flg_wtc_` | 42 |
+| `int` | 内部数据 | `int_wtc_` | 13 |
+| 合计 | 逐台 10 分钟 | — | **595** |
+
+另有 3 个类目不在逐台 10 分钟布局内:`scd`(统计数据)、`std`(标准数据)、`sum`(汇总数据)——它们是 farm 级与台账级(`sum` 含 `AlarmLog` 事件与 `ParkPilot` 限电指令流水、`DailySummary`)。
+
+主件 CSV 实测 598 列 = 3 列元信息(`TimeStamp`、`WTG`、`StationId`)+ 595 通道,**自洽**。
+
+观澜的**契约**只覆盖其中一部分:`reference/rudong/windscada_contract.yaml` 登记 10 个组共 **164 列**(活 155 列),**未契约列 435 列**已登记、不假装覆盖。canonical 词典侧共 **778 条**测点,按系统分布如下(前 8):
+
+**表 5-2 canonical 测点的系统分布(`configs/canonical/dictionary.yaml`)**
+
+| system | 词条数 | 含义 |
+|---|---|---|
+| WROT | 136 | 叶轮与变桨(含轮毂、桨距、主轴) |
+| WTRM | 113 | 传动链与齿轮箱(含机械制动) |
+| WMET | 107 | 气象与风测量 |
+| WTUR | 96 | 机组级(功率、状态、可用率、电量) |
+| WGEN | 88 | 发电机 |
+| WNAC | 54 | 机舱与主控柜 |
+| WCNV | 51 | 变流器 |
+| WYAW | 47 | 偏航 |
+
+词典中 `unit_majority` 为空(未取证单位)的词条 **198 条**;温度类单位取值以 `degC`(182 条)与 `C`(61 条)两种写法并存,**两者同量纲**,本文统一写作 ℃。另有 **55 条**词条带 `unit_variants`(例如 ℃ 的 `C`/`degC`、风速的 `m/s`/`mps`、频率的 `Hz`/`mHz`),跨源合并时必须按变体表归一。
+
+### 5.2 判级与曲线实际取用的 48 列公共子集
+
+四个判级面(变桨、偏航、蓄能、温度)与七镜头曲线不是读全部 595 通道,而是读一个**48 列公共子集**(`scripts/scada_slim_build.py` 的 `COLS_CORE` 23 列 + 温度 NBM 的 27 个 `_mean`,去重后 48 列)。该子集落成按台一份的**窄仓** `outputs/rudong/windscada/slim10min/<台号>.parquet`,进重算链 ③b 步。
+
+#### 5.2.1 48 列公共子集清单
+
+**表 5-3 48 列公共子集**
+
+| 列名 | 中文含义 | 单位 | 必须性 | 用途 |
+|---|---|---|---|---|
+| `grd_wtc_ActPower_mean` | 网侧有功功率 均值 | kW | 必须 | 五态判定、功率分箱、发电态门(全场每条分析线的输入) |
+| `grd_wtc_ActPower_max` | 网侧有功功率 最大值 | kW | 必须 | 控制参数 M9:功率封顶 p_cap = 99.5 分位 |
+| `tur_wtc_GenRpm_max` | 发电机转速 最大值 | r/min | 必须 | 控制参数 M9:转速封顶 w_cap |
+| `tur_wtc_PowerRef_endvalue` | 有功功率给定 期末值 | kW | 必须 | 限电命令面分类(缺它直接 KeyError) |
+| `tur_wtc_SecAnemo_mean` | 第二风速计(原生)均值 | m/s | 必须 | 曲线 X 轴 + 低风待机门(唯一活风源) |
+| `tur_wtc_GenRpm_mean` | 发电机转速 均值 | r/min | 必须 | 曲线、转矩、叶尖速比 |
+| `tur_wtc_MainSRpm_mean` | 叶轮转速 均值 | r/min | 必须 | 曲线、转矩、叶尖速比 |
+| `tur_wtc_PitcPosA_mean` | A 叶片桨距角 均值 | ° | 必须 | 曲线、三叶不平衡 |
+| `tur_wtc_PitcPosB_mean` | B 叶片桨距角 均值 | ° | 必须 | 曲线、三叶不平衡 |
+| `tur_wtc_PitcPosC_mean` | C 叶片桨距角 均值 | ° | 必须 | 曲线、三叶不平衡 |
+| `tmp_wtc_AmbieTmp_mean` | 环境温度 均值 | ℃ | 必须 | 空气密度口径、热汇参考(季节解耦闸的基准) |
+| `prs_wtc_YawPress_mean` | 偏航压力 均值 | bar | 必须 | 偏航面 |
+| `prs_wtc_YawPress_max` | 偏航压力 最大值 | bar | 必须 | 偏航面 |
+| `prs_wtc_YawPress_min` | 偏航压力 最小值 | bar | 必须 | 偏航面 |
+| `dot_wtc_YawLubPu_timeon` | 偏航润滑泵 通电时长 | s | 必须 | 偏航面(润滑 duty) |
+| `din_wtc_Y1LubPis_timeon` | 1 号润滑柱塞 通电时长 | s | 必须 | 偏航面/润滑柱塞 |
+| `flg_wtc_ScYawOpe_counts` | 偏航动作 计数 | 次 | 必须 | 偏航面(动作次数) |
+| `cnt_wtc_WindFauS_endvalue` | 风速故障(S 侧)累计值 | 次 | 必须 | 主控与传感网面(测风故障计数) |
+| `cnt_wtc_WindFauT_endvalue` | 风速故障(T 侧)累计值 | 次 | 必须 | 主控与传感网面(测风故障计数) |
+| `prs_wtc_HydPress_mean` | 液压系统压力 均值 | bar | 必须 | 蓄能面 |
+| `prs_wtc_HydPress_max` | 液压系统压力 最大值 | bar | 必须 | 蓄能面(封顶与保压) |
+| `prs_wtc_HydPress_min` | 液压系统压力 最小值 | bar | 必须 | 蓄能面(衰减与预充压力) |
+| `tmp_wtc_HydOilTm_mean` | 液压油温 均值 | ℃ | 必须 | 蓄能面(同油温档) |
+| `tmp_wtc_A1ExtTmp_mean` | A21 主开关板柜外温 均值 | ℃ | 建议 | 主控与传感网(柜温轴) |
+| `tmp_wtc_A21IntTm_mean` | A21 柜内温 均值 | ℃ | 建议 | 主控与传感网 |
+| `tmp_wtc_A3LefTmp_mean` | A3 柜左温 均值 | ℃ | 建议 | 主控与传感网 |
+| `tmp_wtc_A3RigTmp_mean` | A3 柜右温 均值 | ℃ | 建议 | 主控与传感网 |
+| `tmp_wtc_BrkTmpGn_mean` | 刹车温度(发电机侧)均值 | ℃ | 建议 | 齿轮箱侧(高速轴刹车) |
+| `tmp_wtc_BrkTmpGr_mean` | 刹车温度(叶轮侧)均值 | ℃ | 建议 | 齿轮箱侧(高速轴刹车) |
+| `tmp_wtc_ConvWTmp_mean` | 变流器冷却水温度 均值 | ℃ | 必须 | 热链热汇(绕组−冷却水、齿箱油−冷却水) |
+| `tmp_wtc_GFilB1Tm_mean` | 齿轮箱油滤 B1 温度 均值 | ℃ | 建议 | 冷却回路冷侧(GeOil 减 GFilB1 = 冷却器温降) |
+| `tmp_wtc_GeOilTmp_mean` | 齿轮箱油温 均值 | ℃ | 必须 | 齿轮箱即时热汇(ΔT 判据主用) |
+| `tmp_wtc_Gen1U1Tm_mean` | 发电机定子 U 相绕组温度 均值 | ℃ | 必须 | 发电机面、热链节点、三相极差 |
+| `tmp_wtc_Gen1V1Tm_mean` | 发电机定子 V 相绕组温度 均值 | ℃ | 必须 | 发电机面、热链节点、三相极差 |
+| `tmp_wtc_Gen1W1Tm_mean` | 发电机定子 W 相绕组温度 均值 | ℃ | 必须 | 发电机面、热链节点、三相极差 |
+| `tmp_wtc_GenBeGTm_mean` | 发电机轴承 NDE(非驱动端)温度 均值 | ℃ | 必须 | 发电机面、配对差(NDE 减 DE) |
+| `tmp_wtc_GenBeRTm_mean` | 发电机轴承 DE(驱动端)温度 均值 | ℃ | 必须 | 发电机面、配对差(NDE 减 DE) |
+| `tmp_wtc_HSGenTmp_mean` | 齿轮箱高速轴(发电机侧)温度 均值 | ℃ | 必须 | 齿轮箱面、热链(高速轴−油) |
+| `tmp_wtc_HSRotTmp_mean` | 齿轮箱高速轴(叶轮侧)温度 均值 | ℃ | 必须 | 齿轮箱面、热链(高速轴−油) |
+| `tmp_wtc_HubTemp_mean` | 轮毂温度 均值 | ℃ | 建议 | 轮毂热环境 |
+| `tmp_wtc_IMSGenTm_mean` | 齿轮箱中间轴(发电机侧)温度 均值 | ℃ | 必须 | 齿轮箱面、热链(中速轴−油) |
+| `tmp_wtc_IMSRotTm_mean` | 齿轮箱中间轴(叶轮侧)温度 均值 | ℃ | 必须 | 齿轮箱面、热链(中速轴−油) |
+| `tmp_wtc_MBearGTm_mean` | 主轴承(齿轮箱侧)温度 均值 | ℃ | 必须 | 主轴承面、配对差(双温差) |
+| `tmp_wtc_MBearHTm_mean` | 主轴承(轮毂侧)温度 均值 | ℃ | 必须 | 主轴承面、配对差(双温差) |
+| `tmp_wtc_NacelTmp_mean` | 机舱温度 均值 | ℃ | 建议 | 热汇参考、主控与传感网 |
+| `tmp_wtc_TraOilTF_mean` | 机舱变压器油温 均值 | ℃ | 建议 | 变流器/变压器面(油温—室温温升) |
+| `tmp_wtc_TraRooTF_mean` | 变压器层空气温 均值 | ℃ | 建议 | 变流器/变压器面(温升口径) |
+| `tmp_wtc_TrafoTMa_mean` | 变压器油温(同源副本)均值 | ℃ | 可选 | 与 `TraOilTF` 为**同一条信号**(完全相等 69.67%、相关系数 0.99998757),**禁作第二独立证据** |
+
+**关于单位取证的如实说明**:上表 48 列中,**42 列**能追到 `configs/canonical/dictionary.yaml` 的词条(见第 5.3 节与 5.4 节的逐条 canonical 编号);余下 **6 列**(`tmp_wtc_A21IntTm_mean`、`tmp_wtc_A3LefTmp_mean`、`tmp_wtc_A3RigTmp_mean`、`tmp_wtc_BrkTmpGn_mean`、`tmp_wtc_BrkTmpGr_mean`、`tmp_wtc_ConvWTmp_mean`)在词典里**查不到同名列**,属于"契约 `columns` 段之外"。它们的量纲可由通道命名族(`tmp_` = 温度类目)判定,但**本文不据此补单位**,一律标注 **未取证**,需现场或厂商图纸确认后再入契约。
+
+### 5.3 温度通道(27 个 `_mean` 温度测点与档带宽判据)
+
+温度 NBM(Normal Behavior Model,正常行为模型)按"发电态 × 功率档(500 kW)"做**同工况机群相对偏差**:`ref` 为 2025-07-01 至 2025-12-31 的干净时间窗,`cur` 为末 90 天(或用户所选时间窗),`ref_sm` 为与 `cur` 同历月、早一年的季节匹配时间窗。每台每通道按 `pbin` 分组取中位数(组内样本 `n ≥ 18`),对该通道的机群中位数取偏差,再按 `n` 加权得 `dev_K`(K 即开尔文差,与 ℃ 温差同量纲);档带宽判据为:
+
+**表 5-4 温度 NBM 档带宽判据**
+
+| 判据 | 阈值 | 结论 |
+|---|---|---|
+| 样本不足 | 行数 < 500 | 不可判(样本不足/返服嫌疑) |
+| 传感器不可信 | `dev_K ≥ 15.0 K` | 传感器候选(A 类,非热) |
+| 温度候选 | `dev_K ≥ 4.0 K` 且 `z ≥ 3.0` | 温度候选(定向查) |
+| 记基线观察 | `dev_K ≥ 2.5 K` 且 `z ≥ 2.0` | 记基线观察 |
+| 其余 | — | 不偏离机群(**判"—"不等于该台正常**) |
+
+`z` 的尺度为 `1.4826 × MAD`(中位绝对偏差,稳健尺度),分母下限 0.3 K。另有**批次盲声明**:`dev_K` 是机群相对量,全场同步平移时逐值不变,因此本表**看不见批次退化**(需看共模图 `common_shift()`)。
+
+实测的 27 个活温度 `_mean` 通道及其 canonical 依据如下(契约 `B.温度NBM` 组共 58 列,其中 `_mean` 29 列;`GFilB2`/`GFilB3` 为全场恒 200 哨兵、`liveness=恒值哨兵`,**已被排除**,故活通道 27 个)。
+
+**表 5-5 27 个温度通道与单位依据**
+
+| 契约列名 | canonical 编号(词典键) | system | 单位 | 判级中的角色 |
+|---|---|---|---|---|
+| `tmp_wtc_A1ExtTmp_mean` | `WNAC_cabinet_ext_temp` | WNAC | ℃ | 主控与传感网(柜外温) |
+| `tmp_wtc_A21IntTm_mean` | 未取证 | — | 未取证 | 主控与传感网 |
+| `tmp_wtc_A3LefTmp_mean` | 未取证 | — | 未取证 | 主控与传感网 |
+| `tmp_wtc_A3RigTmp_mean` | 未取证 | — | 未取证 | 主控与传感网 |
+| `tmp_wtc_AmbieTmp_mean` | `WMET_ambient_temp` | WMET | ℃ | 热汇参考 + 季节解耦闸基准 |
+| `tmp_wtc_BrkTmpGn_mean` | 未取证 | — | 未取证 | 齿轮箱(高速轴刹车) |
+| `tmp_wtc_BrkTmpGr_mean` | 未取证 | — | 未取证 | 齿轮箱(高速轴刹车) |
+| `tmp_wtc_ConvWTmp_mean` | 未取证 | — | 未取证 | 热链热汇(冷却水) |
+| `tmp_wtc_GFilB1Tm_mean` | `WTRM_oilfilter_temp_1_b` | WTRM | ℃ | 齿轮箱(冷却回路冷侧) |
+| `tmp_wtc_GeOilTmp_mean` | `WTRM_oil_temp` | WTRM | ℃ | 齿轮箱即时热汇 |
+| `tmp_wtc_Gen1U1Tm_mean` | `WGEN_stator_temp_u` | WGEN | ℃ | 发电机(定子 U 相) |
+| `tmp_wtc_Gen1V1Tm_mean` | `WGEN_stator_temp_v` | WGEN | ℃ | 发电机(定子 V 相) |
+| `tmp_wtc_Gen1W1Tm_mean` | `WGEN_stator_temp_w` | WGEN | ℃ | 发电机(定子 W 相) |
+| `tmp_wtc_GenBeGTm_mean` | `WGEN_bearing_temp_nde` | WGEN | ℃ | 发电机(NDE 轴承) |
+| `tmp_wtc_GenBeRTm_mean` | `WGEN_bearing_temp_de` | WGEN | ℃ | 发电机(DE 轴承) |
+| `tmp_wtc_HSGenTmp_mean` | `WTRM_hss_gen_temp` | WTRM | ℃ | 齿轮箱(高速轴发电机侧) |
+| `tmp_wtc_HSRotTmp_mean` | `WTRM_hss_rot_temp` | WTRM | ℃ | 齿轮箱(高速轴叶轮侧) |
+| `tmp_wtc_HubTemp_mean` | `WROT_hub_temp` | WROT | ℃ | 轮毂热环境 |
+| `tmp_wtc_HydOilTm_mean` | `WHYD_oil_temp` | WHYD | ℃ | 变桨/蓄能(同油温档) |
+| `tmp_wtc_IMSGenTm_mean` | `WTRM_ims_gen_temp` | WTRM | ℃ | 齿轮箱(中速轴发电机侧) |
+| `tmp_wtc_IMSRotTm_mean` | `WTRM_ims_rot_temp` | WTRM | ℃ | 齿轮箱(中速轴叶轮侧) |
+| `tmp_wtc_MBearGTm_mean` | `WROT_mainbrg_g_temp` | WROT | ℃ | 主轴承(齿轮箱侧) |
+| `tmp_wtc_MBearHTm_mean` | `WROT_mainbrg_h_temp` | WROT | ℃ | 主轴承(轮毂侧) |
+| `tmp_wtc_NacelTmp_mean` | `WNAC_temp` | WNAC | ℃ | 机舱(热汇参考之一) |
+| `tmp_wtc_TraOilTF_mean` | `WTRF_oil_temp` | WTRF | ℃ | 变压器(油温) |
+| `tmp_wtc_TraRooTF_mean` | `WTRF_room_air_temp` | WTRF | ℃ | 变压器(层空气温) |
+| `tmp_wtc_TrafoTMa_mean` | `WTRF_oil_temp_dup` | WTRF | ℃ | 变压器(与上者同源,禁作独立证据) |
+
+温度通道 → 系统归属(用于分系统取严合并):主轴承 = `MBearGTm`、`MBearHTm`;齿轮箱 = `GeOilTmp`、`IMSGenTm`、`IMSRotTm`、`HSGenTmp`、`HSRotTmp`、`GFilB1Tm`、`GFilB2Tm`、`GFilB3Tm`、`BrkTmpGn`、`BrkTmpGr`;发电机 = `GenBeGTm`、`GenBeRTm`、`Gen1U1Tm`、`Gen1V1Tm`、`Gen1W1Tm`;变流器 = `ConvWTmp`、`TrafoTMa`、`TraOilTF`、`TraRooTF`;主控与传感网 = `A1ExtTmp`、`A21IntTm`、`A3LefTmp`、`A3RigTmp`、`NacelTmp`、`HubTemp`;变桨 = `HydOilTm`;环境温度 `AmbieTmp` 为热汇参考**不归系统**。
+
+**两组配对差**(同台双测点,用于区分"局部摩擦"与"测量偏差"):主轴承双温差(`MBearHTm` 减 `MBearGTm`)、发电机轴承 NDE 减 DE(`GenBeGTm` 减 `GenBeRTm`)。配对差的机群相对偏移 `|pair_dev| ≥ 4 K` 时计入对应系统的"良好"级证据。
+
+**已知的实测边界**(用数据前必须处理,契约已逐条记):`GFilB2Tm`/`GFilB3Tm` 全场 38 台恒 200(哨兵,未接线或未标定)→ 已清零为缺失;`AmbieTmp` 实测上界 123.4 ℃(物理不可能,仅 1 行);`NacelTmp` 上界 122.9 ℃(1 行);`HubTemp` 下界 −26.7 ℃(1 行);`HydOilTm` 上界 146.6 ℃(2 行)——界外一律转缺失(**不是**压到边界上)。
+
+### 5.4 CMS 振动测点与采样、波形
+
+**测点(9 个)**:主轴前 `Main_bearing_front`、主轴后 `Main_bearing_rear`、行星级 `Gear_planet`、中间轴 `Gear_IMS`、高速轴发电机侧 `Gear_HS_generator_side`、高速轴叶轮侧 `Gear_HS_rotor_side`、发电机驱动端 `Generator_DE`、发电机非驱动端 `Generator_NDE`、系统监视 `System Monitor`。
+
+收资要求 v3 第 8 项点名的是"主轴、齿轮箱、发电机、叶轮转速"——上表覆盖前三者;**叶轮转速**在导出里以 `rpm` 标量、`RpmProfile`、`rawRPM` 三条形式存在(是 1P 与阶次跟踪的分母)。
+
+**测量类型(`meas_type`,15 类)**:`AutoSpectrum`、`TrackedAutoSpectrum`、`TrackedCepstrum`、`TrackedEnvelopeAutoSpectrum`、`TrackedEnvelopeTimeSeries`、`TrackedTimeSeries`、`TimeSeries`、`CrestFactor`、`Kurtosis`、`Peak`、`RmsOverAll`、`RpmProfile`、`Health`、`Disk Usage`、`Memory Usage`。
+
+**测量名(`meas_name`,126 类)**;**测量源(`meas_source`,46 类)**。谱的测量名只转 `FFT_*`,实测 13 类进了谱库(`FFT_1000`、`FFT_1000_vel`、`FFT_16000`、`FFT_2000_Tr`、`FFT_2000_fast_Tr`、`FFT_250_Tr`、`FFT_250_fast_Tr`、`FFT_4_Tr`、`FFT_6000_Tr`、`FFT_62_Tr`、`FFT_8_Tr`、`FFT_10000_Tr`、`FFT_rawRPM`)。
+
+**单位**:`y_unit` 实测 5 种取值——`1`(无量纲指标,含 `CrestFactor`、`Kurtosis`、`Peak`、`Rms_*`、`iso_rms*`、`Indicator_*`、`Tooth_damage_indicator_*`)、`m/s`(速度)、`m/s²`(加速度)、`RPM`(转速)、`%`(磁盘与内存占用)。`x_unit` 3 种:`Hz`(谱)、`s`(波形)、`1`。**量纲闸**是这个链路的关键:标量分析只留 `y_unit ∈ {1, m/s, m/s²}`——这一条是逐值验过的(加它之后 `val` 列做到了 8,887 对 8,887 全一致)。
+
+**采样与波形**:每件 `*_decode.json` 是一次测量记录;索引 54 列记录 `rpm`、`bandwidth_hz`、`lines`、`n_averages`、`duration_s`、`condition_key`(7 个功率分箱)、`red_alarm`/`yellow_alarm`/`blue_alarm` 与迟滞。原始**时域波形**是 `TimeSeries` 与 `TrackedTimeSeries`(`meas_source` 里以 `Time_` 开头的 30 余类,单点 65,536 或 200,000 点),本机**默认不转**(只转 FFT 谱),因此"原始时域波形"在位但未全部落成谱库件。
+
+**表 5-6 振动频谱轴分辨率(`oem_scan_plan.json` 的 11 个部件)**
+
+| 部件 | 测点 | 用哪条谱(shard) | 频率分辨率 Hz |
+|---|---|---|---|
+| `generator_de_6338` | `Generator_DE` | `Env_6000_4000_850_Tr` | 1.0625 |
+| `generator_nde_6338` | `Generator_NDE` | `Env_6000_4000_850_Tr` | 1.0625 |
+| `hs_generator_left` | `Gear_HS_generator_side` | `FFT_6000_Tr` | 0.9375 |
+| `hs_generator_right_fag` | `Gear_HS_generator_side` | `FFT_6000_Tr` | 0.9375 |
+| `hs_generator_right_nsk` | `Gear_HS_generator_side` | `FFT_6000_Tr` | 0.9375 |
+| `hs_rotor` | `Gear_HS_rotor_side` | `FFT_6000_Tr` | 0.9375 |
+| `ims_generator_fag` | `Gear_IMS` | `FFT_2000_fast_Tr` | 0.625 |
+| `ims_generator_nsk` | `Gear_IMS` | `FFT_2000_fast_Tr` | 0.625 |
+| `ims_rotor` | `Gear_IMS` | `FFT_2000_fast_Tr` | 0.625 |
+| `main_front` | `Main_bearing_front` | `FFT_62_Tr` | 0.03875 |
+| `main_rear` | `Main_bearing_rear` | `FFT_62_Tr` | 0.03875 |
+
+谱轴取数配方:`x = x_offset + arange(n_points) * x_delta`,`n_points = lines + 1`(实测内部一致性比例 1.000)。
+
+**实测覆盖**:本机只有一个窗 `w0316`(2026-03-16 17:27:22 至 2026-04-21 10:18:33,约 36 天,**不足一个季度**),38 台齐;索引 2,066,686 行(标量行 861,917、谱行 1,204,769);谱 420,742 条、1,701 个 npz 分片、3.15 GB。
+
+### 5.5 测风塔测点
+
+**该族未交付**,因此下表是"按收资要求 v3 第 3 项写出的要求清单",观澜侧的取数口径**为未取证**;凡本机词典或契约里能找到同族证据的,逐条标出依据。
+
+**表 5-7 测风塔测点要求(对照 canonical 词典里已存在的同族测点)**
+
+| 收资要求 v3 的测点 | 本机是否有同族 canonical 测点 | 依据 | 单位 |
+|---|---|---|---|
+| 温度 | 有(机舱/环境族) | `WMET_ambient_temp`、`WMET_env_temp` | ℃ |
+| 湿度 | 有 | `WMET_humidity` | % |
+| 气压 | 有 | `WMET_pressure`(kPa)、`WMET_pressure_2`(Pa)、`WMET_pressure_mast`(hPa) | kPa 或 Pa 或 hPa |
+| 不同层高风速 | 有(层高 10/30/50/70 m 与轮毂) | `WMET_ws_10m`、`WMET_ws_30m`、`WMET_ws_50m`、`WMET_ws_70m`、`WMET_ws_hub`、`WMET_mast_ws_10m`、`WMET_mast_ws_80m`、`WMET_wind_speed_mast_hub` | m/s |
+| 不同层高风向 | 有 | `WMET_wd_10m`、`WMET_wd_30m`、`WMET_wd_50m`、`WMET_wd_70m`、`WMET_wd_hub`、`WMET_mast_wd_10m`、`WMET_mast_wd_80m` | ° |
+| 风速标准差 | **未取证** | 词典有 `WMET_TI`(ratio)、`WMET_turbulence_intensity`(frac)、`WMET_wind_ti`、`WMET_wind_ti_std`,但均为**未验证**级,且未确认来自测风塔 | 未取证 |
+
+**要求补充说明**:收资要求 v3 允许两条路径——若运行期分钟级数据**具备**风速标准差,则字段为"温度、湿度、气压、不同层高的风速、风速标准差、风向";若**不具备**,则需采样频率 **1 秒**的一整年分钟级数据,字段为"温度、湿度、气压、不同层高的风速、风向"。前期实地测风要求与可研报告时间一致、**至少满一年**。
+
+### 5.6 机型与部件参数
+
+**表 5-8 机型级参数(本机已取证项)**
+
+| 参数 | 取值 | 单位 | 依据 |
+|---|---|---|---|
+| 机型 | SWT-4.0-130(上海电气/西门子许可,海上固定式) | — | `configs/contracts/siemens_swt40_rudong.yaml` |
+| 传动拓扑 | 异步全功率变流 + 齿轮箱 + 液压集中变桨 | — | 同上 |
+| 额定功率 | 4000 | kW | 同上(实证平台 3988 至 3992 kW) |
+| 叶轮直径 | 130 | m | 同上 |
+| 轮毂高度 | 87(= 73 塔筒 + 14 平台) | m | 同上 |
+| 齿轮箱传动比 | 119.752 | 无量纲 | `reference/rudong/gear_mesh_map_measured.json` |
+| 齿轮箱 | 威能极 PZAB 3546,3 级,油约 750 L | — | `reference/rudong/windscada_machine_facts.yaml` |
+| 变桨形式 | 液压集中变桨(三桨给定极差 p99 ≤ 0.4°) | — | 契约 `meta.pitch_type` |
+| 顺桨角 | 88 至 90 | ° | `windscada_machine_facts.yaml` |
+| 运行恒压 | 约 237 | bar | 契约 `turbine_1min.columns` 注记 |
+| 场站机组数 | 38 | 台 | 场配置 |
+| 切入风速、切出风速、额定风速、生存温度 | **未取证** | — | 契约与词典均无该键;需现场技术协议或机型参数表 |
+
+**表 5-9 部件级参数(收资要求 v3 第 21 项逐项)**
+
+| 收资要求 v3 的部件参数 | 本机取证情况 |
+|---|---|
+| 主轴轴承型号及厂家 | 部分取证(`reference/rudong/oem_bearing_freqs.json` 有位置与型号 token 级条目,如 `139.1/1060 f-576853.PRL`、`240.1/800 F-605082.PRL`) |
+| 齿轮箱型号厂家 | 已取证(威能极 PZAB 3546) |
+| 齿轮箱各级传动参数(行星轮内齿圈齿数、行星轮个数、太阳轮齿数、行星轮齿数、平行轮大齿轮齿数、平行轮小齿轮齿数) | 部分取证(`gear_mesh_map_measured.json` 给出各级啮合频率与齿数:一级行星 92、二级行星 91、高速级小齿轮 30,以及各级传动比 7.999、4.24、3.531;**行星轮个数未取证**) |
+| 齿轮箱各级轴承型号 | 部分取证(`oem_bearing_freqs.json` 29 个轴承条目,含 NSK/FAG 候选与位置 token) |
+| 发电机型号厂家 | **未取证**(技术资料中未见机型级发电机型号条目) |
+| 发电机轴承型号 | 部分取证(`oem_bearing_freqs.json` 中 DE/NDE 的 `6338 M/C3`) |
+| 叶片参数 | **未取证** |
+| 变桨电机参数 | **未取证** |
+| 变桨驱动小齿齿数、大齿齿数 | **未取证** |
+| 偏航电机参数 | **未取证** |
+| 偏航驱动小齿齿数、大齿齿数 | **未取证** |
+
+振动侧另有 7 个随机型常量固化的扫描配置(`oem_scan_plan.json` 的 11 个部件),其 `option` 串到理论频率表的映射规则已实测 **11/11 逐值一致**。
+
+### 5.7 状态、事件与故障码表
+
+**状态与状态位**:
+
+**表 5-10 状态与状态位通道**
+
+| 通道 | 语义 | 依据 |
+|---|---|---|
+| `WTUR_fault_code`(`风机SC状态`) | 220 个取值的事件/故障码;0 = 无故障主态(占 86.4%,对应功率中位 711 kW);1xxx 为停机码;2006、19094、2132 为发电中信息码 | 1 分钟契约 |
+| `WTUR_state_static`(`风机状态遥测转发`) | **台常数**元数据(01E=8,04E/05C/17B=19,其余 34 台=10),无时序信息 | 1 分钟契约(`dead_channel: constant_per_turbine`) |
+| `WTUR_brake_state`(`刹车状态AI`) | 活状态位,取值 0/61/80/98/112;**61 = 发电态**(功率均值 1227 至 1446 kW),80 与 98 = 停机态(功率约 −10 kW) | 契约(2026-07-26 修正) |
+| `WTUR_availability`、`WTUR_avail_builtin_daily` | 可用率,%;**1 分钟层的 `风机可利用率` 是日粒度快照**(每日唯一值 1),采信序为 counter 重建 > 本列 > 自定义列(死列) | 契约 |
+
+**事件层(`sum` 层 `AlarmLog`)**:事件驱动、无固定采样率。主键为 `ID`(全场库自增、全表唯一)。机组行筛选取 `StationNr ∈ [4800167, 4800204] ≡ 台 01 至 38`(等于 4800166 + 台号);非机组行有场级 9999、电能质量仪 91、场控子实体 88/90/100001。
+
+**表 5-11 事件层(AlarmLog)列语义**
+
+| 事件列 | 语义 |
+|---|---|
+| `TimeOn` | 激活时刻(微秒精度) |
+| `TimeOff` | 复位时刻,美式秒级字符串 `MM/DD/YY HH:MM:SS`;机组未闭合仅 16 条(0.03%) |
+| `LogType` | 0 = 告警区间账;1 = 瞬时事件/操作日志;2 = 停机相关状态区间账(候选) |
+| `AlarmGroup` | 0 = OPC 信号与远程操作;1 = 机组本体告警事件;5 = 场控(ParkPilot) |
+| `StationNr` | 实体标识(见上) |
+| `Alarmcode` | **三个命名空间禁混判**:alarm(机组,364 个用码)/ event(机组,66 个用码)/ 平台 50xxx 与 73xxx(19 个码,无字典) |
+| `Parameter` | 辅助文本(定宽右填空格,须 strip) |
+| `UserName` | 记录来源通道 |
+| `idKey`、`Comment`、`MailSend`、`Acknowledge`、`Reset` | 部分未用或为死列 |
+
+**故障码表**:如海代码表 **2,793 条**,实际用码 322 个、**覆盖率 100%**;三表(对译表、故障处理手册、如海代码表)直译族一致。事件层用码的字典覆盖率为 alarm 356/364 = 97.8%(未覆盖的 8 个是 50xxx 平台码混入)、event 60/66(缺 170 与 172 至 178 的低频扩展码)。
+
+**首发标识缺席,须自算**:按台对 `LogType=0 ∧ AlarmGroup=1` 按 `TimeOn` 升序,链尾取累计 `max(TimeOff)`,`TimeOn > 链尾 + 30 min` 即新链首 = 首发;`X ∈ {10, 30, 60} min` 的敏感性下 `cascade_ratio` 稳定在 3.49 至 3.83。2025-01 至 2026-07 共 **10,506 条首发链**,每台中位 257 条,离群台为 33 号(666)、19 号(512)、08 号(422)。
+
+### 5.8 控制与保护定值(M9 实际取用的控制参数)
+
+控制参数一致性件(M9)**不读定值表**,而是从 SCADA 数据**反演**四轴:
+
+**表 5-12 控制参数一致性件(M9)的四轴**
+
+| 轴 | 口径 | 取用通道 |
+|---|---|---|
+| C1 双封顶 | 功率封顶 = 有功最大值 p99.5;转速封顶 = 发电机转速最大值 p99.5;按容差 25 kW / 8 r/min 分组,离群台列出 | `grd_wtc_ActPower_max`、`tur_wtc_GenRpm_max` |
+| C2 转矩系数聚类 | `K = P / ω³`(中载区 1200 至 2800 kW),取中位数 × 10⁶ | `grd_wtc_ActPower_mean`、`tur_wtc_GenRpm_mean` |
+| C3 桨距调度差 | 桨距角 × 功率档(500 kW 档)逐台 schedule,与全场同档中位比,平均绝对偏 > 0.5° 判"标定偏移候选" | `tur_wtc_PitcPosA_mean` |
+| C4 尺子巡检 | 满发功率 p99.5、转速封顶、额定桨角(> 3800 kW 时 A 叶桨距角中位)三把尺子的机群散布 | 同上 |
+
+**正式的机组保护定值**在 `outputs/rudong/ontology/turbine_params.parquet`(**1,706 条**整定值,列 `Number`、`Text`、`Min`、`Max`、`Default`、`Current`、`Value`、`Unit`、`Reset`),来源为技术资料里的 `Turbine+Parameters.*.xlsx`。它覆盖温度、压力、速度等保护定值的逐条 `Min`/`Max`/`Default`/`Current` 与单位,因此**定值表本身已在位**(例如 `UP01101: Max ambient temperature` 默认 40 ℃、`UP01202: Tower fan LS tow/nac temp` 默认 25 ℃)。
+
+**缺的是"成册交付"**:定值表以整定值清单形式在位,但收资要求 v3 第 10 项点名的是"温度、压力、速度等保护定值 + 偏航控制策略 + 降载荷控制策略"的**策略文档**。偏航控制策略与降载荷控制策略目前**未取证**,只能从控制参数反演与事件层行为间接反映。
+
+## 6 数据质量与对齐要求
+
+### 6.1 台号命名与别名
+
+**表 6-1 台号命名与别名**
+
+| 场景 | 命名形态 | 说明 |
+|---|---|---|
+| 场配置机组号 | `WTG01` 至 `WTG38` | 10 分钟族用;缺台即空白 |
+| 10 分钟内部台号 | `01E` 至 `38B` | 1 分钟族用;字母 A 至 F 为集电线路 string,**组内不连续** → 邻域分析须用坐标 |
+| 事件层实体号 | `StationNr` 4800167 至 4800204 | 等于 4800166 + 台号 |
+| 现场类目库 | `StationId` 4800167 至 4800204 | 与上一致 |
+| 事件层台号列 | `tid` = `NN` + string 字母 | 装配时加列,非机组行为 NaN |
+
+**要求**:主件文件名必须是场配置里的机组号;1 分钟族用内部台号(按**台数**判完整,不按名字判)。台号别名(`01E` 与 `WTG01` 的对应)必须显式给一张对照表,禁止靠位置或排序猜。
+
+### 6.2 时间戳格式与时区
+
+**表 6-2 时间戳格式与时区**
+
+| 源 | 时间戳写法 | 处理 |
+|---|---|---|
+| 10 分钟 CSV 导出 | `YYYY-MM-DD HH:MM:SS+00:00` | 按 ISO 解析;落成补充件时统一为 `YYYY-MM-DD HH:MM:SS` |
+| 10 分钟 MDB | `MM/DD/YYYY HH:MM:SS` | 解析后按 float32 + ISO 对齐 |
+| 1 分钟 CSV | `YYYY-MM-DD HH:MM:SS` | 契约 `layout.time_format` |
+| 事件层 `TimeOn` | datetime64 微秒 | 直接解析 |
+| 事件层 `TimeOff` | `MM/DD/YY HH:MM:SS` 美式秒级字符串 | 按 `%m/%d/%y %H:%M:%S` 解析(零损失) |
+
+**时区**:导出带 `+00:00` 的按 UTC 记录,落位后统一为无时区的本地时刻字符串——**两侧必须同一约定**,否则同一列会跨日分层。
+
+### 6.3 列名绑定,禁位置索引
+
+**硬要求:一切读取按列名绑定,禁用位置索引。** 依据:本场 38 台**列序互不相同**(契约 `layout.column_binding: name_based_only`),按位置取数会得到"名字对不上数"的数据。补充件与新导出还会去掉类型前缀(`din_wtc_X` 变成 `wtc_X`),因此对齐也**只能按列名**。
+
+### 6.4 float32 量化与精度
+
+现场 CSV 导出以 **float32** 落盘、MDB 存 double。合成同台补充件时按 **float32** 对齐。实测重叠 **19,555,865 格**中,"两侧都有值却不等"的格数为 **0**——即两种形态在 float32 口径下逐值一致。
+
+另有已知的**量化与哨兵族**(用数据前必处理):
+
+**表 6-3 量化与哨兵族**
+
+| 现象 | 实测 |
+|---|---|
+| 1 分钟风速为**补偿风** | 与原生 `SecAnemo` 差约 −1.35 m/s |
+| 1 分钟温度量化 | 1 ℃ |
+| 1 分钟功率量化 | 1 kW |
+| 哨兵 `999` | `active_power`、`generator_speed` 等列 |
+| 哨兵 `200` | 齿箱滤芯 B2/B3(全场恒 200)、`outside_cabin_temperature`(p99 = 31,global_max = 200) |
+| 哨兵 `0.0` | 变压器油温族(采集掉线填充:与 `TraRooTF`/`TrafoTMa`/`A1ExtTmp` 同时刻同为 0 共 15 行) |
+| 负压族 | 桨叶油压实测低至 −621 bar → 转缺失 |
+| 界外 → 缺失(**非** clamp) | 契约 `sop/contract_gate.py` 口径:越物理界 = 无效测量 |
+
+### 6.5 去重与重叠格
+
+同台主件与补充件按**时间戳去重,主件优先**;件名、各件行数、重叠格数会打印并记进产物 `attrs`。报警台账按**键集合并集**去重(键 = `Name`/`Alarmcode`/`TimeOn`),因此累计快照件不会把同一事件算两遍,加减文件也不会让总数乱跳。
+
+### 6.6 残月与时间空洞
+
+- **尾月样本很少也如实出**:实测补充件最后一个时间戳是 `2026-09-01 00:00`,于是温度月表里会多出一个**只有 1 行的 2026-09**,时间覆盖图里也就多一格。口径是:**产物是当前 `data/raw` 的函数**——源件里有这个时间戳就该看得见,加"最少样本"门会让"数据到没到"变成猜的。
+- **时间空洞**:体检器 R 系列规则会报"缺源类或时间空洞"(退出码 7)。整族缺台报"按台取数会空白"。
+
+### 6.7 单位与量纲
+
+单位只认两处真源:`dictionary.yaml` 的 `unit_majority`/`unit_variants` 与契约的 `unit` 字段。注意三个容易混的点:
+
+1. **温度**:词典里 `degC`(182 条)与 `C`(61 条)并存,**同量纲**;
+2. **风速**:`m/s` 与 `mps` 并存(55 条变体表中含 `WMET_wind_speed_10s`/`60s`/`inst` 等),**同量纲**;
+3. **风速计原值与补偿风**:`WMET_wind_speed_compensated` 与 `WMET_wind_speed_nacelle2` 都带 `self_ref_risk: true`,**不能**当独立第二证据。
+
+压力类在词典里同时存在 `bar`(23 条)、`kPa`、`Pa`、`hPa` 四种量纲,**必须逐条看词条**,不能按"压力就是 bar"推定:例如 `WMET_pressure` 是 kPa、`WMET_pressure_2` 是 Pa、`WMET_pressure_mast` 是 hPa、`WTRM_oil_pressure`(canonical `WGBX_oil_pressure`)在词典里被标为 `Pa`,而在如东契约里对应列 `prs_wtc_GearPres_mean` 标 `bar` —— 这属**跨源量纲分歧**,用前须按契约的 `valid_range` 复核。
+
+### 6.8 编码与列名约定
+
+- **CSV 编码必须 `utf-8-sig`**(含 BOM),否则中文列名会读成乱码;1 分钟层的列名为**中英混合**。
+- **缺列如实记**:窄仓构建时缺列写进 `columns_missing`,**不造 0、不静默丢列**;各面按列名取数,缺了就当"不可判"。
+- **Access 列名消毒**:库内列名不能含 `.` `!` `[` `]`,建库时被消毒(非法字符换 `_`、截断 60、撞名加 `_1`);读侧必须用同一套规则把契约列名翻成库内列名再翻回,否则会**静默少列**。
+- **行序按 `rid` 数值排**(库内是文本,直接合并会得到 `1,10,100…` 的字典序)。
+
+## 7 交付方式与落位流程
+
+### 7.1 放置指导
+
+正确形态只有一条判据:**源类目录 → 再往下一层就是文件**。
+
+**表 7-1 约定的目录形态**
+
+| 位置 | 放什么 | 形态要求 |
+|---|---|---|
+| `data\raw\` 顶层 | 场站目录与说明文件 | 只放场站目录与 `README_把原始数据放这里.txt` 一类说明件 |
+| `data\raw\如东\` | 场站目录(下一级目录名 = 场站) | 目录名与场配置 `raw_station` 相同,或与场名互为子串 |
+| `如东\scada_10min\` | 10 分钟导出 | 平铺 `WTG01.csv` 至 `WTG38.csv`,另可放同台补充件 `<台号>-*.csv` |
+| `如东\scada_1min\` | 1 分钟导出 | 平铺内部台号 `01E.csv` 至 `38B.csv` |
+| `如东\scada_mdb\<年>年\<月>月\` | 现场年度归档库 | `<年>-<月>-<类>.mdb`,类目见第 5.1 节 |
+| `如东\故障报警\` | 报警事件导出 | `.xls`(实为 SpreadsheetML XML)、`.xlsx`、`.xml`,不许多套一层目录 |
+| `如东\风机故障记录\` | 检修工单台账 | 年目录 + 月度汇总表 `.xls`/`.xlsx`;`.rar` 与照片为附件 |
+| `如东\油样报告\` | 油液化验报告 | 必须是**两级**:`<台号>\<部件>\*.pdf` |
+| `如东\windcms\` | CMS 原始测量导出 + 厂家报告 | `<包名>\measurement\<年>\<月>\<WTGxx>\*_decode.json`;报告为 `.docx` 或 `.pdf` |
+| `如东\m5_cms_tcm\` | handoff 正本 + 厂家报告 | handoff 正本 `.json` + `厂家报告\` 下的 `.docx` |
+| `西门子4.0技术资料\` | 厂商技术资料(与场站并列) | 整棵树都读,不在场站目录下 |
+
+### 7.2 增量三判定
+
+**"增量" = 只补新数据,不要重放全量。** 放置工具与体检器都按**同名同尺寸**判断:
+
+**表 7-2 增量三判定**
+
+| 情况 | 判定 | 处置 |
+|---|---|---|
+| 目标不存在 | 新增 | 拷进去 |
+| 目标存在、大小相同 | 相同(跳过) | 不重复拷(源件是只读输入,同尺寸视为同一份) |
+| 目标存在、大小不同 | 冲突 | **必须人确认**:是"现场更正了数据"还是"这是另一批";落位工具默认**拒绝**落位,要覆盖须显式加 `--force` |
+
+**为什么不让它悄悄覆盖**:源件是**只读输入**,覆盖不可逆;而台账类产物(报警 39,211 / 工单 5,876 / 油样 404)是**逐行对齐源件**的,覆盖后若没重算,页面与源件就对不上了。
+
+### 7.3 `raw_data_check.py` 与 `place_raw_data.py` 用法
+
+用法分三步,顺序不能颠倒:
+
+1. 先看会新增、跳过、冲突什么(不写盘):`python scripts\raw_data_check.py --src D:\现场增量包`;
+2. 自动落位(落位前会再体检一次,有冲突会被拦下):`python scripts\place_raw_data.py --src D:\现场增量包`;确认要覆盖时才加 `--force`;
+3. 落完再体检一次(含可读性抽样:CSV 编码与表头、TCM json 字段):`python scripts\raw_data_check.py --deep`。
+
+`place_raw_data.py` 另有范围开关:`--scope a2`(默认,四类台账与 SCADA)、`--scope mech`(技术资料 + 1 分钟)、`--scope vib`(振动侧两族)、`--scope full`(三组一起);`--dry-run` 先看计划,重跑幂等(同尺寸自动跳过)。
+
+### 7.4 体检退出码
+
+**表 7-3 `raw_data_check.py` 的退出码**
+
+| 退出码 | 含义 | 处置 |
+|---|---|---|
+| 0 | 合规(可能带提示) | 可以继续 |
+| 5 | 结构或命名违例 | 按提示把文件上提一层、改名或建两级目录 |
+| 6 | 增量冲突(同名不同大小) | 人工确认是更正还是另一批;确认覆盖才加 `--force` |
+| 7 | 缺源类或时间空洞 | 补对应台号或对应月份的件 |
+| 8 | 仅有提示(体量/无消费者目录/散装件) | 信息级,不阻断;但"无消费者"的件不会进产物 |
+
+另有一套**扫描器退出码**(`raw_scan.py --check`):0 = 无变化 · 4 = 有新数据(它会列出"该跑哪几步",**4 不是失败**)· 5 = 还没基线。
+
+### 7.5 放完之后跑什么
+
+放完先跑一次扫描,再按扫描结论重算:
+
+1. `python scripts\raw_scan.py --check` —— rc=0 无变化、rc=4 有新数据、rc=5 还没基线;
+2. `python scripts\rebuild_all.py --auto` —— 先扫一遍再重算;新数据落在被跳过(skip)的族里时会自动取消跳过。
+
+**表 7-4 新增源类 → 该跑什么**
+
+| 新增的源类 | 该跑的重算 | 预计 |
+|---|---|---|
+| `scada_10min` / `scada_1min` | `python scripts\rebuild_all.py`(含 10 个构建器) | 约 15 分钟 |
+| 同台补充件(如 `WTG01-B2.csv`) | 同上(取数层一并读入:按列名对齐 + 按时间戳去重、主件优先) | 同上 |
+| `故障报警` / `风机故障记录` / `油样报告` | `python scripts\rebuild_all.py --skip-scada` | 约 2 分钟 |
+| `windcms` / `m5_cms_tcm` | 同上(第 ④b 步自动摄入索引与谱 + 报告/在升闭环/三层基线) | 按数据量(153 GB 导出约 25 分钟索引与谱) |
+| `scada_mdb`(现场年度归档库) | 同 `scada_10min`(CSV 缺失时取数层回落到 MDB) | 同 ③ 与 ④ |
+| 删除或换掉上面任一类 | 同上(产物随之更新,删掉的行不再出现) | 同对应行 |
+| 只放了文档/技术资料 | 无需重算(不影响页面数值),但技术资料变了要重跑第 ⑦ 步本体层 | — |
+
+重算后核对锚点:报警 **39,211** · 工单 **5,876** · 油样 **404** · `temp_monthly` **19,494**(基线口径)· 本体 **9,702** 对象(审计 0 问题)。
+
+### 7.6 重算链的 14 步(为什么是这个顺序)
+
+重算链把 14 步写死,每步幂等、可整条重跑:
+
+**表 7-5 重算链的 14 步**
+
+| 步 | 动作 | 说明 |
+|---|---|---|
+| ① | 放数据 `place_raw_data.py` | 原样搬现场包到 `data/raw/<场>/`;同尺寸跳过,冲突拦下 |
+| ①b | 输入扫描 `raw_scan.py` | 逐族指纹(件数/体积/最新落盘时间/清单摘要/子目录清单);发现新增或变化就列出"该跑哪几步"并记成新基线 |
+| ② | 三门台账 `rebuild_from_raw.py` | 报警、工单、油样;产物 = 当前源件的函数 |
+| ③ | SCADA 侧 `rebuild_from_raw.py --scada` | 10 个构建器,读约 15 GB 的 10 分钟 CSV,约 15 分钟 |
+| ③b | SCADA 10 分钟窄仓 `scada_slim_build.py` | 48 列公共子集,按时间窗重算的底座,不裁剪行 |
+| ④ | 月度派生件 `windscada_monthly_build.py` | 逐值对齐随包件才落盘(缺基线返回 5,只跳过等价验收) |
+| ④b | 振动摄入 `vib_raw_build.py` | 索引与谱库,读 TCM 导出,约 14 分钟 |
+| ④c | 变桨面 `pitch_face_build.py` | 零位三口径 + 日粒度液压/润滑/柱塞 |
+| ④d | 振动在升与换件闭环 `component_history_build.py` | 窗数不足时如实空表并写数据边界 |
+| ⑤ | 补齐随包件 `products_restore_missing.py` | 无生成端的件从交付包按需补齐(缺源返回 6 跳过) |
+| ⑥ | 重启组件服务 | 只重启组件、保留网关 |
+| ⑦ | 本体六步 | `kb_ingest` → `populate` → `chain_ingest` → `trend_ingest` → 检索索引 → 实机参数表 |
+| ⑧ | 审计与交付 | `ontology.audit` 期望 0 问题;重装门户;页面归口审计;版本记录一致性 |
+
+**依赖不能换**:第 ⑦ 步的 `populate` 要吃第 ②③④④b⑤ 步的产物;第 ⑤ 步必须在第 ⑥ 步之前;第 ⑧ 步必须在产物全部就位之后。
+
+## 8 收资要求 v3 的 21 项逐条对照
+
+### 8.1 逐条对照主表
+
+**表 8-1 收资要求 v3 的 21 类 → 观澜侧判定**
+
+| 序号 | 数据类型 | 观澜是否必须 | 对应数据族/落位 | 状态 | 缺口与催缴要点 |
+|---|---|---|---|---|---|
+| 1 | 10 分钟 SCADA 数据 | 必须 | `scada_10min/` + `scada_mdb/` | 已在位 | 要求统计周期 2 年;实测主件加补充件覆盖 2025-01-01 至 2026-09-01,**未满 2 年**,建议补 2024 年 |
+| 2 | 秒级 SCADA 高频数据 | 建议 | `scada_1min/`(1 分钟) | 部分 | 要求 1 s 或 5 s、至少 3 个月;本机只有 **1 分钟**导出(38 件、2025-01-01 至 2026-06-30),1 s/5 s 原生高频未见交付 |
+| 3 | 测风塔数据 | 建议 | 未约定族 | 未到位 | 契约 `mast: FALSE` —— **零交付**。须催缴:前期实地测风至少满一年(5 或 15 分钟);运行期一整年分钟级;测点含温度、湿度、气压、不同层高风速与风向(有风速标准差则不必 1 s,无则需 1 s) |
+| 4 | 功率曲线 | 必须 | 随族落位 | 部分 | 合同担保曲线为**图片级**(须描点提取),标准空气密度曲线未见 |
+| 5 | 机型参数 | 必须 | 契约 meta + 事实卷 | 已在位 | 额定功率 4000 kW、叶轮直径 130 m、轮毂高度 87 m、传动比 119.752 已取证;**切入切出风速、额定风速、生存温度未取证**,须补 |
+| 6 | 机位分布数据 | 建议 | 未约定族 | 已在位(依契约记载) | 契约 `materials.coordinates` 记有"风机坐标.xlsx 38 台 + 升压站",但**该件不在 `data/raw` 任何族内、仓库内也搜不到**;坐标本身**未取证**,须现场实体交付 |
+| 7 | 机组状态、事件、故障、告警数据 | 必须 | `故障报警/` + `风机故障记录/` + 事件层 | 已在位 | 码表 2,793 条、用码 322 个覆盖 100%;事件层 364 个 alarm 码中 8 个 50xxx 平台码无字典;event 缺 170 与 172 至 178 |
+| 8 | 机舱 CMS 振动数据 | 必须 | `windcms/` | 已在位 | 要求至少一个季度;实测只有约 36 天(2026-03-16 至 2026-04-21)。**须补**:2026-01 的 decode 导出,以及 `w0707`/`w0711`/`w0724`/`w0805`/`w0811` 五个窗 |
+| 9 | 机组统计数据或运行数据 | 必须 | `scada_10min/` + `scada_mdb/sum` 层 | 已在位 | 发电量、可利用率、限电与损失族由 SCADA 与汇总层重建;`DailySummary` 206 列在 `sum` 库内 |
+| 10 | 机组保护定值及控制参数 | 必须 | `西门子4.0技术资料/` | 部分 | 整定值 1,706 条已在位(含 Min/Max/Default/Current 与单位);**M9 的控制参数是反演值**(功率封顶、转速封顶、桨距调度);**偏航控制策略与降载荷控制策略未取证** |
+| 11 | 机组故障录波数据 | 可选 | 故意不落(`fastlog数据/`) | 未到位 | 全库只有 2 处注释提到 `fastlog`,且为 **10 Hz 事件快照、仅 4 台**;观澜不消费,无摄入器 |
+| 12 | 可研报告及微观选址报告 | 建议 | 未约定族 | 未到位 | 目录未见交付;可研内有 2009-12 史料塔记录(100 m × 6 层,**非运行期**)与逐台设计基线(2660 h、尾流 6.8%)可提取 |
+| 13 | 风电场简介 | 建议 | 散见契约与技术资料 | 部分 | 场站元信息散见合同与技术资料,未成册 |
+| 14 | 风电场坐标、机位布置图 | 建议 | 技术资料 | 已在位(依契约记载) | "布置图含于技术资料"为契约记载,**本机在 `data/raw` 内未取证到布置图件**;坐标件同第 6 项 |
+| 15 | 风机技术协议及风机合同 | 必须 | `西门子4.0技术资料/` | 已在位 | 含担保曲线附件(图 2-1、图 2-3)、保证率条款 |
+| 16 | 并网到现在的生产报表 | 必须 | 随族落位 | 部分 | 日粒度发电账 555 天 + 全寿命月账自 2016-06 起;**"并网至今"完整序列与口径说明未成册** |
+| 17 | 周围已建风电场生产报表 | 可选 | 未约定族 | 未到位 | 未见交付;缺则尾流与场级损耗缺外部对照 |
+| 18 | 运维记录 | 建议 | `风机故障记录/` + `油样报告/` | 已在位 | 检修记录 2025 与 2026、油样 404 件(缺 2026-07 批 102 行的合并报告源件) |
+| 19 | 大部件更换记录 | 建议 | `风机故障记录/` | 部分 | 出保备件台账在位(`大部件维修记录.20240619143912557.xlsx` 一类);**更换台账不完整**,在升与换件闭环目前只有 1 个导出色窗、**如实为空** |
+| 20 | 图纸、技术文件 | 必须 | `西门子4.0技术资料/` | 已在位 | 电气原理图、接线表、液压图纸 3 份、主控图纸、故障逻辑、保护定值表、结构树均到位 |
+| 21 | 设备参数 | 必须 | `西门子4.0技术资料/` + `reference/rudong/` | 部分 | 主轴轴承、齿轮箱型号与各级齿数、发电机轴承、齿轮箱各级轴承部分在位;**叶片参数、变桨与偏航电机参数、变桨/偏航大小齿齿数、发电机型号厂家、行星轮个数未取证** |
+
+### 8.2 到位状态分布
+
+**如图 2-3 所示**,21 类中"已在位"10 项、"部分"8 项、"未到位"3 项。
+
+![图 2-3 收资要求 v3 的 21 类在观澜的到位状态](figures/fig-dat-03-收资项覆盖.png)
+
+### 8.3 优先级建议
+
+**表 8-2 收资优先级建议**
+
+| 优先级 | 判定依据 | 数据项 | 现场动作 |
+|---|---|---|---|
+| P0 | 缺则核心分析面不可判或核对锚点对不上 | 1(不足 2 年)、8(不足一季)、3(零交付)、5(切入切出/额定风速/生存温度) | 先补这三样的**缺口部分**:2024 年 10 分钟、2026-01 与 2026-07/08 的 CMS 导出、测风塔全量、机型参数表 |
+| P1 | 缺则某一条面降级或不能交叉互证 | 6、4、10、16、19、21 | 坐标件、标准空气密度功率曲线、保护定值成册与偏航/降载荷控制策略、并网至今生产报表、大部件更换台账、部件参数明细 |
+| P2 | 缺则只影响专题或外部对照 | 2、12、13、14、17 | 1 s/5 s 高频、可研与微观选址、风电场简介、布置图、周边风电场报表 |
+| 不催 | 观澜无消费者 | 11 | 故障录波:如将来要做秒级故障回溯,再新增摄入器与判据 |
+
+## 9 缺失与替代清单
+
+**表 9-1 缺失项、影响与替代件**
+
+| 缺失项 | 影响 | 替代件与口径 | 性质 |
+|---|---|---|---|
+| 测风塔数据(零交付) | 自由流风速与湍流度缺场级参考;只能有机舱风速计与补偿风 | 用 `tur_wtc_SecAnemo_mean`(原生第二风速计)作曲线 X 轴;湍流度只能用 `WMET_ti` 一类的**未验证**代理量 | 现场补件 |
+| `m5_cms_tcm` handoff 正本缺失 | 工作台振动融合面(`/api/fleet` 的 `fus`)如实报缺件 | 观澜自算件:`scripts/rudong_fusion_handoff.py`(融合面)与 `scripts/component_history_build.py`(在升与换件闭环);**本机只有 1 个导出色窗,故在升与闭环如实为空**,数据边界写进 `meta.★数据边界`;现场正本在位时**正本优先** | 替代 |
+| 油样 2026-07 批 102 行 | 油液面从 506 行落到 404 行 | 摄入采取**合并语义**:认不出的老行原样保留、绝不删 | 现场补件(那份合并报告 PDF) |
+| CMS 六层链的模型层与融合层标准答案缺失 | `model_run_l6.parquet` 与 `fusion_38.csv` 从未随包,无法逐值对拍 | 已按包内既有判据**按口径重建**(判据不新写、只接线);实测窗 `w0316`:候选 1,026 条 → 过闸 14 条 → 定级参考 11 / 候选 3;`fusion_38.csv` 38 台 → 正常 13 / 参考 18 / 候选 7 | 替代(**不许当"复现"卖**) |
+| `oem_scan` 的峰值拾取口径缺失 | `oem_frequency_scan` 的分子(三对比值列)留空,脚本默认运行返回 rc=3 且**不写盘**(只写分母的件"看着像扫描结果、实则没有分子") | 分母与行骨架已就位并逐值验过(11/11 部件映射;行骨架 2,420 行逐键一致);配置与理论频率表已固化在 `reference/rudong/` | 研发/振动线补口径 |
+| 秒级高频(1 s/5 s)未用 | 变桨面只用 1 分钟层 | 1 分钟层已含零位三口径所需通道 | 现场补件(1 s/5 s) |
+| 故障录波未接入 | 无(当前分析面不消费) | 无 | 不催 |
+| 厂商月度报告 12 份为纯扫描件 | 「CMS 振动评估报告」的厂商侧数值取不出 | 只登记归档(记 sha256、大小、无文本层);有文本层的 docx 已转录 | 现场给电子件,或上 OCR |
+| 大部件更换台账不完整 | 在升与换件闭环证据不足 | 用出保备件台账与工单台账交叉;闭环**如实为空** | 现场补件 |
+| 坐标件未交付 | 绝对扇区与风玫瑰不可用 | 集电线路 string(A 至 F)可由 1 分钟内部台号反推;风向本身为**伪地理**(未对北,跨台偏移 ±161°),契约已禁绝对扇区 | 现场补件 |
+| 无生成端的组级产物(`pc_monthly_bins`、`duty_monthly`、`thermal_monthly`、`sector_power`、`yaw_*`、`genbearing_monthly`、`mblub_monthly`、`structure.parquet`、`watch_channels_monthly` 等) | 趋势件、热链月度、扇区、偏航与润滑面 | 逐族在补生成端(见 `docs/源代码化清单_v0.1.md`);补齐前如实标"无生成端" | 研发补生成端 |
+| 门户引用的产物树不在本包(`cleaned/`、`gearbox_life/`、`yaw_verify/`、`structured/`) | 传动链回放、四系统判据、控制律仪表台、取数单四个页面引用悬空 | 保留记账(`configs/portal_pages.yaml` 的 `known_gap`),不删交付件正文 | 待该产物线随包后复检 |
+
+## 10 数据量、时间覆盖与核对锚点
+
+### 10.1 体量与件数实测
+
+**表 10-1 实测体量与件数(2026-09-22)**
+
+| 位置 | 件数 | 体量 |
+|---|---|---|
+| `data/raw/如东`(含 1 件散装按月核对件) | 26,504 | 约 224.67 GB |
+| `data/raw/如东/scada_10min` | 114 | 16,749,788,062 B(约 15.6 GB) |
+| `data/raw/如东/scada_1min` | 38 | 13,709,173,574 B(约 12.8 GB) |
+| `data/raw/如东/scada_mdb` | 100 | 48,247,200,121 B(约 44.9 GB) |
+| `data/raw/如东/windcms` | 25,693 | 161,219,600,707 B(约 150.1 GB) |
+| `data/raw/如东/故障报警` | 16 | 23,844,357 B(约 22.7 MB) |
+| `data/raw/如东/风机故障记录` | 137 | 149,795,133 B(约 142.9 MB) |
+| `data/raw/如东/油样报告` | 404 | 176,226,585 B(约 168.1 MB) |
+| `data/raw/如东/m5_cms_tcm` | 1 | 21,823,545 B(约 20.8 MB) |
+| `data/raw/西门子4.0技术资料` | 318 | 2,936,758,089 B(约 2.7 GB) |
+| `outputs/rudong` | 3,549 | 7,157.5 MB |
+
+### 10.2 时间覆盖与缺口
+
+**如图 2-4 所示**,关键数据的时间覆盖如下。
+
+![图 2-4 关键数据的时间覆盖与缺口](figures/fig-dat-04-时间覆盖.png)
+
+**表 10-2 关键数据的时间覆盖**
+
+| 数据 | 起点 | 终点 | 覆盖 | 缺口 |
+|---|---|---|---|---|
+| 10 分钟 SCADA 主件 | 2025-01-01 00:00:00 | 2026-07-07 00:00:00 | 约 18.2 个月 | 逐台行数一致(77,551 行),无单台缺失 |
+| 10 分钟 SCADA 同台补充件 | 2026-08-01 00:00:00 | 2026-09-01 00:00:00 | 1 个月 | 尾月只有 1 行(2026-09-01 00:00),如实出 |
+| 1 分钟 SCADA | 2025-01-01 00:00:00 | 2026-06-30 23:59:00 | 18 个月 | — |
+| 月度派生件(损失与温度月表) | 2025-01 | 2026-09 | 21 个月 | 2026-09 为残月 |
+| 报警台账 | 2025-01 | 2026-07 前后 | 按源件 | 累计快照件自动跳过 |
+| CMS 振动导出 | 2026-03-16 17:27:22 | 2026-04-21 10:18:33 | 约 36 天 | **不足一个季度**;`w0127` 与其余 5 窗的导出件不在本机 |
+| CMS 厂家报告 | 2025-06 | 2026-07 | 14 个月 | 12 份为扫描件(只归档) |
+| 油样报告 | 2024-11-19 | 2025-08-29 | 按文件名日期 | 2026-07 批 102 行源件缺 |
+| 技术资料 | — | — | 318 件 | 定值表与图纸在位 |
+| 可研与测风塔 | — | — | **未交付** | 见第 9 章 |
+
+### 10.3 核对锚点
+
+重算后应与下列锚点对齐(本机实测):
+
+**表 10-3 核对锚点**
+
+| 锚点 | 实测值 | 来源 |
+|---|---|---|
+| 报警台账行数 | 39,211 | `outputs/rudong/windscada/alarms.parquet` |
+| 检修工单行数 | 5,876 | `workorders.parquet`(26 列) |
+| 油样报告行数 | 404 | `oil_samples_index.parquet` |
+| 温度月表行数 | 19,494(基线口径) | `temp_monthly.parquet`;本机当前实测为 **21,492 行**(含补充件带来的 2026-08/09 新增月),两者差异已如实记录 |
+| 本体对象数 | 9,702 | 本体对象库(审计 0 问题) |
+| 损失月表行数 | 3,955 | `loss_monthly.parquet`(21 个月) |
+| 温度比档件行数 | 24,624 | `temp_bins.parquet`(三个时间窗:`ref`/`cur`/`ref_sm`) |
+| 功率曲线偏差 | 38 台 | `powercurve_dev.parquet` |
+| 功率曲线分箱 | 912 行 | `powercurve_bins.parquet` |
+| 停机事件 | 2,755 行 | `stop_events.parquet` |
+| 变桨零位与曲线受检列 | 304 行 | `curve_liveness.parquet`(38 台 × 8 列) |
+| 控制参数剖面 | 38 行 | `control_profile.parquet`(`p_cap`、`w_cap`、`k_med`、`pitch_rated`) |
+| 控制桨距调度 | 304 行 | `control_schedule.parquet`(38 台 × 8 个功率档) |
+| 振动窗索引 | 2,066,686 行 × 54 列 | `m5_cms_tcm/windows/w0316/index.parquet` |
+| 振动谱 | 420,742 条 / 1,701 分片 / 3.15 GB | `spectra_meta.parquet` + `spectra/*.npz` |
+| 整定值 | 1,706 条 | `ontology/turbine_params.parquet` |
+| 故障码表 | 2,793 条(用码 322 个,覆盖 100%) | 如海代码表 |
+
+## 11 面向现场的收资清单
+
+**表 11-1 可打印收资清单**
+
+| 优先级 | 数据项 | 最小可用量 | 必需测点 | 交付形式与命名 |
+|---|---|---|---|---|
+| P0 | 10 分钟 SCADA | 38 台,600 s,时间跨度 2 年 | 48 列公共子集(功率/风况/转速/桨距/温度 27/偏航压力/液压压力/润滑/限电命令) | 平铺 `.csv`,`WTG01.csv` 至 `WTG38.csv`,`utf-8-sig`,时间戳 `YYYY-MM-DD HH:MM:SS` |
+| P0 | CMS 振动导出 | 至少一个季度、秒级、含原始时域波形 | 主轴前/后、行星级、中间轴、高速轴两侧、发电机 DE/NDE、系统监视、叶轮转速 | `measurement/<年>/<月>/<WTGxx>/*_decode.json` |
+| P0 | 测风塔 | 前期至少满一年(5 或 15 分钟);运行期一整年分钟级 | 温度、湿度、气压、不同层高风速、风速标准差、风向(无标准差则需 1 s 采样) | 未约定族,须现场明确形态 |
+| P0 | 机型参数 | 一份机型级参数表 | 切入切出风速、额定风速、额定功率、叶轮直径、轮毂高度、传动比、生存温度 | `.xlsx` 或 `.pdf` |
+| P1 | 机位坐标 | 38 台 + 升压站 | 台号、经度、纬度、海拔(含轮毂高度)、机型 | `.xlsx`,建议命名含"坐标" |
+| P1 | 功率曲线 | 2 条曲线 | 标准空气密度 + 合同担保;担保曲线附空气密度、湍流度、风切变 | 数值表优先(`.csv`/`.xlsx`),避免只给图 |
+| P1 | 保护定值与控制策略 | 成册 | 温度/压力/速度保护定值、偏航控制策略、降载荷控制策略 | `.xlsx` 或 `.pdf` |
+| P1 | 生产报表(并网至今) | 并网至今 | 风速、发电量、故障率、限电量 | 年/月报表格,注明口径 |
+| P1 | 大部件更换记录 | 逐次 | 时间、机组、部件、原因 | `.xlsx` |
+| P1 | 设备参数明细 | 逐部件 | 叶片参数、变桨电机参数、变桨驱动大小齿齿数、偏航电机参数、偏航驱动大小齿齿数、发电机型号厂家、行星轮个数 | `.xlsx` 或 `.pdf` |
+| P1 | 故障报警与故障记录(增量) | 覆盖分析时间窗 | 每行须带 `TimeOn` 与 `Alarmcode`;工单表须含机组编号、故障名称、故障代码 | `.xls`/`.xlsx`/`.xml`,不许多套一层目录 |
+| P2 | 秒级高频 SCADA | 1 s 或 5 s,至少 3 个月 | 全量测点 | `.csv`,内部台号命名 |
+| P2 | 可研与微观选址 | 全本 | 测风依据、逐台设计基线(发电小时、尾流折减) | `.pdf` 或 `.docx` |
+| P2 | 风电场简介 | 一份 | 场站规模、投产时间、海域特性 | `.docx` 或 `.pdf` |
+| P2 | 机位布置图 | 一份 | 全场机组与升压站位置 | `.pdf` 或 `.dwg` |
+| P2 | 周边风电场生产报表 | 覆盖分析时间窗 | 风速、发电量、故障率、限电量 | 表格 |
+
+## 附录 A 编写依据
+
+**表 A-1 章节与来源文件对照**
+
+| 章节 | 来源文件 |
+|---|---|
+| 第 1 章 | 本文自定口径;`configs/canonical/dictionary.yaml`、`configs/contracts/siemens_swt40_rudong.yaml` |
+| 第 2 章 | `docs/输入数据放置指导_v0.1.md`(族、体量实测)、`docs/系统设计说明.md` §13(输入→功能→产物) |
+| 第 3 章 | `docs/数据目录结构与落位约定_v0.2.md`、`scripts/raw_scan.py`(族表与消费者口径)、`scripts/raw_data_check.py`(后缀约定) |
+| 第 4 章 | `docs/输入数据放置指导_v0.1.md`、`docs/数据目录结构与落位约定_v0.2.md`、`docs/振动数据接入_v0.1.md`、`docs/向振动线取料单_v0.1.md`、`configs/contracts/siemens_swt40_rudong.yaml` |
+| 第 5.1 节 | `docs/输入数据放置指导_v0.1.md` §4.1(595 通道构成)、`configs/contracts/siemens_swt40_rudong.yaml` `native_wps` 节、`reference/rudong/windscada_contract.yaml` |
+| 第 5.2 节 | `scripts/scada_slim_build.py`(48 列公共子集与用途注释)、`src/windscada/data.py`(`load_10min`、`contracted_cols`) |
+| 第 5.3 节 | `src/windscada/subsys/temp_nbm.py`(通道、档带宽判据、季节解耦闸)、`configs/contracts/siemens_swt40_rudong.yaml` |
+| 第 5.4 节 | `docs/振动数据接入_v0.1.md`、`docs/振动六层链_接口规格与缺口_v0.1.md`、`reference/rudong/oem_scan_plan.json`、`outputs/rudong/m5_cms_tcm/windows/w0316/`(只读实测) |
+| 第 5.5 节 | `configs/canonical/dictionary.yaml`(WMET 族)、`configs/contracts/siemens_swt40_rudong.yaml` `materials.mast` |
+| 第 5.6 节 | `configs/contracts/siemens_swt40_rudong.yaml`、`reference/rudong/windscada_machine_facts.yaml`、`reference/rudong/gear_mesh_map_measured.json`、`reference/rudong/oem_bearing_freqs.json` |
+| 第 5.7 节 | `configs/contracts/siemens_swt40_rudong.yaml` `event_log` 节、`reference/rudong/alarm_code_cn.json` |
+| 第 5.8 节 | `src/windscada/perf/control.py`、`outputs/rudong/ontology/turbine_params.parquet`(只读实测) |
+| 第 6 章 | `configs/contracts/siemens_swt40_rudong.yaml`、`docs/输入数据放置指导_v0.1.md` §4.1、`src/windscada/subsys/temp_nbm.py`、`src/windscada/mdb_names.py` |
+| 第 7 章 | `docs/输入数据放置指导_v0.1.md`(增量三判定、退出码、放置后动作)、`scripts/raw_scan.py`、`scripts/rebuild_all.py`、`docs/系统设计说明.md` §13.2 |
+| 第 8 章 | `F:\temp\_h9\req_v3_table.tsv`(收资要求 v3 原文)、`scripts/delivery_docs_figures.py` 的到位状态表、`configs/contracts/siemens_swt40_rudong.yaml` `materials` 节、`data/raw` 实测 |
+| 第 9 章 | `docs/振动数据接入_v0.1.md` §7、`docs/振动六层链_接口规格与缺口_v0.1.md`、`docs/数据目录结构与落位约定_v0.2.md` §4、`docs/系统设计说明.md` §7 |
+| 第 10 章 | `data/raw/如东` 与 `outputs/rudong` 只读实测、`docs/输入数据放置指导_v0.1.md` §4 与 §6、`outputs/rudong/_raw_scan.json` |
+| 附录 B | `configs/canonical/dictionary.yaml` 的 `unit_majority` 与 `unit_variants`、`configs/contracts/siemens_swt40_rudong.yaml` 的 `unit` 字段 |
+| 附录 C | `F:\temp\_h9\req_v3_table.tsv`(逐字) |
+
+## 附录 B 单位与符号表
+
+**表 B-1 单位与符号**
+
+| 符号 | 中文名称 | 量纲 | 在本系统中的典型测点 |
+|---|---|---|---|
+| ℃ | 摄氏度 | 温度 | 温度 NBM 全 27 通道(canonical 里写作 `degC` 或 `C`) |
+| kW | 千瓦 | 功率 | `grd_wtc_ActPower_mean`(网侧有功)、`p_cap`(功率封顶) |
+| m/s | 米每秒 | 速度 | `tur_wtc_SecAnemo_mean`(风速)、`WMET_wind_speed_*` |
+| r/min | 转每分 | 旋转速度 | `tur_wtc_GenRpm_mean`(发电机转速)、`tur_wtc_MainSRpm_mean`(叶轮转速);canonical 里写作 `rpm` |
+| ° | 度 | 平面角 | 桨距角、风向、偏航误差、扭缆角 |
+| bar | 巴 | 压力 | 偏航压力、液压系统压力、桨叶油压;词典另有 kPa/Pa/hPa 三种并存 |
+| s | 秒 | 时间 | 润滑泵通电时长、报警持续时长 |
+| 次 | 次 | 计数 | 偏航动作计数、风速故障计数、谱线计数 |
+| kWh | 千瓦时 | 电量 | 发电量、损失电量、整定值表里的 `kW.` |
+| % | 百分比 | 无量纲比 | 可用率、湿度、磁盘占用 |
+| h | 小时 | 时间 | 利用小时、停机小时 |
+| —— | —— | —— | 以上为本表限定的 11 个符号;其余单位(V、A、Hz、mHz、g、m/s²、kg/m³、Nm、kVar、ccm、m)在个别表内单独标注,取值依据见附录 A |
+
+## 附录 C 收资要求 v3 原文 21 项
+
+以下为现场《数据分析收资要求-v3》21 项的**逐字**原文(换行以";"分隔呈现,不改字、不删项)。
+
+**表 C-1 收资要求 v3 原文**
+
+| 序号 | 数据类型 | 备注(逐字) |
+|---|---|---|
+| 1 | 10分钟SCADA数据 | 统计周期为2年的全量测点10分钟SCADA数据。 |
+| 2 | 秒级SCADA高频数据 | 采样频率为1秒或5秒;统计时间为至少3个月的全量测点秒级SCADA高频数据; |
+| 3 | 测风塔数据 | 采样频率为5分钟或15分钟;1.项目前期实地测风塔测风数据(与可研报告中时间一致,需测风至少满一年),测点至少包含风速、风向、温度、气压、湿度。2.运行周期内统计时间为一整年的分钟级测风塔数据;测点包括:温度、湿度、气压、以及不同层高的风速、风速标准差、风向;若分钟级数据不具备"风速标准差",则需按如下要求:采样频率为1秒;统计时间为一整年的分钟级测风塔数据;测点包括:温度、湿度、气压、以及不同层高的风速、风向; |
+| 4 | 功率曲线 | 标准空气密度功率曲线;合同担保功率曲线 |
+| 5 | 机型参数 | 机型整体参数,如切入切出风速、额定风速、额定功率、叶轮直径、轮毂高度、传动比、生存温度等。 |
+| 6 | 机位分布数据 | 机位坐标、机位海拔高度(含轮毂高度)、机组机型的机位分布信息。 |
+| 7 | 机组状态、事件、故障、告警数据 | 机组故障代码、状态代码表及详细说明,详细故障记录、事件记录、告警记录数据。 |
+| 8 | 机舱CMS振动数据 | CMS厂家出具且已确诊报告。统计时间为至少一个季度的秒级振动数据(非加密数据)及原始振动时域波形;测点包括:主轴、齿轮箱、发电机、叶轮转速 |
+| 9 | 机组统计数据或运行数据 | 年月日机组统计运行数据,包括发电量,可利用率,利用小时数、发电量、平均风速、可利用率,故障停机时间,维护停机时间,各类损失电量情况,偏航时间、偏航次数等;参见附录两个示例文件 |
+| 10 | 机组保护定值及控制参数 | 机组各项温度、压力、速度等保护定值,机组偏航控制策略、机组降载荷控制策略等控制参数 |
+| 11 | 机组故障录波数据 | 每次机组故障后,详细记录故障前后机组数据的文件 |
+| 12 | 可研报告及微观选址报告 | (原文无备注) |
+| 13 | 风电场简介 | (原文无备注) |
+| 14 | 风电场坐标、机位布置图 | (原文无备注) |
+| 15 | 风机技术协议及风机合同 | (原文无备注) |
+| 16 | 并网到现在的生产报表 | 包含风速、发电量、故障率、限电量等信息 |
+| 17 | 周围已建风电场生产报表 | 包含风速、发电量、故障率、限电量等信息 |
+| 18 | 运维记录 | 维护台账、检修台账 |
+| 19 | 大部件更换记录 | (原文无备注) |
+| 20 | 图纸、技术文件 | 风机电气控制相关图纸、液压站图纸、逻辑图或控制方式文件 |
+| 21 | 设备参数 | 主轴轴承型号及厂家、齿轮箱型号厂家、齿轮箱各级传动参数(行星轮内齿圈齿数、行星轮个数、太阳轮齿数、行星轮齿数、平行轮大齿轮齿数、平行轮小齿轮齿数)、齿轮箱各级轴承型号、发电机型号厂家、发电机轴承型号;叶片参数、变桨电机参数、变桨驱动小齿齿数、变桨驱动大齿齿数、偏航电机参数、偏航驱动小齿齿数、偏航驱动大齿齿数 |

+ 1152 - 0
docs/src/设计说明_观澜_2.10.0.md

@@ -0,0 +1,1152 @@
+# 设计说明 · 观澜·如东样板 v2 · 版本 2.10.0
+
+## 1 文档说明
+
+### 1.1 目的与范围
+
+本文是"观澜·如东样板 v2"(海上风电场智能分析离线系统)的设计说明,面向版本 2.10.0(2026-09-22),说明系统"由哪些部分组成、各部分怎么实现、数据从哪来到哪去、判据写在哪里、怎么验证、边界在哪里"。
+
+本文覆盖十四个设计面:总体架构与分层、目录结构与路径真源、数据接入与重算链、判级与算法、时间窗口径、服务与前端、本体与知识层、本机模型接入、运维控制台与重算编排、安装与服务化与版本管理、质量保证、安全与离线边界、可移植性与资源占用、已知边界与未实现。
+
+本文不重复需求条目本身(那是《需求分析_观澜_2.10.0.docx》的职责),也不重复操作步骤的逐步手册(那是 docs/重算操作手册_v0.1.md 与 docs/说明书_观澜如东样板v2_v0.2.md 的职责);本文只回答"设计上为什么这样、落在哪个文件的哪一处、用什么机器守卫保证它不漂移"。
+
+### 1.2 读者与用法
+
+现场运维与检修人员可看第 3 章、第 7 章、第 8 章、第 11 章,了解自己能点到的页面背后读的是什么口径;场站管理人员可看第 6 章、第 7 章、第 16 章,了解判级与可靠性指标的口径与边界;研发与交付人员应通读全文,重点是第 4 章、第 5 章、第 12 章、第 13 章;验收方可直接按第 13 章的质量门与附录 A 的编写依据逐条复核。
+
+### 1.3 与《需求分析_观澜_2.10.0.docx》的对应关系
+
+需求分析写"要什么、为谁、优先级与验收门",设计说明写"怎么实现、落在哪、如何自证"。两文的章节对应关系如表 1-1 所示。
+
+| 本文章节 | 需求分析对应章 | 对应关系说明 |
+|---|---|---|
+| 2 设计目标与原则 | 2 设计目标与约束 | 需求给目标,本文给落地机制与机器守卫 |
+| 3 总体架构 | 4 系统架构需求 | 需求要分层与统一入口,本文给分层图与组件端口表 |
+| 4 目录结构与路径真源 | 6 非功能需求(可移植性) | 需求要换机可用,本文给路径唯一真源与助手表 |
+| 5 数据接入与重算链 | 3 数据与功能需求 | 需求列功能,本文给逐步重算链与退出码容忍 |
+| 6 判级与算法 | 3 数据与功能需求(判级部分) | 需求要"带证据的判级",本文给判据、阈值与产物 |
+| 7 时间窗口径 | 3 数据与功能需求(时间窗) | 需求要随所选时间窗变化,本文给词表、缓存与 pending 契约 |
+| 8 服务与前端 | 5 页面与信息架构 | 需求给页面清单,本文给路由族与链接契约 |
+| 9 本体与知识层 | 3 数据与功能需求(知识与问答) | 需求要机制链与决策台,本文给六步摄入与只读工具表 |
+| 10 本机模型接入 | 6 非功能需求(离线模型) | 需求要离线问答,本文给档位、校闸、升档与审计 |
+| 11 运维控制台与重算编排 | 5 页面与信息架构(数据重算) | 需求要一个按钮一件事,本文给动作、状态文件与冲突码 |
+| 12 安装服务化与版本管理 | 7 交付与安装需求 | 需求要可安装可卸载可升级,本文给步骤与三重守卫 |
+| 13 质量保证 | 8 验收门 | 需求给验收门,本文给审计器矩阵与退出码语义 |
+| 14 安全合规与离线边界 | 6 非功能需求(安全与离线) | 需求要无外网与脱敏,本文给边界与降级路径 |
+| 15 可移植性与资源占用 | 6 非功能需求(可移植) | 需求要零配置启动,本文给离线件与实测体积 |
+| 16 已知边界与未实现 | 8 验收门(遗留项) | 需求要求如实留白,本文逐条给影响与处置建议 |
+
+### 1.4 口径与依据
+
+本文所有数字来自仓库文件或命令的实跑输出,不采用估算与推测;查不到、未实现的,一律写"未取证"或"未实现",并在第 16 章汇总。版本号的唯一真源是 src/version.py 的 VERSION 常量,本版为 2.10.0;打包文件名由 src/version.py 的 package_name() 给出,为 app_guanlang_v2.10.0.zip。
+
+本文遵循三条写作口径:含"窗"且确实指时间窗口的,一律写全"时间窗"(天气窗、作业窗、预览窗、观测窗属领域词,保持原样);影响的是风电机组时写"影响机组"或"影响机组数";使用英文简写时必须写成"中文(英文简写)"形式,如平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT)。
+
+本文用到的取证命令与本次输出要点如表 1-2 所示,全部为秒级只读或自检命令(重算与构建类耗时命令未执行)。
+
+| 命令 | 用途 | 本次输出要点 |
+|---|---|---|
+| python scripts/rebuild_all.py --dry-run | 取重算链实际步骤 | 输出"重算全部 · 26 步";加 --skip-scada 为 23 步 |
+| python guanlan.py check | 装机自检(40 余行结论) | 结论"全绿, 可 serve";底稿落在 docs/src/_guanlan_check.txt |
+| python scripts/products_reverse_audit.py --check | 反向呼应审计 | 呼应成立 3,548 件、不成立 0 件、未归类 0 件、rc=0 |
+| python scripts/pages_audit.py --check | 页面归口审计 | 2 项一致、19 项已知缺口、0 项要处理、rc=0 |
+| python scripts/config_audit.py | 配置统一审计 | 171 件配置、5 个域、0 不一致、14 已知缺口、rc=0 |
+| python scripts/raw_scan.py --check | 输入数据指纹比对 | "与上次快照一致,没有新数据"、rc=0 |
+| python scripts/log_audit.py | 日志口径审计 | 0 不一致、87 条提示、rc=0 |
+| python scripts/chain_gap_check.py | 振动六层链完整性 | 四步脚本齐全且已落地、rc=0 |
+| python scripts/version_log.py --check | 版本记录一致性 | 版本记录与代码一致、rc=0 |
+| python scripts/service_ctl.py status | 服务化状态 | 本机"已注册=False 运行中=False",服务名为 guanlan |
+
+***
+
+## 2 设计目标与原则
+
+### 2.1 六条设计原则
+
+**离线单包**:整套系统在一台不接外网的机器上可安装、可运行、可重算、可出报告。程序与依赖随包(wheels/win_amd64 42 件、vendor/python 三平台便携运行时),模型为可选项(未装即降级并如实标注)。落地证据:install.ps1 第 3 步"装依赖(优先包内离线轮子 wheels\win_amd64)",包内无轮子才改为联网安装。
+
+**单一真源**:路径真源是 src/paths.py,版本真源是 src/version.py 的 VERSION,端口与路径真源是 configs/serve.json,页面归口真源是 configs/portal_pages.yaml,配置登记真源是 configs/registry.yaml。落地证据:src/version.py 注释写明"改版本只改这里",打包器 scripts/pack_dist.py 不再自带版本常量而改为读 VERSION。
+
+**产物自算**:页面与接口要用到的每一件数据,都应由包内源代码从 data/raw 算出来,而不是靠随包快照顶上。落地证据:重算链的 ④b 会生成振动时间窗索引与谱库、④c 生成变桨面、④d 生成振动在升与换件闭环、④e 生成三层基线、⑤b 由重算台账生成事实契约的 claim、⑦b 自算烘焙总览页;反向呼应审计当前 rc=0,3,548 件产物全部能指到生成端与输入。
+
+**口径先于结论**:判据写在代码里、模型只做转述;相对判据封顶"候选";不同观测面不因为数量多就当作多源印证;外部原因造成的结果不写成设备不可靠。落地证据:src/ontology/mcp_server.py 头部写明"全部工具只读(L1)——模型零写权;判级零生成——只转述对象里的 verdict 原文与审级";src/windscada/subsys/fusion.py 明确禁止按源数加权;src/windscada/perf/reliability.py 把运维操作、风况(外部)、电网/供电(外部)单列为外部类并写明"外部类不计设备可靠性"。
+
+**缺件如实**:缺数据、缺配置、缺模型时,页面与日志要明确写出缺哪一件、属于哪条产物线、怎么补,不允许静默降级或伪造内容。落地证据:工作台与 CMS 在无产物时返回 HTTP 200 的结构化缺件页(而非裸 404 或直接掐断连接);src/windscada/subsys/temp_nbm.py 在样本不足时判"不可判(样本不足/返服嫌疑)"而不是判正常。
+
+**可审计**:每一条结论都能回抓到源记录,每一件产物都能说明谁生成、谁消费、来源是原始件重算还是随包补齐。落地证据:outputs/场站/_provenance.json 逐件记来源(raw-derived 或 shipped),outputs/场站/_derived_manifest.json 由构建脚本自登记,模型调用逐条写 logs/audit/llm_audit.jsonl。
+
+### 2.2 原则与机器守卫的对应关系
+
+原则不靠人记,靠检查器与退出码守住。对应关系如表 2-1 所示。
+
+| 设计原则 | 落地机制 | 守卫或检查器 | 违反时的表现 |
+|---|---|---|---|
+| 离线单包 | 包内轮子与便携运行时、可选模型 | install.ps1 第 3 步、guanlan.py check 依赖项 | 依赖安装失败即 exit 3,不继续启动 |
+| 单一真源 | paths.py、version.py、serve.json、registry.yaml | config_audit.py R1 至 R8、version_log.py --check | 手拼配置路径或版本不一致即报错(rc=6) |
+| 产物自算 | 构建器 + 自登记清单 | products_reverse_audit.py --check | 有件无生成端或无输入即报 rc=5 |
+| 口径先于结论 | 判据在 taxonomy 与 perf 与 subsys 内 | 反向可逆性矩阵、llm 校闸(校闸未过不出文) | 引用不到契约条目时明写"未被契约背书" |
+| 缺件如实 | 结构化缺件页、缺件说明字段 | pages_audit.py、guanlan.py check | 缺件被登记为已知缺口而不是装作通过 |
+| 可审计 | _provenance.json、llm_audit.jsonl | products_reverse_audit.py、log_audit.py L1 至 L5 | 台账查不到来路即报"溯源缺失"(rc=6) |
+
+### 2.3 关键设计取舍
+
+**判断在代码、模型只转述**:把判级放进模型会让同一份数据在不同机器上给出不同结论,也会让结论无法回溯到证据。因此模型只在问答、解释、引用三个位置出现,且必须过接地闸:src/ontology/mcp_server.py 的 claim_check 工具要求"最终回答前必须调用;台号与数字须来自本会话工具事实,越界即拒"。
+
+**慢就慢在算,不慢在等**:按所选时间窗重算一次判级矩阵约 25 秒、七镜头约 9 至 44 秒每时间窗。若同步等待,HTTP 请求会挂几十秒(对外访问还要过网关)。因此统一为"后台算加进程内缓存,未命中先回上一份口径并明确标注 pending",而不是静默等待或静默给旧口径。
+
+**缺件不静默降级**:缺 handoff 正本时由观澜自算件顶上并在 meta.from 标明"观澜自算";缺振动时间窗索引时按缺件如实返回空表并写明数据边界;缺模型时页面提示"本机模型未启动"(HTTP 503),绝不改用别的来源。
+
+**运行期不从交付包补齐**:2026-09-17 的口径是"运行链只报账、不搬运"——缺件要么放原始件后重算,要么由研发补生成端。交付包默认不含产物,因此"旁边有个含产物的压缩包"不能成为隐式依赖。
+
+***
+
+## 3 总体架构
+
+### 3.1 分层结构
+
+系统分为输入层、摄入构建层、产物层、服务层、前端层,另有贯穿的本体与模型层和两套贯穿全链的守卫(单一真源与审计门),如图 2-1 所示。
+
+![图 2-1 观澜总体架构](figures/fig-des-01-总体架构.png)
+
+输入层是 data/raw/场站名称/ 下的七类约定目录(scada_10min、scada_1min、scada_mdb、故障报警、风机故障记录、油样报告、windcms,另有 m5_cms_tcm 与场站并列的共享技术资料目录),原始件只读、不随包分发。
+
+摄入构建层是 scripts/rebuild_all.py 串起来的重算链(默认 26 步),把原始件算成产物,并把每一步的日志、退出码、耗时写进日志目录与任务状态文件。
+
+产物层是 outputs/场站/ 下的九个产物仓(windscada、ontology、windcms、m5_cms_tcm、tcm_compatible_replay、sop、guanlan、pitch、paradigm_r1),另有 report 与 guanlan/cloud 作为报告交付与可上云面孔的落点。
+
+服务层是六个常驻进程加一个可选的本机模型服务:网关 28084、分析工作台 18033、振动诊断 18020、仿真与回放 18791、仿真合页 18792、三维工作台 64292,以及可选的 Ollama 11434。
+
+前端层是门户(release/portal.html)、工作台单页(/detail/v2)、CMS 页面、仿真页与三维页,全部通过网关的同一前缀与同一入口访问。
+
+### 3.2 组件清单
+
+组件、端口、入口与职责如表 3-1 所示;端口取自 configs/serve.json(第 4 至 10 行),入口脚本取自 guanlan.py 的组件启动表。
+
+| 组件 | 端口 | 入口 | 职责 |
+|---|---|---|---|
+| 门户网关 | 28084 | scripts/guanlan_gateway.py | 统一入口;前缀改写;data-abs 豁免;/ops 与 /release 只读暴露;门户静态件按 mtime 缓存 |
+| 综合详细分析工作台 | 18033 | scripts/windscada_serve.py | 判级矩阵、部件问题、该问题页、发电性能、可靠性、本体页、问答、报告导出 |
+| CMS 振动诊断 | 18020 | scripts/windcms.py serve | 六层振动模型逐台判读、报告与逐台页、谱图与标量 |
+| 仿真与回放 | 18791 | scripts/static_server.py | 静态服务 release 与 resources 下的仿真页 |
+| 仿真·四系统合页 | 18792 | release/sim_sys_server.py | 从资料包 zip 现读四系统合页与 5 页资料 |
+| 三维拆装工作台 | 64292 | scripts/static_server.py | 三维资产页(release/viewer) |
+| 本机模型(可选) | 11434 | Ollama 本体进程 | 只服务问答、解释与初筛;未装即降级 |
+
+三处部署形态说明:组件(分析、振动、仿真、三维)出厂只绑 127.0.0.1(configs/serve.json 的 host),只有门户网关使用 public_host(出厂 0.0.0.0);因此对外只有 28084 一个入口,且页面没有鉴权,配置注释明确提示"对外监听请同时限来源(防火墙白名单或反代认证),别直接挂公网"。本次实测监听状态与之一致:网关在所有网卡的 28084 上监听,而工作台 18033、振动 18020、仿真 18791、四系统合页 18792、三维 64292 与本机模型 11434 都只在 127.0.0.1 上监听。
+
+### 3.3 端口与进程拓扑
+
+端口与进程拓扑如图 3-2 所示。网关按前缀把请求转给内部组件,组件端口可被 configs/serve.json 的同名键覆盖(scripts/guanlan_gateway.py 的上游端口表 _DEFAULT_ROUTES 与 _upstream_ports())。
+
+![图 3-2 端口与进程拓扑(依据 configs/serve.json)](figures/fig-des-02-端口与进程.png)
+
+网关的前缀路由表(依据 scripts/guanlan_gateway.py 第 31 至 38 行)为:/detail 转 18033 默认路径 /v2;/cms 转 18020 默认路径 /?theme=light;/sim/sys 转 18792 默认路径 /0_四系统合页.html;/sim 转 18791 默认路径 /sc1_sim_demo.html;/viewer 转 64292 默认路径 /unit-workbench.html;/local-ai 转 11434 探活路径 /api/tags。另有 /healthz、/api/version、/ops、/release/ 与门户根路径由网关自己处理。
+
+运行态实测(run/pids.json,本机一次运行中)如表 3-2 所示。该文件是"当前跑着谁"的唯一凭据,服务停止时删除;它换一台机器就是无效引用,因此交付包默认排除 run 目录。
+
+| 组件键 | 端口 | 进程号(实测) | 说明 |
+|---|---|---|---|
+| gateway | 28084 | 16448 | 对外唯一入口 |
+| detail | 18033 | 35436 | 工作台 |
+| cms | 18020 | 56720 | 振动诊断 |
+| sim | 18791 | 21780 | 仿真与回放 |
+| sim_sys | 18792 | 25936 | 四系统合页 |
+| viewer | 64292 | 58488 | 三维工作台 |
+
+### 3.4 产物仓的生成端与消费端
+
+九个产物仓的生成端与消费端如表 3-3 所示(依据 docs/系统设计说明.md 的产物全景块与本次盘上实测件数)。本次实测 outputs/rudong 合计 3,549 件、7,157.5 MB。
+
+| 产物仓 | 件数(实测) | 体积(实测) | 生成端 | 主要消费端 |
+|---|---|---|---|---|
+| windscada | 57 | 569.7 MB | rebuild_from_raw.py、10 个 SCADA 构建器、windscada_monthly_build.py、scada_slim_build.py | 工作台各视图、taxonomy、subsys/fusion |
+| m5_cms_tcm | 3,417 | 6,555.0 MB | rudong_tcm_index.py、rudong_tcm_spectra.py、vib_raw_build.py、rudong_model_run.py、rudong_fusion_run.py、rudong_fusion_handoff.py、component_history_build.py、baseline_38_build.py | subsys/fusion.py、windcms/data.py、报告构建器 |
+| ontology | 9 | 15.2 MB | src.ontology.kb_ingest 至 maintenance.refresh_params 六步 | 本体页、问答、事实契约 |
+| windcms | 49 | 15.2 MB | scripts/windcms.py report 与 kb、vib_reports_build.py | CMS 自服务 18020、taxonomy 转录列 |
+| pitch | 3 | 0.4 MB | scripts/pitch_face_build.py | 变桨面判级、零位卡片 |
+| guanlan | 5 | 0.0 MB | scripts/guanlan_facts_contract.py | 门户结论段、/api/facts、报告摘要 |
+| sop | 1 | 0.0 MB | scripts/sop_findings_from_ledger.py | 事实契约输入 |
+| paradigm_r1 | 3 | 0.0 MB | scripts/sop_findings_from_ledger.py | 事实契约输入 |
+| tcm_compatible_replay | 2 | 0.6 MB | scripts/tcm_mask_thresholds_build.py | 报告构建器、掩码阈值 |
+
+***
+
+## 4 目录结构与路径真源
+
+### 4.1 路径唯一真源与助手表
+
+src/paths.py 是全部路径的唯一真源。硬约束有三条:代码与配置里只写相对路径(相对安装根),不写机器相关绝对路径;运行时由该模块解析成绝对路径,解析基准是安装根而不是当前工作目录;需要写进产物与清单的路径字符串用 POSIX 相对形式(rel()),只在给人看的显示场景用本机分隔符(disp() 与 disp_dir())。安装根由环境变量 WINDSCADA_ROOT 决定,未设置时按 src/paths.py 的位置回溯上一级,因此整个安装目录可以整体拷到别的电脑或别的盘而不改任何路径。
+
+路径助手的功能如表 4-1 所示。
+
+| 助手 | 返回 | 用途 |
+|---|---|---|
+| P.ROOT | 安装根 | 一切解析的基准(WINDSCADA_ROOT 可覆盖) |
+| P.RAW_ROOT | data/raw | 现场原始件根(WINDSCADA_RUDONG_SRC 或 serve.json 的 raw_dir 可覆盖) |
+| P.station_dir(name) | data/raw/场站名称 | 兜底约定位置,权威值来自场配置的扫描辨识 |
+| P.out_root(name) | outputs/场站 | 产物仓根 |
+| P.store(name) | outputs/场站/windscada | L0 标准仓,页面主取数处 |
+| P.ont(name) 与 P.objects_json(name) | outputs/场站/ontology 与其 objects.json | 本体对象库 |
+| P.cms(name) | outputs/场站/windcms | CMS 振动诊断产物 |
+| P.m5(name) | outputs/场站/m5_cms_tcm | 振动线出件、时间窗索引、谱库 |
+| P.tcm_replay(name) | outputs/场站/tcm_compatible_replay | TCM 兼容链回放资产 |
+| P.sop(name) | outputs/场站/sop | SOP 中间件与评审落盘 |
+| P.guanlan(name) 与 P.cloud(name) | outputs/场站/guanlan 与其 cloud | 事实契约与可上云面孔 |
+| P.pitch(name) | outputs/场站/pitch | 变桨侧派生件 |
+| P.paradigm(name) | outputs/场站/paradigm_r1 | 范式实验件(E3/E5/E8 底稿) |
+| P.report_dir(name) | outputs/场站/report | 报告交付件 |
+| P.contract(name) | reference/场站/windscada_contract.yaml | 机型判据契约(属 reference 侧) |
+| P.config() 与 P.config_dir() 与 P.farm_config() | configs 下的配置路径 | 配置的唯一取用口 |
+| P.rel(p) 与 P.disp(p) 与 P.disp_dir(p) | 相对 POSIX 串与显示串 | 写进产物用 rel(),给人看用 disp() |
+| P.resolve(p) | 绝对路径 | 把可能是相对的值按安装根解析 |
+| P.venv_python() 与 P.python_exe() | 解释器路径 | 跨平台探测 .venv/Scripts/python.exe 或 .venv/bin/python |
+
+### 4.2 安装根逐目录说明
+
+安装根下的目录分工如表 4-2 所示,体积与件数为本次递归实测值。
+
+| 目录 | 件数(实测) | 体积(实测) | 放什么 |
+|---|---|---|---|
+| src | 281 | 6.4 MB | 分析代码(windscada、windcms、ontology、sop)与通用模块(paths、version、proc、logfile、entry_refs) |
+| scripts | 223 | 13.9 MB | 服务与工具脚本、构建器、审计器、打包器 |
+| configs | 171 件配置 | 见下 | 配置:serve.json、models.json、portal_pages.yaml、registry.yaml 与四个域目录 |
+| data | 26,504 | 224.67 GB | 原始件(data/raw/如东 与 data/raw/西门子4.0技术资料),只读输入 |
+| outputs | 3,549 | 7,157.5 MB | 产物仓,只由构建器写 |
+| release | 2,898 | 708.4 MB | 门户 portal.html、仿真服务与资料包、三维 viewer 资产、如东治理清单交付件 |
+| resources | 30 | 5.2 MB | 仿真回放资产(resources/oem_envision_sc1_rudong2014) |
+| wheels | 42 | 169.4 MB | 离线依赖轮子(wheels/win_amd64) |
+| vendor | 4 | 81.4 MB | 三平台便携 Python 运行时(3.12.14)与 vendor/MANIFEST.json |
+| docs | 32 | 2.6 MB | 交付文档与设计正本(含本文) |
+| logs | 155 | 3.3 MB | 长驻服务日志、运维动作日志、构建日志、审计流水、历史留档 |
+| run | 2 | 0.0 MB | 运行态:pids.json 与 ops_job.json |
+| .venv | 17,779 | 688.1 MB | 虚拟环境(换机必失效,安装时重建) |
+
+### 4.3 配置、日志、运行态与交付件的落点
+
+四类运行期文件的落点与命名是唯一口径,如表 4-3 所示。配置只允许经 src/paths.py 的 config() 与 config_dir() 取路径,不许手拼 configs 字符串(config_audit.py 的 R6 规则专门查这一条)。
+
+| 类别 | 落点 | 命名或字段 | 谁读 |
+|---|---|---|---|
+| 运行期单件配置 | configs/serve.json | host、public_host、gateway、detail、cms、sim、sim_sys、viewer、ollama、release_dir、viewer_dir、sim_dir、python、raw_watch、raw_dir | guanlan.py、网关、运维控制台、指纹、移植检查 |
+| 运行期单件配置 | configs/models.json | runtime、endpoint、tiers、profiles、pull_commands | guanlan.py check、src/ontology/llm_gate.py、fast_agent.py |
+| 运行期单件配置 | configs/portal_pages.yaml | 25 条主条目与 7 条子条目的 id 与 kind | scripts/pages_audit.py |
+| 配置登记表 | configs/registry.yaml | top_level、domains、known_missing | scripts/config_audit.py |
+| 长驻服务日志 | logs 组件名.log | gateway、detail、cms、sim、sim_sys、viewer、serve、start_hidden、service | 现场排障、log_audit.py |
+| 运维动作日志 | logs/ops/ | ops_动作_YYYYmmdd-HHMMSS.log,保留最近 20 份或 30 天 | 运维控制台页面、现场排障 |
+| 构建日志 | logs/build/场站/原相对路径 | 沿用原相对路径 | 构建排障 |
+| 审计流水 | logs/audit/ | llm_audit.jsonl、cloud_qa.jsonl、terms_audit.json | 审计与复核 |
+| 运行态 | run/ | pids.json(组件到进程号与端口)、ops_job.json(当前任务) | guanlan.py stop、运维控制台 |
+| 交付件 | release/ 与 outputs/场站/report 与 outputs/场站/guanlan/cloud | 门户、治理清单、报告交付件、可上云面孔 | 门户、网关 /release 只读暴露 |
+
+日志行格式统一为"时间戳 级别 组件 消息"(级别取值 DEBUG、INFO、WARN、ERROR),UTF-8 无 BOM、行尾 LF、无 ANSI 颜色码。六个服务不需要各自改写打印语句,入口处调用一次 src/logfile.py 的 prefix_stdout() 即可让每一行自动带前缀。
+
+### 4.4 已统一的路径问题(选列)
+
+2026-09-16 曾集中统一十处路径问题,选列五处如表 4-4 所示,用以说明"为什么必须只有一个真源"。
+
+| 位置 | 原样 | 改成 | 为什么要改 |
+|---|---|---|---|
+| scripts/products_restore_missing.py | 判断不存在的常量后落到写死的 rudong | P.out_root() | 多场部署会把如东的随包件补进别的场(静默串场) |
+| src/ontology/maintenance.py | 自写安装根回溯与自己的显示规则 | P.ROOT 与 P.RAW_ROOT 与 P.disp | 同一文件里出现三处影子真源,显示规则两个实现 |
+| src/sop/wrapup.py | 硬编码 cleaned/turbine.parquet | 读 clean_gate.json 的 section,取不到则目录内唯一 parquet,都没有则响亮报错 | 写侧落的是 cleaned/节名.parquet,读侧硬编码时静默取空并悄悄降级 |
+| src/windscada/subsys/pitch.py | P.store() 跟环境变量 | 配置透传的 store 键 | 多场部署下跨场串数据且不报错 |
+| src/windcms/pipeline.py 的 ingest | 任何压缩包都调未随包的脚本 | 先判断包内容:含解码导出则按已解码路线走,否则报明确错误并给两条可行路径 | 把 CMS 导出包直接指过来时原本必然崩,而其中一条路径并不需要那个脚本 |
+
+***
+
+## 5 数据接入与重算链设计
+
+### 5.1 输入数据族与落位约定
+
+输入层是 data/raw/场站名称/ 下的约定目录,多套一层包名目录等于没放(摄入脚本按"源类目录再往下一层就是文件"读)。本次实测各族的件数与体积如表 5-1 所示(data/raw/如东 合计 26,504 件、224.67 GB;guanlan.py check 报 26,503 件源件,差额 1 件是目录内的放置说明文件 README_把原始数据放这里.txt)。
+
+| 源类 | 件数(实测) | 体积(实测) | 结构要求 | 消费该族的重算步 |
+|---|---|---|---|---|
+| scada_10min | 114 | 15.60 GB | 平铺,每台一个主件(WTG01.csv 等),可含同台补充件 | ③ 与 ③b 与 ④ 与 ④c |
+| scada_1min | 38 | 12.77 GB | 平铺,名字是内部台号 | ④c |
+| scada_mdb | 100 | 44.93 GB | 按年与月分目录的月度通道组库 | 上游归档(转换后才被消费) |
+| windcms | 25,693 | 150.15 GB | 含解码导出 json,可套包名与 measurement 层 | ④b 与 ④d 与 ④e |
+| 故障报警 | 16 | 0.02 GB | 年度或季度 xls 与 XML 导出 | ② |
+| 风机故障记录 | 137 | 0.14 GB | 年目录加月度汇总表,另含压缩包与现场照片 | ② |
+| 油样报告 | 404 | 0.16 GB | 两级:台号目录下部件目录内的 pdf | ② |
+| m5_cms_tcm | 1 | 0.02 GB | handoff 正本与厂家报告 | ④b 与融合面 |
+| 西门子4.0技术资料(与场站并列) | 318 | 2,800.7 MB | 技术资料与四个台账 | ⑦ 本体六步 |
+
+### 5.2 重算链逐步说明
+
+重算链由 scripts/rebuild_all.py 写死顺序、依赖与退出码容忍。默认口径(不含 --src、不含 --with-verify)实跑为 26 步;加 --skip-scada 为 23 步(少了 SCADA 侧的 ③、③b、④c 三步);加 --src 为 27 步;再加 --with-verify 为 28 步。重算链的步骤关系如图 5-1 所示,该图按 --skip-scada 口径绘制,因此图中的步骤数为 23 步。
+
+![图 5-1 重算链(依据 scripts/rebuild_all.py --dry-run)](figures/fig-des-03-重算链.png)
+
+逐步说明如表 5-2 所示(步号顺序即执行顺序;⑦ 的六个子步合并为一行列出)。
+
+| 步号 | 脚本 | 输入 | 输出 | 退出码容忍 |
+|---|---|---|---|---|
+| ① 放数据(仅 --src 时) | scripts/place_raw_data.py --src 包目录 --scope full | 现场包 | data/raw/场站/ | 0 |
+| ①b 输入数据扫描 | scripts/raw_scan.py --check --write | data/raw 逐族指纹 | outputs/场站/_raw_scan.json 快照 | 容忍 4 与 5(4 有新数据不是失败;5 首次无快照顺手记一份) |
+| ② 三门台账 | scripts/rebuild_from_raw.py | 故障报警、风机故障记录、油样报告 | alarms.parquet、workorders.parquet、oil_samples_index.parquet | 0 |
+| ③ SCADA 侧 10 个构建器 | scripts/rebuild_from_raw.py --scada | scada_10min 逐台约 14 GB | 温度、功率曲线、损失、控制、偏航、热链、停机事件等 parquet | 0,实测约 15 分钟 |
+| ③b SCADA 10min 窄仓 | scripts/scada_slim_build.py | 同上,一次全量 | windscada/slim10min/台号.parquet 与 _manifest.json | 0,实测一次扫约 2 分钟 |
+| ④ 月度派生件 | scripts/windscada_monthly_build.py | raw 与随包基线 | temp_monthly 等同族月表 | 容忍 5(找不到随包基线即跳过等价验收,不是通过) |
+| ④b 振动侧摄入与报告与标量 | scripts/vib_raw_build.py | windcms 解码导出 | 时间窗索引、谱库、CMS 报告与逐台页、融合 z | 0;现场无振动原始件时空跑退出 0 |
+| ④c 变桨面 | scripts/pitch_face_build.py | scada_10min 与 scada_1min | pitch/pitch_daily.parquet 与 pitch_zero_monthly.parquet | 容忍 4(缺 SCADA 原始件按缺件如实报) |
+| ④d 振动在升与换件闭环 | scripts/component_history_build.py | 振动时间窗索引 | m5_cms_tcm/component_history.json | 容忍 4 |
+| ④e 三层基线 | scripts/baseline_38_build.py | 振动时间窗索引 | m5_cms_tcm/baseline_38.json | 容忍 4 |
+| ⑤b 事实契约 claim 生成 | scripts/sop_findings_from_ledger.py | 重算台账 | sop/findings.json 与 paradigm_r1 三份底稿 | 0 |
+| ⑤c 事实契约构建与渲染 | scripts/guanlan_facts_contract.py build | findings 与底稿 | guanlan/facts_contract_v0.json 与 derived 四件 | 容忍 2(契约自检不过需人看) |
+| ⑤a 逐件来源台账 | scripts/products_restore_missing.py --refresh | 盘上产物与族表 | outputs/场站/_provenance.json | 0;只记账不搬件 |
+| ⑤ 反向呼应审计 | scripts/products_reverse_audit.py --check | 产物与生成端 | 审计清单(只报账) | 容忍 5 |
+| ⑥ 重启组件服务(停) | scripts/_ops_stop_keep_gateway.py | run/pids.json | 组件停、网关留 | 0 |
+| ⑥ 重启组件服务(起) | guanlan.py serve | configs/serve.json | run/pids.json 与六个服务 | 容忍 1(有模块 DOWN 属 degraded 正常) |
+| ⑦ 本体六步 | src.ontology.kb_ingest、populate、chain_ingest、trend_ingest、retrieval.build、maintenance.refresh_params | 上级产物与技术资料 | objects.json、检索索引、turbine_params.parquet | 0 |
+| ⑦b 全场状态总览页 | scripts/windscada_overview_build.py | 重算产物与 objects.json | windscada/index.html | 0,缺产物时响亮报错不产出半张页 |
+| ⑧ 本体审计 | src.ontology.audit | objects.json | 审计结论(期望 0 问题) | 0 |
+| ⑧b 重装门户 | scripts/portal_build.py | 契约产物 | release/portal.html | 容忍 1(缺契约产物时门户保留原样) |
+| ⑧c 页面归口审计 | scripts/pages_audit.py --check | 页面登记表与产物 | 陈旧检测结论 | 容忍 5 与 6 与 7 |
+| ⑧d 版本记录一致性 | scripts/version_log.py --check | 版本记录与 version.py 的 HISTORY | 一致性结论 | 容忍 6(属文档同步这一路,不打断整条链) |
+| ⑧ 台账等价验收(仅 --with-verify) | scripts/rebuild_from_raw.py --verify | 随包基线 | 差异分类 | 容忍 4 与 5(4 有需人工看的差异;5 没基线即没验收) |
+
+退出码容忍的语义汇总如表 5-3 所示。主循环的判定是"退出码为 0 或落在该步容忍集合内即视为通过",非容忍的非零退出立即中断整条链。
+
+| 退出码 | 语义 | 典型来源 |
+|---|---|---|
+| 0 | 通过 | 各步正常完成 |
+| 1 | 有模块未起(degraded 正常态)或门户缺契约产物 | ⑥ 重启服务、⑧b 重装门户 |
+| 2 | 契约自检不过(sha 或脱敏或枚举) | ⑤c |
+| 4 | 有新数据,或缺现场原始件而按缺件如实报 | ①b、④c、④d、④e、⑧台账等价验收 |
+| 5 | 随包件这一路缺失(无基线、有未归类、被引产物不在位) | ④、⑤、⑧c |
+| 6 | 溯源缺失,或版本记录与代码不一致 | ⑧c、⑧d |
+| 7 | 数据派生页面陈旧(内嵌快照指纹与当前产物不一致) | ⑧c |
+
+### 5.3 幂等与可重入
+
+链上各步都是普通命令行程序,幂等且可整条重跑:② 与 ③ 与 ④b 按"产物是当前原始件的函数"重建(源件删了行也随之消失并大声报出缺了哪件、少了多少行、涉及哪些月);③b 全量重写窄仓;⑤a 由盘上账目重算台账;⑦ 的六个子步幂等覆盖。重算可以在系统运行中做(门户的「数据重算」或运维控制台按钮),也可以在系统全停时做(先 guanlan.py stop,跑完再启动,页面直接读新产物)。
+
+为让"新数据必进重算"不靠人记,链上有两条设计:①b 步每次扫描 data/raw 的逐族指纹并把本次指纹记成新基线;rebuild_all.py --auto 会先扫一遍,发现新数据落在被 --skip-scada 或 --skip-vib 跳过的族里就自动取消跳过并打印原因(实测示例:scada_10min 新增 1 件时计划从 22 步变 24 步)。
+
+### 5.4 等价验收与基线
+
+④ 步与可选的 ⑧ 台账等价验收都以"随包基线"为标准答案,逐值比对随包件与自算件。基线目录是 outputs/场站/windscada/_pre_rebuild_20260911/,缺基线时 ④ 返回 5(跳过等价验收,不是通过);全新机器上交付包按口径不含产物,因此 ⑧ 在全新机器上必然返回 5,这与"重算失败"是两件事,链上如实标出退出码并写明"这不是通过"。要真做验收,用 scripts/set_baseline.py 把已核实的当前产物快照登记成基线,之后这一项才会真比对。
+
+### 5.5 raw_scan 指纹与增量
+
+逐族指纹的构成是:递归统计该族目录下全部文件(不限扩展名)的件数、体积、最新落盘时间,加上清单摘要与子目录清单;每件按"相对路径、字节数、微秒级修改时间"排序串联后取 sha1,默认不叠内容哈希(--deep 才对不大于 8 MiB 的件逐件加内容 sha1)。快照落在 outputs/场站/_raw_scan.json,退出码为 0 无变化、4 有新增或变化、5 还没有基线。指纹字段与含义如表 5-4 所示。
+
+| 字段 | 含义 |
+|---|---|
+| files、bytes、newest | 该族递归文件数、总字节、最新落盘时间 |
+| matched | 扩展名白名单命中的件数(差额单列为"信息,不计变化") |
+| dirs | 子目录清单(新建空目录也能被发现) |
+| unmatched、unmatched_n | 族表之外的件与件数(按"未归类、没有已知消费者"如实报出,不猜) |
+| consumed、consume_how | 被消费者读取的件数与消费者的真实取数口径 |
+| upstream、upstream_how | 上游归档件(要先转换才被消费)与其转换口径 |
+| gaps、gaps_n | 缺口级件(放了但没有任何消费者读) |
+| digest、top | 整族指纹与样例件 |
+
+"增量"在放置侧另有三清单口径(scripts/raw_data_check.py 与 scripts/place_raw_data.py):目标不存在为新增;目标存在且大小相同为"相同(跳过)"(源件是只读输入,同尺寸视为同一份);目标存在但大小不同为冲突,默认拒绝落位并列出,必须人确认后加 --force 才覆盖,体检器的退出码语义是 0 合规、5 结构或命名违例、6 增量冲突、7 缺源类或时间空洞、8 仅提示。
+
+### 5.6 slim10min 窄仓为什么存在
+
+按所选时间窗重算的真实卡点,是每换一个时间窗就要重读 15 GB 级的 10min CSV(实测约 2 分钟每面)。而各面真正需要的列只是公共子集。scripts/scada_slim_build.py 把这份公共列子集抽成逐台一份的窄仓,一次全量扫后,任何时间窗的重算都退化为"窄仓过滤加分组"的秒级操作。
+
+窄仓的实测口径是 48 列(23 条核心列加 25 条温度 NBM 的均值列),分七个分组:功率、风况、转速、变桨、温度 NBM、偏航、润滑液压;不裁剪行(时间范围等于该台原始件全量),缺列如实记进 columns_missing 而不造 0。本次实测 outputs/rudong/windscada/slim10min 下 39 件(38 台加一份 _manifest.json),_manifest.json 记 columns_n 为 48,构建行合计 3,300,822 行,无缺列。窄仓是 v2.9.0 补入 grd_wtc_ActPower_max 与 tur_wtc_GenRpm_max 两列之后的口径,因此文档中旧的"46 列、温度 27 列"写法已过期,实测为 48 列。
+
+### 5.7 scada_mdb 类目库是上游归档
+
+现场会按"月份乘通道组"打包交付 SCADA 归档,例如某月目录下的通道组库,共 12 个类目(中文目录名与类目码对应关系是:风机数据对应 tur、温度数据对应 tmp、压力数据对应 prs、电网数据对应 grd、DigiIn 数据对应 din、DigiOut 数据对应 dot、计算数据对应 cnt、标志数据对应 flg、内部数据对应 int、统计数据对应 scd、标准数据对应 std、汇总数据对应 sum)。
+
+取数层不直接读类目库(它只认按月分片的主件与同台补充件),因此类目库是上游归档:扫描器按信息级列出、不计变化、不假装被摄入(这是 v2.8.2 的消缺点,修前 raw_scan --check 会永远返回 4)。要让它进产物,需按时间戳与机组标识把逐台类目表对齐合成同台 10min 补充件 scada_10min/台号-YYYYMM.csv(列与主件逐列同名同序、时间戳为 ISO 形式),之后 ③ 与 ④ 与 ④c 会自动纳入。实测 9 个逐台类目与主件的前缀块一一对应(tur 91、din 93、dot 69、cnt 69、tmp 116、prs 52、grd 50、flg 42、int 13,合计 595 通道,0 缺 0 多),精度与格式按 float32 加 ISO 对齐(实测重叠 19,555,865 格,两侧都有值却不相等的为 0 格)。
+
+SCADA 形态的接纳规则是"CSV 优先、该台没有 CSV 才回落 MDB",并把这次从哪种形态取的记在返回表的属性里供台账与页面说明来路,不静默换源。MDB 侧有两个硬限制:单表不超过 255 列,因此库内按 250 列拆表;2026-09-19 之前建的老库缺机组列,10min 老库还能用源件自带的机组列兜底,1min 老库救不了(源件没有机组标识列),需重跑 scripts/csv_to_mdb.py 重建。
+
+***
+
+## 6 判级与算法设计
+
+### 6.1 判级矩阵与四维证据
+
+判级矩阵覆盖九个系统(src/windscada/taxonomy.py 的 SYSTEMS):变桨、偏航、主轴承、齿轮箱、发电机、变流器、主控与传感网、叶片与叶根、塔架与基础。其中叶片与叶根、塔架与基础不参与 SCADA 侧判级(代码里列为无 SCADA 数据的两个系统),因此页面上常见的"七系统判级矩阵"指的是可由 SCADA 判级的七个系统;液压(蓄能)并入变桨,温度 NBM 是方法而不是系统,这两点写在代码注释里。
+
+判级词表有五态:报警、良好、不可判、优秀、无数据;取严合并的权重是报警最严、其次良好、再次不可判、优秀与无数据并列(src/windscada/taxonomy.py)。子系统面另用四键取严(报警、良好、优秀、不可判),偏航面先过滤不可判再取三键。七系统判级矩阵与四源融合数据流如图 6-1 所示。
+
+![图 6-1 判级矩阵与四源融合数据流](figures/fig-des-04-判级与融合.png)
+
+各系统的判级函数与证据维度如表 6-1 所示。四个子判定面各自有四个维度(四维证据),逐维度给出判词,再按取严序归到系统级。
+
+| 系统 | 判级函数 | 四维证据或依据 | 判词示例 |
+|---|---|---|---|
+| 变桨 | src/windscada/subsys/pitch.py 的 registry | 蓄能与压力调节、变桨轴承与轮毂润滑、执行与位置反馈、油路与密封 | 蓄能失效候选(定向查)、记基线观察 |
+| 偏航 | src/windscada/subsys/yaw.py 的 registry | 偏航轴承与润滑、驱动装置、液压制动、对风控制 | 报警、良好、优秀 |
+| 主轴承与齿轮箱与发电机 | 温度通道经 temp_nbm.registry,再按通道到系统的映射归口 | 温度 NBM 同工况比档与相对离群 | 温度候选(定向查)、传感器候选(A 类,非热)、记基线观察 |
+| 变流器 | 可靠性归因码到部件的映射与状态统计 | 停机段归因、故障码统计 | 报警、良好 |
+| 主控与传感网 | 报警台账与系统辅助件 | 多报警台、传感器偏差、通信错误 | 报警、良好 |
+| 叶片与叶根、塔架与基础 | src/windscada/subsys/structure.py 的 registry | 塔筒频率、塔筒振动、塔筒湿度、基础与法兰;叶片 A 与 B 与 C、叶根螺栓 | 报警、不可判 |
+
+温度判词到矩阵级的映射是:温度候选与传感器不可信(A 类)归为报警,记基线观察归为良好,不可判(返服嫌疑)归为不可判。融合面的矩阵级枚举是 ok、meta、bad、warn、note 五种,关闭类最先判,取严权重为 bad 最重、其次 warn、再次 note、ok 最轻。
+
+### 6.2 温度 NBM 与同油温档带宽
+
+温度 NBM 的口径是"同一功率档内比同族温度",功率档按 500 kW 一档划分(0 至 4500 kW)。样本门是同一单元样本数不少于 18;台级样本少于 500 行时判"不可判(样本不足或返服嫌疑)"。偏差与稳健 z 值为:偏差等于实测中位减去基准中位,稳健标准差取 1.4826 倍中位绝对偏差并从下限 0.3 起算,z 等于偏差除以该稳健标准差。判级阈值为:偏差不小于 15 K 判 A 类(传感器不可信,非热);偏差不小于 4.0 K 且 z 不小于 3.0 判温度候选;偏差不小于 2.5 K 且 z 不小于 2.0 判记基线观察。
+
+同油温档带宽的判据在蓄能(液压)面而不是温度 NBM 文件里:按油温 5 K 一档分箱(发电态功率大于 100 kW 的行),同一档内样本数不少于 24 时计算压力带宽(段内最大值减最小值);带宽比值不小于 1.5 时入轴,轴数不少于 2 时判"蓄能失效候选(定向查)"。热链面另按高油温档统计占空:档位不小于 45 摄氏度且样本数不少于 30 才出值。
+
+温度面还有两个故意固定的对照时间窗:ref 是 2025-07-01 至 2026-01-01(限电前干净时间窗),ref_sm 是同历月早一年;它们不随用户所选时间窗漂移,因为季节解耦的对照基准一旦跟着漂就失去对照意义,页面上如实标注这一点。
+
+### 6.3 七镜头曲线与限电前干净时间窗
+
+七镜头曲线的定义与口径在 src/windscada/perf/curves.py:L1 风速与功率、L2 功率与桨距(含三叶互比)、L3 功率与发电机转速、L4 转矩与转速(转矩等于功率除以发电机角速度,理论形态为转速平方族)、L5 风速与风轮转速(叶尖速比)、L6 功率与功率系数、L7 发电机转速与风轮转速之比。机型常数是风轮半径 65.0 米、扫风面积 13,273 平方米、铭牌齿比 119.752、贝兹极限 0.593。
+
+分箱边界为:风速 3.0 至 15.5 米每秒、步长 0.5;功率 0 至 4250 kW、步长 250;转速比 600 至 1760、步长 40。显著门共八项按绝对值锚定;离群判据是稳健 z 绝对值不小于 3 且残差不小于对应门限;工况段闸是功率分箱不小于 250 kW、转速比分箱不小于 900。另有三条物理硬闸:功率系数不得超过贝兹极限、齿比恒等(偏差超过 1.5 判离群)、叶尖速比须呈平台。列活性铁律要求同一列取值数大于 1、卡死比例小于 0.5 且落在物理域内,否则该列不参与镜头。
+
+限电前干净时间窗是 2025-07-01 至 2026-01-01(2025-H2),代码里把它写成常量并注明这是"限电前干净判别时间窗",与七镜头曲线的判别时间窗一致。选到 2025-07 至 2025-12 这一段时,发电性能页直接使用正式产物而不重算。
+
+### 6.4 M9 控制参数一致性
+
+M9 是控制参数一致性检查,摆在发电性能页上,口径在 src/windscada/perf/control.py。四个判据如表 6-2 所示。P 封顶取有功功率上限通道的 99.5 百分位,ω 封顶取发电机转速上限通道的 99.5 百分位,只取有功功率均值大于 100 kW 的行参与统计。
+
+| 判据 | 输入列 | 方法 | 阈值 |
+|---|---|---|---|
+| C1 双封顶 | grd_wtc_ActPower_max、tur_wtc_GenRpm_max | 按容差分组(容差 25 kW 与 8 rpm),组内计数不少于 2,逐台与组中位比 | 偏差超过 3 倍容差即离群,判"版本或参数差异·准定论载体" |
+| C2 K 聚类 | 有功功率均值与发电机转速均值 | 中载区 1200 至 2800 kW 内计算 K 等于功率除以转速立方,取中位乘 100 万 | 离群门为 5 倍 1.4826 倍中位绝对偏差,稳健标准差下限为百万分之一 |
+| C3 桨距调度 | 功率分箱与桨距角 | 同功率档内比桨距角 | 同档差门 0.5 度 |
+| C4 尺子 | 满发功率 99.5 百分位、转速封顶、额定桨角 | 满发段取功率大于 3800 kW 且样本不少于 50 | 出 pitch_rated,供交叉核对 |
+
+M9 按所选时间窗重算(control.registry 接受 span 参数),产物是 control_profile.parquet(逐台的 P 封顶、ω 封顶、样本数、K 中位、额定桨角)与 control_schedule.parquet(逐台逐功率档的桨距)。实测(38 台):P 封顶中位从干净时间窗的 4190 kW 变为 2026 年的 4181.7 kW 与 2026 年一季度的 4177.5 kW。
+
+### 6.5 可靠性指标
+
+部件可靠性表的口径在 src/windscada/perf/reliability.py 与 src/windscada/perf/faults.py。停机段门是"连续 10min 行不少于 6 行"(即不少于 1 小时),段结束时间是末行加 10 分钟。归因有两级:T1 紧归因时间窗取段起点前后 30 分钟内的首发故障码,T2 宽归因时间窗取前 6 小时内的最近一条;实测紧归因时间窗未命中的段里有 53% 在段前 6 小时内能找到报警,中位段长 2.5 小时、最长 174 小时。调度令 1007 与 1008 判为远程停机(业主与西门子),不计入设备责任。
+
+三项指标的口径如表 6-3 所示。外部类(运维操作、风况(外部)、电网或供电(外部))单列统计并明确"外部类不计设备可靠性"。
+
+| 指标 | 公式或口径 | 注意点 |
+|---|---|---|
+| 平均无故障间隔(MTBF) | 台时减去停机小时再除以事件数;无记录停机不给值 | 部件可靠性模块的口径,台时目前用理想日历 |
+| 平均停机间隔(MTBO) | 在上一项基础上再扣除调度令停机后除以事件数 | 停机归因模块的口径,台时已改为实测台时 |
+| 单次停机时长(MDT) | 停机段时长的均值;就地与切入边界类码按口径剔除 | 两个模块同口径 |
+
+台时口径的实测对比很重要:2026 年 1 至 7 月理想日历台时为 194,050,实测台时为 165,475(差 17%),原因是尾月只有 6 天数据;若沿用理想日历,平均停机间隔会虚高 18%(239 小时实为 203 小时)。因此 faults.py 已改为用实测台时(由 loss_monthly 的行数除以每小时 6 个节拍得到),并在不可得时才回退理想日历。需要如实指出:reliability.py 的部件可靠性表仍使用理想日历台时,两处口径目前不一致,属待统一项(见第 16 章)。
+
+### 6.6 融合面四源与逐台判级
+
+融合面的四源是振动、温度、润滑、油液,口径写在 src/windscada/subsys/fusion.py 的模块头。融合面读取的输入件包括:handoff_vibration_v2.json、oil_samples_index.parquet、watch_channels_monthly.parquet、由 temp_nbm.registry 出的温度比档结果、mblub_monthly.parquet、alarms.parquet、workorders.parquet、油样合并报告 json,以及 CMS 报告转录件。
+
+融合纪律有两条:一是禁止按源数加权(同一机制的多个观测面不等于多源印证,源多不代表结论更强);二是 claim 时间窗对齐(振动数据到 2026-08、SCADA 数据到 2026-07-07、油样到 2025-08),避免拿不同时点的证据互相印证。逐台判级的产物字段包含台号、部件、振动结论、语义类、级别、设备状态、证据状态、振动信号、温度通道、温度偏差、温度判词、时间窗区间、末次油样、油样新鲜度、claim 时间窗等。
+
+四源的权重不是数值表而是优先级:关闭类最先判(销案、正样本、无新证、可降、降观察、无异常先归为正常),其余按取严合并;振动侧判 bad 或 warn 且非 meta 时,对应系统升级为报警;windcms 报告三方对拍也取严(危险与报警抬级,不可判且本层无异常证据则判不可判,良好与优秀不动)。系统关键词映射表把部件名归到系统,未收录的单元写"未收录"而不是写正常。
+
+### 6.7 残月识别
+
+"产物是当前原始件的函数"这条原则要求尾月样本很少时也如实出值,而不是加最小样本门去猜。同时,样本严重不足的月份必须能被识别出来,否则环比与均值会被拉偏。残月识别的实现在服务层:覆盖度等于该月行数合计除以每小时 6 个节拍再除以机组台数再除以当月日历小时;日条数等于该月条数除以覆盖度与当月天数的乘积,仅当覆盖度大于 0.02 才出值;覆盖度小于 0.5 的月份列为残月。
+
+实测的典型残月是 2026-07:只有 6 天数据,覆盖度 19.4%。页面上残月以灰虚线柱表示,并写"仅 N 天"(N 等于覆盖度乘 30 后取整);报告导出侧对覆盖度小于 0.5 的月份写"(不满月)",并声明环比不纳入残月。融合面与温度面也在各自口径里写明"6 天时间窗"的样本事实(例如温度面记"应有 288 个节拍、实测 145 个")。
+
+### 6.8 算法到输入列到判据到产物
+
+各算法与输入列、判据、产物的对应关系如表 6-4 所示,用以支撑"每条判据都能指到输入列"这一要求。
+
+| 算法或模块 | 关键输入列 | 判据要点 | 产物 |
+|---|---|---|---|
+| 温度比档(subsys/temp_nbm.py) | 各温度通道的均值列(按 500 kW 功率档分箱) | 偏差与稳健 z 双门限;样本门 18 | windscada/temp_bins.parquet,注册表出判词与依据 |
+| 蓄能带宽(subsys/hydraulic.py) | 液压油温均值、液压压力最大最小均值 | 5 K 油温档内带宽比不小于 1.5 且轴数不少于 2 | windscada/hydraulic_accum.parquet |
+| 变桨面(subsys/pitch.py) | 开关量 on 秒、柱塞动作次数、压力、三叶桨距角与有功与转速 | 停机顺桨段与满发段与运行段同工况分档的零位偏差 | pitch/pitch_daily.parquet、pitch/pitch_zero_monthly.parquet |
+| 偏航面(subsys/yaw.py) | 偏航压力均值、偏航润滑与柱塞时间、偏航计数、风偏差 | 四维取严;偏差清洗与相关位置 | windscada/yaw_daily.parquet、yaw_err_clean.parquet |
+| 七镜头(perf/curves.py) | 有功功率均值、限电命令、风速、发电机转速、风轮转速、三叶桨距角、环境温度 | 八项显著门;离群 z 与残差双门;三条物理硬闸 | windscada/curve_lenses.parquet、curve_liveness.parquet |
+| M9(perf/control.py) | 有功功率上限、发电机转速上限、有功功率均值、转速均值 | C1 至 C4 四判据 | windscada/control_profile.parquet、control_schedule.parquet |
+| 停机与归因(perf/faults.py) | 停机事件、报警台账、调度令码 | 段不少于 1 小时;T1 与 T2 两级归因 | windscada/stop_events.parquet |
+| 损失与可用率(perf/availability.py) | 风速、有功功率、功率曲线分箱 | 理论功率插值后算损失;风速低于 3.0 米每秒记 0 | windscada/loss_monthly.parquet |
+| 融合面(subsys/fusion.py) | 振动 handoff、温度比档、润滑月度、油样索引、报警与工单 | 关闭类最先判加取严;禁按源数加权 | 内存逐台表(融合面链盘)与 handoff 出件 |
+| 系统矩阵(taxonomy.py) | 各面上级产物与 windcms 报告转录 | 取严合并;未收录不判正常 | system_aux.parquet 与内存矩阵 |
+
+***
+
+## 7 时间窗口径设计
+
+### 7.1 时间窗词表
+
+2026-09-21 的用户令要求"部件问题与发电性能随所选时间窗变化",并新增自定义起止日期(含两端)。实现的词表如表 7-1 所示(scripts/windscada_serve.py 的 WINDOWS 与 months_of 与 win_range)。
+
+| 时间窗写法 | 含义 | 对月度件的作用 | 对日粒度件的作用(含两端) |
+|---|---|---|---|
+| 近30日 | 数据末端 1 个月 | 该 1 个月 | 该段月首至月末 |
+| 近90日 | 数据末端 3 个月 | 该 3 个月 | 该段月首至月末 |
+| 2026年 | 预设段 | 2026 年 1 月起至数据末端 | 段首月首至段末月末 |
+| 2026H1 | 预设段 | 2026-01 至 2026-06 | 2026-01-01 至 2026-06-30 |
+| 2025H2 | 预设段 | 2025-07 至 2025-12 | 2025-07-01 至 2025-12-31 |
+| 全程 | 全部月份 | 全部 | 0001-01-01 至 9999-12-31 |
+| YYYY-MM | 单月 | 该月 | 该月 1 日至月末 |
+| YYYY-MM~YYYY-MM | 月区间 | 区间内各月 | 起月 1 日至止月月末 |
+| YYYY-MM-DD~YYYY-MM-DD | 自定义起止(含两端) | 与之相交的月 | 原样,含两端 |
+
+含两端的实现方式是:win_range 原样返回日期串并按起不大于止排序,日粒度过滤在 in_win 里用"上界加 1 天再减 1 秒"来含住止日当天。月度件按所跨月份取整,日粒度件按日精确,页面上写明这句,避免读者以为月度均值能切到天。默认时间窗是 2026年(服务端取参数默认值与前端一致),工作台 v2 顶栏提供两个日期输入框与「应用」按钮,校验起不大于止,非法输入拒绝并提示。
+
+### 7.2 哪些随所选时间窗、哪些不随
+
+随与不随的划分如表 7-2 所示。需要强调的是:不随时间窗变化的内容不是"忘了改",而是各有自己的数据时点或对照意义。
+
+| 内容 | 是否随时间窗 | 机制 |
+|---|---|---|
+| 报障统计、五态控制策略、温度月轨迹、停机台账 | 随 | 月度件按 months_of 过滤 |
+| 判级四轴(变桨、偏航、蓄能、温度) | 随 | taxonomy.system_matrix 接受 span,各面 registry 接受 span;变桨用日粒度件精确切片,偏航与蓄能与温度走窄仓重算 |
+| 部件可靠性表(平均无故障间隔(MTBF)、单次停机时长(MDT)、停机时长) | 随 | reliability.overview 接受 span;停机事件与报警都是日粒度 |
+| 发电性能的 7 张月度时序图 | 随 | 按 months_of 过滤(月度件按月取整) |
+| 发电性能的七镜头曲线 | 随 | /api/curves 带时间窗参数,按时间窗重算分箱;选到 2025-07 至 2025-12 时直接用正式产物 |
+| M9 控制参数一致性 | 随 | control.registry 接受 span,走窄仓 |
+| 温度面的 ref 与 ref_sm 对照时间窗 | 不随(故意固定) | 季节解耦的对照基准,跟着漂就失去对照意义 |
+| 振动判级与厂家报告转录与工单台账 | 不随 | 各有自己的数据时点;页面上如实标注"不随本时间窗" |
+
+### 7.3 按时间窗缓存与 pending 契约
+
+按所选时间窗重算一律走"后台算加进程内缓存":缓存分三类(判级矩阵、曲线、M9),键由起止日期组成;命中即回,未命中则起后台线程并把"正在算"的状态回给页面,页面轮询再取。三类缓存的状态变量是 sysmx_pending、m9_pending 与 win_pending(win_pending 等于前两者之一为真),曲线另用 building 标记。缓存是进程内常驻字典,没有过期时间也没有容量上限;另有产物指纹缓存,过期时间为 2 秒,用于产物更新后自动重载。
+
+同一时间窗只算一次靠"在算集合"去重;算失败会记名并保留错误摘要,供页面与日志查看。为避免"预热线程与请求线程同时算两份中间帧"把内存吃光(远端实测问题),重活加了一把全局串行锁(_HEAVY_LOCK),并在拿锁时打印可用内存。服务启动末尾会起一个名为 win-warm 的守护线程做后台预热:先算六个预设时间窗的判级矩阵(每时间窗等待上限 180 秒),再算默认时间窗的七镜头(等待上限 240 秒);M9 不预热。预热可用环境变量关闭。
+
+pending 契约的关键是"绝不静默把旧口径当新时间窗":在算期间页面在脸上写明"下面这几张卡还是上一份口径",并在轮询到缓存就位后整段替换。时间窗与 pending 契约的时序如图 7-1 所示。
+
+![图 7-1 时间窗按窗重算与 pending 契约](figures/fig-des-05-时间窗与pending.png)
+
+前后端的等待参数如表 7-3 所示。
+
+| 环节 | 参数 | 实测或口径值 |
+|---|---|---|
+| 判级矩阵与曲线按时间窗重算 | 预估耗时 | 判级约 25 秒每时间窗;七镜头约 9 至 44 秒每时间窗 |
+| M9 控制参数按时间窗重算 | 预估耗时 | 服务注释约 5 秒每时间窗,版本记录约 0.6 秒每时间窗(两处不一致,待统一) |
+| 前端判级 pending 轮询 | 间隔与次数 | 每 5 秒一次,至多 40 次 |
+| 前端曲线 building 轮询 | 间隔与次数 | 每 6 秒一次,至多 40 次 |
+| 前端问答轮询 | 间隔 | 每 2 秒一次 |
+| 后台预热 | 覆盖范围 | 六个预设时间窗的判级加默认时间窗的镜头(不含 M9) |
+| 运维控制台 | 轮询间隔 | 每 2 秒一次 |
+
+### 7.4 时间窗口径下的实测对比
+
+按所选时间窗重算生效后,逐系统报警机组数会随时间窗变化,这是"判级真的按所选时间窗重算"的直接证据。本机实测(38 台):变桨报警为 15 与 13 与 10 台,偏航报警为 15 与 11 与 5 台,齿轮箱报警为 7 与 4 与 4 台,分别对应 2026年、2025H2、2026-07 三个时间窗;曲线图注随所选时间窗走且样本量随时间窗变化。远端复核(2026-09-21):变桨报警从 15 台变 7 台、偏航从 15 台变 3 台,曲线图注写"时间窗 2026-07-01 至 2026-07-31(随所选时间窗)"。
+
+***
+
+## 8 服务与前端设计
+
+### 8.1 HTTP 服务与路由族
+
+工作台服务用 Python 标准库的线程化 HTTP 服务实现(scripts/windscada_serve.py 引入 ThreadingHTTPServer 与 BaseHTTPRequestHandler),首请求把产物读进内存,之后每次取数前比对产物指纹(相关文件的路径、微秒级修改时间与大小的摘要,过期时间 2 秒),指纹变了自动重载,因此产物更新后不需要重启服务。路由族如表 8-1 所示。
+
+| 路由族 | 代表路由 | 说明 |
+|---|---|---|
+| 页面族 | / 与 /v2 与 /v2/snapshot 与 /turbine/台号 与 /problem/台号/系统 | 经典总览、v2 单页、快照下载、逐台页、单问题页 |
+| 取数族 | /api/fleet 与 /api/curves 与 /api/problem 与 /api/turbine 与 /api/vibcms 与 /api/maint_survey 与 /api/maint_std 与 /api/maint_framework 与 /api/dq_findings | 判级矩阵与链盘、七镜头、单问题、逐台、振动融合、检修四链与数据质量 |
+| 本体族 | /api/ontology 与 /api/ont_list 与 /api/ont_obj 与 /api/ont_chain 与 /api/channels | 本体页与机制链;v2 实际只懒取 /api/ont_chain |
+| 模型与报告族 | POST /api/ask 与 /api/ask_status 与 /api/ask_models 与 /api/rpt_compose 与 /api/rpt_export | 问答、状态轮询、档位列表、汇报纸组装与导出 |
+| 快照与事实与运维族 | /api/snapshot 与 /api/facts 与 /api/facts/claim 与 /api/reload | 快照、事实契约与单条 claim、显式清缓存并回显新旧指纹 |
+| 静态件族 | /static/* 与 /turbines/* 与 /windcms/* | 总览页与逐台静态件、振动页面件(限目录只读) |
+| 兜底 | 其余路径 | 缺件走结构化缺件页(HTTP 200),其它异常返回 500 页并记日志 |
+
+### 8.2 网关前缀改写与 data-abs 豁免
+
+网关把组件页里的根相对链接按前缀改写,否则组件页里的 / 会指向网关根而不是组件根。改写规则如表 8-2 所示(scripts/guanlan_gateway.py 的 rewrite())。
+
+| 改写对象 | 规则 | 例外 |
+|---|---|---|
+| 属性类 | href、src、action、content 后紧跟单个斜杠的根相对写法加前缀 | 双斜杠协议相对与 data: 不动 |
+| 样式类 | url() 内的单个斜杠根相对写法加前缀 | 同上 |
+| 脚本调用类 | fetch、open、EventSource、WebSocket、assign、replace 后字符串字面量里的根相对写法加前缀 | 同上 |
+| 赋值类 | 裸 location 赋值的根相对写法加前缀 | location.href 已被属性类规则覆盖 |
+| 字符串常量类 | 字符串里出现的 /api/ 加前缀(避免拼接出的接口地址漏改) | 同上 |
+| 绝对地址类 | 指向本机旧端口与新端口的绝对地址归到对应前缀 | 旧端口映射表保留在代码里 |
+| data-abs 豁免 | 标签上标 data-abs="1" 的 href 先换成哨兵、改写完再摘掉,即这条链接不被前缀改写 | 两种书写顺序(href 在前或 data-abs 在前)都认 |
+
+data-abs 豁免的实际用途是"组件页要指回网关根":工作台页面里的「返回观澜门户」用根相对写法并在标签上标 data-abs="1",否则它会被改写成 /detail/ 而永远回不到门户。此外网关对 /ops 系列的响应加 no-store 缓存头(否则页面脚本更新后浏览器仍用旧版,表现为"改了没反应"),门户静态件按修改时间与大小缓存,/release/ 提供只读暴露且不合并主库。
+
+### 8.3 页面烘焙与静态件
+
+除实时取数外,系统还有一条"烘焙"路线:把接口响应烤进单个 HTML 文件,使其离线也能打开。烘焙器是 src/windscada/ui/snapshot.py 的 bake(),注入 snap 标记、六个预设时间窗与语言对等文件指针,并放行问答与报告导出类接口走网络。总览「全场状态」页就是这条路线:重算链的 ⑦b 步用组件同一套取数函数生成 outputs/场站/windscada/index.html,工作台 v2 的 iframe 指向它。
+
+静态件落点是:/static/* 从 L0 标准仓读,/turbines/* 从同一仓读并注入浅色主题,/windcms/* 只读振动产物目录并限制在本目录内。三者缺件时都返回结构化缺件页(HTTP 200),写明缺哪一件、属于哪条产物线、怎么补,并给出运维控制台与自检入口。
+
+### 8.4 前端单页、轮询、中英切换与链接契约
+
+工作台 v2 是单页应用(src/windscada/ui/app.js,133 KB,另有 charts.js 30.8 KB 与 app.css 24.2 KB)。首屏只取 /api/fleet(判级矩阵与链盘),其余标签按需懒取;待算状态按第 7 章的间隔轮询。中英切换用查询参数 ?lang=en(语言来源是页面的 lang 属性,服务端据此投影权威英文名),localStorage 中只有一个键 guanlan.portal,用于记住门户来路;静态快照模式下查询参数不生效,改用对等语言的真实文件指针。前端契约如表 8-3 所示。
+
+| 契约 | 写法 | 说明 |
+|---|---|---|
+| 标签切换 | #tab=标签名 | 十个工作台标签;切换触发 hashchange |
+| 系统下钻 | #tab=component 与 sys=系统 slug | 从系统格下钻到该系统的问题清单 |
+| 逐台页 | /turbine/台号 | 独立页,含返回门户链接(标 data-abs) |
+| 单问题页 | /problem/台号/系统 | 系统标识经 sys_norm 双向归一(中文名、前端 slug、大小写都认) |
+| 回经典页 | / 加 lang 与 win 参数 | 从 v2 回到经典总览并保留语言与时间窗 |
+| 系统标识认不出时 | 页面写"系统标识无法识别" | 绝不写"该系统在判级时间窗内无非常态"(对报警机组那是假陈述) |
+
+v2 页面还有一条硬要求:可展开的「查看完整依据」不得为空。该类卡片的行来自融合面链盘,而 SCADA 侧只覆盖多报警机组,两边集合不同时旧的查表会取空;现在的口径是缺依据时按本行字段自组句(部件、链路走到第几步、卡在哪一步、证据源),永不空。
+
+***
+
+## 9 本体与知识层设计
+
+### 9.1 六个摄入步与对象库规模
+
+本体层的构建是重算链 ⑦ 的六个子步,逐步说明如表 9-1 所示。本体层"零判级权":它只转录 L1 正本(判级矩阵与融合面)的结论,每条判级对象带审级与 claim 时间窗,证据指回 L1 产物,同部件多源判级并存不合并。
+
+| 顺序 | 命令 | 输入 | 输出 |
+|---|---|---|---|
+| 1 | python -m src.ontology.kb_ingest | 西门子 4.0 技术资料、四个台账、码表与手册 | 码表、作业指导、故障树、预防性任务等对象 |
+| 2 | python -m src.ontology.populate | 上述全部产物与判级矩阵 | 铺开判级与台账对象(含证据与判级对象) |
+| 3 | python -m src.ontology.chain_ingest | 运行中的链盘(/api/fleet) | 逐台一个决策对象,记六步状态与卡点与下一步 |
+| 4 | python -m src.ontology.trend_ingest | component_history.json 与链盘 | 在升与闭环趋势证据(挂到机组与部件) |
+| 5 | python -m src.ontology.retrieval 的 build(use_vec=False) | 对象库 | retrieval_index.json 检索索引 |
+| 6 | src.ontology.maintenance 的 refresh_params | 技术资料与契约 | turbine_params.parquet 实机参数表 |
+
+对象库实测规模如表 9-2 所示(outputs/rudong/ontology/objects.json,4,452,520 字节,对象结构为 id、type、props、links 四个字段)。
+
+| 对象类型前缀 | 对象数(实测) | 说明 |
+|---|---|---|
+| workorder | 5,567 | 检修工单展开 |
+| component | 580 | 部件与机群级系统对象 |
+| wi | 559 | 作业指导步骤 |
+| task | 559 | 预防性任务 |
+| alarmcode | 555 | 报警码(含中文码义) |
+| verdict | 411 | 判级对象(带审级与证据) |
+| evidence | 411 | 证据对象(指回 L1 产物) |
+| oilsample | 404 | 油样索引对象 |
+| catalog | 207 | 目录与分册 |
+| doc | 303 | 文档与图纸索引 |
+| failmode | 150 | 失效模式 |
+| turbine | 38 | 机组 |
+| decision | 39 | 决策链进度(一台一个) |
+| mechanism | 8 | 失效机制 |
+| 合计 | 9,791 | 对象库当前对象数 |
+
+按对象自身的类型字段统计(与上表按顶层键前缀统计口径略有差异,差异来自 Doc 类含文档与目录两种前缀):工单 5,567、部件 580、预防性任务 559、作业指导 559、报警码 555、文档类 510、判级 411、证据 411、油样 404、失效模式 150、决策 39、机组 38、失效机制 8。对比参考:docs/系统设计说明.md 记录的 2026-09-17 时点为 9,613 个对象,本次实测为 9,791 个(对象库文件修改时间 2026-09-21),两个数字都如实保留并注明口径日期;检索索引文件 2,842,037 字节,实机参数表 58,397 字节。本体审计(python -m src.ontology.audit)的期望值是 0 问题,判级时间窗口径常量为"近 90 日固定时间窗(至 2026-07-07,曲线轴为 2025H2)"。
+
+### 9.2 MCP 纯函数只读复用
+
+本体层对外暴露一组只读工具(src/ontology/mcp_server.py),全部为纯函数、模型零写权、判级零生成,只转述对象里的判级原文与审级。工具共 18 个,另有 1 个接地闸,如表 9-3 所示。
+
+| 工具 | 用途 |
+|---|---|
+| ont_stats | 本体库概览与判读纪律(全场级问题开局) |
+| ont_fleet_verdicts | 全场判级速查(一次给全 38 台的台号、部件、级别、审级) |
+| ont_turbine_brief | 台级汇总(判级带审级、机制、工单、油样、决策) |
+| ont_brief_pack | 一站式取证包(台级问题首选,一轮取齐) |
+| ont_get | 按 id 取对象全文(props 与 links) |
+| ont_query | 按类型、台号、关键词过滤查询 |
+| ont_links | 对象的正向链接与反向被引 |
+| ont_search | 混合检索(术语加语义召回相关对象 id) |
+| ont_mechanism_search | 失效机制库检索 |
+| ont_scenario | 排期沙盘推演结果(建议级) |
+| ont_loss | 损失分析(近 N 月分态与台级损失结构) |
+| ont_risk | 风险跟踪面(监视读数、机制状态、验收状态机、盲区纪律) |
+| ont_workorder | 检修工单查询(按台、系统、关键词) |
+| ont_chain_fault | 链二抢修:报警码到码义到失效模式到手册分步处置 |
+| ont_chain_preventive | 链一预防:系统或部件到失效模式到预防性任务到作业指导步骤 |
+| ont_fault_tree | 链三故障树:系统失效模式按实证量排序加征兆码 |
+| ont_maint_plan | 链四计划:按风险级与命中数排序的检修排程建议 |
+| ont_doc_search | 技术支持:图纸、原理、手册检索并返回现场可调阅路径 |
+| claim_check | 接地闸:最终回答前必须调用,台号与数字须来自本会话工具事实,越界即拒 |
+
+这组工具同时被多处复用,且"只读"不是口头声明而是可核事实:整份模块里文件写入、目录创建、进程调用与网络请求四类接口零命中;对象库缺失时直接响亮报错并提示先跑铺开步,不静默降级;读库按对象库文件的修改时间失效重载,避免读到静默陈旧的库。复用方有五处:离线问答代理(同进程直连,工具与接地闸零复制)、工作台本体页的三个服务端函数、工作台机制链接口的按类派发、问答服务的取证调用,以及总览页烘焙时的本体数据注入。工作台侧的回答会自动附结构化事实契约引文块,引不到契约条时明写"未被契约背书"。
+
+### 9.3 机制链、决策台与对象浏览器
+
+机制链(chain_ingest)把逐台处置进度落成决策对象,记六个步骤的状态、卡点与下一步;它不是证据对象,而是系统推出来的处置状态,因此需要服务在跑(它从工作台的链盘接口取数)。本体层不设独立页面标签,承载页是工作台的第 7 个标签「检修决策」(口径原话:"检修决策等于本体展现的窗口"),页内四段分别是「机制链视图 · 29 号范例(证据到机制到判级到沙盘到动作到验收)」「失效机制库」「决策台(决策捕获加月度滚动)」「对象浏览器」。这四段的取数接口是本体概览、对象列表、对象详情与机制链派发(按故障、预防、故障树、计划、文档五类分别派给对应的只读工具);这些接口在生成单文件快照时也会被逐个烘焙进页面,因此离线打开也能看。页面页脚写着一句纪律:本体是只读语义层,判级权在 L1 加审级,模型与网页均零判级权。
+
+事实契约是把结论对外化的那一层:每条结论由 scripts/guanlan_facts_contract.py 生成,包含 claim 编号、脱敏来源引用(文件名加 sha16 与章节)、本条 sha256、时间窗、聚合层级与裁决(六枚举之一),并派生出四类消费者件(门户结论段、工作台卡片、问答引用、报告摘要)。当前口径是"由重算台账生成 claim"(⑤b 步),因此门户结论段、问答与报告都随重算刷新;claim 六族是报警集中、重复检修、停机损失集中、温度相对离群、功率曲线偏离、振动面过闸线,相对判据一律封顶"候选"。
+
+***
+
+## 10 本机模型接入设计
+
+### 10.1 档位与配置
+
+模型接入完全可选:判断在代码,模型只做问答、解释与初筛。档位与显存需求写在 configs/models.json,如表 10-1 所示;配置注释明确"换模型改这里加 ollama pull,不改结论"。
+
+| 档位键 | 模型 | 显存需求(写入配置) | 用途 |
+|---|---|---|---|
+| default_qa | qwen3:8b | 6 GB | 默认问答,关闭思考模式;校闸不过自动升档一次 |
+| escalate_qa | qwen3:32b | 22 GB | 复杂题升档(2026-09-22 由本机未装的旧档名改为本机实装档) |
+| review | deepseek-r1:14b | 10 GB | 交叉审核初筛票(非独立审级) |
+| embed | bge-m3 | 2 GB | 检索向量 |
+
+配置文件另给出三套显存档位组合(24 GB 及以上四档、12 至 16 GB 三档、纯 CPU 两档)与四条拉取命令,运行地址是本机 11434。本机实测四档全部可用(自检逐档打印档位与模型名)。
+
+### 10.2 问答校闸与升档
+
+问答链路(src/ontology/fast_agent.py)的口径是"校闸不过不出正文":回答里引用的对象编号必须存在、引用判级对象的句子必须写出该对象的判级词、句中的系统名必须与所引对象一致;拦截原因会明确写给用户(换问法、点名机组号或结论编号、查看契约结论清单)。慢档(32B 级)单轮改写超过 60 秒时只给一次改写机会,避免为一个不过闸的答案烧掉十几分钟。
+
+升档的规则是:默认档被闸拦下则用升档目标重答一次;升档也拦则照旧不出文,但把两次拦截原因都带回。升档目标不再硬编码为单一档名,而是按"配置档位且确已安装"解析(先取 configs/models.json 的 escalate_qa 且验证本机确有该模型,再回落到代码内的映射表,都没有则返回空并响亮回落同时打日志)。这一改动来自 2026-09-22 的人工测试消缺:原先升档目标写的是本机未安装的档名,导致升档调用拿不到摘要、问答终态为 error,用户只看到"复核中"。
+
+### 10.3 耗时预算
+
+耗时预算按档位给出(src/ontology/fast_agent.py 的 BUDGET_S):8B 档 30 秒、另一轻档 30 秒、32B 档 120 秒(32B 权重约 18.8 GB,单轮可达数分钟,属"离线慢审")。超预算后不再改写,直接交给升档或不出文。实测参考:本机自检的 Ollama 探针耗时 171.8 秒(同一底稿另一次记录为 114.09 秒),审计流水里也有探针超时 90 秒的失败记录,说明本机模型可用但慢,属纯 CPU 或显存不足场景。
+
+### 10.4 审计日志
+
+每次模型调用写一条 JSON 到 logs/audit/llm_audit.jsonl,字段如表 10-2 所示(依据该文件当前 40,751 字节内容与末两条实样)。
+
+| 字段 | 含义 |
+|---|---|
+| ts | 调用时间(ISO 形式) |
+| purpose | 用途(如 probe、ask、review) |
+| model | 模型名 |
+| digest | 模型摘要指纹 |
+| prompt_sha16 | 提示指纹前 16 位 |
+| options | 参数(温度、随机种子、生成长度上限) |
+| think | 是否开启思考模式 |
+| tools | 是否带工具 |
+| attempt | 第几次尝试 |
+| latency_s | 耗时(秒) |
+| status | 结果(ok 或 fail) |
+| error | 失败原因(异常类型与消息,截断保存) |
+| output_sha16 | 输出指纹前 16 位 |
+| out_chars | 输出字数(可用于判断是否"零字成文") |
+
+审计流水与模型闸的其他纪律(固定随机种子与温度使同问同证据同答、同一时刻只有一个生成请求、瞬断自动重试一次、连续三次失败熔断 60 秒并明示、探针成功即清熔断)共同保证"模型部分可复核"。两条如实记录的观测:一是本机的审计流水在服务运行期间持续追加(本次只读观测中从 108 行增至 114 行),因此它是"活的"证据而不是静态文件;二是成功记录里会残留上一次尝试的失败原因(重试成功的那一行仍带超时字样),这是字段清理不彻底的小瑕疵,已列入待统一项。
+
+### 10.5 未装模型时的降级与如实标注
+
+未装模型时的降级分两层,口径不同。检索层是"真降级":向量检索不可用时自动改为纯词法检索并打印说明,绝不让检索层因为缺模型而整个不可用;重算链因此刻意只建词法索引(不建向量索引),所以离线机器上的对象库、机制链、对象浏览器与图纸检索全部照常可用。模型层是"响亮报错、不静默回落":模型闸在连续三次失败后熔断 60 秒并明确报"本机模型未就绪(调用方应报 503 或明示,不得静默回落)";页面把连接被拒一类的底层错误改写成"本机模型未启动(Ollama 127.0.0.1:11434 拒连);离线版不转云,请先启动 Ollama",问答接口返回 503,档位列表接口给出本机可用档位,自检里的模型探针判 FAIL 并记录原因。远端部署机(2026-09-22)实测没有装 Ollama(无二进制、命令不在 PATH、11434 端口拒绝连接),因此远端的问答与升档功能不可用,其余页面不受影响;这一点已记入版本记录的遗留项,处置建议是"装 Ollama 并把有网机器上的模型目录整体拷到目标机同位置",是否安装由用户决定。
+
+***
+
+## 11 运维控制台与重算编排
+
+### 11.1 运维控制台与动作
+
+运维控制台由网关提供两个入口:整页 /ops 与内嵌版 /ops/recalc(只保留重算、产物与动作进度三块,是门户「数据重算」页的 iframe 目标)。接口是 GET /ops/api/state(真实状态:各服务端口通不通、产物在不在、有没有任务在跑、上次结果)与 POST /ops/api/动作。动作只有 POST,其它方法返回 405(避免链接被预取或刷新时误触发),未知动作返回 404。四个动作如表 11-1 所示。
+
+| 动作 | 语义 | 语义冲突时的返回 |
+|---|---|---|
+| stop_services | 停止组件服务(保留控制台本身) | 组件都已停止时回 409 |
+| start_services | 启动组件服务并打开门户 | 组件都在运行时回 409 |
+| rebuild | 执行重算(等价于 python scripts/rebuild_all.py) | 已有任务在跑时回 409 |
+| products_off | 清除产物(真删除,不留备份) | 产物已清空时回 409 |
+
+按钮的禁用只是提示,后端一律再校验一次;直接打接口也绕不过并发与语义前置。页面每 2 秒轮询一次状态。
+
+### 11.2 任务为什么必须脱离网关的父子树
+
+若网关直接用子进程方式起重算,任务就是网关的子进程;而停止服务用的是"按进程号并沿父子树强杀",会连带把正在执行的重算一起杀掉(实测日志停在第六步"重启组件服务(停)"那一行,之后什么都没有,服务也停在半路)。因此运维动作经一个只做一件事的启动器二次拉起执行器后立即退出:执行器的父进程变成一个已退出进程,于是它不再是网关的子孙,沿树强杀不会再连带杀掉它。启动器把执行器的进程号打到标准输出的最后一行,供父进程记账。
+
+与此配套,重算链的第六步只重启组件服务、保留网关,理由有二:控制台页面本身由网关提供,连它一起停会把用户正在看的界面弄没;重算本身可能就是从控制台起的任务,停网关会沿父子树把重算一起杀掉。
+
+### 11.3 状态文件、心跳与日志
+
+任务状态写在 run/ops_job.json,字段与本次实测内容如表 11-2 所示。执行器每 15 秒更新该文件的心跳;网关侧超过 150 秒没有心跳就把运行中收尾为完成并写"心跳停了 N 秒,任务多半被强杀,未拿到退出码";判失败前会重读一次状态文件,防止轮询竞态把退出码覆写。
+
+| 字段 | 含义 | 本次实测值 |
+|---|---|---|
+| kind | 动作类型 | rebuild |
+| cmd | 实际命令 | scripts/rebuild_all.py --with-verify |
+| pid | 执行器进程号 | 54848 |
+| log | 动作日志路径 | logs/ops/ops_rebuild_20260919-095635.log |
+| status | 任务状态 | done |
+| rc | 退出码 | 1 |
+| started、finished | 起止时间 | 2026-09-19 09:56:35 至 2026-09-19 10:43:31 |
+| seconds | 耗时(秒) | 2815.5 |
+| note | 备注 | 空 |
+
+动作日志落在 logs/ops/,命名为 ops_动作_YYYYmmdd-HHMMSS.log,保留最近 20 份或 30 天(写前清理);本次实测该目录恰有 20 个文件,符合保留策略。运行态文件 run/pids.json 与 run/ops_job.json 是"当前跑着谁、当前在做什么"的唯一凭据,交付包默认排除它们。
+
+### 11.4 服务化重启与端口占用处置
+
+停止与启动的口径是:停止用按进程号并沿树强杀(Windows)或按进程组发信号(类 Unix),成功后删除运行态文件;启动按配置起六个组件并等待健康检查就绪(40 次 1 秒探测),全部就绪返回 0,有模块未起返回 1(属预期降级态),健康检查 60 秒未就绪返回 2。端口占用时的处置顺序是:先停服务再启动;仍被占用则在运维控制台看端口状态与进程,按端口清理占用进程;仍不行则改 configs/serve.json 的端口并同步网关路由表(网关的上游端口表默认值与 serve.json 可覆盖的键名一一对应)。
+
+***
+
+## 12 安装、服务化与版本管理设计
+
+### 12.1 安装与卸载
+
+Windows 安装入口是 install.bat 与 install.ps1,步骤号写死在输出里,如表 12-1 所示(依据 install.ps1 的实际打印文案)。类 Unix 入口是 install.sh,口径与 Windows 侧同构(选解释器、建虚拟环境、装依赖、写配置、可选装 systemd 单元、写安装记录、跑自检)。卸载入口是 uninstall.bat 与 uninstall.sh,共用同一份实现 scripts/guanlan_uninstall.py。
+
+| 步骤 | 做什么 | 失败时的处置 |
+|---|---|---|
+| 0/7 安装前检查 | 读 src/version.py 取本包版本,读安装根下的安装记录比版本差异(同版本、升级、降级、老安装无记录四类),交互式询问是否重装 | 交互式答非 y 即取消且不改任何文件;加 -Force 跳过询问;非交互自动继续 |
+| 1/7 选 Python | 需 3.11 及以上(依赖 pandas 3,3.10 及以下没有轮子);机器上没有则解包内便携运行时 | 两者都没有则打印两条办法后退出 |
+| 2/7 建虚拟环境 | 建 .venv 并升级 pip | 失败即停 |
+| 3/7 装依赖 | 优先用包内离线轮子 wheels/win_amd64,无轮子才联网安装 | 依赖安装失败退出码 3,并提示先解决再启动 |
+| 4/7 写配置 | 把 configs/serve.json 的 python 字段写成相对安装根的 POSIX 路径 | 写配置失败退出码 4 |
+| 5/7 建启动快捷方式 | 建"启动观澜.lnk"(中文名给人)与"start_guanlan.lnk"(ASCII 名给脚本)与桌面快捷方式,指向无窗口启动器 | 建不出来只告警不影响使用,并给手工建法与 start.bat 替代 |
+| 6/7 服务化(可选) | 调 services/service_ctl.py install 注册 Windows 服务,或加 --user-login 写登录自启 | 注册失败只告警,并提示需要管理员权限与 --dry-run 预览 |
+| 7/7 模型(可选) | 包内有离线安装包则静默安装 Ollama,并提示模型目录的拷贝办法 | 未装且包内无离线包时明确写"问答不可用",其余页面不受影响 |
+| 收尾 | 写安装记录 install-info.json 并跑一次自检 | 自检输出作为装机结论 |
+
+安装时间为整轮口径"首次约 10 分钟"(说明书),逐步耗时无实测证据。卸载的默认口径是"只拆本程序装上去的东西":停服务、注销服务、撤开机自启、删快捷方式、清运行态、删安装记录;默认保留 data、outputs、logs、.venv 与程序目录,加 --purge 才删数据(会再问一次),加 --all 才删程序本体。之所以默认不删程序目录,是因为 Windows 上正在运行的 python.exe 就在 .venv 里,"删自己必然只删一半",留个删了一半的目录比留着整棵目录更难查;卸载脚本因此先切到临时目录再调 Python。
+
+### 12.2 服务化形态
+
+两平台的服务化形态如表 12-2 所示。两平台共用同一个工作体(起等于 guanlan.py serve、守护等于每 15 秒巡检同样命令、停等于 guanlan.py stop),因此"服务起的"与"手工起的"口径完全一致,不会出现两套行为。
+
+| 平台 | 形态 | 服务体 | 注册器 | 前置条件 |
+|---|---|---|---|---|
+| Windows | Windows 服务(SCM 托管,独立进程) | scripts/win_service.py(用标准库直连服务控制管理器,不依赖第三方库) | scripts/service_ctl.py install(内部调系统命令创建、描述、配置失败重启、启动) | 管理员 |
+| Linux | systemd 单元 guanlan.service(简单类型加始终重启) | scripts/service_main.py(前台守护) | scripts/service_ctl.py install(写单元文件并启用) | root |
+| Windows 备选 | 当前用户登录自启(不需要管理员) | scripts/guanlan_start_hidden.py(无窗口启动器) | scripts/service_ctl.py install --user-login | 无 |
+
+服务名与显示名是固定值(guanlan 与"观澜·如东样板 v2"),服务失败重启策略是 24 小时复位、重启延迟 60 秒。本机实测(appraisal):service_ctl.py status 报"已注册=False 运行中=False",即本机没有注册服务(本机以手工方式在跑,六个端口处于运行中);Windows 服务的真注册与 systemd 的真装在本次环境未取证(非管理员环境只有 --dry-run 与免权限守护自证两条路径,其输出与设计说明一致),属如实留白项。
+
+### 12.3 安装记录与版本三守卫
+
+安装记录 install-info.json 是"这台机器装的是哪一版"的唯一凭据,字段如表 12-3 所示。装完写、卸载时删(删掉等于这台机器回到"没装过")。需要如实指出:本机该文件记录的版本是 2.5.0(装于 2026-09-17),落后于当前代码版本 2.10.0,因此它正是"安装前检查会提示版本差异"的活样本;重装或升级后会随之更新。
+
+| 字段 | 含义 |
+|---|---|
+| name | 系统名 |
+| version | 已装版本(唯一凭据) |
+| edition | 版本性质(离线单包) |
+| installed_at | 安装时间 |
+| python | 安装时使用的解释器绝对路径 |
+| mode | 运行方式(手工、Windows 服务、systemd、登录自启) |
+| host | 安装根 |
+
+版本号只写在 src/version.py 的 VERSION 一行,改动自动传导到各处;三条守卫如表 12-4 所示,都是"对不上就报错",不靠人记得。
+
+| 守卫 | 读哪里比对 | 违反时的表现 |
+|---|---|---|
+| 版本真源守卫 | scripts/pack_dist.py 读 src/version.py 的 VERSION 写进包清单并拼默认包名 | 打包器不再自带版本常量,避免"包说一个版本、安装记录说另一个版本" |
+| 版本记录守卫 | scripts/version_log.py --check 比对 docs/版本记录.md 的自动块与 src/version.py 的 HISTORY | 不一致即 rc=6,提示重生成;重算链 ⑧d 步容忍 6 但报出来 |
+| 安装前检查守卫 | install.ps1 与 install.sh 从 src/version.py 取版本并与安装记录比对 | 同版本、升级、降级、无记录四类分别提示,降级会明确警告 |
+
+另有一条与版本同级的守卫是入口引用闭合与入口脚本编码守则(见 13.3 节):打包、开箱验证、装机自检三处都查,缺一个文件就不出包。
+
+### 12.4 打包器与开箱验证
+
+打包器 scripts/pack_dist.py 的默认口径是"一条命令、零开关":程序与脚本、配置、发布件、文档、离线依赖件与入口脚本进包;输入数据、产物、运行日志与临时文件不进包(例外用 --with-data 与 --with-products),另排除装机产物(安装记录与快捷方式,因为它们只在本机有效)。包内清单 dist-manifest.json 逐条记版本、构建时间、包含项、逐条排除理由与四类标志(不含数据、不含产物、不含日志、不含临时文件)。
+
+打包实测口径:一次打包报 3,430 件、1,032.0 MB(该数字来自变更记录,非本次实测;旧口径含缓存与日志为 3,741 件、1,091 MB)。开箱验证一条命令给出"能不能装、能不能跑"的证据,步骤如表 12-5 所示。
+
+| 步骤 | 核验内容 | 判定 |
+|---|---|---|
+| ① 解压 | 解压到临时目录并打印件数与耗时 | 解压失败即失败 |
+| ①b 闭合与编码 | 入口脚本引用闭合、编码守则核对 | 任一不通过即 rc=1 |
+| ①c 数据骨架 | 输入数据目录骨架逐个在位,且除放置说明外无混入文件 | 不通过即失败 |
+| ② 离线安装 | 在副本上真跑安装 | 非 0 即失败 |
+| ②b 无窗口与快捷方式 | 两份快捷方式在位、无窗口自证通过、包内无脚本宿主脚本 | 不通过即失败 |
+| ③ 起服务核验 | 核验门户根、工作台、振动、运维控制台、健康检查五个地址 | 要求至少 4 个可用 |
+| ③b 卸载核验 | 在刚装好的副本上真跑卸载,断言记录、运行态、快捷方式都拆掉且数据与产物一件不少 | 不通过即失败 |
+| ④ 收尾 | 只停副本自己的进程、清理临时目录 | 副本端口无残留 |
+
+一项现场教训写进了设计:开箱验证原先必须占用默认组件端口,于是验证期间用户正在用的工作台与振动页是断的(用户点门户「登录」正好撞上)。现在网关的上游端口表可被 configs/serve.json 覆盖,验证改为副本整套换到空闲端口(例如网关 28700、组件 28800 起),主实例照常运行;找不到空闲端口才降级为"跳过启动核验"并打印提示。
+
+***
+
+## 13 质量保证设计
+
+### 13.1 审计脚本矩阵
+
+系统的质量保证是"一条链加重算里的审计门加一个装机自检"。审计器矩阵如表 13-1 所示,退出码与本次实测结论一并列出。
+
+| 审计器 | 查什么 | 退出码语义 | 本次实测 |
+|---|---|---|---|
+| scripts/products_reverse_audit.py | 逐件回溯"输出到功能与算法到输入",族表匹配与人工件识别 | 0 全部成立;5 有未归类或判据失败 | rc=0,呼应成立 3,548 件、不成立 0、人工件 0、未归类 0 |
+| scripts/pages_audit.py | 页面归口五条机器规则(静态页不得引产物、实时页端口须在端口表内、引用产物须在位、数据派生页须有来源与指纹、冻结交付件须有版本与日期) | 0 一致;5 登记缺项或文件缺失;6 溯源缺失;7 数据派生页面陈旧;8 交付件缺版本号;9 分类错误 | rc=0,检查 21 项、2 项一致、19 项已知缺口、0 项要处理 |
+| scripts/config_audit.py | 配置登记表 R1 至 R8(顶层只放登记过的运行期单件、无备份垃圾、命名、内容不得含本机绝对路径、代码引用的配置必须存在、不许手拼配置路径、每域须声明消费者、场目录每件要么是场定义要么登记为 profile) | 0 通过;5 结构问题;6 悬空引用;7 内容含本机绝对路径;8 手拼路径;9 场配置不合约定 | rc=0,171 件配置、5 个域、1 个场定义、10 件机型 profile、0 不一致、14 已知缺口、7 提示 |
+| scripts/log_audit.py | 日志 L1 至 L5(落点唯一、命名、按末尾 200 行校验行格式、行尾与颜色、保留策略与审计流水每行合法 JSON) | 0 通过;5 日志跑到 logs 之外;6 命名不合口径;7 行格式不合规;8 行尾或颜色问题;9 保留策略超限 | rc=0,0 不一致、87 条提示(历史日志与旧行只提示) |
+| scripts/raw_scan.py | 输入数据逐族指纹比对与增量识别 | 0 无变化;4 有新增或变化;5 还没有基线 | rc=0,"与上次快照一致,没有新数据";另 --selftest rc=0(族表覆盖反向族表的 7 个输入、10 个步骤标签都能在链上找到) |
+| scripts/chain_gap_check.py | 振动六层链四步脚本是否齐全、关键产物是否在位、能否逐值对拍 | 0 齐全;5 缺料 | rc=0,四步脚本齐全;两件关键产物按口径重建但无标准答案可对拍 |
+| scripts/version_log.py | 版本记录自动块与代码 HISTORY 是否逐字一致 | 0 一致或已写;2 文件缺失;6 需重生成 | rc=0 |
+| scripts/detail_deps.py | 工作台各页面与接口的产物依赖台账(页面到接口到产物件到来源与生成端) | 0 通过;5 文档与现场不一致或证据核对不通过 | rc=0,清单 10 页签、16 接口、缺口 0 件、产物仓 3,549 件,文档与现场一致 |
+| scripts/inventory_products.py | 产物清点与输入到产物呼应校验(产物跨度必须落在输入跨度内) | 0 呼应正常;5 有 problems | rc=0(抽样:振动输入 25,693 件对时间窗索引 2,066,686 行、跨度 2026-03-16 至 2026-04-21) |
+| scripts/check_portability.py | 换机可移植性门禁三条硬规则(不许机器相关绝对路径、不许工作目录相对路径字面量、不许用系统分隔符拼库存字符串)加三条提示规则 | 0 通过;1 有严重项 | rc=0,通过(无机器相关绝对路径、无工作目录相对路径、未用系统分隔符拼串),例外登记 16 项加 2 类 |
+| scripts/check_transferable.py | 移植性大扫描五项(全类型文件的机器相关路径、虚拟环境解释器指向、各平台轮子是否备齐、便携运行时与清单、配置与端口是否相对) | 0 通过;1 有机器相关路径 | rc=1,290 处机器相关路径(HTML 烘入绝对路径 0 处,文档旧口径写 350 处)、提到网关端口的文本文件 43 个;轮子与便携运行时两项判通过 |
+| scripts/audit_chinese_terms.py | 中文表达审核(OEM 术语基准与行话规则),机器给候选人裁 | 0 通过;2 仅严格模式下有已裁改条目仍命中 | rc=0,扫 5 个文件与 7,322 条中文串,规则命中 123 条,其中显示层已覆盖 111 条、内部面豁免 42 条、仍待处理 0 条;无根词 3,902 条 |
+| guanlan.py check | 装机自检(约 40 行结论:解释器、源码可编译、语言包成对、子进程口径、入口闭合与编码、版本、卸载入口、反向呼应、页面归口、配置、日志、输入数据、依赖、产物与本体与契约与门户、六个端口、模型探针与四档、模型可用性) | 0 全绿;2 有 FAIL | rc=0,结论"全绿, 可 serve";底稿 docs/src/_guanlan_check.txt |
+
+自检底稿里的几个可核对数字:Python 3.12.10;源码 229 个文件可编译、无非法转义或语法错;语言包前后端 zh 与 en 成对 845 条;入口脚本引用闭合 48 条;入口脚本编码守则通过;反向呼应 3,548 件;页面归口检查 21 项;配置与日志无一致性问题;输入数据 26,503 件源件结构合规;依赖九项(numpy、pandas、pyarrow、polars、yaml、matplotlib、plotly、jinja2、docx)全部就位;L0 与 L1 产物、本体对象库、findings、事实契约、门户、仿真合页服务与资料包、三维资产、仿真回放资产、治理清单交付件全部在位;六个端口处于运行中;本机模型探针通过(耗时 171.8 秒)。
+
+有一处过程值得记录:本轮取证期间,工作台依赖台账门与可移植性门禁各自返回过一次"未通过"(一个是文档清单与现场不一致,一个是运行期源码里用系统分隔符拼串),随后两处都按同一口径修好并复跑转绿(可移植性门禁复跑输出"通过,路径可移植:只有相对路径加经路径模块解析",依赖台账复跑输出"文档与现场一致")。这正好说明"每条门都能独立重跑并给出退出码"的价值:问题是被跑出来的,不是被猜出来的。
+
+### 13.2 退出码语义与质量门闭环
+
+各审计器的退出码语义汇总如表 13-2 所示,语义与重算链里的容忍集合一一对应(见 5.2 与 5.3 节)。
+
+| 退出码 | 含义 | 谁在用 |
+|---|---|---|
+| 0 | 通过或无变化 | 全部审计器与构建器 |
+| 1 | 有严重可移植性问题;或链级失败汇总 | 可移植性检查器;重算链汇总非零即返回 1 |
+| 2 | 自检有 FAIL;契约自检不过;非交互下需要确认;文件缺失;严格模式下术语命中 | guanlan.py check、事实契约构建、卸载器、版本记录、中文表达审核 |
+| 3 | 权限不足 | 安装脚本与服务化注册器、卸载器 |
+| 4 | 有新数据;或按缺件如实报 | 输入数据扫描、变桨面、振动历史与基线、等价验收 |
+| 5 | 随包件这一路缺失(无基线、有未归类、被引产物不在位、文档与现场不一致、缺料、结构问题) | 月度派生件、反向呼应审计、页面归口审计、等价验收、链完整性检查、工作台依赖台账、产物清点、配置审计、日志审计 |
+| 6 | 溯源缺失;文档与代码版本记录不一致;增量冲突;悬空引用;命名不合口径 | 页面归口审计、版本记录一致性、输入数据结构体检、配置审计、日志审计 |
+| 7 | 数据派生页面陈旧;内容含本机绝对路径;行格式不合规;缺源类或时间空洞 | 页面归口审计、配置审计、日志审计、输入数据结构体检 |
+| 8 | 交付件缺版本号;手拼配置路径;行尾或颜色问题;输入结构仅提示 | 页面归口审计、配置审计、日志审计、输入数据结构体检 |
+| 9 | 分类错误;场配置不合约定;保留策略超限 | 页面归口审计、配置审计、日志审计 |
+
+需要如实说明的是:多数审计器只定义了自己会用到的那几个码,未列出的码在对应脚本里并未定义。例如反向呼应审计与链完整性检查只定义 0 与 5,版本记录只定义 0 与 2 与 6,装机自检只定义 0 与 2,可移植性门禁只定义 0 与 1。读退出码时应同时看该脚本自己的说明,不要假设"非零都一样"。
+
+质量门形成闭环:改代码或改配置后,重算链在⑤、⑧、⑧b、⑧c、⑧d 五处设审计门;装机时 guanlan.py check 再统一体检一遍;打包时入口闭合与编码守则在打包、开箱验证、装机自检三处强制执行;出包以后在目标机上安装再由自检复核。闭环如图 13-1 所示。
+
+![图 13-1 质量门与审计闭环](figures/fig-des-06-质量门闭环.png)
+
+### 13.3 入口脚本的字符守则
+
+三条编码守则由 src/entry_refs.py 实现,并在打包、开箱验证第①b 步、装机自检三处强制执行,违反就不出包或直接判 FAIL,如表 13-3 所示。
+
+| 文件类型 | 守则 | 违反的后果(现场实炸过) |
+|---|---|---|
+| .bat | 纯 ASCII、CRLF、无 BOM、不含连续小于号、大于号后必须是重定向目标 | 批处理注释里的中文被命令行解释器当成命令执行;连续小于号直接报重定向错 |
+| .ps1 | UTF-8 带 BOM 加 CRLF | PowerShell 5.1 按本地代码页解码导致中文乱码与级联语法错,安装一秒即退出 |
+| .sh | 行尾 LF | 每个词尾粘回车,set -e 变非法选项,变量判空翻转,脚本从一开始就是坏的 |
+
+与此配套的一条纪律是"入口脚本引用的文件必须齐全":凡入口脚本里出现的路径且在本机源码树真实存在,打包时自动补入,打包后对着包内条目复核(缺一个就删包并返回非 0),开箱验证与装机自检各再查一遍。这条守卫的价值在替换无窗口启动实现时体现过:去掉脚本宿主方案后入口列表与引用条目自动重算且全部在位。
+
+### 13.4 文档纪律
+
+文档与代码同级受管:版本记录由 scripts/version_log.py 从代码里的 HISTORY 生成,自动块禁止手改;重算链的 ⑧d 步以 rc=6 容忍但不静默的方式盯"文档与代码版本一致"。这条门的由来是一次实逮:远端升级的差异盘点只覆盖源码、脚本与配置,文档不在盘点面上,于是代码升到新版本而版本记录与系统设计说明留在旧版,而重算链当时只到 ⑧c,导致 24 步全绿、控制台写"重算完成",文档漂移没有任何自动拦截点。现在的口径是"升级差异盘点必须包含 docs",并由 ⑧d 步兜底。
+
+本文本身也按同一纪律编写:每章的事实都能在附录 A 找到来源文件,数字都能按第 1.4 节的命令复跑复现;本版同时新增三份交付文档(需求分析、设计说明、数据要求说明)与其生成端(文档渲染器与图表构建器),图表里的数字全部从真件取数而不是手画。
+
+***
+
+## 14 安全、合规与离线边界
+
+### 14.1 离线边界与只读输入
+
+系统的安全边界是四个方面,如表 14-1 所示。
+
+| 边界 | 设计口径 | 落地证据 |
+|---|---|---|
+| 只读输入 | 原始件只读,摄入器不写 data/raw;放置工具的增量落位对"同名不同大小"默认拒绝并拦下,必须显式确认才覆盖 | scripts/place_raw_data.py 默认拒绝冲突;产物是"当前原始件的函数"(源件删了行也随之消失并报出) |
+| 默认离线运行 | 全部计算在本机;依赖随包离线安装;模型可选且指向本机 | wheels/win_amd64 42 件、vendor/python 三平台便携运行时;模型端点是本机 11434 |
+| 唯一外发通道且强制脱敏 | 代码内保留一条显式的"可上云问答"通道(三个兼容云端接口,需环境变量提供密钥);未配置密钥时回落本机模型;外发前强制对每条消息做脱敏,脱敏模块不可用时直接拒绝外发而不是照发 | 离线模型代理模块的外发分支与脱敏调用;脱敏实体映射 16 条加三条正则;产物脱敏词表 32 条加 11 类回扫 |
+| 监听范围 | 组件只绑本机回环地址,只有门户网关使用 public_host(出厂为所有网卡) | configs/serve.json 的 host 与 public_host 及注释;启动时打印对外地址并提示"页面无鉴权,请在防火墙侧限来源";实测网关在所有网卡监听,其余六个进程只在回环监听 |
+| 无鉴权风险如实告知 | 页面没有登录与权限体系,对外监听等于对同网开放 | 配置注释与启动提示;处置建议是防火墙白名单或反向代理认证,不要直接挂公网 |
+
+### 14.2 脱敏与可上云面孔
+
+对外披露的内容走独立的一层:可上云面孔落在 outputs/场站/guanlan/cloud,由专门的面孔与页面与问答脚本生成;事实契约的每条结论带脱敏来源引用(文件名加内容摘要前 16 位与章节)与本条 sha256,因此"这句话出自哪份文件的哪一节、当时那份文件是什么内容"都可核对。脱敏实现分布在若干模块,口径可以概括为四层:一是外发消息的实体映射(16 条,把业主集团、整机厂、部件供应商、场站名与机型泛化,另有本机路径、电价与机群规模三条正则);二是产物与交付侧的公开词表(32 条替换词加 11 类独立回扫规则,替换与回扫分开写,并要求交叉自检"回扫认识每个替换词、替换后回扫干净",有残留即抛错拒绝出件);三是振动产物里唯一的字段级黑名单(8 个频率类键整值遮蔽,另有 17 条阈值或频率正则),交付件使用的是不看开关的强制脱敏版本;四是机组代号化(按定序摘要映射,不可由编号反推)。对外面孔生成后仍要回扫一次,命中本机绝对路径、邮箱、手机、身份证、精确坐标、密钥格式等任一类即返回 2 不出件;云问答出口同一口径,脱敏模块导入失败即拒绝启动。脱敏的边界是"对外面孔这一层做,内部产物不脱敏",两层由目录与生成端区分;机组号与机群台数在对外面孔里只报不拦,是否代号化由使用方裁决并写进面孔清单。需要如实说明的是,交付前另有一个安全核查器(密钥、个人信息、金额与业主名、内部件四类规则),但它目前尚未接入打包链,属手工步骤。
+
+### 14.3 日志与审计
+
+日志是审计的载体:长驻服务与启动器的日志落在 logs 组件名.log 并按统一行格式写;运维动作落在 logs/ops/ 并按保留策略清理;构建与摄入日志落在 logs/build/场站/ 下按产物子路径归档;机器审计流水落在 logs/audit/(模型调用流水 llm_audit.jsonl、云问答流水 cloud_qa.jsonl、术语审计 terms_audit.json)。统一格式之前的日志全部搬到 logs/legacy/ 留档,不悄悄删除。
+
+### 14.4 权限不足时的降级路径
+
+权限不足时不抛"访问被拒绝",而是给出可执行的替代路径,如表 14-2 所示。
+
+| 场景 | 无权限时的行为 | 可用替代 |
+|---|---|---|
+| 注册 Windows 服务 | 打印前置检查结果、逐条将执行的命令,返回码 3 | 管理员重试;先用 --dry-run 看它要做什么;改用登录自启(--user-login) |
+| 注销服务(卸载第②步) | 缺权限时照做其余步骤并打印手工命令,返回码 3 | 管理员/root 执行注销命令 |
+| 注册 systemd 单元 | 打印将写入的单元全文,不写入 | root 重试 |
+| 端口被占用 | 不静默重试,给出占用排查与改端口路径 | stop 后重启;按端口清理;改配置端口并同步网关路由表 |
+| 缺产物或缺模型 | 返回结构化缺件页或明确的降级提示,不伪造内容 | 放原始件后重算;补齐生成端;安装并拉取模型 |
+
+***
+
+## 15 可移植性与资源占用
+
+### 15.1 另一台机器零配置启动
+
+迁移到另一台机器的步骤如表 15-1 所示。要点是"程序与配置路径无关(全部相对安装根),但虚拟环境不能直接搬"——虚拟环境的配置文件绑定基础解释器路径,因此换机后跑一次安装即离线自足。
+
+| 步骤 | Windows | Linux 或 macOS |
+|---|---|---|
+| 1 解包 | 解压交付包到安装目录(目录不要带空格与中文) | 解包到目标目录(同样避免空格与中文) |
+| 2 安装 | 双击 install.bat(或调用 install.ps1) | sh install.sh(可用 GUANLAN_PY 指定解释器,--service 装 systemd) |
+| 3 自检 | 双击 check.bat 或跑 guanlan.py check | .venv/bin/python guanlan.py check |
+| 4 放数据 | 把原始件放进 data/raw/场站名/ 下并按源类目录落位 | 同上 |
+| 5 重算 | guanlan.py serve 后从门户「数据重算」触发,或直接跑 rebuild_all.py | 同上 |
+| 6 启动 | 双击"启动观澜"快捷方式(无窗口)或 start.bat | .venv/bin/python guanlan.py serve |
+
+安装根可用环境变量覆盖(路径模块优先读 WINDSCADA_ROOT),原始件根可用 WINDSCADA_RUDONG_SRC 覆盖,场名可用 WINDSCADA_FARM 覆盖;配置里的路径一律写相对安装根的形式,因此换盘、换机不需要改任何路径。
+
+### 15.2 Python 与依赖 wheel
+
+解释器要求是 3.11 及以上(依赖 pandas 3,3.10 及以下没有轮子);机器上没有 3.11 时使用包内便携运行时(3.12.14,三个平台各一份)。依赖清单共 19 条固定版本:polars 1.42.0、pandas 3.0.3、pyarrow 24.0.0、numpy 2.5.0、pyyaml 6.0.3、python-frontmatter 1.3.0、matplotlib 3.11.0、plotly 6.8.0、jinja2 3.1.6、python-docx 1.2.0、docxtpl 0.20.2、python-dotenv 1.2.2、requests 2.34.2、rich 15.0.0、tqdm 4.68.3、openpyxl 3.1.5、scipy 1.18.0、xlrd 2.0.2,另有一条不锁版本的 tzdata(Windows 无系统时区库,而 pandas 3 硬依赖它)。离线件如表 15-2 所示(件数与体积为本次实测)。
+
+| 离线件目录 | 件数(实测) | 体积(实测) | 覆盖平台 |
+|---|---|---|---|
+| wheels/win_amd64 | 42 | 169.4 MB | Windows 64 位完全离线可装 |
+| vendor/python | 3 | 81.4 MB(含清单文件共 4 件) | Windows x86_64、Linux x86_64、Linux aarch64 便携运行时 3.12.14 |
+
+Linux 与 macOS 侧包内没有轮子时,安装脚本自动改为联网安装;要完全离线需把对应平台的轮子放进相应目录。模型离线件(Ollama 安装包与模型目录)单独交付,不在主包内;未装时问答不可用,其余页面不受影响。
+
+### 15.3 磁盘、内存与算力估算
+
+资源需求与实测体积如表 15-3 所示。内存与磁盘的硬性要求来自说明书(最低内存 16 GB、推荐 32 GB;磁盘最低 5 GB 加三维资产 1 GB,另加模型 30 至 60 GB);目录体积为本次递归实测。
+
+| 项 | 数值 | 出处 |
+|---|---|---|
+| 内存最低或推荐 | 16 GB 或 32 GB | docs/说明书_观澜如东样板v2_v0.2.md |
+| 磁盘最低 | 5 GB 加三维资产 1 GB | 同上 |
+| 模型占用(可选) | 30 至 60 GB(固态盘) | 同上 |
+| 显存(只影响问答与本地审核) | 12 GB 及以上可跑默认档;24 GB 及以上可开升档;无显卡走纯 CPU 慢档 | 同上 |
+| data(原始件,本机实测) | 26,504 件、224.67 GB(其中振动原始导出 25,693 件、150.15 GB) | 本次递归实测 |
+| outputs(产物,本机实测) | 3,549 件、7,157.5 MB | 本次递归实测 |
+| release(发布件,本机实测) | 2,898 件、708.4 MB | 本次递归实测 |
+| 虚拟环境(本机实测) | 17,779 件、688.1 MB | 本次递归实测 |
+| 日志与配置(本机实测) | 日志 154 件、3.3 MB;配置 171 件、1.6 MB | 本次递归实测 |
+| 全仓不含版本库(本机实测) | 52,049 件、241,707.5 MB | 本次递归实测 |
+| 运行内存(本机实测快照) | 七个监听进程工作集合计 568 MB(网关 238.9、工作台 198.6、振动 53.8、本机模型 33.7、三维 15.4、四系统合页 13.9、仿真 13.7 兆字节);同批私有提交合计 3,293.7 MB | 本次实测快照,非稳态峰值,仅供量级参考 |
+| 交付包(口径) | 3,430 件、1,032.0 MB | 变更记录,非本次实测 |
+| 重算耗时(口径) | SCADA 侧 10 个构建器约 15 分钟;振动摄入按现场导出体量约 14 分钟(另有 25 分钟口径);变桨面约 10 至 12 分钟;窄仓一次扫约 2 至 3 分钟 | 代码注释与手册,多口径未统一 |
+| 单次完整重算(实测) | 本机一次带等价验收的重算 2,815.5 秒(约 47 分钟),该次以退出码 1 结束(末步等价验收因无随包基线一路属预期) | run/ops_job.json |
+| 按所选时间窗重算 | 判级约 25 秒每时间窗;七镜头约 9 至 44 秒每时间窗 | 服务注释 |
+
+算力方面的判断是:系统的重活集中在重算链(一次性、可后台跑),页面侧的实时计算只有按所选时间窗的分组与过滤,最重的两类已经用窄仓与进程内缓存压到秒级;模型问答是唯一可能占用数分钟的交互,且已按档位给预算并在超时后明确不出文。
+
+### 15.4 Linux 适配现状
+
+Linux 侧的适配程度如表 15-4 所示,全部为"已有实现但在本次环境未实测"或"已在本机验证"两类之一,不含估计。
+
+| 项 | 现状 |
+|---|---|
+| 安装脚本 | install.sh 已有,逻辑与 Windows 同构(选解释器、建虚拟环境、装依赖、写配置、可选装 systemd、写安装记录、自检) |
+| 便携运行时 | vendor/python 下已含 Linux x86_64 与 aarch64 两份 3.12.14 |
+| 依赖轮子 | 包内只有 win_amd64 轮子;Linux 需联网安装或自行放轮子 |
+| 服务化 | systemd 单元 guanlan.service 由注册器写入(简单类型、始终重启、停止超时 120 秒、混合杀进程模式、环境变量强制 UTF-8) |
+| 路径与换行 | 路径统一用 pathlib 与 POSIX 相对串;入口脚本按类型守行尾(.sh 用 LF) |
+| 进程与日志 | 无窗口子进程的口径在类 Unix 上是 0 标志位;日志行格式与保留策略平台无关 |
+| 未取证项 | Linux 安装、systemd 真装、国产化发行版首装均未在本次环境实测(历史交付件的 Linux 安装脚本曾有过行尾问题,已由编码守则与打包校验拦住) |
+
+***
+
+## 16 已知边界与未实现
+
+### 16.1 逐条边界与处置建议
+
+本系统坚持"缺件如实"。边界、影响与处置建议如表 16-1 所示,每条的处置建议都是可执行的一条命令或一次催缴动作。
+
+| 边界 | 影响 | 处置建议 |
+|---|---|---|
+| 本机未装配原生 MDB 驱动 | 本机无 pyodbc 与 mdbtools,读 MDB 走 ACE 提供程序加 PowerShell(与写库同一套);若目标机未装该提供程序,MDB 形态不可读(未在无 ACE 的机器上实测) | 目标机安装 ACE 提供程序;或让现场导出 CSV(取数层本就 CSV 优先) |
+| 测风塔数据零交付 | 风资源与尾流类分析缺外部对照(收资要求第 3 项记为未到位) | 按收资要求催缴测风塔数据;到位后重算对应族 |
+| 油样 2026-07 批 102 行缺源件 | 油样索引少该批,等价验收会报"有需人工看的差异" | 现场补该份合并报告 PDF 后重算 ② 步 |
+| 远端未装 Ollama | 远端问答与升档不可用,模型探针必然 FAIL;其余页面不受影响 | 安装 Ollama 并把有网机器上的模型目录整体拷到目标机同位置;是否安装由用户决定 |
+| SOP 相关配置悬空 6 条 | 分析锁校验不可用;场景解析不可用(本包无调用方,不影响页面);案例判别器外键校验与模块键校验缺单源;经济性换算退回内置假设并在结果里标注 | 需要哪一场就建对应配置;或明确删除该模块的入口引用;逐条影响已登记在配置登记表的 known_missing |
+| 振动六层链扫描件与正本类产物 | 四步脚本齐全且关键两件已按口径重建,但"无样件可对拍"(链完整性检查自述) | 由研发给口径或提供样件做逐值对拍 |
+| 数据派生页面无生成端 | 脱敏状态一览与取数单是客户手里的交付件,性质上随运行状态变但没有指纹与生成端,重算不会刷新它们 | 已按冻结交付件管理并把"会变旧"写进登记理由;将来若要随数据变,再补生成端与来源指纹 |
+| 页面引用的四棵产物树不在包内 | 传动链回放、四系统判据、控制律仪表台、取数单等页面存在 19 条已知的引用悬空 | 保留记账不删(删了门户裂口、交付缺件);待该产物线随包后复检 |
+| 3 件内嵌快照缺指纹 | 只能按生成时间判断新旧,无法做逐字节陈旧检测 | 下次生成时把来源与来源摘要写进页面(机制已就绪) |
+| 无生成端的历史组级产物清单 | 趋势件、热链、扇区、偏航与润滑面等族的历史随包件没有生成端;当前反向审计显示盘上 3,548 件全部呼应成立、未归类 0,但清单本身仍在文档里留档 | 由研发补口径(照月度派生件的办法反推并逐值验证);或承认其为人工件不按产物管 |
+| 收资要求其余未到位项 | 故障录波仅 4 台、原生秒级高频数据未见交付、保护定值未成册、可行性研究与微观选址未交付、周围已建风电场报表未交付、大部件更换台账不完整 | 按收资要求逐项催缴;到位后按族重算 |
+| 本轮曾出现的两处门禁未通过(已修复留档) | 取证期间出现两个未通过:工作台依赖台账门返回 5(文档清单与现场不一致)、运行期可移植性门禁返回 1(源码里用系统分隔符拼库存字符串) | 两处都已按原口径修好并复跑转绿(依赖台账输出"文档与现场一致"、可移植性门禁输出"通过");留档的目的是提醒:这两处都不是靠人记得,而是被检查器跑出来的 |
+| 移植性大扫描仍报机器相关路径 | 第二个可移植性扫描器返回 1:290 处机器相关路径(HTML 烘入绝对路径 0 处),均在交付留档脚本、历史文档与注释里,不在运行路径上;同一份扫描同时确认虚拟环境绑定本机基础解释器路径,换机必须重跑一次安装 | 出包前把该扫描器纳入打包前置检查,或把这份清单当具名白名单维护并逐条写明理由;不要靠"人工记得它不影响运行" |
+| 交付前安全核查器未接入打包链 | 该脚本有四类规则(密钥、个人信息、金额与业主名、内部件)与严格模式退出码,但打包器、打包壳脚本里都没有调用它 | 在打包器里加一步调用(严格模式),命中即删包并返回非零;否则它只是手工步骤 |
+| 包内离线轮子只覆盖 Windows | wheels 下只有 win_amd64 一个平台目录(42 件、169.4 MB);Linux 与 macOS 安装会自动改为联网 | 要在国产化平台上完全离线安装,需在有网同类机器上取件后放进对应平台目录 |
+| 文档引用的模型离线包不在包内 | vendor 目录实测只有 4 件(清单文件加三份便携运行时),没有模型安装包目录;而说明书与安装脚本多处引用该目录 | 现场需要模型时单独交付模型安装包与模型目录;或在文档中明确"该目录随单独交付件提供" |
+| Windows 服务与 systemd 未在本次环境实测 | 本机未注册服务(状态报已注册为假、运行中为假),Windows 服务真注册与 Linux 单元真装未取证 | 用 --dry-run 预览、用免权限守护自证;具备管理员或 root 权限时执行注册并复核状态 |
+
+### 16.2 口径冲突与待统一项
+
+系统里仍有多处文档或代码口径不一致,如实列出如表 16-2 所示。这些不是"功能坏了",而是同一件事有两种写法,读者与维护方必须知道以哪一处为准。
+
+| 冲突项 | 两种口径 | 以哪一处为准 |
+|---|---|---|
+| 重算链步数 | 系统设计说明写 14 步、重算操作手册写 8 步、移植文档写 12 步,实测默认 26 步(加 --skip-scada 为 23 步) | 以 scripts/rebuild_all.py --dry-run 的实跑输出为准 |
+| 窄仓列数 | 文档写 46 列(温度均值 27 列),实测 48 列(温度均值 25 列,另补两列控制通道) | 以脚本 --check 输出与 _manifest.json 的 columns_n 为准 |
+| 窄仓一次扫耗时 | 一处写约 2 分钟,一处写约 3 分钟 | 以现场实测为准,两处都保留为口径区间 |
+| 变桨面耗时 | 一处写约 10 分钟,手册写约 12 分钟 | 同上 |
+| 振动摄入耗时 | 一处写约 14 分钟,放置指导写约 25 分钟(对应 153 GB 导出) | 同上(体量不同) |
+| M9 按时间窗耗时 | 服务注释约 5 秒每时间窗,版本记录约 0.6 秒每时间窗 | 以本机实测为准;两处已记入待统一 |
+| 平均无故障间隔(MTBF)台时口径 | 停机归因模块已改为实测台时,部件可靠性表仍用理想日历台时 | 需统一到实测台时(否则同一指标两个值) |
+| 网关离线守卫端口 | 守卫里硬编码判断旧端口 8033,而端口表里工作台是 18033 | 以 configs/serve.json 为准,守卫待同步 |
+| 前端一处外链端口 | 工作台一处外链硬编码 8030,该端口不在端口表内 | 以 configs/serve.json 为准,外链待改 |
+| 多场机制未接通 | 场配置目录下有多件 YAML,但可用场列表只认 JSON,且磁盘上这些 YAML 实为另一条链的机型物理约束档;当前只有如东一场可用 | 以场配置加载函数的实行为准;要么让加载器同时认 YAML,要么把场定义转成 JSON |
+| 重算操作手册的操作表 | 手册的步骤表仍写"默认不重生成 CMS 报告",而现行链自 2026-09-18 起默认生成报告(要旧行为用 --no-vib-report);手册第⑤步仍写旧的补齐脚本,而现行链第⑤步是反向呼应审计 | 以 scripts/rebuild_all.py 为准,手册待同步 |
+| 运维日志路径写法 | 一处写 logs/ops_动作_时间.log,实际是 logs/ops/ops_动作_时间.log | 以日志口径模块与实盘为准 |
+| 问答档位表 | 工作台服务端保留着旧的档位名(一个未安装的中档与一个混合专家档),配置文件的升档档位已经是本机实装的 32B 档 | 以 configs/models.json 为准(它同时是升档解析的第一顺位),服务端展示表待同步 |
+| 审计流水的字段清理 | 重试成功的那条记录里仍带着上一次尝试的失败原因 | 以字段语义为准(成功行不应带错误文本),生成端待修正 |
+| 检索索引文件形态 | 一处注释写索引落 json 与 npz 两种,而代码只写 json 与向量文件 npy,盘上也没有 npz | 以代码与盘上实物为准,注释待改 |
+| 本体对象库规模 | 设计文档写 9,613 个对象(2026-09-17 时点),盘上实测 9,791 个(2026-09-21 修改时间) | 引用规模时须注明口径日期,或重测 |
+| 安装说明的口径 | 说明书里仍写"所有服务只监听本机",与后来"门户监听所有网卡"的口径不一致;同一份说明书一处的"不含重算链"与另一处的"重算链已进包"互相矛盾 | 以 configs/serve.json 与 scripts/rebuild_all.py 的现状为准,说明书待修订 |
+
+### 16.3 未实现项
+
+以下三项是"设计上这样定,但功能未实现",如实列出:按所选时间窗的缓存没有过期时间与容量上限(进程内常驻、无淘汰、无持久化),长时间运行且频繁换时间窗时内存会持续增长;后台预热不覆盖 M9(只预热判级与默认时间窗的镜头);时间窗词表没有"今日、近 7 日、本月、本年"这类词(只有六个预设加单月、月区间、自定义起止)。此外判级矩阵本身只在内存里返回,不落 parquet 产物,因此"某一时刻的判级矩阵"没有独立留档件,只能靠当时的页面快照与日志。
+
+***
+
+## 附录 A 编写依据
+
+本文每章的主要来源文件如表 A-1 所示(章节与来源文件)。
+
+| 章节 | 来源文件 |
+|---|---|
+| 1 文档说明 | src/version.py(VERSION 与 HISTORY 的 2.10.0 条目)、docs/需求分析_观澜_2.10.0.docx(进行中)、scripts/delivery_docs_figures.py |
+| 2 设计目标与原则 | docs/系统设计说明.md(四条设计铁律)、src/ontology/mcp_server.py 头部、src/windscada/subsys/fusion.py 头部、src/windscada/perf/reliability.py |
+| 3 总体架构 | configs/serve.json、scripts/guanlan_gateway.py、guanlan.py(组件启动表)、run/pids.json、docs/系统设计说明.md(产物全景) |
+| 4 目录结构与路径真源 | src/paths.py、configs/registry.yaml、docs/系统设计说明.md(路径约定与统一记录)、本次目录实测 |
+| 5 数据接入与重算链 | scripts/rebuild_all.py(--dry-run 实跑)、scripts/scada_slim_build.py 与 slim10min/_manifest.json、scripts/raw_scan.py、scripts/raw_data_check.py、scripts/place_raw_data.py、docs/输入数据放置指导_v0.1.md、src/windscada/scada_source.py、scripts/csv_to_mdb.py |
+| 6 判级与算法 | src/windscada/taxonomy.py、src/windscada/perf/curves.py、src/windscada/perf/control.py、src/windscada/perf/reliability.py、src/windscada/perf/faults.py、src/windscada/perf/availability.py、src/windscada/subsys/pitch.py、src/windscada/subsys/yaw.py、src/windscada/subsys/hydraulic.py、src/windscada/subsys/temp_nbm.py、src/windscada/subsys/thermal_chain.py、src/windscada/subsys/structure.py、src/windscada/subsys/fusion.py、src/windcms/report_std.py、src/windscada/ui_en.py |
+| 7 时间窗口径 | scripts/windscada_serve.py(词表、按时间窗缓存、pending、预热)、src/windscada/ui/app.js、src/version.py(2.9.0 条目) |
+| 8 服务与前端设计 | scripts/windscada_serve.py(路由与烘焙调用)、scripts/guanlan_gateway.py(改写与豁免)、src/windscada/ui/snapshot.py、src/windscada/ui/app.js、scripts/windscada_overview_build.py |
+| 9 本体与知识层 | src/ontology/kb_ingest.py、populate.py、chain_ingest.py、trend_ingest.py、retrieval.py、maintenance.py、mcp_server.py、scripts/guanlan_facts_contract.py、scripts/sop_findings_from_ledger.py、outputs/rudong/ontology(实测规模) |
+| 10 本机模型接入 | configs/models.json、src/ontology/fast_agent.py、src/ontology/llm_gate.py、logs/audit/llm_audit.jsonl、docs/版本记录.md(2.9.1 与 2.9.2 条目) |
+| 11 运维控制台与重算编排 | scripts/guanlan_ops.py、scripts/_ops_launch.py、scripts/_ops_run.py、scripts/_ops_stop_keep_gateway.py、run/ops_job.json、logs/ops/、src/logfile.py |
+| 12 安装服务化与版本管理 | install.ps1、install.sh、uninstall.bat、uninstall.sh、scripts/guanlan_uninstall.py、scripts/service_ctl.py、scripts/win_service.py、scripts/service_worker.py、scripts/service_main.py、install-info.json、src/version.py、scripts/pack_dist.py、scripts/version_log.py、docs/版本记录.md |
+| 13 质量保证 | scripts/products_reverse_audit.py、pages_audit.py、config_audit.py、log_audit.py、raw_scan.py、chain_gap_check.py、version_log.py、detail_deps.py、inventory_products.py、check_portability.py、audit_chinese_terms.py、src/entry_refs.py、guanlan.py check(底稿 docs/src/_guanlan_check.txt) |
+| 14 安全合规与离线边界 | scripts/security_scan.py、scripts/deliver_desensitize.py、src/windscada/deid.py、src/windscada/deid_public.py、src/windcms/redact.py、configs/serve.json 注释 |
+| 15 可移植性与资源占用 | docs/移植与独立运行_v0.1.md、docs/说明书_观澜如东样板v2_v0.2.md、requirements.txt、wheels/ 与 vendor/ 实测、install.sh、scripts/service_ctl.py、本次目录实测 |
+| 16 已知边界与未实现 | docs/系统设计说明.md(缺口与边界、变更记录)、docs/源代码化清单_v0.1.md、docs/重算缺口与补件清单_v0.1.md、configs/registry.yaml(known_missing)、src/windscada/scada_source.py、docs/版本记录.md(遗留项) |
+| 附录 B 与附录 C | 仓库实测路径与模块清单、docs/系统设计说明.md 的术语用法 |
+
+## 附录 B 关键模块清单
+
+仓库相对路径到职责的对应关系如表 B-1 所示。
+
+| 仓库相对路径 | 职责 |
+|---|---|
+| guanlan.py | 启动器与自检:check 与 serve 与 status 与 stop 与 open 与 qa 与 version 七个子命令 |
+| src/paths.py | 路径唯一真源:安装根解析、各产物仓与配置与解释器助手 |
+| src/version.py | 版本唯一真源:VERSION 与版本规则与 HISTORY 与包名与安装记录读写 |
+| src/proc.py | 子进程统一口径:无窗口标志、后台起进程、前台短命令 |
+| src/logfile.py | 日志唯一口径:目录与命名与行格式与保留策略与标准输出包装 |
+| src/entry_refs.py | 入口引用闭合与入口脚本编码守则 |
+| src/derived_manifest.py | 由构建脚本自登记的派生产物清单 |
+| scripts/rebuild_all.py | 重算链编排(默认 26 步)与逐步退出码容忍 |
+| scripts/rebuild_from_raw.py | 三门台账与 SCADA 侧 10 个构建器的入口 |
+| scripts/scada_slim_build.py | 10min 公共列子集窄仓(按所选时间窗重算的底座) |
+| scripts/windscada_monthly_build.py | 月度派生件(与随包基线逐值对齐后才落盘) |
+| scripts/vib_raw_build.py | 振动侧一键摄入:时间窗索引与谱库与报告与逐台页与融合标量 |
+| scripts/pitch_face_build.py | 变桨面生成端(日粒度液压润滑柱塞与零位三口径月表) |
+| scripts/component_history_build.py 与 baseline_38_build.py | 振动在升与换件闭环、三层基线 |
+| scripts/sop_findings_from_ledger.py | 由重算台账生成事实契约 claim 族 |
+| scripts/guanlan_facts_contract.py | 事实契约构建与四类消费者派生件 |
+| scripts/products_restore_missing.py | 逐件来源台账维护(只记账不搬件) |
+| scripts/products_reverse_audit.py | 反向呼应审计与可逆性矩阵 |
+| scripts/pages_audit.py 与 configs/portal_pages.yaml | 页面归口登记与五条机器规则 |
+| scripts/config_audit.py 与 configs/registry.yaml | 配置登记与八条机器规则 |
+| scripts/raw_scan.py 与 raw_data_check.py 与 place_raw_data.py | 输入数据指纹扫描、放置体检与增量落位 |
+| scripts/windscada_serve.py | 工作台服务:路由族、按所选时间窗重算、pending 契约、页面烘焙调用 |
+| scripts/guanlan_gateway.py | 门户网关:前缀改写、data-abs 豁免、只读发布层、运维控制台 |
+| scripts/guanlan_ops.py 与 _ops_launch.py 与 _ops_run.py | 运维控制台后端、动作启动器与重算执行器 |
+| scripts/windcms.py 与 src/windcms | 振动诊断服务与报告、逐台页、工作台 |
+| src/windscada/taxonomy.py | 判级矩阵与系统归口与取严合并 |
+| src/windscada/perf 与 subsys | 曲线、控制、可靠性、停机、可用率、变桨、偏航、蓄能、温度、热链、结构、融合各面算法 |
+| src/ontology | 本体摄入六步、对象库、检索、MCP 只读工具、离线问答代理与校闸 |
+| install.ps1 与 install.sh 与 uninstall.bat 与 uninstall.sh | 安装与卸载入口 |
+| scripts/service_ctl.py 与 win_service.py 与 service_main.py 与 service_worker.py | 服务化注册器与服务体 |
+| scripts/pack_dist.py 与 pack.bat 与 pack.sh | 打包器、预演与开箱验证入口 |
+| scripts/delivery_docs_build.py 与 delivery_docs_figures.py | 交付文档渲染器与图表构建器(本版新增) |
+
+## 附录 C 术语与缩写
+
+本文使用的术语与缩写如表 C-1 所示。
+
+| 术语或缩写 | 含义 |
+|---|---|
+| 观澜 | 本系统的产品名;如东样板指以如东海上风电场为样板场站 |
+| 产物 | 由原始件算出来、供页面与接口消费的件(parquet、json、md、html 等),只落在 outputs/场站/ 下 |
+| 产物仓 | outputs/场站/ 下的九个顶层目录(windscada、ontology、windcms、m5_cms_tcm、tcm_compatible_replay、sop、guanlan、pitch、paradigm_r1) |
+| 原始件 | 现场交付的输入数据,只读,落在 data/raw/场站/ 下 |
+| 重算链 | scripts/rebuild_all.py 串起来的构建步骤序列(默认 26 步) |
+| 窄仓 | 10min 公共列子集的逐台 parquet 仓,用于把按所选时间窗重算从分钟级降到秒级 |
+| 判级矩阵 | 逐台乘各系统的判级表,由各面判级取严合并得到 |
+| 四维证据 | 变桨与偏航等面内部用于取严合并的四个判据维度 |
+| 融合面 | 把振动、温度、润滑、油液四源对齐后逐台判级的分析面 |
+| 残月 | 样本覆盖度低于 0.5 的月份(如只有 6 天数据的月份),页面与报告均如实标注 |
+| 平均无故障间隔(MTBF) | 日历或实测台时扣除停机后除以事件数得到的平均无故障间隔 |
+| 平均停机间隔(MTBO) | 进一步扣除调度令停机后除以事件数得到的平均停机间隔 |
+| 单次停机时长(MDT) | 停机段时长的平均值 |
+| 时间窗 | 页面顶部可选的数据范围(预设段、单月、月区间、自定义起止日期,含两端) |
+| pending 契约 | 按所选时间窗重算未完成时,先回上一份口径并用 pending 标记明示,页面轮询再取的约定 |
+| 单一真源 | 同一事实只允许一处权威定义(路径、版本、端口、页面归口、配置登记) |
+| 质量门 | 重算链与自检里的审计检查点,每处都能独立重跑并给出退出码 |
+| 开箱验证 | 打包器对交付包做解压、安装、无窗口启动、页面可用、卸载的整链核验 |
+| 骨架 | 交付包里只带目录结构不带文件的输入数据目录骨架 |
+| MCP | 本体层的只读工具接口,供离线模型按纯函数取证 |
+| claim | 事实契约里的一条结论,带来源引用、本条摘要、时间窗与裁决 |
+| 校闸 | 问答结果的接地核查,不过闸不出正文 |
+| 升档 | 默认模型被校闸拦下后改用更强档位重答一次 |
+| 中间件 | SOP 中间件与评审落盘件(outputs/场站/sop) |
+| 冻结交付件 | 按交付版本发布、不随输入数据自动变的客户交付件 |

+ 765 - 0
docs/src/需求分析_观澜_2.10.0.md

@@ -0,0 +1,765 @@
+# 需求分析 · 观澜·如东样板 v2 · 版本 2.10.0
+
+## 1 文档说明
+
+### 1.1 目的
+
+本文是「观澜·如东样板 v2」的**需求分析文档**,回答四个问题:这套系统究竟要满足谁的什么需要;这些需要从哪来、经过哪些版本变成现在的样子;每一条需要对应什么功能、什么输入、什么输出、拿什么判据验收;以及哪些事本版明确不做。本文与同批交付的《设计说明_观澜_2.10.0.docx》《数据要求说明_观澜_2.10.0.docx》配套:本文讲「要什么、凭什么算做到了」,设计说明讲「怎么做的」,数据要求说明讲「要哪些数据、什么形态、什么单位」。
+
+本文的写作口径是**只写能取证的事实**:每个数字、每条结论都能指到仓库里的某个文件、某次实跑输出或某条版本记录;查不到、取不到的一律写明「未取证」或「未到位」,不做推测性补全。因此文中会出现少量「未取证」的说明句,那是刻意留下的诚实边界,不是遗漏。
+
+### 1.2 读者与用法
+
+本文面向四类读者:场站侧的运行、点检、管理人员用它核对「系统答应做的事是否都做到了」;研发与交付人员用它做需求变更的基线(改需求先改本文的 FR 编号与验收判据);验收与审计人员用第 9 章的门清单与退出码语义逐条核对;新加入项目的工程师用第 2、3 章在半小时内建立全局认识。
+
+需要提醒的是:本文不是操作手册。放数据、重算、安装、卸载的具体命令在《重算操作手册》《输入数据放置指导》《README_先读我.MD》里,本文只在功能需求条目里给出判据与依据文件的位置。
+
+### 1.3 版本对应关系
+
+系统版本号只有一个真源:仓库内的 src/version.py 的 VERSION 一行。本版 VERSION = 2.10.0,版本记录(HISTORY)共 12 条,人读的版本表由 scripts/version_log.py 从 HISTORY 生成到 docs/版本记录.md,并由 version_log.py --check 与 guanlan.py check 双重校验「记录表与代码一致」。
+
+表 1-1 版本与文档的对应关系
+
+| 项 | 值 | 取证方式 |
+|---|---|---|
+| 系统版本 | 2.10.0 | src/version.py 的 VERSION;guanlan.py check 实跑报「版本管理: 观澜·如东样板 v2 v2.10.0」 |
+| 交付包名 | app_guanlang_v2.10.0.zip | src/version.py 的 package_name();同一条 check 输出 |
+| 本文版本 | 2.10.0(与系统版本同号) | 本文标题与 src/version.py 的 VERSION |
+| 版本史条目数 | 12 条(最新一条即本次文档交付) | src/version.py 的 HISTORY;docs/版本记录.md |
+| 版本号规则 | v大版本号.中版本号.小版本号;大改方案或架构、中改非核心功能、小为消缺 | src/version.py 的 BUMP_RULE 与 LEVEL_MEANING;docs/系统设计说明.md §15 |
+| 版本级别机器判据 | level_of(旧, 新) 返回 major 或 minor 或 patch 或 same | src/version.py 的 level_of() |
+| 本次变化级别 | 中(minor,非核心功能新增,即三份交付文档与其生成器) | HISTORY 中 2.10.0 条目的 level 字段 |
+
+本版相对 2.9.2 的实质变化是**交付物新增**:三份 Word 文档(需求分析、设计说明、数据要求说明)落地,且文档的源件与插图生成器都源代码化(docs/src 下的 markdown 源件加上渲染器与插图生成器)。依据用户令原文:「整理观澜的需求/设计/数据接入,各写一份 Word 文档,要求区分章节目录、文表图并茂、字体字号分类统一;数据要求说明结合现场《数据分析收资要求-v3.docx》」。
+
+### 1.4 编写依据与取证口径
+
+本文的取证方式分四种,文中出现数字时都能归到其中一种:**读源**(读代码、配置、登记表)、**读文档**(读仓库内已交付的设计与操作文档)、**实跑**(跑只读的自检与只读的审计脚本)、**实测计数**(对 data 与 outputs 目录做文件级计数与求和)。为避免把「文档里写过」当成「现在就是这样」,凡与现状有关的数字,本文优先采用实跑与实测计数,并在文中标注日期。
+
+表 1-2 取证方式与本版关键数字
+
+| 取证方式 | 本版用到的实跑或实测 | 关键数字 |
+|---|---|---|
+| 只读自检 | guanlan.py check(2026-09-22 本机) | 结论「全绿,可 serve」,退出码 0,无 FAIL 项,41 条判定行 |
+| 只读审计 | scripts/pages_audit.py --check(2026-09-22) | 检查 21 项,不一致 0,已知缺口 19,退出码 0 |
+| 只读计划 | scripts/rebuild_all.py --dry-run(2026-09-22) | 默认 26 步;加 --skip-scada 为 23 步 |
+| 实测计数 | data/raw/如东 与 outputs/rudong 递归计数(2026-09-22) | 输入 26,504 件 / 224.67 GB;产物 3,549 件 / 7,157.5 MB |
+| 读源 | src/version.py、configs/portal_pages.yaml、configs/serve.json、guanlan.py、scripts/rebuild_all.py | 12 条版本史;25 条顶层页面条目(含 7 条子页共 32 条);6 个服务端口 |
+| 读文档 | docs/系统设计说明.md(56 个标题)、docs/说明书_观澜如东样板v2_v0.2.md、docs/输入数据放置指导_v0.1.md、docs/数据目录结构与落位约定_v0.2.md、docs/重算操作手册_v0.1.md、docs/页面输出体检_v0.1.md、README_先读我.MD | 逐章依据见附录 A |
+
+### 1.5 术语与描述口径约定
+
+本版全文遵守三条写作口径,它们是用户令落成的硬要求,也是机器清扫过的(依据 docs/系统设计说明.md §17.1):**第一**,描述里含「窗」且确实指时间窗口的,一律写全「时间窗」——预设时间窗、判级时间窗、证据时间窗、随所选时间窗、时间窗内;而领域词里的天气窗、作业窗、预览窗、观测窗保持原样,不误改。**第二**,影响的是风电机组时统一写「影响机组」或「影响机组数」,不使用以「台」计数的旧写法;数据字段名里的旧写法暂留以兼容数据契约,显示层统一。**第三**,用英文简写时必须写成「中文(英文简写)」,例如平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT);英文映射的键同步改,否则英文页会回落中文。
+
+表 1-3 三条描述口径与落地位置
+
+| 口径 | 要求 | 落地与证据 |
+|---|---|---|
+| 时间窗 | 含「窗」且指时间窗口者写全「时间窗」 | docs/系统设计说明.md §17.1;v2 页面顶栏控件与图注均写「随所选时间窗」 |
+| 影响机组 | 影响风电机组时写「影响机组」 | 表头与说明含报告构建器;数据字段名暂保留 |
+| 英文简写 | 中文(英文简写) | 平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT);i18n_en 键同步 |
+
+术语的完整解释见附录 B。
+
+## 2 系统概述
+
+### 2.1 如东样板是什么
+
+观澜·如东样板 v2 是一套**纯离线的海上风电场智能分析单包系统**,对象是中广核江苏如东海上风电场的 38 台西门子 SWT-4.0-130 机组(容量 4.0 MW 级)。它把现场给来的运行数据、状态监测、报警、工单、油样落到每台机组的正确部件上,给出带证据、带边界、带责任人的检修结论;**判断在代码里,本机模型只负责问答、解释与引用,不发明结论**。软件不含任何联网功能,也不需要现场系统配合改造:原始件从 data/raw 读入,产物落在 outputs 下,页面只读产物,产物由数据算出来。
+
+一页话术(给第一次接触的人的三十秒版本):这套系统不控制风机、不下发指令、不改现场任何系统。它做的是「把现场已有的数据读进来,算出一份能说清依据的体检报告,然后用一个门户和三个服务把它讲清楚」。放数据之前,页面会如实显示「无产物」并告诉你缺哪一件、怎么补;放了数据之后,一条命令重算,页面立刻有数。所有结论都能往回指到源件与算法,指不到的地方它会写「不可判」或「缺件」,而不是编一个数字。
+
+### 2.2 部署形态与运行边界
+
+交付形态是**离线单包**(EDITION = offline-single-package):一个安装目录里同时是程序、配置、交付件与数据入口。安装目录不要带空格与中文。程序侧有 6 个常驻服务,全部由安装目录下的启动器或系统服务托管;组件的内部接口只监听本机回环地址,只有门户网关按配置可对外监听。
+
+表 2-1 常驻服务与端口(依据 configs/serve.json 与 guanlan.py 内置默认,两者口径一致)
+
+| 服务 | 默认端口 | 监听地址 | 作用 |
+|---|---|---|---|
+| 门户网关 | 28084 | host 与 public_host(出厂 public_host 为 0.0.0.0) | 门户单文件页、路由与改写、运维控制台、/healthz、/api/version |
+| 分析工作台 | 18033 | host(127.0.0.1) | 十个标签的分析工作台与全部 /api 取数 |
+| 振动诊断 | 18020 | host(127.0.0.1) | CMS 振动报告页、逐台页、工作台页、谱图 |
+| 仿真与回放 | 18791 | host(127.0.0.1) | 整机联调仿真台静态资产 |
+| 仿真四系统合页 | 18792 | host(127.0.0.1) | 从冻结资料包读取的四系统合页 |
+| 三维拆装工作台 | 64292 | host(127.0.0.1) | 三维资产页 |
+| 本机模型(可选) | 11434 | 本机 | 本地大模型推理服务,只影响问答与本地初筛 |
+
+对外只有一个入口 28084。这不是省事,而是边界:页面没有鉴权,把组件直接暴露出去等于把全场的运行数据摊开。要对外访问时的正确做法是限来源(防火墙白名单或反向代理加认证),或把 public_host 改回空串只用本机加隧道。
+
+### 2.3 系统能力清单
+
+表 2-2 系统能力清单(能力域、给谁用、载体、验收锚点)
+
+| 能力域 | 能力 | 主要载体 | 验收锚点或判据 |
+|---|---|---|---|
+| 判级 | 38 台乘九系统判级矩阵,四轴随时间窗重算 | 工作台系统矩阵、逐台页、问题页 | 判级报警机组数随所选时间窗变化;样本不足如实写不可判 |
+| 部件问题 | 部件可靠性表、问题下钻、完整依据 | 工作台 component 与 vibration 标签 | 平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT)随所选时间窗变化;依据展开永不为空 |
+| 发电性能 | 七镜头曲线、七张月度时序图、控制参数一致性 | 工作台 energy 与 generation 标签 | 图注写「随所选时间窗」;样本量随时间窗变化;控制参数按所选时间窗重算 |
+| 振动 | 振动窗索引、谱库、CMS 报告、逐台页、在升与换件闭环、三层基线 | CMS 服务、工作台 vibration 标签 | 振动窗 w0316 索引 2,066,686 行;谱 420,742 条;缺件时如实空表并写边界 |
+| 台账 | 报警、工单、油样三门台账与停机事件 | 工作台 fault 标签、数据层维护页 | 报警 39,211 行、工单 5,876 行、油样 404 行 |
+| 本体与问答 | 对象库、检索索引、决策链、趋势证据、问答与校闸 | 工作台 assistant 与 decision 标签 | 本体审计 0 问题;两档模型都拦则不出文并给原因 |
+| 报告 | CMS 标准模版报告、事实契约与派生、门户结论段 | 报告页、门户、qc 派生件 | 契约改一条 claim 则四个消费端全变 |
+| 运维 | 一条命令重算、控制台按钮、清产物、数据扫描与看门狗 | 门户数据重算页、运维控制台 | 默认 26 步计划;并发动作返回 409;扫描退出码 0 与 4 与 5 |
+| 门户与检索 | 单文件门户、页面归口与陈旧检测、知识检索 | 门户、页面归口登记表 | 归口审计退出码 0;页面陈旧报 7 |
+| 数据接入 | 落位、体检、增量三清单、场站辨识 | 命令行脚本与维护页 | 结构合规;冲突拦下;场站辨识四条规则 |
+| 交付与运维 | 打包、开箱验证、服务化、版本检查、卸载 | install、pack、uninstall 入口与服务注册器 | 入口引用闭合 48 条;卸载入口 2 个;版本记录与代码一致 |
+
+## 3 需求来源与演进
+
+### 3.1 需求来源的三条线
+
+需求从三条线汇拢。**第一条是现场运行的需要**:值班要一眼看清全场哪几台机组有问题,点检要顺着问题找到部件与证据,管理要一份能对外的结论。**第二条是交付与运维的需要**:机器上没有网络、现场不会敲命令、数据每月增量、程序要能装能卸能升级。**第三条是用户令**:每一次人工测试或现场事故之后,用户用一句话把要求定下来(例如「所有的计算均要形成观澜的源代码」「对 data/raw 下文件的增减做到监听」「增加自定义时间段」),研发把它落成代码、机器守卫与文档。
+
+三条线里,用户令是最硬的驱动力:它带来的是**可核验的验收判据**,而不是一段描述。本版 12 条版本记录里,绝大多数条目的形态都是「检查到的问题 → 根因 → 解决 → 验证」,其中「验证」一栏写的就是实跑的退出码与逐值对拍结果。
+
+### 3.2 需求演进时间轴
+
+表 3-1 版本史与用户令要点(依据 src/version.py 的 HISTORY,共 12 条)
+
+| 版本 | 日期 | 用户令要点 | 变化级别 |
+|---|---|---|---|
+| 2.10.0 | 2026-09-22 | 整理需求、设计、数据接入各写一份 Word 文档,并源代码化其生成器 | 中(minor) |
+| 2.9.2 | 2026-09-22 | 升级后验收补缺:重算链增 ⑧d 版本记录一致性门,版本与文档漂移有了自动拦截点 | 小(patch) |
+| 2.9.1 | 2026-09-22 | 人工测试 7 项消缺:描述口径、依据空白、问题页空、门户链接 404、闭环文案、本机模型升档档位 | 小(patch) |
+| 2.9.0 | 2026-09-21 | 时间窗真正生效:部件问题与发电性能随所选时间窗重算,新增自定义起止日期时间窗 | 中(minor) |
+| 2.8.2 | 2026-09-21 | 扫描器的变化判据被两类非摄入件常年占住:新増上游归档口径与忽略模式 | 小(patch) |
+| 2.8.1 | 2026-09-20 | 台账等价验收在全新机器上必然非零:改为容忍并写明「没做验收」 | 小(patch) |
+| 2.8.0 | 2026-09-20 | 输入数据自动扫描识别:逐族指纹发现新增与变化,指明该跑哪几步并纳入重算 | 中(minor) |
+| 2.7.0 | 2026-09-19 | 远程部署消缺:振动窗发布被占用不再打断重算链;门户可对外监听;控制台打印不炸 | 中(minor) |
+| 2.6.0 | 2026-09-19 | 所有的计算均要形成观澜的源代码:融合面、总览页、事实契约、发布层、变桨面、振动基线与在升全部自算 | 中(minor) |
+| 2.5.0 | 2026-09-17 | 卸载闭环、版本管理与打包命名规则、版本记录 | 中(minor) |
+| 0.4.0 | 2026-09-17 | 打包默认不含输入数据与产物与日志与临时文件;无窗口启动;统一配置与日志;页面归口;服务化 | 旧编号(legacy) |
+| 0.2.0 | 2026-09-01 | 含产物的旧交付基线,历史上用作「包内无生成端」那批产物的补救源 | 旧编号(legacy) |
+
+需求的演进如图 1-1 所示。该图依据 src/version.py 的 HISTORY 绘制,覆盖上表 12 条记录(即两级旧编号 0.2.0 与 0.4.0,以及从 2.5.0 起按规则递增的十个版本)。按级别统计,这十个版本里「中」级变化六条(2.5.0、2.6.0、2.7.0、2.8.0、2.9.0、2.10.0),「小」级变化四条(2.8.1、2.8.2、2.9.1、2.9.2),没有大版本升级。这个分布清楚地显示出这条线的性质——**功能面在 2.5.0 到 2.7.0 之间快速铺开,之后转入以消缺与口径统一为主的密集收敛期**。
+
+![图 1-1 观澜需求演进时间轴(依据 src/version.py 的 HISTORY)](figures/fig-req-01-需求演进时间轴.png)
+
+从时间轴能看出三个事实。第一,2.5.0 到 2.7.0 三天内出了三个中版本,全部是「装上、卸掉、打包、对外监听」这类交付能力,说明这一阶段的需求主线从「算得对」转向「交付得出去」。第二,2.7.0 之后密集出现小版本消缺,且每一条都带实跑验证,说明需求进入了「现场怎么用就怎么改」的收敛期。第三,2.10.0 是规格化的标志:需求不再只由代码与脚本承载,而是被写成可评审的文档,且文档本身也源代码化,可机器重生成。
+
+### 3.3 需求如何被人工程序化
+
+用户令通常是一句话,落到系统里要经过「变成判据、变成机器守卫、变成可复核证据」三步。第一步最关键:把一句中文要求翻译成一个**可判定的条件**。例如「时间窗变化时部件问题要跟着变」,翻译成「给定时间窗 w,判级矩阵的返回必须随 w 而变,且月度件按月取整、日粒度件按日精确」,这样才有办法验。第二步是把它做成每次都会跑的守卫,而不是靠人记得。第三步是留下可复核的证据(日志、退出码、逐值对拍结果、sha256)。
+
+表 3-2 用户令到程序化落点的对照(节选,按版本递进)
+
+| 用户令(原话要点) | 程序化落点 | 可核验证据 |
+|---|---|---|
+| 部件问题与发电性能应随下拉框值变化,并增加自定义时间段 | months_of(win) 与 win_range 与 in_win;taxonomy.system_matrix(span=);reliability.overview(span=);/api/curves?win= | 变桨报警 15 与 13 与 10、偏航 15 与 11 与 5、齿轮箱 7 与 4 与 4 分别对应 2026年与 2025H2 与 2026-07 |
+| 对 data/raw 含嵌套子目录下文件的增减做到监听 | scripts/raw_scan.py 逐族指纹(件数、体积、最新落盘、清单摘要、子目录清单);重算链 ①b 步;rebuild_all --auto | 实测 --check 由 rc=4 转 rc=0(与上次快照一致);空目录新建也能发现 |
+| 所有的计算均要形成观澜的源代码 | 逐族补生成端:变桨面、在升闭环、三层基线、融合面 handoff、总览页、由台账生成 claim | 反向呼应审计成立 3,548 件、不成立 0 件、未归类 0 件 |
+| 运行期一律不从交付包补齐 | 第 ⑤ 步由「补齐随包件」改为「反向呼应审计」,只报账不搬运 | rebuild_all --dry-run 计划中 ⑤ 步命令为 products_reverse_audit.py --check |
+| 清除产物不留备份 | products_state.py --off --yes 改真删除;--on 与门户恢复按钮移除 | 控制台按钮语义与 409 前置校验;docs/系统设计说明.md §7 |
+| 装成服务并在安装时检查版本 | win_service.py 以 ctypes 直连 SCM;service_main.py 与 systemd 单元;install-info.json 记录版本并比对 | check 实跑报「卸载入口在位 2 个」「版本管理 v2.10.0」 |
+| 观澜改为监听所有 IP | serve.json 增 public_host 且出厂为 0.0.0.0,仅门户网关用它,组件仍绑本机 | check 实跑显示 6 个端口全部运行中;README 第二节写明无鉴权风险与回退口径 |
+| 描述口径:时间窗、影响机组、中文(英文简写) | 界面文案与报告构建器统一改写;i18n_en 键同步 | docs/系统设计说明.md §17.1 与 §17.2;语言包 845 条前后端成对 |
+| 整理需求与设计与数据接入各写一份 Word 文档 | docs/src 下 markdown 源件;渲染器输出带域目录的 docx;插图生成器生成 14 张图,数字全部从真件取 | HISTORY 2.10.0 条目;本版三份文档 |
+
+## 4 用户与场景
+
+### 4.1 角色
+
+系统没有账号体系,角色划分是**使用场景的划分**,不是权限的划分。这一点必须在需求层面写清:当前版本没有页面鉴权,谁打开浏览器谁就能看到全场的运行数据;对外可达时的风险控制靠网络层(防火墙白名单、反向代理加认证、只绑内网地址)而不是靠系统内部。角色表的价值在于把「谁要什么」变成可核对的功能覆盖清单。
+
+表 4-1 角色与核心诉求
+
+| 角色 | 典型身份 | 核心诉求 | 主要页面与入口 | 对应功能需求 |
+|---|---|---|---|---|
+| 值班运行 | 场站值班员、集控值班员 | 一眼看清全场状态;报警是否集中;有没有正在恶化的机组 | 门户总览、系统状态、工作台 overview 与 energy、振动与 CMS | FR-01 至 FR-05、FR-09 至 FR-12 |
+| 点检与检修 | 点检员、检修工程师、专业工程师 | 从现象定位到部件;要证据、要边界、要处置建议;看换件后是否回落 | 工作台 component 与 vibration 与 decision、问题页、CMS 逐台页 | FR-03 至 FR-05、FR-13 至 FR-17、FR-22 至 FR-25 |
+| 场站管理 | 场长、技术负责人、生产管理 | 结论能否对外;停机损失与可靠性账;治理进展 | 门户经验发现与交付文档、汇报纸与报告导出、治理清单交付件 | FR-03、FR-09 至 FR-12、FR-26 至 FR-28 |
+| 研发与交付 | 观澜维护方、交付工程师、审计人员 | 数据怎么接、重算是否成功、安装升级卸载是否干净、口径是否一致 | 运维控制台、数据重算页、命令行入口、版本记录与设计说明 | FR-29 至 FR-43、NFR 全部 |
+
+### 4.2 场景到任务到页面
+
+场景不必多,八个够覆盖日常。每个场景都要能回答「谁、在什么时刻、要完成什么、打开哪一页、看到什么算成功」。这一步是需求分析里最有用的动作:它把抽象的「系统要好用」变成「点开这页三秒内能看出哪几台报警」。角色、场景与页面的关系如图 2-1 所示。
+
+表 4-2 场景到任务到页面
+
+| 场景 | 任务 | 页面或入口 | 成功的样子 |
+|---|---|---|---|
+| 交班巡检 | 看全场状态与报警集中度 | 门户总览、系统状态、数据重算页 | 一屏内看清哪几台报警、服务是否在跑 |
+| 例行点检 | 逐台看判级与四轴 | 工作台系统矩阵、逐台页 | 点台号即出该台各系统判级与依据 |
+| 异常追因 | 从报警下钻到部件与证据 | 问题页、部件问题页 | 依据展开有部件、链路进度、卡点、证据源 |
+| 振动专题 | 看振动判级、谱图、在升与换件闭环 | CMS 服务、工作台 vibration | 六层判读清楚,未闭环时明写「没有成对记录」 |
+| 时间窗对比 | 换一个时间窗看判级与性能是否变化 | v2 顶栏时间窗与两个日期输入框 | 图注写「随所选时间窗」;样本量随之变化 |
+| 月度汇报 | 出结论与报告 | 汇报纸、CMS 标准模版报告、门户结论段 | 结论带证据级、审级与处置措施 |
+| 数据接入 | 放新数据、体检、重算 | 命令行落位与体检脚本、运维控制台 | 冲突被拦下;重算结束出每步 OK 与耗时 |
+| 交付与升级 | 打包、开箱验证、装服务、卸载 | pack 与 install 与 uninstall 入口、服务注册器 | 版本检查提示正确;卸载后数据一件不少 |
+
+![图 2-1 角色—场景—页面关系](figures/fig-req-02-角色场景页面.png)
+
+### 4.3 角色与可用性边界
+
+与角色相关的一条硬需求是「**没有产物也要能打开页面**」。现场第一次装完、或者在控制台上点了「清除产物」之后,页面必须仍然可访问,并明确说出缺哪一件、怎么补,而不是白屏或掐断连接。这条需求来自两次现场事故:振动服务在无产物时曾因空表拼接抛异常带走进程,工作台在缺件时曾直接返回 500 让人分不清「没放数据」还是「程序坏了」。现在的口径是:缺件时返回结构化缺件页与 err=no_source,页面写清缺件与补齐办法。相关功能需求见 FR-33 与 NFR-06。
+
+## 5 功能需求
+
+### 5.1 功能需求编号与总览
+
+功能需求按十一个域组织,编号从 FR-01 到 FR-43,共 43 条。编号是**稳定的**:新增需求往后排号,不重排已有编号;需求作废时保留编号并标注作废,不改用它。每条需求都给出五项:需求描述、输入、输出、验收判据、依据文件。其中「验收判据」是可判定的条件,「依据文件」指向仓库内的源或文档,便于复核。
+
+表 5-1 功能需求域与编号分配
+
+| 域 | 编号区间 | 条数 | 一句话概括 |
+|---|---|---|---|
+| 判级与部件问题 | FR-01 至 FR-05 | 5 | 38 台乘九系统判级、四轴随时间窗重算、部件问题与依据 |
+| 时间窗与自定义区间 | FR-06 至 FR-08 | 3 | 时间窗词表、自定义起止日期、按时间窗重算的等待契约 |
+| 发电性能与可靠性 | FR-09 至 FR-12 | 4 | 七镜头、月度时序、控制参数一致性、损失与功率曲线 |
+| 振动与融合面 | FR-13 至 FR-17 | 5 | 摄入、报告、融合判级、在升闭环、三层基线 |
+| 停机与台账 | FR-18 至 FR-21 | 4 | 报警、工单、油样、停机与损失 |
+| 本体与问答 | FR-22 至 FR-25 | 4 | 知识层、铺开、决策链与趋势、问答与校闸 |
+| 报告导出 | FR-26 至 FR-28 | 3 | 标准模版报告、事实契约与派生、门户结论段与汇报纸 |
+| 运维控制台与重算 | FR-29 至 FR-32 | 4 | 一条命令重算、控制台、清产物、数据扫描 |
+| 门户与检索 | FR-33 至 FR-35 | 3 | 门户装配、页面归口与陈旧检测、检索索引 |
+| 数据接入与体检 | FR-36 至 FR-38 | 3 | 落位与体检、场站辨识、同台多件与上游归档 |
+| 安装与服务化与卸载 | FR-39 至 FR-43 | 5 | 离线安装、服务化、版本检查、卸载、打包与开箱验证 |
+
+### 5.2 判级与部件问题
+
+表 5-2 判级与部件问题(FR-01 至 FR-05)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-01 | 给出 38 台机组乘九系统的判级矩阵,逐台逐系统带判级与依据,四轴判级随所选时间窗重算 | scada_10min 与 scada_1min 序列、报警台账、振动报告转录、机型契约 | 工作台系统矩阵、逐台页、问题页;taxonomy.system_matrix 结果 | 判级报警机组数随所选时间窗变化(实测变桨 15 与 13 与 10、偏航 15 与 11 与 5、齿轮箱 7 与 4 与 4);样本不足时如实写不可判 | docs/系统设计说明.md §16.2;src/windscada/taxonomy.py |
+| FR-02 | 判级四轴(变桨、偏航、蓄能、温度)随时间窗重算,其中变桨按日粒度精确切片、其余走窄仓重算 | 窄仓 slim10min 公共列子集;pitch 面的日粒度件;1min 桨距角 | 各面 registry(span=) 结果与判级单元格 | 同一台同一系统在不同时间窗下可给出不同判级;温度面的对照基线 ref 与 ref_sm 故意固定并在页面标注 | docs/系统设计说明.md §16.2 与 §16.3;scripts/scada_slim_build.py |
+| FR-03 | 部件可靠性表按所选时间窗重算,给出平均无故障间隔(MTBF)、平均停机间隔(MTBO)、单次停机时长(MDT)与停机时长 | 停机事件、报警事件(均为日粒度) | 可靠性表与部件问题页表格 | 停机事件与报警按时间窗精确过滤;三个指标随时间窗变化 | docs/系统设计说明.md §16.2;src/windscada/perf/availability |
+| FR-04 | 部件问题下钻:问题页与问题接口按中文系统名取数,服务端做双向归一,认不出时如实报「系统标识无法识别」 | 台号、系统标识(中文名或前端标识或大小写变体)、时间窗 | 问题页、/api/problem 响应 | 传前端标识与中文名都能取到同一份问题;认不出时不写「本时间窗无异常」这类假陈述 | docs/系统设计说明.md §17.2;scripts/windscada_serve.py 的 sys_norm() |
+| FR-05 | 需要关注的卡片必须给出完整依据,依据按本行字段自组句,永不为空 | 融合面链盘行、SCADA 关注集合、部件与链路进度 | 卡片依据文本 | 实测曾出现依据空白(WTG09 齿轮箱),现行口径为按部件、链路走到第几步、卡在哪一步、证据源自组句 | docs/系统设计说明.md §17.3;HISTORY 2.9.1 |
+
+### 5.3 时间窗与自定义区间
+
+时间窗是本版需求里最容易被误解的一块,因此口径要写死两句话:**月度件按月取整,日粒度件按日精确**;页面上要写明这两句,不让读者以为月度均值能切到天。
+
+表 5-3 时间窗与自定义区间(FR-06 至 FR-08)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-06 | 提供预设时间窗词表:近30日、近90日、2026年、2026H1、2025H2、全程、单月、月区间 | 用户选择 | months_of(win) 与 win_range(win) 结果 | 每种写法都能落到覆盖的月集合与起止日期;近30日在末月只有 1 个节拍时如实按样本不足处理 | docs/系统设计说明.md §16.1 |
+| FR-07 | 新增自定义起止日期时间窗,写法为起日期~止日期,**含两端**;顶栏提供两个日期输入框与「应用」并校验起不晚于止 | 两个日期输入框 | 与区间相交的月集合(月度件)、原样的日区间(日粒度件) | 非法输入被拒绝并给出提示;相交月取整与按日精确两套口径同时生效 | docs/系统设计说明.md §16.1;HISTORY 2.9.0 |
+| FR-08 | 按时间窗重算一律「命中进程缓存即回,未命中先回等待状态并由页面轮询」,绝不静默拿旧口径当新时间窗 | 时间窗参数、进程缓存、后台重算线程 | 判级矩阵与七镜头结果、等待与构建中的状态标记 | 判级约 25 秒每个时间窗、镜头约 10 至 45 秒每个时间窗;等待期间页面写明「下面这几张卡还是上一份口径」;选到 2025-07 至 2025-12 时直接用正式产物不重算 | docs/系统设计说明.md §16.4;docs/系统设计说明.md §16.2 |
+
+### 5.4 发电性能与可靠性
+
+表 5-4 发电性能与可靠性(FR-09 至 FR-12)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-09 | 发电性能七镜头曲线随所选时间窗重算,曲线图注写明随所选时间窗且样本量随时间窗变化 | 窄仓 10min 子集、风速与功率通道、限电命令 | /api/curves 响应与七张镜头图 | 换时间窗后分箱结果变化,样本量变化;限电前干净判别窗(2025-07 至 2025-12)走正式产物 | docs/系统设计说明.md §16.2;HISTORY 2.9.0 |
+| FR-10 | 发电性能七张月度时序图随时间窗过滤 | 月度派生件 | 七张月度时序图 | 图注写「随所选时间窗」;月度件按月取整 | docs/系统设计说明.md §16.2 |
+| FR-11 | 控制参数一致性随时间窗重算,输出功率封顶与转速上限等一致性结论 | 窄仓补入的控制参数列(窄仓共 48 列) | 控制参数一致性卡片 | 按时间窗重算约 0.6 秒;功率封顶中位实测由 4,190(干净时间窗)变为 4,181.7(2026年)与 4,177.5(2026Q1) | HISTORY 2.9.0;docs/系统设计说明.md §16.3 |
+| FR-12 | 损失、功率曲线与可用率派生件由原始件重算,替换随包件时逐值对拍 | scada_10min 与报警台账 | loss_monthly、powercurve_bins、powercurve_dev、stop_events 等 | 实测损失月表 3,729 行、功率曲线偏差 38 行、功率曲线分箱 912 行与基线逐值一致 | docs/数据目录结构与落位约定_v0.2.md §4 |
+
+### 5.5 振动与融合面
+
+振动这一域的写法要特别克制:它是全系统里**人工判断成分最多**的一段,因此需求必须区分「测量数据的函数」与「需要人工裁定的件」,不能把后者当成自动产物来承诺。
+
+表 5-5 振动与融合面(FR-13 至 FR-17)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-13 | 振动窗索引与谱库摄入:解析厂家导出文件,按振动窗落索引表与谱分片,并登记清单 | windcms 下的解码导出文件(25,693 件) | 振动窗索引表(54 列)、谱分片、谱元数据、摄入清单 | 振动窗 w0316 索引 2,066,686 行、谱 420,742 条与 1,712 分片;重复摄入同名时间窗默认替换并留档旧窗 | docs/系统设计说明.md §13.1;docs/数据目录结构与落位约定_v0.2.md §2b |
+| FR-14 | 生成 CMS 标准模版评估报告与逐台页、工作台页,缺融合级时如实写破折号 | 振动窗索引、标量缓存、三层基线 | 报告 md 与 docx、逐台页、总览页、登记 csv | 38 台判级齐全(实测报警 3 台、优秀 35 台);缺融合级列时写破折号而不是编造 | docs/系统设计说明.md §13.1;HISTORY 2.6.0 |
+| FR-15 | 融合面判级:由链盘、手交件与模型过闸线生成融合表与融合级,作为报告与判级矩阵的输入 | 振动窗索引、模型过闸线、厂家报告转录 | 融合表、handoff、全场标量 z 值 | 融合表 38 台逐台有裁决;handoff 与手交件同结构且标注由观澜自算 | docs/系统设计说明.md §13.1 与 §13.6 |
+| FR-16 | 在升与换件闭环证据:逐振动窗标量算在升判据,给出检出、换件、复测回落的成对记录 | 振动窗索引、融合面链盘 | 部件历史件、趋势证据对象 | 时间窗数不足时如实出空表并写明数据边界;现场提交的数据包里没有成对记录时文案写「暂无端到端闭环证据」 | HISTORY 2.6.0 与 HISTORY 2.9.1 |
+| FR-17 | 三层基线:自基线、机群基线与绝对限,用于振动判级的对照 | 振动窗索引标量、健康期分段 | 三层基线件 | 健康期分段中位与正常带按件算;绝对限引用标准限值;样本不足时如实空表 | docs/系统设计说明.md §13.1 |
+
+### 5.6 停机与台账
+
+表 5-6 停机与台账(FR-18 至 FR-21)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-18 | 报警台账摄入,按事件键集合并集去重,归属取最窄源件;源件删改后产物随之变化并大声报出 | 故障报警导出件(16 件) | 报警台账 | 39,211 行稳定;同一事件不重复计数;删一个源件不会让总数虚增 | HISTORY 2.8.0;docs/系统设计说明.md §13.1 |
+| FR-19 | 工单台账摄入,按固定模板解析(第三行为列名,数据自第四行起),字符串化口径与随包件一致 | 风机故障记录(137 件,其中台账 123 件) | 工单台账 | 5,876 行;与基线差异逐条可归类 | docs/系统设计说明.md §13.1 |
+| FR-20 | 油液化验索引,从文件名抽取台号、部件、日期与样品号,不读正文;采合并语义,认不出的历史行原样保留 | 油样报告(404 件) | 油样索引 | 404 行;2026-07 批 102 行的源件不在现场包内,清空重建会缺这 102 行,如实记账 | docs/系统设计说明.md §7 与 §13.1 |
+| FR-21 | 停机事件与停机损失:由报警与运行状态派生停机记录并绑定损失,作为可靠性表的输入 | scada_10min、报警台账 | 停机事件件、损失月表 | 停机事件按台落盘;损失月表与输入跨度对拍通过 | docs/系统设计说明.md §13.1;docs/数据目录结构与落位约定_v0.2.md §4 |
+
+### 5.7 本体与问答
+
+表 5-7 本体与问答(FR-22 至 FR-25)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-22 | 本体知识层:把厂商技术资料(手册、对译表、维护作业指导书、图纸)结构化,每条声明出处 | 西门子4.0技术资料(318 件 / 2.94 GB) | 对象库、检索索引、实机参数表 | 故障码覆盖 263 与 322;本体审计 0 问题 | docs/系统设计说明.md §13.1 |
+| FR-23 | 本体铺开:零判级权,只转录一层正本;同部件多源判级并存不合并;每条判定带审级与证据回溯 | 判级矩阵、融合表、各产物台账 | 对象库对象与判定条目 | 对象库实测规模为 9,613 对象(另见口径差异说明,§11.3);审计退出码 0 | docs/系统设计说明.md §13.1 |
+| FR-24 | 决策链进度与趋势证据:每台一个决策对象记六步状态、卡点与下一步;在升与闭环证据挂到机组与部件 | 运行时链盘接口、部件历史件 | 决策对象与趋势证据对象 | 需服务在跑;重复执行幂等覆盖 | docs/系统设计说明.md §13.1 |
+| FR-25 | 问答:模型只转述本体与事实契约,不产生新判级;校闸拦不下的句子不出文并给原因;升档目标按「配置档位且确已安装」解析 | 问题文本、本体与契约、本机模型(可选) | 答案与引文块、校闸结论、模型审计日志 | 台号与数字能回抓源记录;两档都拦则不出文并给原因;升档目标没装时响亮回落并打日志 | docs/说明书_观澜如东样板v2_v0.2.md §4.2;HISTORY 2.9.1 |
+
+### 5.8 报告导出
+
+表 5-8 报告导出(FR-26 至 FR-28)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-26 | 生成 CMS 标准模版评估报告,三轴表述:状态等级、证据状态、行动等级建议;同时出登记表 | 振动窗索引标量、三层基线、融合级 | 报告 md 与 docx、登记 csv | 38 台逐台有条目;报告章节可跳转;相对判据封顶为候选 | docs/系统设计说明.md §13.1;HISTORY 2.6.0 |
+| FR-27 | 事实契约:每条结论含结论编号、脱敏来源引用(文件加 sha16 与章节)、本条 sha256、时间窗、聚合层级与裁决枚举;派生四个消费端 | 由重算台账生成的 claim、评审底稿 | 契约件与门户结论段、工作台卡片、问答引用、报告摘要 | 实测改一条 claim 则四个消费端全变;契约自检不过时报退出码 2 并需人看 | docs/系统设计说明.md §13.1 与 §13.2 |
+| FR-28 | 门户结论段重装与汇报纸导出:产物变化后门户结论段随重装刷新;工作台可导出汇报纸 | 契约派生件、门户外壳与内嵌件 | 门户单文件页、汇报纸 | 装配先写临时文件再原子替换,失败不破坏在服务的门户;行尾强制 LF;产物挪走时缺该段属预期 | docs/数据目录结构与落位约定_v0.2.md §6;docs/系统设计说明.md §13.2 |
+
+### 5.9 运维控制台与重算
+
+表 5-9 运维控制台与重算(FR-29 至 FR-32)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-29 | 一条命令重算全部:固定依赖顺序、写死验收锚点、幂等可整条重跑;默认 26 步,加跳过 SCADA 为 23 步 | data/raw 原始件、可选现场包目录 | 各仓产物、每步退出码与耗时汇总 | 实跑 --dry-run 得默认 26 步、跳过 SCADA 23 步;某步失败即停并打印该步说明;各步幂等 | scripts/rebuild_all.py;docs/src/_rebuild_plan.txt |
+| FR-30 | 运维控制台:按钮按真实状态启用,一个时刻只允许一个动作,越权调用返回 409;显示端口、产物件数与来源台账、进度、退出码与日志尾巴 | 服务状态、产物状态、任务状态 | 控制台页面与动作接口 | 组件都在跑时点启动返回 409;有任务在跑时全按钮灰;停组件服务保留控制台 | docs/重算操作手册_v0.1.md §0b |
+| FR-31 | 清除产物为直接删除不留备份,且不动原始件与交付层;运行期一律不从交付包补齐缺件 | 用户确认动作 | 产物清空后的状态与缺件说明 | 清除后再点返回 409;页面显示无产物并给出补件办法;补齐入口定位为离线人工补救 | docs/说明书_观澜如东样板v2_v0.2.md §4.1;docs/系统设计说明.md §13.5 |
+| FR-32 | 输入数据自动扫描:逐族指纹发现新增与变化,列出该跑哪几步并记基线;支持自动取消跳过与可选看门狗 | data/raw 递归文件树、族表 | 扫描结论与基线快照、看门狗日志 | 退出码 0 表示无变化、4 表示有新数据(不是失败)、5 表示还没有基线;族表外的目录报未归类且不猜 | docs/输入数据放置指导_v0.1.md §4;HISTORY 2.8.0 |
+
+### 5.10 门户与检索
+
+表 5-10 门户与检索(FR-33 至 FR-35)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-33 | 门户为单文件装配产物,壳与件分离;顶部菜单含总览、系统架构、方法、经验发现、案例、振动与 CMS、仿真与回放、交付文档、系统状态、数据重算、登录 | 受管外壳、内嵌交付件、契约派生件 | 门户单文件页 | 拆装回到逐字节相同;行尾 LF 有自检;数据重算页用内嵌版懒加载避免空转轮询 | docs/系统设计说明.md §5.2;docs/数据目录结构与落位约定_v0.2.md §6 |
+| FR-34 | 页面归口:登记每一页与子页的内容类型、是否随输入数据变、凭什么这么判;对数据派生页面做陈旧检测 | 页面登记表、产物台账、页面实物 | 归口表与审计结论 | 实跑检查 21 项、不一致 0、已知缺口 19;陈旧检测报退出码 7;未登记页单独列出 | configs/portal_pages.yaml;scripts/pages_audit.py |
+| FR-35 | 检索索引:由本体与知识层构建检索索引,词法检索在任何机器上可用,向量检索在装有嵌入模型时启用 | 对象库、CMS 知识库、嵌入模型(可选) | 检索索引件 | 无模型时保持纯词法可用并写明;索引缺失时页面如实说明而不是报错 | docs/数据目录结构与落位约定_v0.2.md §4;docs/系统设计说明.md §13.1 |
+
+### 5.11 数据接入与体检
+
+表 5-11 数据接入与体检(FR-36 至 FR-38)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-36 | 现场包按映射表落位,落位前自动体检:结构、命名、后缀与增量三清单(新增、相同跳过、冲突) | 现场包目录、约定目录树 | 落位结果与体检结论 | 同名不同大小判为冲突并默认拒绝落位;覆盖需显式确认;同尺寸自动跳过可反复跑 | docs/输入数据放置指导_v0.1.md §2 与 §3 |
+| FR-37 | 场站扫描辨识:按四条规则辨识 data/raw 下的场站目录,多目录且都不匹配时报未识别并列出扫到的目录 | data/raw 下一级目录、场配置 | 辨识结论与依据 | 依据逐条打印;单站唯一性也要标注为凭唯一性而不是精确匹配;不猜 | docs/数据目录结构与落位约定_v0.2.md §2 |
+| FR-38 | 同台多件的 10min 件合并与上游归档口径:同台补充件按列名对齐并按时间戳去重(主件优先);类目库按信息级列出且不计变化 | 同台补充件、现场年度归档库、类目库 | 合并后的取数结果与扫描结论 | 实测 WTG01 主件 77,551 行加补充件 5,820 行合并 83,353 行、重叠 18 行去重;类目库不再造成每轮误报变化 | HISTORY 2.8.0 与 HISTORY 2.8.2;docs/输入数据放置指导_v0.1.md §4.1 |
+
+### 5.12 安装、服务化与卸载
+
+表 5-12 安装与服务化与卸载(FR-39 至 FR-43)
+
+| 编号 | 需求描述 | 输入 | 输出 | 验收判据 | 依据文件 |
+|---|---|---|---|---|---|
+| FR-39 | 离线安装:Windows 侧包内带全部离线依赖轮子可完全离线安装;无合格解释器时用包内便携运行时;安装目录不得含空格与中文 | 交付包、机器上的解释器与架构 | 虚拟环境与配置、安装记录 | 实装实测约 10 分钟量级(本机记录 172 秒);麒麟与统信与 Ubuntu 与 CentOS 走对应脚本 | docs/说明书_观澜如东样板v2_v0.2.md §3;README_先读我.MD 第一节 |
+| FR-40 | 服务化常驻:Windows 服务或 systemd 单元或登录自启三种形态,两平台共用同一工作体,行为与手工启动一致 | 服务注册器、服务体脚本、服务配置 | 托管进程与开机自启 | 巡检周期 15 秒;无权限时给出可执行建议并返回退出码 3 而不是抛异常 | docs/系统设计说明.md §14.1 与 §14.3 |
+| FR-41 | 安装时版本检查:读安装记录与本包版本比对,分没装过、相同、升级、降级、老安装无记录五类提示并询问是否重装 | install-info.json、本包版本 | 提示与交互、更新的安装记录 | 五类提示文案与动作与规则一致;跳过询问的开关可用;非交互时自动继续并打印说明 | docs/系统设计说明.md §14.2 |
+| FR-42 | 卸载闭环:默认只拆本程序装上去的东西(停服务、注销服务、撤自启、删快捷方式、清运行态、删安装记录),默认保留数据、产物、日志、虚拟环境与程序目录 | 卸载入口与实现脚本 | 卸载结果与手工收尾命令 | 自检两类断言:步骤齐全、只读预演无副作用;实跑断言记录与运行态与快捷方式都被拆掉且数据与产物一件不少 | docs/系统设计说明.md §14.5 |
+| FR-43 | 打包与开箱验证:一条命令出包,默认不含输入数据、产物、运行日志与临时文件;打包器自带入口引用闭合与编码守则两道闸;开箱验证解压后真装真起 | 源码树、版本真源 | 交付包与清单文件、开箱验证结论 | 入口引用闭合实跑 48 条全在位;包内出现安装记录或快捷方式即删包并非零退出;页面可用数要求不少于 4 个 | docs/系统设计说明.md §9 与 §9.1 |
+
+## 6 非功能需求
+
+### 6.1 非功能需求清单
+
+非功能需求编号 NFR-01 至 NFR-11,共 11 条。它们大多是「性质」而不是「功能」:判据往往是一次实跑的输出,或者一条机器守卫的退出码。
+
+表 6-1 非功能需求(NFR-01 至 NFR-06)
+
+| 编号 | 需求 | 判据 | 依据文件 |
+|---|---|---|---|
+| NFR-01 | 离线性:全部计算与页面在本机完成,不依赖外网;本机模型为可选件,缺它时只有问答与本地初筛不可用 | 组件只绑回环地址;模型服务不可达时探针报失败但其余功能不受影响;远端未装模型时如实记录为遗留项 | docs/说明书_观澜如东样板v2_v0.2.md §3.3 与 §4.2;HISTORY 2.9.2 |
+| NFR-02 | 单机性能:一条命令重算在单机上可完成,按时间窗重算的单次等待可控 | 实测量级见 6.2;判级约 25 秒每个时间窗、镜头约 10 至 45 秒每个时间窗;模型探针实测 0.77 秒 | docs/系统设计说明.md §16.4;F:\temp\_req_check_out2.txt |
+| NFR-03 | 可移植与零配置:路径只有一个真源,全部按安装根解析而不按当前工作目录;跨 Windows 与国产化 Linux | 路径助手清单与解析基准;便携运行时覆盖三平台;跨平台部署三条硬约定由脚本门禁 | docs/系统设计说明.md §3.1;docs/数据目录结构与落位约定_v0.2.md §7 |
+| NFR-04 | 中文与描述口径:界面文案前后端成对,三条描述口径机器可查 | 语言包实跑 845 条成对;口径清扫覆盖时间窗写法、机组计量说法、英文简写三类 | F:\temp\_req_check_out2.txt;docs/系统设计说明.md §17.1 |
+| NFR-05 | 审计与可追溯:每件产物能说清谁生成、谁消费、来源是原始件重算还是随包补齐 | 逐件来源台账;反向呼应审计成立 3,548 件、不成立 0 件、未归类 0 件;契约每条带自身 sha256 | F:\temp\_req_check_out2.txt;docs/系统设计说明.md §13.4 |
+| NFR-06 | 可用性:没有产物也要能打开页面并说清缺件与补齐办法 | 清产物后工作台与振动页返回结构化缺件说明而不是白屏或掐断;无源件时返回 err=no_source | docs/系统设计说明.md §11.4;docs/数据目录结构与落位约定_v0.2.md §5 |
+
+表 6-2 非功能需求(NFR-07 至 NFR-11)
+
+| 编号 | 需求 | 判据 | 依据文件 |
+|---|---|---|---|
+| NFR-07 | 可观测性:日志只有一个落点与统一行格式,服务状态与任务进度可见 | 日志实跑无不一致、提示 87 条;网关提供健康自检与版本指纹;控制台显示退出码与日志尾巴并有心跳 | F:\temp\_req_check_out2.txt;docs/系统设计说明.md §11.2;docs/重算操作手册_v0.1.md §0b |
+| NFR-08 | 数据体量:能在单机上承载本场当前的数据规模并给出资源下限 | 输入实测 26,504 件 / 224.67 GB、产物 3,549 件 / 7,157.5 MB;内存建议 16 GB 起、32 GB 更稳;磁盘 5 GB 加三维资产 1 GB、模型另计 30 至 60 GB | 2026-09-22 实测计数;docs/说明书_观澜如东样板v2_v0.2.md §2 |
+| NFR-09 | 安全只读:不写回现场系统、不改原始件、不产生控制指令;对外暴露时须自行在网侧限来源 | 原始件按只读输入对待,落位冲突默认拒绝;对外只有一个网关入口;启动日志每次提醒页面无鉴权 | docs/输入数据放置指导_v0.1.md §3;README_先读我.MD 第二节 |
+| NFR-10 | 可维护性:单一真源,改一处不必改多处;关键口径有机器守卫 | 版本号只有一行真源并由三处守卫盯住;路径、配置、日志、语言各有唯一取用口与审计器 | docs/系统设计说明.md §15.2 与 §11.1;docs/系统设计说明.md §3.1 |
+| NFR-11 | 无人值守与自愈:长跑的服务要能被托管并自动拉起 | 服务体每 15 秒巡检并拉起掉线组件;崩溃重启策略;远程部署一律用服务,不用会话前台进程 | docs/系统设计说明.md §14.1 与 §14.4;README_先读我.MD 第二节 |
+
+### 6.2 性能与耗时实测
+
+性能需求不能只写「要快」,要写「在哪台机器上、什么规模、花了多久」。下表把已取证的耗时集中列出,未取证的项如实标注。
+
+表 6-3 耗时实测汇总(来源见「证据」列;未标日期者为 HISTORY 或文档中的既有记录)
+
+| 环节 | 耗时 | 证据 |
+|---|---|---|
+| 自检整体与模型探针 | 模型探针 0.77 秒;同一次 check 结论为全绿 | guanlan.py check 实跑(2026-09-22,本机) |
+| 一行命令重算(远端一轮 24 个实质步骤汇总) | 单步量级:输入扫描 53.7 秒、三门台账 164 秒、SCADA 侧 1,025 秒、月度派生 99 秒、振动摄入 2,118 秒、变桨面 385 秒、在升 41.6 秒、基线 15.3 秒、总览页 78 秒、本体审计 7.1 秒、门户重装 3 秒、页面归口 9.5 秒 | HISTORY 2.8.1(远端复核记录) |
+| 窄仓全量扫(按时间窗重算的底座) | 约 2 分钟一次全量 | docs/系统设计说明.md §16.3 |
+| 判级矩阵按时间窗重算 | 约 25 秒每个时间窗 | docs/系统设计说明.md §16.4 |
+| 七镜头曲线按时间窗重算 | 约 10 至 45 秒每个时间窗 | docs/系统设计说明.md §16.4 |
+| 控制参数一致性按时间窗重算 | 约 0.6 秒每个时间窗 | HISTORY 2.9.0 |
+| SCADA 侧十个构建器(逐台读 10min) | 约 15 分钟 | scripts/rebuild_all.py 步说明 |
+| 振动摄入(索引与谱) | 约 14 分钟;153 GB 量级导出约 25 分钟 | docs/系统设计说明.md §13.2;docs/输入数据放置指导_v0.1.md §4.1 |
+| 启动到门户就绪 | 约 7 秒内健康自检就绪;批处理入口约 2.3 秒返回 | docs/系统设计说明.md §4.2 |
+| 安装(Windows 离线) | 约 10 分钟量级;本机记录 172 秒 | docs/说明书_观澜如东样板v2_v0.2.md §3.1;docs/系统设计说明.md §4.2 |
+| 本机问答 | 显存不足时首答 30 至 60 秒;复杂题在升档下 5 至 15 分钟 | docs/说明书_观澜如东样板v2_v0.2.md §2 与 §11 |
+
+### 6.3 可移植与零配置
+
+「零配置」在本系统里有一个明确的技术含义:**路径只有一个真源,且解析基准是安装根而不是当前工作目录**。这条需求来自一串真实缺陷:写死场站名的路径在多场部署时会静默串场;按当前目录解析的相对路径在从别处启动时会读不到;同一个规则在三个文件里各写一遍就会出现两套显示口径。现在的口径是:代码与配置里只写相对路径,运行期由路径助手解析成绝对路径;产物只落在产物仓,原始件只从原始件目录读,交付静态层只为分发而存在;三者不互相写。
+
+与可移植相关的还有两条:一是解释器探测顺序(先看配置里指定的解释器,再看安装目录下的虚拟环境,最后回落到当前解释器),这让安装目录整体拷到别的盘或别的机器后仍然可用;二是输入输出脚本的字符守则(安装脚本用带 BOM 的 UTF-8 加 CRLF、批处理纯 ASCII 且 CRLF 无 BOM、shell 脚本 LF),三条都是现场真实事故换来的,并由打包、开箱验证与装机自检三处强制执行。
+
+### 6.4 可维护性与单一真源
+
+单一真源的范围在本版已经覆盖五项:版本号、路径、配置、日志、界面文案。每一项都配了「改一处、机器替你查其余几处」的守卫,例如版本号改动后由版本记录一致性门与打包器读真源保证同步,配置改动由配置审计器检查目录约定、命名、内容与引用闭合,日志改动由日志审计器检查唯一落点与行格式,界面文案改动由语言包检查保证前后端成对。
+
+表 6-4 单一真源与对应守卫
+
+| 真源 | 管什么 | 守卫与实跑结果 |
+|---|---|---|
+| src/version.py | 名称、版本、版本规则、版本史、包名 | 版本记录一致性检查;check 实跑报 v2.10.0 且记录表与代码一致 |
+| src/paths.py | 一切路径解析的基准与助手 | 配置审计器检查不手拼路径;实跑无不一致 |
+| configs 目录与登记表 | 端口、模型档、场配置、页面归口、配置登记 | 配置审计实跑已知缺口与白名单 14 条、提示 7 条 |
+| src/logfile.py | 日志目录、命名、行格式、保留策略 | 日志审计实跑无不一致、提示 87 条 |
+| src/windscada/lang.py | 中英界面文案成对 | 语言包实跑 845 条成对 |
+| src/entry_refs.py | 入口脚本引用的文件必须都在包里 | check 实跑入口引用闭合 48 条全在位;编码守则通过 |
+
+## 7 数据需求概述
+
+### 7.1 数据族与功能映射
+
+系统的数据需求可以概括成一句话:**七类现场源件加一类共享机理资料,喂出九个产物仓,页面只读产物**。每个源类目录名就是摄入接口,改名等于换接口。数据族与功能的对应关系如下表;逐类的字段、单位、必须性与质量要求见同批交付的《数据要求说明_观澜_2.10.0.docx》。
+
+表 7-1 数据族到功能的映射
+
+| 源族(约定落位) | 谁消费 | 喂出什么功能 | 进链步骤 | 现状 |
+|---|---|---|---|---|
+| 故障报警 | 报警摄入器 | 报警台账、停机损失、限电绑定 | ② 三门台账 | 16 件 / 23.8 MB,结构合规 |
+| 风机故障记录 | 工单摄入器 | 检修工单台账、重复检修、闭环验证 | ② 三门台账 | 137 件 / 149.8 MB,其中台账 123 件、附件 14 件 |
+| 油样报告 | 油样索引摄入器 | 油液化验索引、油液时效、融合面油样轴 | ② 三门台账 | 404 件 / 176.2 MB,两级目录合规 |
+| scada_10min | 取数层与十个构建器 | 功率曲线、损失、温度、停机、偏航、液压、热链、变桨面日粒度 | ③ 与 ④ 与 ④c 与 ⑤b | 114 件 / 16.75 GB,含同台补充件 |
+| scada_1min | 取数层 | 变桨面零位三口径与越线计数 | ④c 变桨面 | 38 件 / 13.71 GB |
+| scada_mdb | 取数层(CSV 缺失时回落) | 同上(年度归档库) | ③ 与 ④ 与 ④c | 100 件 / 48.25 GB,其中消费 76 件、缺口 24 件 |
+| windcms | 振动摄入与报告转录 | 振动窗索引、谱库、CMS 报告、逐台页、在升闭环、三层基线 | ④b 与 ④d 与 ④e | 25,693 件 / 161.22 GB,其中解码导出 25,679 件 |
+| m5_cms_tcm | 融合面与报告侧 | 现场正本优先的 handoff 与部件历史、厂家报告转录 | ④b 振动摄入 | 1 件 / 21.8 MB;正本缺失时由观澜自算件顶上 |
+| 西门子4.0技术资料(与场站并列的共享目录) | 本体层 | 码表、手册、工单展开、失效树、实机参数、检索索引 | ⑦ 本体层 | 318 件 / 2.94 GB,结构合规 |
+
+### 7.2 体量与现状
+
+表 7-2 数据与产物现状(源族数字取自 2026-09-21 的扫描快照,合计取自 2026-09-22 实测计数)
+
+| 项 | 数量 | 体积 | 备注 |
+|---|---|---|---|
+| 输入源件(data/raw/如东,递归) | 26,504 件 | 224.67 GB | 同一棵树在 guanlan.py check 里报 26,503 件,两者相差 1 件,计数口径差异未取证 |
+| 产物(outputs/rudong,递归) | 3,549 件 | 7,157.5 MB | 含仓级台账 3 件;反向呼应审计报成立 3,548 件 |
+| 其中振动窗与谱仓 | 3,417 件 | 占比最大 | 谱分片与窗级分析件 |
+| 其中标准仓 | 57 件 | — | 含窄仓子目录与总览单页 |
+| 未归类件 | 1 件 | — | 场站目录下的一份按月提取核对件,按设计不作为摄入源 |
+| 缺口 | 24 项 | — | 年度归档库中 7 月的十个类目库,扫描器按缺口级列出并写明消费者口径 |
+
+数据现状里有两处必须如实说明。第一处是**缺口**:年度归档库 2026 年 7 月有 10 个类目库不在位(每类一件,扫描器按缺口级列出并提供消费者口径),另外类目库本身是上游归档件,取数层不直接读它,需要先合成为同台补充件才会被摄入。第二处是**按月提取核对件**:场站目录下有一份按月提取的核对件,位置由用户指定、不是摄入源,扫描器按名字模式忽略,既不报未归类也不计入变化。
+
+### 7.3 与数据要求说明的分工
+
+本文只回答「要哪些数据、这些数据支撑什么功能」。数据的字段级要求(核心测点的名称、单位、必须性、缺失替代、对齐规则、质量门与核对锚点)以及面向现场的收资清单,写在《数据要求说明_观澜_2.10.0.docx》里。该文档按用户令要求与现场收资文件逐条对照,并对现场收资层面的已知缺失逐条如实记录,例如测风塔数据为零交付、故障录波只有 4 台、振动侧 handoff 正本缺失由观澜自算件顶上、远端机器未安装本机模型等。
+
+## 8 页面与信息架构需求
+
+### 8.1 页面归口规则
+
+页面归口要回答两个问题:这一页该不该随输入数据变化;它算不算产物。答案落在五类内容类型上,每一类都有机器可查的规则,登记表里逐条写明判定依据(看的是页面里有没有内嵌数据、有没有实时取数、有没有产物引用,以及内容是不是由原始件算出来的)。要改分类,先改证据,这是需求层面的一条纪律。
+
+表 8-1 五类页面内容类型与机器规则
+
+| 类型 | 该随数据变 | 算不算产物 | 机器查什么 |
+|---|---|---|---|
+| 受管静态 | 否 | 不是 | 正文不得引用产物,引用了说明分类错了 |
+| 实时取数 | 是,永远一致 | 页面本体不是 | 内嵌或链接的端口必须在已知服务表里 |
+| 产物引用脚注 | 否,正文不变 | 正文不是,被引产物是 | 被引用的产物路径必须存在,悬空即报 |
+| 数据派生 | 是 | 是 | 必须有来源加指纹或生成时间;有指纹就与当前产物比对做陈旧检测 |
+| 冻结交付件 | 否,按交付版本变 | 不是运行时产物 | 文件名或正文必须能读到版本号与日期 |
+
+### 8.2 门户页面归口表
+
+表 8-2 按登记表逐条列出页面与子页的归口结论。登记表顶层条目 25 条,含 7 条子页共 32 条;归口审计实跑输出为「检查 21 项:不一致 0、已知缺口 19」,其中 21 项是审计的**检查行数**(19 条为引用悬空或溯源缺失的已知缺口项,2 条为两个数据派生页面的溯源检查项,均一致),与登记表的条目数不是同一个口径,两者都不矛盾,此处一并写明以免混淆。
+
+表 8-2 门户页面与子页归口表(25 条顶层条目加 7 条子页)
+
+| 页面或子页 | 类型 | 随输入数据变 | 判定依据摘要 |
+|---|---|---|---|
+| index 总览 | 受管静态 | 否 | 外壳段约 2,519 字节,无内嵌与取数与产物引用,正文是产品承诺与入口导航 |
+| architecture 系统架构 | 受管静态 | 否 | 外壳段约 3,037 字节,无数据引用,描述分层架构 |
+| method 方法 | 受管静态 | 否 | 外壳段约 2,959 字节,无数据引用 |
+| findings 经验发现 | 受管静态 | 否 | 外壳段约 2,322 字节,内容是公司级经验与内部检查条数,不是本场站数据的函数 |
+| case_hydraulic 案例与液压 | 受管静态 | 否 | 外壳段约 6,187 字节,无数据引用 |
+| cms 振动与 CMS | 实时取数 | 是 | 一个内嵌加两个链接指向振动服务,服务按需读产物,页面本身不存快照 |
+| recalc 数据重算 | 实时取数 | 是 | 链接到运维控制台,后端即真实状态 |
+| login 登录 | 实时取数 | 是 | 链接到工作台 |
+| admin 系统状态 | 实时取数 | 是 | 外壳脚本拉健康自检接口渲染,段内无静态数字 |
+| sim 仿真与回放 | 受管静态 | 否 | 外壳段约 50,767 字节为静态叙述,六个链接指向仿真台与三维台;输入是图纸与机型参数 |
+| 子页 控制律仿真台资料包(5 页) | 冻结交付件 | 否 | 仿真合页服务直接从冻结资料包读取,日期在包名里 |
+| 子页 三维拆装工作台 | 受管静态 | 否 | 三维资产由图纸与模型构建,与原始件无关 |
+| 子页 控制律仪表台面板(门户内嵌) | 产物引用脚注 | 否 | 静态判据说明加一条产物脚注引用,被引目录不在包内,已按悬空如实记账 |
+| 子页 四系统判据面板(门户内嵌 4 份) | 产物引用脚注 | 否 | 同上,静态正文加产物脚注 |
+| documents 交付文档 | 受管静态 | 否 | 外壳段约 5,690 字节为导航页,一个内嵌指向网关下发的治理清单页,段内不含数据快照 |
+| 子页 治理清单交付包 | 冻结交付件 | 否 | 文件名自带版本号与日期,客户交付件按交付版本冻结 |
+| 子页 全场状态一览(脱敏) | 冻结交付件 | 否 | 性质是数据派生但无指纹无生成端,按用户裁决按冻结交付件管并把会变旧的风险如实记账 |
+| 子页 如东取数单 | 冻结交付件 | 否 | 只有日期没有指纹,无生成端,且它引用的两棵产物树不在包内;保留不删并记账 |
+| page_product_cms CMS 报告页与件 | 数据派生 | 是 | 振动服务读的就是产物页面件;已在产物台账中登记并有溯源 |
+| page_product_vib_raw_manifest 振动摄入清单 | 数据派生 | 是 | 每次振动摄入重写,记件数、跨度与未随包缺口;已在台账中登记 |
+| embed_replay_chain 传动链回放(内嵌) | 数据派生 | 是 | 内嵌数据带来源与指纹,源产物在本包不在位,故现只能按生成时间判新旧 |
+| embed_standard_panel 标准面板(内嵌) | 数据派生 | 是 | 正文带生成时间与产物引用,缺指纹;被引的发布件不在包内 |
+| embed_coverage_ch0 覆盖度报告(内嵌) | 数据派生 | 是 | 正文写有来源与指纹,但那份源件不在包内,溯源链断在包外 |
+| embed_u6_sim 变桨液压仿真台(内嵌) | 受管静态 | 否 | 图纸派生的物理回路仿真面板,无内嵌数据与指纹 |
+| embed_u6_health 液压公共站健康报告(内嵌) | 冻结交付件 | 否 | 客户交付报告,无数据指纹,按交付件冻结 |
+| embed_gearbox_report 整机综合诊断报告(内嵌) | 冻结交付件 | 否 | 报告类交付件,文件名带修订版号 |
+| embed_reports_4 分系统评估报告(内嵌 4 份) | 冻结交付件 | 否 | 交付报告,无内嵌数据与指纹 |
+| embed_governance_12 治理清单分册(内嵌 12 份) | 冻结交付件 | 否 | 与交付层分册同源,冻结交付件 |
+| embed_sc1_local 本地占位(内嵌) | 受管静态 | 否 | 小体积占位面板 |
+| detail_tabs 工作台十个标签 | 实时取数 | 是 | 单页应用按标签现场取数,产物指纹变化自动重载,页面本体不存快照 |
+| delivery_pages_all 交付页 | 冻结交付件 | 否 | 客户交付件自带版本号与日期;其中两页性质上是数据派生,另见逐页判定 |
+| viewer_pages_all 三维页 | 受管静态 | 否 | 图纸与模型派生的资产页,与原始件无关 |
+
+按类型统计的分布如图 3-1 所示。本表口径下 32 条条目中,受管静态 11 条、实时取数 5 条、数据派生 5 条、冻结交付件 9 条、产物引用脚注 2 条。这张统计的意义在于:**真正需要随输入数据变的只有 5 条数据派生条目与 5 条实时取数条目**,其余 22 条要么是受管正文、要么是按版本冻结的交付件,把它们混进「产物管理」只会制造无谓的维护负担。
+
+![图 3-1 门户页面归口分布(依据 configs/portal_pages.yaml)](figures/fig-req-03-页面归口分布.png)
+
+### 8.3 页面陈旧检测要求
+
+数据派生页面的核心风险是「数据换了页面没换」,也就是页面里烘进去的快照与当前产物不一致。需求因此要求三件事:一是每条数据派生条目必须有来源路径,并且有指纹或生成时间;二是有指纹的必须与当前产物比对(逐字节比对哈希),不一致即报陈旧;三是登记表里写清「为什么这么判」,可复核。检查器的退出码语义为:0 表示一致,5 表示缺文件或未登记,6 表示溯源缺失,7 表示页面陈旧,8 表示交付件缺版本号,9 表示分类错。
+
+当前如实状态是:登记表里 19 条已知缺口全部是「页面引用了本包没有的产物树」,逐条写明理由而不装作通过;这些页面因此拿不到当前值做比对,其中两条数据派生页面没有生成端、三条内嵌快照缺指纹只能按生成时间判断。这三类都记账在案,待相应产物线随包后复检。
+
+## 9 验收需求与质量门
+
+### 9.1 质量门全景
+
+系统的验收分两层:**装机自检**看「这台机器能不能用」,**重算链验收**看「这一轮数据算得对不对」。前者是 guanlan.py check 的四十余条判定行,后者是重建命令每一环的退出码与容忍口径。两层的关系是:自检不通过就不该跑重算(环境或依赖有问题),重算链跑完再由自检复核产物与台账。
+
+表 9-1 验收门分类与数量(依据 guanlan.py check 实跑输出,2026-09-22,本机)
+
+| 分类 | 判定条数 | 代表门 | 实跑结果 |
+|---|---|---|---|
+| 运行环境与依赖 | 10 | Python 版本不低于 3.11(实测 3.12.10);九个第三方依赖逐个导入 | 全 OK |
+| 静态质量门 | 4 | 源码可编译 229 个文件;语言包 845 条成对;入口引用闭合 48 条;入口脚本编码守则 | 全 OK |
+| 子进程口径 | 3 | 捕获输出可用;无窗口启动输出进日志;无窗口位已设,标志位 0x8000200 | 全 OK |
+| 版本与卸载入口 | 2 | 版本管理与记录表一致(v2.10.0,包名 app_guanlang_v2.10.0.zip);卸载入口两个都在位 | 全 OK |
+| 制品与台账审计 | 5 | 反向呼应 3,548 件成立;页面归口检查 21 项;配置统一;日志统一;输入数据放置体检 26,503 件结构合规 | 全 OK |
+| 产物与发布件在位 | 10 | 标准仓、本体对象库、findings、事实契约、门户、仿真合页服务与资料包、三维资产、仿真回放资产、治理清单交付件 | 全 OK |
+| 原始件目录 | 1 | 原始件目录存在(无数据时此项不影响页面) | 全 OK |
+| 模型闸 | 6 | 默认档探针 0.77 秒;四个模型档逐个在位;模型可用性正常并记录审计日志 | 全 OK |
+| 合计 | 41 | 另有 6 条端口状态行不计入判定 | 结论全绿,可 serve,退出码 0 |
+
+验收门的分类与数量如图 7-1 所示。该图与上表同源,都取自本次自检实跑的 41 条判定行;按上表的分类口径,环境与依赖一类共 10 条、静态质量门 4 条、子进程口径 3 条、版本与卸载入口 2 条、制品与台账审计 5 条、产物与发布件在位 10 条、原始件目录 1 条、模型闸 6 条。其中真正体现「这个系统有没有把口径管住」的是制品与台账审计那五条门与四条静态质量门。
+
+![图 7-1 验收门分类与数量(依据 guanlan.py check 实跑输出)](figures/fig-req-04-验收门分类.png)
+
+### 9.2 重算链的逐步验收门
+
+重算链默认 26 步,加跳过 SCADA 为 23 步(另有第 ① 步仅在给了现场包目录时才跑)。每一步都是一个验收门:非零退出即停下并打印该步说明,容忍清单里的退出码不算失败,但会在汇总行里如实标出退出码。这个设计的关键在于「容忍不等于通过」:例如月度派生件找不到基线时返回 5,含义是「跳过了等价验收」,而不是「验收通过」。
+
+表 9-2 重算链逐步验收门(默认 26 步)
+
+| 序 | 步骤 | 主要脚本或命令 | 容忍退出码 | 非零的含义 |
+|---|---|---|---|---|
+| 1 | ① 放数据(仅当给了现场包目录) | place_raw_data.py | 无 | 落位失败或冲突被拦下 |
+| 2 | ①b 输入数据扫描 | raw_scan.py --check --write | 4 与 5 | 4 表示有新数据(不是失败),5 表示还没有快照 |
+| 3 | ② 三门台账 | rebuild_from_raw.py | 无 | 报警或工单或油样摄入失败 |
+| 4 | ③ SCADA 侧十个构建器 | rebuild_from_raw.py --scada | 无 | 原始件缺失或读取失败 |
+| 5 | ③b 标准仓窄仓 | scada_slim_build.py | 无 | 按时间窗重算的底座没建起来 |
+| 6 | ④ 月度派生件 | windscada_monthly_build.py | 5 | 找不到随包基线,跳过等价验收 |
+| 7 | ④b 振动摄入与报告与标量 | vib_raw_build.py | 无 | 解析出错;没有振动原始件时空跑属正常 |
+| 8 | ④c 变桨面 | pitch_face_build.py | 4 | 找不到 SCADA 原始件,按缺件如实报 |
+| 9 | ④d 振动在升与换件闭环 | component_history_build.py | 4 | 本机没有振动窗索引,按缺件如实报 |
+| 10 | ④e 三层基线 | baseline_38_build.py | 4 | 同上 |
+| 11 | ⑤b 由重算台账生成 claim | sop_findings_from_ledger.py | 无 | 台账不足以生成结论 |
+| 12 | ⑤c 事实契约构建与渲染 | guanlan_facts_contract.py build | 2 | 契约自检不过,需要人看 |
+| 13 | ⑤a 逐件来源台账 | products_restore_missing.py --refresh | 无 | 账实不符 |
+| 14 | ⑤ 反向呼应审计 | products_reverse_audit.py --check | 5 | 有未归类件或判据失败,停下看清单 |
+| 15 | ⑥ 重启组件服务(停,保留网关) | _ops_stop_keep_gateway.py | 无 | 停服务失败 |
+| 16 | ⑥ 重启组件服务(起) | guanlan.py serve | 1 | 有模块未就绪属降级正常状态 |
+| 17 | ⑦ 本体码表与手册与文档 | kb_ingest | 无 | 技术资料缺失会降级并打印提示 |
+| 18 | ⑦ 本体铺开判级与台账 | populate | 无 | 一层正本缺失或来源不足 |
+| 19 | ⑦ 本体决策链进度 | chain_ingest | 无 | 服务未在跑则取不到链盘 |
+| 20 | ⑦ 本体在升与闭环证据 | trend_ingest | 无 | 源缺失 |
+| 21 | ⑦ 本体检索索引 | retrieval.build | 无 | 索引构建失败 |
+| 22 | ⑦ 本体实机参数表 | maintenance.refresh_params | 无 | 参数源缺失 |
+| 23 | ⑦b 全场状态总览页 | windscada_overview_build.py | 无 | 缺产物时响亮报错,不产出半张页 |
+| 24 | ⑧ 本体审计 | ontology.audit | 无 | 期望 0 问题 |
+| 25 | ⑧b 重装门户 | portal_build.py | 1 | 缺契约产物时属预期,门户保留原样 |
+| 26 | ⑧c 页面归口审计 | pages_audit.py --check | 5 与 6 与 7 | 7 表示有页面陈旧;5 表示被引产物不在位;6 表示溯源缺失 |
+| 27 | ⑧d 版本记录一致性 | version_log.py --check | 6 | 文档与版本真源不一致,需同步文档 |
+| 28 | ⑧ 台账等价验收(仅加开关时) | rebuild_from_raw.py --verify | 4 与 5 | 4 表示有需人工看的差异;5 表示没基线即没做验收 |
+
+关于步数需要两点说明以免误读。第一,表中共列出 28 条命令行,其中第 1 步(放数据)仅在给了现场包目录时才执行,最后一步(台账等价验收)仅在加了对应开关时才执行,因此默认计划的实跑步数是 26 步,加跳过 SCADA 选项后是 23 步。第二,「门」与「步」不是一一对应:第 ⑥ 步拆成停与起两条命令各算一步,第 ⑦ 步拆成六条命令各算一步,所以表里的命令行数多于环节数。
+
+### 9.3 检查器的退出码语义
+
+表 9-3 主要检查器的退出码语义与容忍口径
+
+| 检查器 | 退出码语义 | 链上容忍 |
+|---|---|---|
+| guanlan.py check | 0 全绿可启动;2 有失败项 | 不作为链上步骤,装机后人工核 |
+| rebuild_all.py | 0 全部完成;1 有步骤失败(失败即停) | 汇总行逐条标 OK 或 OK 带退出码或 FAIL |
+| pages_audit.py | 0 一致;5 缺文件或未登记;6 溯源缺失;7 页面陈旧;8 交付件缺版本;9 分类错 | 容忍 5 与 6 与 7 |
+| products_reverse_audit.py | 0 全部有来路;5 有未归类件或判据失败 | 容忍 5 |
+| version_log.py | 0 与版本真源一致;6 记录表需重生成 | 容忍 6 |
+| config_audit.py | 0 无不一致;8 存在旧副本等结构问题 | 未进链,由 check 报 |
+| raw_scan.py | 0 无变化;4 有新增或变化(不是失败);5 还没有基线 | 容忍 4 与 5 |
+| raw_data_check.py | 0 合规;5 结构或命名违例;6 增量冲突;7 缺源类或时间空洞;8 仅有提示 | 未进链,由 check 报 |
+| rebuild_from_raw.py --verify | 0 逐值一致;4 有需人工看的差异;5 找不到基线即没做验收 | 容忍 4 与 5 |
+| ontology.audit | 0 无问题 | 不容忍 |
+| service_ctl.py | 0 成功;3 权限不足(给出可执行建议) | 未进链,人工执行 |
+
+### 9.4 开箱验证与卸载核验
+
+交付质量有两条独立的核验路径:打包后的**开箱验证**(在临时副本上真解压、真安装、真起服务、真核页面)与**卸载核验**(在刚装好的副本上真跑卸载并断言拆除范围与保留范围)。两条路径的意义是:它们都作用在副本上,不打扰正在运行的实例。
+
+表 9-4 开箱验证与卸载核验的项与判据
+
+| 核验路径 | 核验项 | 判据 | 实测记录 |
+|---|---|---|---|
+| 开箱验证 | 入口引用闭合与编码守则 | 入口脚本引用的文件必须都在包内;编码与字符守则全部通过 | 解压 3,734 件,入口引用闭合通过(2026-09-17 记录) |
+| 开箱验证 | 离线安装 | 安装脚本退出码 0 | 本机记录 172 秒、退出码 0 |
+| 开箱验证 | 快捷方式与无窗口 | 两份快捷方式在位;无控制台窗口断言成立;包内无被弃用的脚本宿主文件 | 窗口句柄为 0,断言通过 |
+| 开箱验证 | 页面可用性 | 核验门户与工作台与振动与运维控制台与健康自检五个入口,要求不少于 4 个可用 | 实测 4 个可用(振动入口因无产物返回 503,属预期) |
+| 开箱验证 | 逐件复核 | 包内每个文件与工作树逐字节相同 | 3,733 件逐字节相同 |
+| 卸载核验 | 拆除范围 | 安装记录、运行态、快捷方式都被拆掉 | 在临时副本上真跑卸载,退出码 0 |
+| 卸载核验 | 保留范围 | 数据与产物一件不少,端口已释放 | 件数不变,断言通过 |
+| 卸载核验 | 只读预演 | 内容断言(步骤齐全)与副作用断言(预演后关键目录与文件的存在性与字节数完全不变) | 自检全绿 |
+
+关于「五个入口全可用」这一条,需要如实说明:判据本身是「不少于 4 个可用」,实测记录为 4 个可用,第 5 个(振动入口)在没有产物的机器上按设计返回 503 并给出缺件说明。本次编写未取到 5 个全部可用的实测记录,因此本文不写「5 个全通过」,只写「判据为不少于 4 个、实测 4 个」,并把「在产物在位的机器上复跑取证」列为后续动作(未取证)。
+
+### 9.5 本次实跑结论
+
+本次编写期间对只读项做了三次实跑:自检(guanlan.py check)结论为全绿可启动、退出码 0、无失败项;归口审计(pages_audit.py --check)结论为全部一致、退出码 0、21 项检查里 19 项是已登记的已知缺口;重算计划(rebuild_all.py --dry-run)打印默认 26 步、跳过 SCADA 为 23 步,全部步骤命令与脚本都在位。没有执行任何重算或耗时计算,本文中与耗时有关的数字全部来自既有的 HISTORY 记录与设计文档,来源逐条列在表 6-3。
+
+## 10 约束、假设与明确不做
+
+需求的可信度一半来自「写清了不做什么」。本系统的边界不多但很硬:它是**只读分析系统**,不是控制系统,也不是云端服务。
+
+表 10-1 约束、假设与明确不做
+
+| 类别 | 条目 | 说明与依据 |
+|---|---|---|
+| 约束 | 不写回现场系统 | 全部产物落在本机产物仓,不下发指令、不修改现场任何系统;原始件按只读输入对待,落位冲突默认拒绝 |
+| 约束 | 不做实时控制 | 最小时间粒度是分钟级数据(10min 与 1min),系统的输出是分析与建议(带证据级与处置措施),没有控制回路 |
+| 约束 | 无外网 | 计算、页面、检索都在本机完成;Windows 侧离线安装件与便携运行时随包;本机模型为可选件且单独交付 |
+| 约束 | 不做云端 | 事实契约层留有可上云面孔(脱敏派生件),但本版不部署云服务、不做账号体系、不做多租户 |
+| 约束 | 页面无鉴权 | 这是知情取舍而非疏忽:对外监听等于把全场运行数据摊开,必须在网侧限来源或在反向代理加认证,启动日志每次提醒 |
+| 约束 | 安装目录不得含空格与中文 | 安装脚本、批处理与部分第三方组件的既有约束,写在说明书的系统要求一节 |
+| 约束 | 数据规模受单机资源限制 | 当前输入 26,504 件 / 224.67 GB;整条重算在单机上以小时计,建议内存 16 GB 起 |
+| 假设 | 现场数据按约定目录与命名交付 | 源类目录名即摄入接口,改名或多套一层会导致摄入静默读不到,因此用体检器与落位脚本前置把关 |
+| 假设 | 现场具备解释器或允许使用包内便携运行时 | 麒麟与统信自带解释器多为 3.7 与 3.9,跑不了本项目依赖,故包内带便携运行时 |
+| 假设 | 有至少一份能解析的厂商报告与至少一段原始运行数据 | 否则相关面按缺件如实报,不产出假数;这也是标定一次基线的前提 |
+| 假设 | 无人值守场景会使用服务而非会话前台进程 | 会话里起的进程会随会话关闭被一起收掉,远程部署必须用服务 |
+| 不做 | 不做训练回路 | 本版不含训练,模型只做转述与解释,判断在代码里 |
+| 不做 | 不做知识图谱的自动推断 | 本体层零判级权,只转录一层正本;同部件多源判级并存不合并 |
+| 不做 | 不做实时数据流接入 | 数据以批的方式落位、体检、重算;服务侧看门狗只做到发现变化并列出该跑的步骤,不做流式处理 |
+| 不做 | 不为「无生成端」的件做运行期补齐 | 运行期一律不从交付包补齐;缺件要么放原始件重算,要么由研发补生成端 |
+| 不做 | 不做多场切换的完整支持 | 场配置机制留有接口,但当前只有内置场可用,多场机制尚未接通 |
+
+## 11 需求跟踪矩阵
+
+跟踪矩阵把 43 条功能需求接到实现、页面或产物、验收门三列上。用它的方式很简单:改一条需求,先在矩阵里找到它对应的实现与验收门,改完之后按验收门那一列重跑对应检查;如果某一行的验收门是「人工核」,说明这条目前没有机器守卫,评审时要特别留意。
+
+表 11-1 需求跟踪矩阵
+
+| 编号 | 实现(脚本或模块) | 页面或产物 | 验收门 |
+|---|---|---|---|
+| FR-01 | taxonomy.system_matrix;scripts/windscada_serve.py | 系统矩阵、逐台页、问题页 | 判级随所选时间窗变化的实测记录;check 归口与反向呼应 |
+| FR-02 | scada_slim_build.py;subsys 各面 registry | 窄仓、判级单元格 | 重算链 ③b 步;窄仓列数与口径同源 |
+| FR-03 | perf/availability;reliability.overview | 可靠性表、部件问题页 | 判级与可靠性随时间窗变化的实测记录 |
+| FR-04 | windscada_serve.py 的 sys_norm() | 问题页与问题接口 | 传前端标识与中文名都能取到同一份问题(HTTP 抽样) |
+| FR-05 | 组件页面自组句逻辑 | 需要关注卡片 | HTTP 抽样:依据不再为空(WTG09 齿轮箱) |
+| FR-06 | months_of;win_range;in_win | v2 顶栏时间窗控件 | 时间窗词表逐项验证;退化时间窗按样本不足处理 |
+| FR-07 | 顶栏两个日期输入框与校验 | v2 页面自定义起止控件 | 起晚于止时拒绝并提示 |
+| FR-08 | 进程缓存与后台线程与预热 | 等待与构建中状态标记 | 判级约 25 秒每个时间窗;等待期页面写明旧口径 |
+| FR-09 | curves.build_store;/api/curves | 七镜头曲线 | 图注写随所选时间窗;样本量随时间窗变化 |
+| FR-10 | 月度派生件过滤 | 七张月度时序图 | 图注与按月取整口径 |
+| FR-11 | control.registry(span=) | 控制参数一致性卡片 | 按时间窗重算约 0.6 秒;封顶中位随窗变化 |
+| FR-12 | rebuild_from_raw.py --scada | 损失、功率曲线、停机事件 | 与基线逐值对拍;重算链 ③ 步 |
+| FR-13 | rudong_tcm_index.py;rudong_tcm_spectra.py;vib_raw_build.py | 振动窗索引、谱分片、摄入清单 | 振动窗 w0316 索引 2,066,686 行、谱 420,742 条 |
+| FR-14 | windcms.py report;report_std 与 report 构建器 | 报告 md 与 docx、逐台页、总览页 | 38 台判级齐;缺融合级写破折号 |
+| FR-15 | rudong_fusion_run.py;rudong_fusion_handoff.py;fusion 模块 | 融合表、handoff、全场标量 | 反向呼应审计按族逐件成立 |
+| FR-16 | component_history_build.py;trend_ingest | 部件历史件、趋势证据对象 | 重算链 ④d 步;时间窗不足出空表并写边界 |
+| FR-17 | baseline_38_build.py | 三层基线件 | 重算链 ④e 步;样本不足如实空表 |
+| FR-18 | windscada_alarms_ingest.py | 报警台账 | 39,211 行;键集合并集去重 |
+| FR-19 | windscada_workorder_ingest.py | 工单台账 | 5,876 行;差异可归类 |
+| FR-20 | windscada_watch_channels_build.py | 油样索引 | 404 行;102 行缺口如实记账 |
+| FR-21 | 停机与损失构建器 | 停机事件件、损失月表 | 与输入跨度对拍 |
+| FR-22 | src/ontology/kb_ingest.py | 对象库、检索索引、实机参数表 | 本体审计退出码 0;故障码覆盖 263 与 322 |
+| FR-23 | src/ontology/populate.py | 对象库对象 | 本体审计退出码 0 |
+| FR-24 | chain_ingest;trend_ingest | 决策对象与趋势证据对象 | 幂等覆盖;服务在跑时取链盘 |
+| FR-25 | src/ontology/fast_agent.py;llm_gate;检索 | 问答区与引文块、模型审计日志 | 已知答案回放;两档都拦时不出文并给原因 |
+| FR-26 | src/windcms/report_std.py;report.py | 报告 md 与 docx、登记 csv | 38 台逐台有条目;相对判据封顶候选 |
+| FR-27 | guanlan_facts_contract.py | 契约件与四个消费端派生件 | 改一条 claim 四个消费端全变;自检不过报 2 |
+| FR-28 | portal_build.py;门户装配脚本 | 门户单文件页、汇报纸 | 拆装逐字节相同;产物挪走时缺结论段属预期 |
+| FR-29 | scripts/rebuild_all.py | 各仓产物与步汇总 | 计划步数实跑;失败即停;幂等 |
+| FR-30 | guanlan_ops.py;_ops_run.py;_ops_launch.py | 运维控制台 | 并发动作返回 409;停组件保留控制台 |
+| FR-31 | products_state.py;products_restore_missing.py | 产物状态与缺件说明 | 清除后再点返回 409;补齐定位为离线人工补救 |
+| FR-32 | raw_scan.py;rebuild_all --auto;service_worker 的看门狗 | 扫描结论与基线快照、看门狗日志 | 退出码 0 与 4 与 5;族表外目录不猜 |
+| FR-33 | portal_build.py;shell 与模板装配 | 门户单文件页 | 拆装逐字节相同;行尾 LF 自检 |
+| FR-34 | configs/portal_pages.yaml;pages_audit.py | 归口表与审计结论 | 检查 21 项、不一致 0;陈旧报 7 |
+| FR-35 | retrieval.build;windcms 知识库构建 | 检索索引件 | 无模型时纯词法可用;索引缺失如实说明 |
+| FR-36 | place_raw_data.py;raw_data_check.py | 落位结果与体检结论 | 冲突默认拒绝;同尺寸跳过 |
+| FR-37 | scan_stations.py;config 场站辨识 | 辨识结论与依据 | 四条规则逐条打印;多目录不匹配报未识别 |
+| FR-38 | scada_source 取数层;raw_scan 族表 | 合并取数结果与扫描结论 | 主件与补充件合并行数与重叠数打印;类目库不计变化 |
+| FR-39 | install.bat 与 install.ps1 与 install.sh;便携运行时 | 虚拟环境、配置、安装记录 | 离线安装退出码 0;入口引用闭合 |
+| FR-40 | service_ctl.py;win_service.py;service_main.py;service_worker.py | 托管进程与自启项 | 巡检 15 秒;无权限返回 3 并给建议 |
+| FR-41 | 安装脚本的版本检查步骤;src/version.py | 安装记录与提示 | 五类提示与动作;check 版本行一致 |
+| FR-42 | guanlan_uninstall.py;uninstall 入口两个 | 卸载结果 | 自检两类断言;实跑断言拆除与保留范围 |
+| FR-43 | pack_dist.py;pack 入口;开箱验证入口 | 交付包与清单文件 | 两道闸;包内不得有安装记录或快捷方式;页面可用不少于 4 个 |
+
+## 附录 A 编写依据
+
+表 A-1 编写依据(章节或事实与来源文件)
+| 章节或事实 | 来源文件(仓库相对路径) |
+|---|---|
+| 版本号真源、版本规则、12 条版本史、包名 | src/version.py |
+| 系统是什么、产物全景、路径约定、进程口径、页面取数 | docs/系统设计说明.md 第 1 至第 6 章 |
+| 缺口与边界(未随包脚本、缺件、源件缺失) | docs/系统设计说明.md 第 7 章 |
+| 变更记录(按日期逐条事故与处置) | docs/系统设计说明.md 第 8 章 |
+| 交付包组成与入口引用闭合 | docs/系统设计说明.md 第 9 章与 §9.1 |
+| 页面归口规则、五类内容类型、门户逐页识别、全部网页总清点 | docs/系统设计说明.md 第 10 章与第 12 章 |
+| 配置与日志统一口径、无产物时的可用性 | docs/系统设计说明.md 第 11 章 |
+| 输入到功能到算法到产物、端到端顺序、反向呼应审计、可逆性 | docs/系统设计说明.md 第 13 章与 §13.6 |
+| 服务化安装与卸载、安装时版本检查 | docs/系统设计说明.md 第 14 章 |
+| 版本管理与打包命名、单一真源与三处守卫 | docs/系统设计说明.md 第 15 章 |
+| 时间窗词表、随窗与不随窗、窄仓、等待契约 | docs/系统设计说明.md 第 16 章 |
+| 三条描述口径与服务端链接契约、依据不得为空 | docs/系统设计说明.md 第 17 章 |
+| 面向用户的功能与使用、系统要求、模型档、目录结构、常见故障、已知限制 | docs/说明书_观澜如东样板v2_v0.2.md |
+| 页面与子页的归口登记(类型、依据、已知缺口) | configs/portal_pages.yaml |
+| 服务端口与监听地址真源、看门狗开关 | configs/serve.json |
+| 模型档位与拉取命令 | configs/models.json |
+| 自检门清单与退出码、服务定义 | guanlan.py |
+| 重算链步骤、容忍口径、自动取消跳过 | scripts/rebuild_all.py |
+| 归口审计规则与退出码语义 | scripts/pages_audit.py |
+| 输入数据放置规则、增量语义、放完跑什么 | docs/输入数据放置指导_v0.1.md |
+| 落位约定、场站辨识四条规则、各目录功用、能重建与不能重建 | docs/数据目录结构与落位约定_v0.2.md |
+| 手工重算步骤、运维控制台三条纪律、二次启动原因 | docs/重算操作手册_v0.1.md |
+| 页面输出体检的判定口径与逐页原因 | docs/页面输出体检_v0.1.md 与 docs/页面输出体检_逐页原因_v0.1.md |
+| 打包与安装与卸载与版本检查的用户视角说明 | README_先读我.MD |
+| 自检实跑输出(41 条判定行) | docs/src/_guanlan_check.txt 与本机 2026-09-22 实跑 |
+| 重算计划实跑输出(23 步) | docs/src/_rebuild_plan.txt |
+| 四张插图的数字来源 | docs/figures 下的四张需求分析插图与其生成器 scripts/delivery_docs_figures.py |
+
+## 附录 B 术语表
+
+表 B-1 术语表(按主题分组)
+
+| 术语 | 英文或写法 | 含义 | 出处 |
+|---|---|---|---|
+| 时间窗 | — | 一个由用户选择或自定义的数据观察区间;月度件按月取整、日粒度件按日精确 | docs/系统设计说明.md §16 |
+| 预设时间窗 | — | 词表内置的时间窗:近30日、近90日、2026年、2026H1、2025H2、全程 | docs/系统设计说明.md §16.1 |
+| 自定义起止日期时间窗 | — | 写法为起日期~止日期的时间窗,含两端 | docs/系统设计说明.md §16.1 |
+| 判级时间窗 | — | 判级矩阵与四轴重算所用的时间窗 | docs/系统设计说明.md §17.1 |
+| 证据时间窗 | — | 支撑某条结论的证据所在的时间窗,随判定一并给出 | docs/系统设计说明.md §17.1 |
+| 天气窗 | — | 领域词,指气象条件适合的时段,不属于时间窗口径的统一范围 | docs/系统设计说明.md §17.1 |
+| 作业窗 | — | 领域词,指检修作业可实施的时段,保持原样 | docs/系统设计说明.md §17.1 |
+| 平均无故障间隔(MTBF) | MTBF | 相邻两次故障之间的平均运行时长,按所选时间窗内的停机事件与报警计算 | docs/系统设计说明.md §16.2 与 §17.1 |
+| 平均停机间隔(MTBO) | MTBO | 相邻两次停机之间的平均间隔,与可靠性表同源 | docs/系统设计说明.md §17.1 |
+| 单次停机时长(MDT) | MDT | 每次停机的平均持续时长 | docs/系统设计说明.md §17.1 |
+| 窄仓 | slim10min | 由十分钟数据抽出的公共列子集(不裁剪行),作为按时间窗重算的秒级底座 | docs/系统设计说明.md §16.3 |
+| 等待契约 | pending | 按时间窗重算未命中缓存时先回等待或构建中,由页面轮询,绝不静默用旧口径 | docs/系统设计说明.md §16.4 |
+| 判级四轴 | — | 变桨、偏航、蓄能、温度四个随所选时间窗重算的判级方向 | docs/系统设计说明.md §16.2 |
+| 融合面 | — | 把模型过闸线与振动窗索引裁决合到一起的判级面,输出融合级 | docs/系统设计说明.md §13.1 |
+| 链盘 | — | 运行时的决策链进度集合,每台一个对象记六步状态、卡点与下一步 | docs/系统设计说明.md §13.1 |
+| 产物 | outputs | 由原始件算出来、页面只读的输出数据,统一落在产物仓 | docs/系统设计说明.md §3 与 §13.4 |
+| 逐件来源台账 | — | 记录每件产物属于原始件重算还是随包补齐的台账 | docs/系统设计说明.md §2.1 |
+| 反向呼应审计 | — | 从输出回溯到生成端与输入的审计,检查每件产物有没有来路 | docs/系统设计说明.md §13.6 |
+| 页面归口 | — | 判定每一页是否随输入数据变、是否算产物,并逐条写明依据 | configs/portal_pages.yaml |
+| 陈旧检测 | — | 对数据派生页面比对内嵌指纹与当前产物,不一致即报页面陈旧 | scripts/pages_audit.py |
+| 数据派生 | data-derived | 五类页面内容类型之一:把产物烘进页面,属于产物,必须登记来源并做陈旧检测 | configs/portal_pages.yaml 头部 |
+| 受管静态 | static | 受管外壳正文,不随输入数据变,也不得引用产物 | configs/portal_pages.yaml 头部 |
+| 实时取数 | live | 页面本体不存快照,永远与产物一致 | configs/portal_pages.yaml 头部 |
+| 产物引用脚注 | data-citing | 静态正文加产物引用,正文不变但被引产物必须在位 | configs/portal_pages.yaml 头部 |
+| 冻结交付件 | frozen-delivery | 带版本号与日期的客户交付件,按交付版本冻结,不随数据自动变 | configs/portal_pages.yaml 头部 |
+| 开箱验证 | — | 打包后在临时副本上真解压真安装真起服务的核验路径 | docs/系统设计说明.md §9 |
+| 容忍口径 | tolerate | 重算链允许某步返回特定退出码而不打断整条链,且明确「容忍不等于通过」 | scripts/rebuild_all.py |
+| 核对锚点 | — | 重算后应达到的条数指标,用于快速判断数据是否真的进去了 | docs/输入数据放置指导_v0.1.md §4 |
+| 收资 | — | 向现场收集原始数据与资料的过程与其清单要求 | docs/现场收资接入_v0.1.md |

BIN
docs/数据要求说明_观澜_2.10.0.docx


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 3 - 3
docs/版本记录.md


BIN
docs/设计说明_观澜_2.10.0.docx


BIN
docs/需求分析_观澜_2.10.0.docx


+ 33 - 1
guanlan.py

@@ -10,7 +10,7 @@
 端口与路径在 configs/serve.json (没有则用内置默认). 组件默认只绑 127.0.0.1;
 门户网关可用 serve.json 的 public_host 放到所有网卡 (用户令 2026-09-19「监听所有 IP」, 见 §7)。"""
 from __future__ import annotations
-import json, os, socket, subprocess, sys, time, urllib.request, warnings, webbrowser, signal
+import json, os, re, socket, subprocess, sys, time, urllib.request, warnings, webbrowser, signal
 from pathlib import Path
 ROOT = Path(__file__).resolve().parent; RUN = ROOT / "run"; LOGS = ROOT / "logs"; PIDS = RUN / "pids.json"
 sys.path.insert(0, str(ROOT))
@@ -241,6 +241,38 @@ def cmd_check(c):
             f"{len(_un)}/2 个入口" + ("" if _uok else " —— 缺卸载入口: 装得上卸不掉"))
     except Exception as _e:
         row("版本管理与卸载入口", False, f"{type(_e).__name__}: {_e}")
+    # 交付文档三件在位(2026-09-22 用户令「需求分析/设计说明/数据要求说明」): 文档名里带**当前版本号**,
+    # 所以升一次版本号, 旧文档名就过期了 —— 不发 FAIL 的话, 包会悄悄带着 v2.9.2 名字的文档进 v2.10.0 交付。
+    # 判据: ① 三份 docx 在位且文件名含当前 VERSION; ② markdown 源件与插图都在位(可重跑重建)。
+    try:
+        import importlib.util as _ilu4
+        _sp4 = _ilu4.spec_from_file_location('_ddb', ROOT / "scripts" / "delivery_docs_build.py")
+        _ddb = _ilu4.module_from_spec(_sp4)
+        _sp4.loader.exec_module(_ddb)
+        _V2 = _ddb.VERSION
+        _miss, _stale, _figmiss = [], [], []
+        for _k, _spec in _ddb.DOCS.items():
+            _out = ROOT / _spec['out'].format(v=_V2)
+            _src = ROOT / _spec['src'].format(v=_V2)
+            if not _out.is_file():
+                _miss.append(_out.name)
+                _old = sorted((ROOT / "docs").glob(_spec['out'].format(v='*').split('/')[-1]))
+                _stale += [p.name for p in _old if p.name != _out.name]
+            if not _src.is_file():
+                _miss.append(_src.name)
+                continue
+            for _rel in re.findall(r'!\[[^\]]*\]\((figures/[^)]+)\)', _src.read_text(encoding='utf-8')):
+                if not (ROOT / "docs" / _rel).is_file():
+                    _figmiss.append(_rel)
+        _miss += _figmiss
+        _ok4 = not _miss
+        row(f"交付文档三件 (需求分析/设计说明/数据要求说明 · v{_V2})", _ok4,
+            ("3/3 份在位, 文档名含当前版本号, 插图全在位" if _ok4 else
+             f"缺 {len(_miss)} 件: {', '.join(_miss[:3])}"
+             + (f";同名旧版残留: {', '.join(_stale[:3])}" if _stale else '')
+             + " —— 跑 python scripts/delivery_docs_build.py 重建"))
+    except Exception as _e:
+        row("交付文档三件", False, f"{type(_e).__name__}: {_e}")
     # 反向呼应审计 (用户令 2026-09-17): 自**输出**回溯 功能与算法 → **输入**, 逐件问"这件产物能不能
     # 由输入推导出来"。正向那几条跨度判据查不出"盘上这件产物到底有没有来路"(缺件时一条都不报),
     # 反向一对账就清楚了: 哪些有来路(生成端+输入+判据)、哪些没有(无生成端, 只能从交付包补)。

+ 582 - 0
scripts/delivery_docs_build.py

@@ -0,0 +1,582 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+r"""交付文档构建器:`docs/src/*.md` → `docs/*.docx`(用户令 2026-09-22)。
+
+## 为什么要有它(而不是拿 Word 手排)
+用户令要求三份文档「区分章节目录、文表图并茂、字体字号分类统一」。手排的三份文档**必然**字体字号漂移、
+图表编号对不上、改一处忘一处。所以内容用 markdown 写(可 diff、进版本库),排版由本器**一套样式表**统一执行:
+
+| 元素 | 字体 | 字号 | 对齐 / 缩进 |
+|---|---|---|---|
+| 封面标题 | 黑体 | 小一(26pt) | 居中 |
+| 章标题(一级 `##`) | 黑体 | 三号(16pt) | 左对齐,**章前分页** |
+| 节标题(二级 `###`) | 黑体 | 四号(14pt) | 左对齐 |
+| 小节标题(三级 `####`) | 黑体 | 小四(12pt) 加粗 | 左对齐 |
+| 正文 | 宋体 | 小四(12pt) | 首行缩进 2 字符、1.5 倍行距、两端对齐 |
+| 列表 | 宋体 | 小四(12pt) | 悬挂缩进 |
+| 表题(表上方) | 黑体 | 五号(10.5pt) | 居中 |
+| 表头 | 黑体 | 五号(10.5pt) 加粗 | 居中 + 浅灰底纹 + 跨页重复 |
+| 表格正文 | 宋体 | 五号(10.5pt)(≥6 列降 9pt) | 左对齐 |
+| 图(居中)与图题(图下方) | 黑体 | 五号(10.5pt) | 居中 |
+| 页眉 / 页脚页码 | 宋体 | 小五(9pt) | 居中 |
+
+英文与数字用 Times New Roman(中式公文惯例:中文宋体/黑体 + 西文 Times New Roman)。
+
+## markdown 约定(渲染器支持的子集,写手必须照此写)
+    # 文档标题                       (只此一处,进封面)
+    ## 1 章 / ### 1.1 节 / #### 1.1.1 小节
+    普通段落(一行一段,段间空行)
+    - 无序项           1. 有序项
+    | 表头 | 表头 |   +  |---| 分隔行           → 表格
+    **表 3-1 标题**(紧挨表格上方一行)            → 表题
+    ![图 3-1 标题](figures/xxx.png)                → 图片 + 图题
+    > 引用段
+    ---                                            → 忽略
+
+## 用法
+    python scripts/delivery_docs_build.py                 # 三份全渲染
+    python scripts/delivery_docs_build.py --only req      # 只渲染一份 (req/des/dat)
+    python scripts/delivery_docs_build.py --check         # 只体检源件(章/节/表/图/字数、图是否在位)
+    python scripts/delivery_docs_build.py --spec          # 打印本器的排版规范表
+
+退出码: 0 全部成功 · 5 有源件缺件/图缺件(逐条打印,仍会尽力渲染其余内容)
+"""
+from __future__ import annotations
+
+import argparse
+import pathlib
+import re
+import sys
+
+ROOT = pathlib.Path(__file__).resolve().parents[1]
+
+from docx import Document                                                    # noqa: E402
+from docx.enum.section import WD_SECTION                                     # noqa: E402
+from docx.enum.table import WD_TABLE_ALIGNMENT                               # noqa: E402
+from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING               # noqa: E402
+from docx.oxml import OxmlElement                                            # noqa: E402
+from docx.oxml.ns import qn                                                  # noqa: E402
+from docx.shared import Cm, Pt, RGBColor                                     # noqa: E402
+
+VERSION = __import__('importlib').import_module('src.version').VERSION
+
+HEI, SONG, WEST = '黑体', '宋体', 'Times New Roman'
+SZ = dict(cover=26, h1=16, h2=14, h3=12, body=12, table=10.5, table_small=9, caption=10.5, header=9)
+
+DOCS = {
+    'req': dict(src='docs/src/需求分析_观澜_{v}.md', out='docs/需求分析_观澜_{v}.docx',
+                title='需求分析', subtitle='观澜 · 如东样板 v2'),
+    'des': dict(src='docs/src/设计说明_观澜_{v}.md', out='docs/设计说明_观澜_{v}.docx',
+                title='设计说明', subtitle='观澜 · 如东样板 v2'),
+    'dat': dict(src='docs/src/数据要求说明_观澜_{v}.md', out='docs/数据要求说明_观澜_{v}.docx',
+                title='数据要求说明', subtitle='观澜 · 如东样板 v2(结合现场《数据分析收资要求-v3》)'),
+}
+
+SPEC_ROWS = [
+    ('封面标题', '黑体', '小一(26pt)', '居中'),
+    ('章标题(一级)', '黑体', '三号(16pt)', '左对齐,章前分页'),
+    ('节标题(二级)', '黑体', '四号(14pt)', '左对齐'),
+    ('小节标题(三级)', '黑体', '小四(12pt)加粗', '左对齐'),
+    ('正文', '宋体(西文 Times New Roman)', '小四(12pt)', '首行缩进 2 字符,1.5 倍行距,两端对齐'),
+    ('列表', '宋体', '小四(12pt)', '悬挂缩进 0.74 cm'),
+    ('表题', '黑体', '五号(10.5pt)', '居中,置于表格上方'),
+    ('表头', '黑体', '五号(10.5pt)加粗', '居中,浅灰底纹,跨页重复'),
+    ('表格正文', '宋体', '五号(10.5pt);≥6 列降 9pt', '左对齐,垂直居中'),
+    ('图', '—', '宽度 ≤ 15 cm', '居中'),
+    ('图题', '黑体', '五号(10.5pt)', '居中,置于图下方'),
+    ('页眉', '宋体', '小五(9pt)', '居中'),
+    ('页脚页码', '宋体', '小五(9pt)', '居中(第 X 页 / 共 Y 页)'),
+]
+
+WARN: list[str] = []
+
+
+# ── 低层 docx 工具 ─────────────────────────────────────────────────────────────
+def _rfonts(el, ascii_font: str, ea_font: str):
+    rpr = el.get_or_add_rPr()
+    rf = rpr.find(qn('w:rFonts'))
+    if rf is None:
+        rf = OxmlElement('w:rFonts')
+        rpr.append(rf)
+    rf.set(qn('w:ascii'), ascii_font)
+    rf.set(qn('w:hAnsi'), ascii_font)
+    rf.set(qn('w:eastAsia'), ea_font)
+
+
+def set_run(run, size: float, *, ea: str = SONG, ascii_font: str = WEST, bold: bool = False):
+    run.font.size = Pt(size)
+    run.font.bold = bold
+    run.font.name = ascii_font
+    run.font.color.rgb = RGBColor(0, 0, 0)
+    _rfonts(run._element, ascii_font, ea)
+
+
+def style_setup(doc: Document):
+    """把内置样式(Heading 1-4 / Normal / 表格)改成文档的统一口径。"""
+    normal = doc.styles['Normal']
+    normal.font.name = WEST
+    normal.font.size = Pt(SZ['body'])
+    _rfonts(normal.element, WEST, SONG)
+    normal.paragraph_format.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
+    normal.paragraph_format.line_spacing = 1.5
+    normal.paragraph_format.space_after = Pt(0)
+
+    spec = {'Heading 1': (SZ['h1'], True), 'Heading 2': (SZ['h2'], True),
+            'Heading 3': (SZ['h3'], True), 'Heading 4': (SZ['h3'], True)}
+    for name, (size, bold) in spec.items():
+        st = doc.styles[name]
+        st.font.name = WEST
+        st.font.size = Pt(size)
+        st.font.bold = bold
+        st.font.italic = False
+        st.font.color.rgb = RGBColor(0, 0, 0)
+        _rfonts(st.element, WEST, HEI)
+        pf = st.paragraph_format
+        pf.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
+        pf.line_spacing = 1.5
+        pf.space_before = Pt(12 if name != 'Heading 1' else 0)
+        pf.space_after = Pt(6)
+        pf.first_line_indent = Pt(0)
+        if name == 'Heading 1':
+            pf.page_break_before = True
+            pf.space_after = Pt(12)
+    sec = doc.sections[0]
+    sec.page_width, sec.page_height = Cm(21.0), Cm(29.7)
+    sec.top_margin = sec.bottom_margin = Cm(2.54)
+    sec.left_margin = sec.right_margin = Cm(3.17)
+
+
+def add_field(paragraph, instr: str, placeholder: str = ''):
+    r = paragraph.add_run()
+    b = OxmlElement('w:fldChar'); b.set(qn('w:fldCharType'), 'begin')
+    i = OxmlElement('w:instrText'); i.set(qn('xml:space'), 'preserve'); i.text = instr
+    s = OxmlElement('w:fldChar'); s.set(qn('w:fldCharType'), 'separate')
+    t = OxmlElement('w:t'); t.text = placeholder
+    e = OxmlElement('w:fldChar'); e.set(qn('w:fldCharType'), 'end')
+    for el in (b, i, s, t, e):
+        r._r.append(el)
+    return r
+
+
+def para(doc, text: str = '', *, size=SZ['body'], ea=SONG, bold=False, align='left',
+         indent_chars=0, space_before=0, space_after=6, hanging=False):
+    p = doc.add_paragraph()
+    pf = p.paragraph_format
+    pf.line_spacing_rule = WD_LINE_SPACING.MULTIPLE
+    pf.line_spacing = 1.5
+    pf.space_before = Pt(space_before)
+    pf.space_after = Pt(space_after)
+    pf.first_line_indent = Pt(size * indent_chars)
+    if hanging:
+        pf.left_indent = Cm(0.74)
+        pf.first_line_indent = Cm(-0.0)
+    pf.alignment = {'left': WD_ALIGN_PARAGRAPH.LEFT, 'center': WD_ALIGN_PARAGRAPH.CENTER,
+                    'justify': WD_ALIGN_PARAGRAPH.JUSTIFY}[align]
+    if text:
+        for chunk, is_bold, mono in split_bold(text):
+            r = p.add_run(chunk)
+            set_run(r, size, ea=SONG, ascii_font='Consolas' if mono else WEST, bold=bold or is_bold)
+    return p
+
+
+INLINE_RE = re.compile(r'\*\*(.+?)\*\*|`([^`]+)`')
+CODE_RE = re.compile(r'`([^`]+)`')
+BOLD_RE = re.compile(r'\*\*(.+?)\*\*')
+
+
+def _bold_parts(s: str):
+    parts, pos = [], 0
+    for m in BOLD_RE.finditer(s):
+        if m.start() > pos:
+            parts.append((s[pos:m.start()], False, False))
+        parts.append((m.group(1), True, False))
+        pos = m.end()
+    tail = s[pos:]
+    if '**' in tail:
+        # 落单的 `**`(例如通配路径 `pitch/**` 没包在行内代码里)—— 交付文档里不能留字面量,
+        # 去掉并记一条告警,让写手回去补行内代码。
+        WARN.append('落单的 ** 已剔除: ' + tail.strip()[:60])
+        tail = tail.replace('**', '')
+    if tail:
+        parts.append((tail, False, False))
+    return parts
+
+
+def split_bold(text: str):
+    """行内标记 → [(片段, 是否粗体, 是否等宽)]。
+
+    ★单遍分词,支持两种嵌套(2026-09-22 两次实逮后的定稿):
+      ① 代码里含 `**`(如通配路径 `` `pitch/**` ``)—— 不能当成粗体起始;
+      ② 粗体里含代码(如 ``**体量以 `data/raw` 为准**``)—— 不能按代码先切、把粗体配对切断。
+    先按 `**…**` 找配对,配对内部再递归扫代码;代码段内部一律**字面**取,不再解析粗体。
+    落单的 `**` 剔除并记告警(交付文档里不能留字面星号)。
+    """
+    out: list[tuple[str, bool, bool]] = []
+
+    def emit(t: str, bold: bool, mono: bool):
+        if t:
+            out.append((t, bold, mono))
+
+    def scan(s: str, bold: bool):
+        i = 0
+        while i < len(s):
+            if s.startswith('**', i):
+                j = s.find('**', i + 2)
+                if j > i + 2:
+                    scan(s[i + 2:j], True)
+                    i = j + 2
+                    continue
+                WARN.append('落单的 ** 已剔除: ' + s[max(0, i - 20):i + 20].strip())
+                i += 2
+                continue
+            if s[i] == '`':
+                j = s.find('`', i + 1)
+                if j > i:
+                    emit(s[i + 1:j], bold, True)
+                    i = j + 1
+                    continue
+            nxt = len(s)
+            for k in range(i, len(s)):
+                if s[k] == '`' or s.startswith('**', k):
+                    nxt = k
+                    break
+            emit(s[i:nxt], bold, False)
+            i = nxt
+
+    scan(text, False)
+    return out or [(text, False, False)]
+
+
+def shade(cell, fill='F2F2F2'):
+    tcPr = cell._tc.get_or_add_tcPr()
+    shd = OxmlElement('w:shd')
+    shd.set(qn('w:val'), 'clear')
+    shd.set(qn('w:color'), 'auto')
+    shd.set(qn('w:fill'), fill)
+    tcPr.append(shd)
+
+
+def repeat_header(row):
+    trPr = row._tr.get_or_add_trPr()
+    th = OxmlElement('w:tblHeader')
+    th.set(qn('w:val'), 'true')
+    trPr.append(th)
+
+
+def add_table(doc, rows: list[list[str]]):
+    ncol = max(len(r) for r in rows)
+    t = doc.add_table(rows=0, cols=ncol)
+    t.style = 'Table Grid'
+    t.alignment = WD_TABLE_ALIGNMENT.CENTER
+    t.autofit = True
+    size = SZ['table_small'] if ncol >= 6 else SZ['table']
+    for i, r in enumerate(rows):
+        cells = t.add_row().cells
+        for j in range(ncol):
+            txt = (r[j] if j < len(r) else '').strip()
+            cell = cells[j]
+            cell.text = ''
+            p = cell.paragraphs[0]
+            p.paragraph_format.line_spacing = 1.15
+            p.paragraph_format.space_after = Pt(0)
+            p.paragraph_format.first_line_indent = Pt(0)
+            p.alignment = WD_ALIGN_PARAGRAPH.CENTER if i == 0 else WD_ALIGN_PARAGRAPH.LEFT
+            for chunk, is_bold, mono in split_bold(txt):
+                run = p.add_run(chunk.replace('\\|', '|'))
+                set_run(run, size, ea=HEI if i == 0 else SONG,
+                        ascii_font='Consolas' if mono else WEST, bold=(i == 0) or is_bold)
+            if i == 0:
+                shade(cell)
+    if t.rows:
+        repeat_header(t.rows[0])
+    return t
+
+
+# ── markdown 解析 ──────────────────────────────────────────────────────────────
+IMG_RE = re.compile(r'^!\[(?P<cap>[^\]]*)\]\((?P<path>[^)]+)\)\s*$')
+CAP_RE = re.compile(r'^\*\*(?P<cap>表\s*[\d\-–.]+.*?)\*\*\s*$')
+
+
+def parse_md(text: str):
+    """→ [('h1'|'h2'|'h3'|'h4'|'p'|'ul'|'ol'|'table'|'img'|'quote', payload)]"""
+    lines = text.splitlines()
+    blocks, i = [], 0
+    if lines and lines[0].startswith('# '):
+        i = 1
+    while i < len(lines):
+        ln = lines[i]
+        s = ln.strip()
+        if not s or re.match(r'^([-*_])(\s*\1){2,}$', s):
+            i += 1                       # 空行与分隔线(`---` / `***` / `___`)
+            continue
+        m = re.match(r'^(#{2,5})\s+(.*)$', s)
+        if m:
+            blocks.append(('h%d' % (len(m.group(1)) - 1), m.group(2).strip()))
+            i += 1
+            continue
+        m = IMG_RE.match(s)
+        if m:
+            blocks.append(('img', (m.group('cap'), m.group('path'))))
+            i += 1
+            continue
+        if s.startswith('|'):
+            rows = []
+            while i < len(lines) and lines[i].strip().startswith('|'):
+                raw = lines[i].strip().strip('|')
+                if not re.match(r'^[\s:\-|]+$', raw):
+                    rows.append([c.strip() for c in raw.split('|')])
+                i += 1
+            if rows:
+                cap = None
+                if blocks and blocks[-1][0] == 'p' and CAP_RE.match(blocks[-1][1]):
+                    cap = blocks.pop()[1]
+                blocks.append(('table', (cap, rows)))
+            continue
+        if s.startswith('- '):
+            items = []
+            while i < len(lines) and lines[i].strip().startswith('- '):
+                items.append(lines[i].strip()[2:].strip())
+                i += 1
+            blocks.append(('ul', items))
+            continue
+        if re.match(r'^\d+[.)]\s+', s):
+            items = []
+            while i < len(lines) and re.match(r'^\d+[.)]\s+', lines[i].strip()):
+                items.append(re.sub(r'^\d+[.)]\s+', '', lines[i].strip()))
+                i += 1
+            blocks.append(('ol', items))
+            continue
+        if s.startswith('> '):
+            q = []
+            while i < len(lines) and lines[i].strip().startswith('> '):
+                q.append(lines[i].strip()[2:].strip())
+                i += 1
+            blocks.append(('quote', ' '.join(q)))
+            continue
+        blocks.append(('p', s))
+        i += 1
+    return blocks
+
+
+def count_md(text: str) -> dict:
+    blocks = parse_md(text)
+    body = re.sub(r'!\[[^\]]*\]\([^)]*\)', '', text)
+    cn = len(re.findall(r'[\u4e00-\u9fff]', body))
+    return dict(
+        章=sum(1 for k, _ in blocks if k == 'h1'),
+        节=sum(1 for k, _ in blocks if k == 'h2'),
+        小节=sum(1 for k, _ in blocks if k == 'h3'),
+        表=sum(1 for k, _ in blocks if k == 'table'),
+        图=sum(1 for k, _ in blocks if k == 'img'),
+        段落=sum(1 for k, _ in blocks if k == 'p'),
+        汉字=cn)
+
+
+# ── 渲染 ───────────────────────────────────────────────────────────────────────
+def cover(doc, title: str, subtitle: str):
+    for _ in range(4):
+        para(doc, '', space_after=0)
+    para(doc, f'{title}', size=SZ['cover'], ea=HEI, bold=True, align='center', space_after=12)
+    para(doc, subtitle, size=14, ea=HEI, align='center', space_after=6)
+    for _ in range(3):
+        para(doc, '', space_after=0)
+    para(doc, f'版本 {VERSION}(与系统版本一致)', size=14, align='center', space_after=6)
+    para(doc, f'编制:观澜项目组  日期:2026-09-22', size=12, align='center', space_after=6)
+    para(doc, '本文件由 scripts/delivery_docs_build.py 依 docs/src 下的 markdown 源件生成(可重跑、可 diff)',
+         size=9, align='center', space_after=0)
+    doc.add_page_break()
+
+
+def toc(doc):
+    para(doc, '目录', size=SZ['h1'], ea=HEI, bold=True, align='center', space_after=12)
+    p = doc.add_paragraph()
+    p.paragraph_format.line_spacing = 1.5
+    add_field(p, r'TOC \o "1-3" \h \z \u',
+              '(目录域:在 Word 中按 Ctrl+A 后 F9,或右键“更新域”即可生成带页码的目录)')
+    doc.add_page_break()
+
+
+def render_blocks(doc, blocks, fig_root: pathlib.Path):
+    tno = 0
+    for kind, payload in blocks:
+        if kind.startswith('h'):
+            lvl = int(kind[1])
+            style = {1: 'Heading 1', 2: 'Heading 2', 3: 'Heading 3', 4: 'Heading 4'}[lvl]
+            p = doc.add_paragraph(style=style)
+            size = {1: SZ['h1'], 2: SZ['h2'], 3: SZ['h3'], 4: SZ['h3']}[lvl]
+            for chunk, _b, _m in split_bold(payload):
+                set_run(p.add_run(chunk), size, ea=HEI, bold=True)
+            continue
+        if kind == 'p':
+            para(doc, payload, align='justify', indent_chars=2)
+            continue
+        if kind in ('ul', 'ol'):
+            for n, item in enumerate(payload, 1):
+                mark = '· ' if kind == 'ul' else f'({n})'
+                para(doc, mark + item, align='justify', hanging=True, space_after=3)
+            continue
+        if kind == 'quote':
+            p = para(doc, payload, align='justify', space_before=6, space_after=6)
+            p.paragraph_format.left_indent = Cm(0.74)
+            continue
+        if kind == 'table':
+            cap, rows = payload
+            tno += 1
+            para(doc, cap or f'表 {tno}', size=SZ['caption'], ea=HEI, bold=True, align='center',
+                 space_before=8, space_after=4)
+            add_table(doc, rows)
+            para(doc, '', space_after=2)
+            continue
+        if kind == 'img':
+            cap, rel = payload
+            # md 里的图路径是相对 `docs/` 的(约定 `figures/xxx.png`);容错:只给文件名也认。
+            cand = fig_root / rel if (fig_root / rel).exists() else fig_root / 'figures' / pathlib.Path(rel).name
+            f = cand.resolve()
+            if f.exists():
+                p = doc.add_paragraph()
+                p.alignment = WD_ALIGN_PARAGRAPH.CENTER
+                p.paragraph_format.space_before = Pt(8)
+                p.paragraph_format.space_after = Pt(4)
+                p.paragraph_format.first_line_indent = Pt(0)
+                p.add_run().add_picture(str(f), width=Cm(15.0) if f.stat().st_size > 90_000 else Cm(13.0))
+            else:
+                WARN.append(f'图缺件: {rel}')
+                para(doc, f'(图缺失:{rel})', size=SZ['caption'], align='center', space_after=4)
+            para(doc, cap or rel, size=SZ['caption'], ea=HEI, bold=True, align='center', space_after=8)
+            continue
+    return tno
+
+
+def header_footer(doc, title: str):
+    sec = doc.sections[0]
+    hp = sec.header.paragraphs[0]
+    hp.alignment = WD_ALIGN_PARAGRAPH.CENTER
+    set_run(hp.add_run(f'{title} · 观澜·如东样板 v2 · 版本 {VERSION}'), SZ['header'])
+    fp = sec.footer.paragraphs[0]
+    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
+    set_run(fp.add_run('第 '), SZ['header'])
+    add_field(fp, 'PAGE', '1')
+    set_run(fp.add_run(' 页 / 共 '), SZ['header'])
+    add_field(fp, 'NUMPAGES', '1')
+    set_run(fp.add_run(' 页'), SZ['header'])
+
+
+def appendix_spec(doc):
+    para(doc, '附:排版规范(字体字号分类)', size=SZ['h1'], ea=HEI, bold=True, align='left',
+         space_before=12, space_after=8)
+    para(doc, '本文件与同批交付的另外两份文档由同一个渲染器生成,字体字号分类完全一致;'
+              '中文用黑体(标题)/宋体(正文),西文与数字用 Times New Roman。', align='justify',
+         indent_chars=2)
+    add_table(doc, [['元素', '字体', '字号', '对齐 / 缩进']] + [list(r) for r in SPEC_ROWS])
+
+
+def build(key: str) -> int:
+    spec = DOCS[key]
+    src = ROOT / spec['src'].format(v=VERSION)
+    out = ROOT / spec['out'].format(v=VERSION)
+    if not src.exists():
+        WARN.append(f'源件缺件: {src.relative_to(ROOT).as_posix()}')
+        print(f'✗ {key}: 源件不在位 {src}')
+        return 5
+    blocks = parse_md(src.read_text(encoding='utf-8'))
+    doc = Document()
+    style_setup(doc)
+    cover(doc, spec['title'], spec['subtitle'])
+    toc(doc)
+    ntab = render_blocks(doc, blocks, ROOT / 'docs')
+    appendix_spec(doc)
+    header_footer(doc, spec['title'])
+    out.parent.mkdir(parents=True, exist_ok=True)
+    doc.save(out)
+    st = count_md(src.read_text(encoding='utf-8'))
+    print('%s → %s(%d 章 · %d 节 · %d 表 · %d 图 · 约 %d 汉字 · %.0f KB)'
+          % (key, out.relative_to(ROOT).as_posix(), st['章'], st['节'], max(ntab, st['表']), st['图'],
+             st['汉字'], out.stat().st_size / 1024))
+    return 0
+
+
+def verify_docx(key: str) -> int:
+    """回读渲染结果自检(交付前必跑):计数 + 残留 markdown 标记 + 域 + 字体。"""
+    spec = DOCS[key]
+    out = ROOT / spec['out'].format(v=VERSION)
+    if not out.is_file():
+        print('✗ %-4s 不在位 %s' % (key, out.relative_to(ROOT).as_posix()))
+        return 5
+    doc = Document(str(out))
+    texts = [p.text for p in doc.paragraphs]
+    cells = [c.text for t in doc.tables for r in t.rows for c in r.cells]
+    anti = chr(96)
+    # ★行内代码里的字面量不算残留(实逮: `` `pitch/**` `` 是 glob 通配路径,Word 里就该原样显示)。
+    mono, plain = [], []
+    for p in doc.paragraphs:
+        for r in p.runs:
+            (mono if (r.font.name or '').startswith('Consolas') else plain).append(r.text)
+    for t in doc.tables:
+        for row in t.rows:
+            for c in row.cells:
+                for p in c.paragraphs:
+                    for r in p.runs:
+                        (mono if (r.font.name or '').startswith('Consolas') else plain).append(r.text)
+    allt = '\n'.join(texts + cells)
+    body = '\n'.join(plain)
+    heads = [p.text for p in doc.paragraphs if p.style.name.startswith('Heading')]
+    residual = dict(反引号=body.count(anti), 星号=body.count('**'), 管道=body.count(' | '),
+                    代码内字面星号='\n'.join(mono).count('**'))
+    foot = doc.sections[0].footer.paragraphs[0]._p.xml
+    head = doc.sections[0].header.paragraphs[0].text
+    ok_field = ('TOC' in doc.element.xml) and ('PAGE' in foot) and ('NUMPAGES' in foot)
+    # 只把"正文里残留的 markdown 标记"判为不合格;代码内的字面星号是内容本身(glob 通配),只报数。
+    bad = [k for k, v in residual.items() if v and k != '代码内字面星号']
+    print('%-4s %-38s 段落 %3d · 表 %2d · 图 %2d · 标题 %2d · 页眉 %r'
+          % (key, out.name, len(doc.paragraphs), len(doc.tables), len(doc.inline_shapes),
+             len(heads), head[:28]))
+    print('      域(目录/页码): %s · 残留标记: %s' % ('OK' if ok_field else '缺', bad or '无'))
+    return 0 if ok_field and not bad else 5
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--only', choices=sorted(DOCS))
+    ap.add_argument('--check', action='store_true', help='只体检源件与图,不渲染')
+    ap.add_argument('--verify', action='store_true', help='渲染后回读自检(计数/残留标记/域)')
+    ap.add_argument('--spec', action='store_true', help='打印排版规范表')
+    a = ap.parse_args()
+    if a.spec:
+        for row in SPEC_ROWS:
+            print(' | '.join(row))
+        return 0
+    rc = 0
+    if a.verify:
+        for k in ([a.only] if a.only else sorted(DOCS)):
+            rc = max(rc, verify_docx(k))
+        return rc
+    if a.check:
+        for k, spec in DOCS.items():
+            src = ROOT / spec['src'].format(v=VERSION)
+            if not src.exists():
+                print('✗ %-4s 源件不在位 %s' % (k, src.relative_to(ROOT).as_posix()))
+                rc = 5
+                continue
+            st = count_md(src.read_text(encoding='utf-8'))
+            miss = []
+            for _cap, rel in re.findall(r'!\[([^\]]*)\]\(([^)]+)\)', src.read_text(encoding='utf-8')):
+                f = ROOT / 'docs' / rel.split('figures/')[-1]
+                if not (ROOT / 'docs' / 'figures' / rel.split('figures/')[-1]).exists():
+                    miss.append(rel)
+            print('%-4s 章 %2d · 节 %2d · 小节 %2d · 表 %2d · 图 %2d · 约 %5d 汉字%s'
+                  % (k, st['章'], st['节'], st['小节'], st['表'], st['图'], st['汉字'],
+                     '' if not miss else '  ✗ 缺图 %d: %s' % (len(miss), ', '.join(miss))))
+            if miss:
+                rc = 5
+        return rc
+    for k in ([a.only] if a.only else sorted(DOCS)):
+        rc = max(rc, build(k))
+    if WARN:
+        print('\n告警:')
+        for w in sorted(set(WARN)):
+            print(' -', w)
+    return rc
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())

+ 666 - 0
scripts/delivery_docs_figures.py

@@ -0,0 +1,666 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+r"""交付文档图表构建器(需求分析 / 设计说明 / 数据要求说明 三份 docx 的插图)。
+
+## 为什么单开一个器
+三份交付文档要求"文表图并茂"。图里出现的每个数字都必须能追到仓库里的真件,所以图**不手画**:
+本器从 `src/version.py` 的 HISTORY、`configs/portal_pages.yaml`、`configs/serve.json`、
+`data/raw/<场>/**`、`outputs/<场>/**` 与 `guanlan.py check` / `rebuild_all.py --dry-run` 的**实跑输出**
+里取数,落到 `docs/figures/*.png`。纯示意图(架构/链路/时序)也在图注里写明"依据"。
+
+## 落点与口径
+    docs/figures/fig-req-01..04 / fig-des-01..06 / fig-dat-01..04 .png
+    docs/src/_guanlan_check.txt        (验收门实跑原文,图的取证底稿)
+    docs/src/_rebuild_plan.txt         (重算链 dry-run 原文,图的取证底稿)
+
+## 用法
+    python scripts/delivery_docs_figures.py            # 全画
+    python scripts/delivery_docs_figures.py --only req # 只画某一组 (req/des/dat)
+    python scripts/delivery_docs_figures.py --check    # 只报缺哪些图
+
+退出码: 0 全部生成 · 5 有图没生成(源件缺或绘图失败,逐条打印原因)
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import pathlib
+import re
+import subprocess
+import sys
+import time
+
+ROOT = pathlib.Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+
+import matplotlib                                                          # noqa: E402
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt                                            # noqa: E402
+from matplotlib.patches import FancyArrowPatch, FancyBboxPatch             # noqa: E402
+
+from src import paths as P                                                 # noqa: E402
+
+FIG_DIR = ROOT / 'docs' / 'figures'
+SRC_DIR = ROOT / 'docs' / 'src'
+CJK = 'Microsoft YaHei'
+plt.rcParams['font.sans-serif'] = [CJK, 'SimHei']
+plt.rcParams['axes.unicode_minus'] = False
+plt.rcParams['figure.dpi'] = 200
+
+WARN: list[str] = []
+VERSION = __import__('importlib').import_module('src.version').VERSION
+
+
+# ── 通用绘图元件 ────────────────────────────────────────────────────────────────
+def canvas(w: float, h: float):
+    fig, ax = plt.subplots(figsize=(w, h))
+    ax.set_xlim(0, 100)
+    ax.set_ylim(0, 100)
+    ax.axis('off')
+    return fig, ax
+
+
+def box(ax, x, y, w, h, text, fc='#eef3fb', ec='#3b6ea5', fs=10, bold=False, align='center'):
+    ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle='round,pad=0.6,rounding_size=1.4',
+                                linewidth=1.2, edgecolor=ec, facecolor=fc))
+    ax.text(x + w / 2, y + h / 2, text, ha='center', va='center', fontsize=fs,
+            wrap=True, fontweight='bold' if bold else 'normal', linespacing=1.5)
+
+
+def arrow(ax, p, q, color='#555555', style='-|>', lw=1.3, rad=0.0):
+    ax.add_patch(FancyArrowPatch(p, q, arrowstyle=style, mutation_scale=12,
+                                 linewidth=lw, color=color,
+                                 connectionstyle=f'arc3,rad={rad}'))
+
+
+def save(fig, name: str):
+    """落盘前统一做一次**字形兜底**:把 YaHei 缺字的符号换成等价可显字符。
+
+    实逮(2026-09-22): 图里引用的原文(如 `configs/portal_pages.yaml` 的说明)含 `⇒`(U+21D2),
+    matplotlib 报 `Glyph 8658 missing from font(s) Microsoft YaHei` 并画成方框 —— 交付文档里不能有方框。
+    """
+    swap = {'⇒': '→', '⇐': '←', '↔': '↔', '⇔': '↔', '✓': '√', '✗': '×', '≥': '≥'}
+    import matplotlib.text as _mt
+    for t in fig.findobj(_mt.Text):
+        s = t.get_text()
+        if any(k in s for k in swap):
+            for k, v in swap.items():
+                s = s.replace(k, v)
+            t.set_text(s)
+    FIG_DIR.mkdir(parents=True, exist_ok=True)
+    out = FIG_DIR / name
+    fig.tight_layout()
+    fig.savefig(out, bbox_inches='tight', facecolor='white')
+    plt.close(fig)
+    print('  ✓', out.relative_to(ROOT).as_posix(), '%.0f KB' % (out.stat().st_size / 1024))
+
+
+def barh(ax, labels, values, color='#3b6ea5', fmt='{:.0f}', xlabel=''):
+    y = range(len(labels))
+    ax.barh(list(y), values, color=color, height=0.62)
+    ax.set_yticks(list(y))
+    ax.set_yticklabels(labels, fontsize=9)
+    ax.invert_yaxis()
+    for i, v in enumerate(values):
+        ax.text(v, i, ' ' + fmt.format(v), va='center', fontsize=8.5)
+    ax.set_xlabel(xlabel, fontsize=9)
+    ax.tick_params(axis='x', labelsize=8.5)
+    for s in ('top', 'right'):
+        ax.spines[s].set_visible(False)
+
+
+# ── 取数(全部真读,不猜)────────────────────────────────────────────────────────
+def version_history():
+    from src import version as V
+    return list(V.HISTORY), V.VERSION
+
+
+def portal_pages():
+    import yaml
+    d = yaml.safe_load((P.config('portal_pages.yaml')).read_text(encoding='utf-8'))
+    pages = d.get('pages') or d.get('entries') or []
+    if isinstance(d, dict) and not pages:
+        for k, v in d.items():
+            if isinstance(v, list) and v and isinstance(v[0], dict):
+                pages = v
+                break
+    return pages
+
+
+def serve_cfg():
+    return json.loads((P.config('serve.json')).read_text(encoding='utf-8'))
+
+
+def raw_station_stats():
+    """→ [(族, 件数, GB)],实测 data/raw/<场站>/ 下一层目录。
+
+    场站目录名以**场配置**为准(`windscada.config.farm()['raw_station']`),`P.station_dir()` 只是兜底
+    (它按场配置**名**拼,实测本场是 `data/raw/如东` 而非 `data/raw/rudong`)。
+    """
+    from src.windscada.config import farm as _farm
+    home = P.RAW_ROOT / str(_farm().get('raw_station') or '如东')
+    out = []
+    if not home.exists():
+        WARN.append(f'data/raw 场站目录不在位: {home}')
+        return out
+    for d in sorted(p for p in home.iterdir() if p.is_dir()):
+        fs = [p for p in d.rglob('*') if p.is_file()]
+        out.append((d.name, len(fs), sum(p.stat().st_size for p in fs) / 1e9))
+    return out
+
+
+def md_tables(path: pathlib.Path):
+    """→ [ [ [cell,...], ... ] ]:把 markdown 里的表逐个解析成二维列表(不含表头分隔行)。"""
+    if not path.exists():
+        WARN.append('源件不在位: %s' % path.relative_to(ROOT).as_posix())
+        return []
+    lines = path.read_text(encoding='utf-8').splitlines()
+    tables, i = [], 0
+    while i < len(lines):
+        if lines[i].startswith('|') and i + 1 < len(lines) and re.match(r'^\|[\s:\-|]+\|$', lines[i + 1].strip()):
+            rows = []
+            j = i + 2
+            while j < len(lines) and lines[j].strip().startswith('|'):
+                rows.append([c.strip() for c in lines[j].strip().strip('|').split('|')])
+                j += 1
+            tables.append(rows)
+            i = j
+        else:
+            i += 1
+    return tables
+
+
+def md_find_table(path: pathlib.Path, must_cols, first_cell=None):
+    """找**表头含指定列**(列名在本器里以首行单元格匹配)的表。"""
+    lines = path.read_text(encoding='utf-8').splitlines() if path.exists() else []
+    for i, l in enumerate(lines):
+        if not l.startswith('|') or not all(c in l for c in must_cols):
+            continue
+        rows, j = [], i + 2
+        while j < len(lines) and lines[j].strip().startswith('|'):
+            rows.append([c.strip() for c in lines[j].strip().strip('|').split('|')])
+            j += 1
+        if not rows:
+            continue
+        if first_cell is not None and rows[0][0] != first_cell:
+            continue
+        return rows
+    return []
+
+
+def ensure_evidence(refresh=False):
+    """把两张取证底稿跑出来(已存在且未 --refresh 时复用)。"""
+    SRC_DIR.mkdir(parents=True, exist_ok=True)
+    py = str(ROOT / '.venv' / 'Scripts' / 'python.exe')
+    chk, plan = SRC_DIR / '_guanlan_check.txt', SRC_DIR / '_rebuild_plan.txt'
+    if refresh or not chk.exists():
+        t0 = time.time()
+        r = subprocess.run([py, '-X', 'utf8', 'guanlan.py', 'check'], cwd=str(ROOT),
+                           capture_output=True, timeout=1800)
+        chk.write_text((r.stdout or b'').decode('utf-8', 'replace'), encoding='utf-8')
+        print('  · guanlan.py check 底稿写入 (%.0fs, rc=%d)' % (time.time() - t0, r.returncode))
+    if refresh or not plan.exists():
+        r = subprocess.run([py, '-X', 'utf8', 'scripts/rebuild_all.py', '--dry-run', '--skip-scada'],
+                           cwd=str(ROOT), capture_output=True, timeout=600)
+        plan.write_text((r.stdout or b'').decode('utf-8', 'replace'), encoding='utf-8')
+        print('  · rebuild_all --dry-run 底稿写入')
+
+
+# ── 需求分析组 ─────────────────────────────────────────────────────────────────
+def fig_req_01():
+    hist, ver = version_history()
+    fig, ax = plt.subplots(figsize=(9.2, 0.62 * len(hist) + 1.4))
+    ax.axis('off')
+    n = len(hist)
+    ax.set_ylim(-0.6, n - 0.4)
+    ax.set_xlim(0, 10)
+    ax.plot([0.7, 0.7], [0, n - 1], color='#3b6ea5', lw=2.2, zorder=1)
+    lvl_cn = {'major': '大', 'minor': '中', 'patch': '小', 'legacy': '旧编号'}
+    for i, h in enumerate(hist):
+        y = n - 1 - i
+        ax.scatter([0.7], [y], s=90, color='#3b6ea5', zorder=3)
+        title = h.get('title', '')
+        title = re.sub(r'(.*?)', '', title)
+        title = title[:46] + ('…' if len(title) > 46 else '')
+        ax.text(1.05, y + 0.18, f"v{h.get('version')}  {h.get('date')}  [{lvl_cn.get(h.get('level'), '')}版本]",
+                fontsize=9.5, fontweight='bold', va='center')
+        ax.text(1.05, y - 0.20, title, fontsize=9, color='#333333', va='center')
+    ax.text(0.7, n - 0.15, '需求演进(自下而上)', fontsize=10, fontweight='bold', ha='center')
+    ax.set_title(f'观澜·如东样板 v2 需求演进时间轴(当前 v{ver};依据 src/version.py 的 HISTORY,'
+                 f'共 {n} 条)', fontsize=11)
+    save(fig, 'fig-req-01-需求演进时间轴.png')
+
+
+def fig_req_02():
+    fig, ax = canvas(9.4, 6.2)
+    roles = [('值班运行', 88, '今日态势/部件问题/该问题页/报警与故障统计'),
+             ('点检与检修', 65, '机组页/振动与融合面/油样与工单/报告导出'),
+             ('场站管理', 42, '发电性能/可靠性(MTBF·MTBO·MDT)/可用率/限电'),
+             ('研发与交付', 19, '本体与机制链/数据重算/运行体检/版本与打包')]
+    for name, y, pages in roles:
+        box(ax, 3, y, 20, 16, name, fc='#e8f1e4', ec='#5b8c4a', fs=11, bold=True)
+        box(ax, 30, y, 66, 16, pages, fc='#eef3fb', ec='#3b6ea5', fs=10)
+        arrow(ax, (23.5, y + 8), (29.4, y + 8))
+    ax.text(50, 94, '角色 — 场景 — 页面关系(依据 docs/说明书_观澜如东样板v2_v0.2.md 与 configs/portal_pages.yaml)',
+            ha='center', fontsize=11, fontweight='bold')
+    save(fig, 'fig-req-02-角色场景页面.png')
+
+
+def fig_req_03():
+    pages = portal_pages()
+    if not pages:
+        WARN.append('portal_pages.yaml 未解析出页面条目')
+        return
+    from collections import Counter
+    kinds = Counter(str(p.get('kind', '未标')) for p in pages)
+    order = ['data-derived', 'source-derived', 'frozen-delivery', 'static', 'shipped']
+    ks = [k for k in order if k in kinds] + [k for k in kinds if k not in order]
+    vals = [kinds[k] for k in ks]
+    fig, ax = plt.subplots(figsize=(8.6, 0.62 * len(ks) + 2.0))
+    cn = {'data-derived': 'data-derived(随数据变,须重算)',
+          'source-derived': 'source-derived(随源代码变)',
+          'frozen-delivery': 'frozen-delivery(冻结交付件)',
+          'static': 'static(静态)', 'shipped': 'shipped(随包件)'}
+    barh(ax, [cn.get(k, k) for k in ks], vals, xlabel='页面/子页数(共 %d 项)' % len(pages))
+    ax.set_title('门户页面归口分布(依据 configs/portal_pages.yaml)', fontsize=11)
+    save(fig, 'fig-req-03-页面归口分布.png')
+
+
+def fig_req_04():
+    """验收门分类 —— **与《需求分析》§9.1 表同源**(直接解析那张表,避免图文数字打架)。"""
+    rows = md_find_table(SRC_DIR / f'需求分析_观澜_{VERSION}.md', ('分类', '判定条数'))
+    if not rows:
+        WARN.append('需求分析 §9.1 的分类表未解析到,图 fig-req-04 用实跑输出兜底')
+        ensure_evidence()
+        txt = (SRC_DIR / '_guanlan_check.txt').read_text(encoding='utf-8', errors='replace')
+        marks = re.findall(r'^\s*\[(OK|FAIL|--|!|i|\?)\]\s*([^—\n]+)', txt, re.M)
+        cats = {'通过': sum(1 for m, _ in marks if m == 'OK'),
+                '需处置': sum(1 for m, _ in marks if m == 'FAIL'),
+                '跳过或提示': sum(1 for m, _ in marks if m not in ('OK', 'FAIL'))}
+        ks, vals, note = list(cats), list(cats.values()), '依据 guanlan.py check 实跑输出'
+    else:
+        ks, vals, note = [], [], '依据《需求分析》§9.1 表(同源于 guanlan.py check 实跑输出)'
+        for r in rows:
+            if r[0] == '合计':
+                break
+            ks.append('%s(%s)' % (r[0], (r[2][:22] + '…') if len(r) > 2 and len(r[2]) > 22 else (r[2] if len(r) > 2 else '')))
+            vals.append(int(re.sub(r'[^0-9]', '', r[1]) or 0))
+    fig, ax = plt.subplots(figsize=(9.6, 0.62 * len(ks) + 1.9))
+    barh(ax, ks, vals, xlabel='判定条数(合计 %d 条)' % sum(vals))
+    ax.set_title('验收门分类与数量(%s)' % note, fontsize=11)
+    save(fig, 'fig-req-04-验收门分类.png')
+
+
+# ── 设计说明组 ─────────────────────────────────────────────────────────────────
+def fig_des_01():
+    fig, ax = canvas(9.6, 7.4)
+    ax.text(50, 96, '观澜·如东样板 v2 总体架构(离线单包,无外网依赖)', ha='center',
+            fontsize=11.5, fontweight='bold')
+    box(ax, 4, 84, 92, 9, '使用侧:浏览器(门户 / 分析总览 /detail/v2 / 本体网 / 运维控制台 /ops)',
+        fc='#f6f2e8', ec='#b08a3e', fs=10)
+    box(ax, 24, 71, 52, 9, '接入层:组件网关 28084(前缀改写 + data-abs 豁免 + 静态件)',
+        fc='#eef3fb', ec='#3b6ea5', fs=10)
+    box(ax, 3, 56, 18, 11, '分析组件\n18033 /detail', fs=9)
+    box(ax, 23, 56, 17, 11, '振动组件\n18020 /cms', fs=9)
+    box(ax, 42, 56, 16, 11, '仿真台\n18791·18792', fs=9)
+    box(ax, 60, 56, 15, 11, '三维 viewer\n64292', fs=9)
+    box(ax, 77, 56, 20, 11, '本机模型档\nOllama 11434\n(可选,未装即降级)', fs=9,
+        fc='#f4eef6', ec='#7d5ba6')
+    for x in (12, 31.5, 50, 67.5, 87):
+        arrow(ax, (50, 71), (x, 67.4), color='#8a8a8a')
+    box(ax, 3, 41, 94, 10, '产物层 outputs/<场>/:windscada 判级与曲线 · slim10min 窄仓 · 振动六层链 · '
+                           '本体对象库 · SOP/findings · guanlan 契约', fc='#e8f1e4', ec='#5b8c4a', fs=9.5)
+    arrow(ax, (50, 56), (50, 51.4))
+    box(ax, 3, 27, 94, 9, '摄入与构建层:rebuild_all.py 23 步(place_raw_data → 10min/1min 摄入 → '
+                          '10 个 SCADA 构建器 → 振动摄入 → 本体六步 → 审计)', fc='#e8f1e4', ec='#5b8c4a', fs=9.5)
+    arrow(ax, (50, 41), (50, 36.4))
+    box(ax, 3, 13, 46, 9, '输入层 data/raw/<场>/:scada_10min·scada_1min·scada_mdb\n故障报警·风机故障记录·'
+                          'windcms·m5_cms_tcm·油样报告', fc='#f6f2e8', ec='#b08a3e', fs=9)
+    box(ax, 51, 13, 46, 9, '共享资料:西门子 4.0 技术资料(本体 kb_ingest 读)\n配置 configs/:serve.json·'
+                           'models.json·portal_pages.yaml', fc='#f6f2e8', ec='#b08a3e', fs=9)
+    arrow(ax, (26, 23), (26, 26.6))
+    arrow(ax, (74, 23), (74, 26.6))
+    box(ax, 3, 2, 94, 7, '贯穿全链的守卫:单一真源(src/paths.py·src/version.py) · 审计门(guanlan.py check / '
+                         '7 个审计器) · 逐件来源台账(_provenance.json)', fc='#ffffff', ec='#555555', fs=9)
+    save(fig, 'fig-des-01-总体架构.png')
+
+
+def fig_des_02():
+    """serve.json 是**扁平**结构(`gateway`/`detail`/… 直接是端口整数),不是嵌套 dict。"""
+    cfg = serve_cfg()
+    host, pub = cfg.get('host', '?'), cfg.get('public_host', '')
+    gw_port = cfg.get('gateway', '?')
+    comps = [(k, cfg.get(k)) for k in ('detail', 'cms', 'sim', 'sim_sys', 'viewer')]
+    fig, ax = canvas(9.6, 5.2)
+    box(ax, 30, 82, 40, 12, '网关 gateway\n%s:%s(对外唯一入口)' % (pub or host, gw_port),
+        fc='#eef3fb', ec='#3b6ea5', fs=10, bold=True)
+    n = max(1, len(comps))
+    for i, (name, port) in enumerate(comps):
+        x = 3 + i * (94 / n)
+        w = 94 / n - 3
+        box(ax, x, 52, w, 14, '%s\n%s:%s' % (name, host, port), fs=9)
+        arrow(ax, (50, 82), (x + w / 2, 66.6), color='#8a8a8a')
+    box(ax, 3, 30, 94, 12, '绑定与自启:guanlan 服务(Windows 服务 / systemd)· start.bat 静默启动 · '
+                           'run/*.pid 运行态 · logs/ 统一落点', fc='#f6f2e8', ec='#b08a3e', fs=9)
+    arrow(ax, (50, 52), (50, 42.6))
+    box(ax, 3, 12, 94, 12, '端口冲突处置:netstat -ano 找占用进程 → 按端口清理 → 重启服务\n'
+                           '(service_ctl stop 不杀子进程,只按端口杀会留下孤儿实例)', fc='#ffffff',
+        ec='#555555', fs=9)
+    arrow(ax, (50, 30), (50, 24.6))
+    ax.text(50, 96, '端口与进程拓扑(依据 configs/serve.json 实读)', ha='center', fontsize=11.5,
+            fontweight='bold')
+    save(fig, 'fig-des-02-端口与进程.png')
+
+
+def fig_des_03():
+    ensure_evidence()
+    txt = (SRC_DIR / '_rebuild_plan.txt').read_text(encoding='utf-8', errors='replace')
+    steps = []
+    for line in txt.splitlines():
+        m = re.match(r'^\s{2}(\S.*?)\s{2,}(\S.*)$', line)
+        if m and not line.strip().startswith('('):
+            steps.append((m.group(1).strip(), m.group(2).strip()))
+    if not steps:
+        WARN.append('rebuild_all --dry-run 未解析出步骤')
+        return
+    fig, ax = plt.subplots(figsize=(9.6, 0.42 * len(steps) + 1.8))
+    ax.axis('off')
+    ax.set_xlim(0, 10)
+    ax.set_ylim(-0.5, len(steps) - 0.2)
+    for i, (name, cmd) in enumerate(steps):
+        y = len(steps) - 1 - i
+        ax.add_patch(plt.Rectangle((0.2, y - 0.34), 1.75, 0.68, facecolor='#eef3fb',
+                                   edgecolor='#3b6ea5', lw=0.9))
+        ax.text(1.07, y, name[:22], fontsize=8.6, ha='center', va='center')
+        ax.text(2.15, y, cmd[:92], fontsize=8.2, va='center', color='#333333', family='monospace')
+        if i < len(steps) - 1:
+            ax.annotate('', xy=(1.07, y - 0.42), xytext=(1.07, y - 0.30),
+                        arrowprops=dict(arrowstyle='-|>', color='#8a8a8a', lw=0.9))
+    ax.set_title('重算链步骤(依据 scripts/rebuild_all.py --dry-run --skip-scada,共 %d 步)' % len(steps),
+                 fontsize=11)
+    save(fig, 'fig-des-03-重算链.png')
+
+
+def fig_des_04():
+    fig, ax = canvas(9.6, 6.4)
+    ax.text(50, 96, '判级矩阵与四源融合数据流(每条判据都能指到输入列)', ha='center',
+            fontsize=11.5, fontweight='bold')
+    srcs = ['SCADA 10min\n(slim10min 48 列)', 'CMS 振动\n(六层链)', '状态·故障·告警\n(台账)', '油样与工单\n(现场记录)']
+    for i, s in enumerate(srcs):
+        box(ax, 1.5 + i * 24.6, 78, 22, 12, s, fc='#f6f2e8', ec='#b08a3e', fs=9)
+    box(ax, 6, 58, 40, 12, '七系统判级矩阵\n变桨·偏航·主轴承·齿轮箱·发电机·变流器·主控与传感网',
+        fc='#eef3fb', ec='#3b6ea5', fs=9.5)
+    box(ax, 52, 58, 42, 12, '融合面四源对齐\n振动 × SCADA × 台账 × 人工\n(逐台判级 + 证据链)',
+        fc='#eef3fb', ec='#3b6ea5', fs=9.5)
+    for x in (12, 36, 62, 86):
+        arrow(ax, (x, 78), (26 if x < 50 else 73, 70.6), color='#8a8a8a')
+    box(ax, 6, 40, 88, 11, '时间窗:判级与曲线按所选时间窗重算(月/年/自定义起止日期,含两端);'
+                           '温度与停机台账按真窗取月', fc='#e8f1e4', ec='#5b8c4a', fs=9.5)
+    arrow(ax, (26, 58), (26, 51.6))
+    arrow(ax, (73, 58), (73, 51.6))
+    outs = ['部件问题清单\n(按系统×机组)', '该问题页\n(单问题+完整依据)', '发电性能与可靠性\n(MTBF/MTBO/MDT)',
+            '控制参数一致性\n(P/ω 封顶·K 聚类)', '报告导出\n(docx/md/页面)', '本体机制链\n与决策台']
+    for i, o in enumerate(outs):
+        x = 2 + (i % 3) * 32.3
+        y = 24 if i < 3 else 8
+        box(ax, x, y, 30, 12, o, fc='#f4eef6', ec='#7d5ba6', fs=9)
+    arrow(ax, (50, 40), (50, 36.6), color='#5b8c4a')
+    box(ax, 2, 21.5, 96, 2.6, '', fc='#ffffff', ec='#ffffff')
+    ax.text(50, 36, '产物(outputs/<场>/…,逐件登记生成端与输入)', ha='center', fontsize=9, color='#444444')
+    save(fig, 'fig-des-04-判级与融合.png')
+
+
+def fig_des_05():
+    fig, ax = canvas(9.6, 5.6)
+    ax.text(50, 96, '时间窗按窗重算与 pending 契约(实测:冷窗首查 pending,约 30 s 后落位)',
+            ha='center', fontsize=11.5, fontweight='bold')
+    lanes = [('前端 app.js', 82), ('服务 windscada_serve.py', 60), ('按窗缓存 _WIN_CACHE', 38), ('重算线程(_HEAVY_LOCK 串行)', 16)]
+    for name, y in lanes:
+        box(ax, 2, y, 24, 11, name, fc='#eef3fb', ec='#3b6ea5', fs=9)
+        ax.plot([26.5, 97], [y + 5.5, y + 5.5], color='#d0d0d0', lw=0.9, zorder=0)
+    msgs = [(82, 39, 'GET /api/fleet?win=2026-07'),
+            (60, 82, 'win 未命中 → 起后台线程,先回上一份口径并标 pending=1'),
+            (38, 60, '缺 → 入队 sysmx/curves/m9'),
+            (16, 38, '算(变桨/偏航/蓄能/温度 + 曲线 + M9)'),
+            (38, 16, '算完写入缓存'),
+            (60, 38, '5 s 轮询再取 → 命中'),
+            (82, 60, 'pending=0,页面换成该时间窗口径')]
+    for y1, y2, text in msgs:
+        arrow(ax, (36, y1 + 5.5), (36, y2 + 5.5), color='#3b6ea5')
+        ax.text(38.5, (y1 + y2) / 2 + 5.5, text, fontsize=8.8, va='center')
+    ax.text(50, 5, '为什么这样:算得慢(每面 5~120 s),不能把页面卡住;宁可先给旧口径并**如实标注**,也不静默等待。',
+            ha='center', fontsize=9, color='#444444')
+    save(fig, 'fig-des-05-时间窗与pending.png')
+
+
+def fig_des_06():
+    fig, ax = canvas(9.0, 6.0)
+    cx, cy, r = 50, 50, 30
+    items = [('改代码/配置', '#eef3fb'), ('重算 rebuild_all 23 步', '#e8f1e4'),
+             ('审计器 7 件(反向/页面/配置/链路/版本…)', '#f6f2e8'),
+             ('guanlan.py check 全绿', '#f4eef6'), ('打包 pack_dist + 开箱验证', '#eef3fb'),
+             ('装到目标机 install.ps1', '#e8f1e4')]
+    import math
+    for i, (t, fc) in enumerate(items):
+        a = math.radians(90 - i * 360 / len(items))
+        x, y = cx + r * math.cos(a), cy + r * math.sin(a)
+        box(ax, x - 15, y - 6, 30, 12, t, fc=fc, ec='#3b6ea5', fs=9)
+        a2 = math.radians(90 - (i + 1) * 360 / len(items))
+        arrow(ax, (cx + (r - 6) * math.cos(a), cy + (r - 6) * math.sin(a)),
+              (cx + (r - 6) * math.cos(a2), cy + (r - 6) * math.sin(a2)),
+              color='#8a8a8a', rad=-0.25)
+    box(ax, 38, 44, 24, 12, '版本纪律\n单一真源 src/version.py', fc='#ffffff', ec='#555555', fs=9, bold=True)
+    ax.text(50, 96, '质量门与审计闭环(每环都能独立重跑、逐条给退出码)', ha='center', fontsize=11.5,
+            fontweight='bold')
+    save(fig, 'fig-des-06-质量门闭环.png')
+
+
+# ── 数据要求说明组 ─────────────────────────────────────────────────────────────
+def fig_dat_01():
+    stats = raw_station_stats()
+    if not stats:
+        return
+    stats = sorted(stats, key=lambda x: -x[1])
+    labels = ['%s\n(%d 件)' % (s[0], s[1]) for s in stats]
+    fig, axes = plt.subplots(1, 2, figsize=(10.6, 0.55 * len(stats) + 2.2))
+    barh(axes[0], labels, [s[1] for s in stats], xlabel='件数')
+    axes[0].set_title('件数', fontsize=10)
+    barh(axes[1], labels, [s[2] for s in stats], color='#b08a3e', fmt='{:.2f}', xlabel='体量 GB')
+    axes[1].set_title('体量(GB)', fontsize=10)
+    tot_n = sum(s[1] for s in stats)
+    tot_g = sum(s[2] for s in stats)
+    fig.suptitle('输入数据族件数与体量(依据实测 data/raw/如东 下一层目录;合计 %d 件 / %.1f GB)'
+                 % (tot_n, tot_g), fontsize=11)
+    save(fig, 'fig-dat-01-数据族体量.png')
+
+
+def fig_dat_02():
+    fig, ax = canvas(9.6, 5.8)
+    ax.text(50, 96, '数据 → 功能 → 产物 流向(只列主干,逐族对应见正文表)', ha='center',
+            fontsize=11.5, fontweight='bold')
+    left = ['scada_10min / 1min\nscada_mdb(归档)', '故障报警 / 风机故障记录', 'windcms / m5_cms_tcm',
+            '油样报告', '西门子 4.0 技术资料']
+    for i, t in enumerate(left):
+        box(ax, 2, 78 - i * 15, 26, 12, t, fc='#f6f2e8', ec='#b08a3e', fs=9)
+    mid = ['判级矩阵与时间窗重算', '发电性能与可靠性', '故障与停机台账', '振动六层链与融合面',
+           '本体对象库与问答']
+    for i, t in enumerate(mid):
+        box(ax, 37, 78 - i * 15, 26, 12, t, fc='#eef3fb', ec='#3b6ea5', fs=9)
+    right = ['部件问题 / 该问题页', '可靠性表(MTBF·MTBO·MDT)', '停机台账与五态', '融合面逐台判级',
+             '机制链 / 决策台 / 报告']
+    for i, t in enumerate(right):
+        box(ax, 72, 78 - i * 15, 26, 12, t, fc='#e8f1e4', ec='#5b8c4a', fs=9)
+    for i in range(5):
+        y = 84 - i * 15
+        arrow(ax, (28.4, y), (36.4, y), color='#8a8a8a')
+        arrow(ax, (63.4, y), (71.4, y), color='#8a8a8a')
+    ax.text(15, 8, '输入层', ha='center', fontsize=10, fontweight='bold')
+    ax.text(50, 8, '功能与算法层', ha='center', fontsize=10, fontweight='bold')
+    ax.text(85, 8, '产物与页面层', ha='center', fontsize=10, fontweight='bold')
+    save(fig, 'fig-dat-02-数据到产物.png')
+
+
+# 收资要求 v3 的 21 项 → 观澜侧到位状态(依据 docs/输入数据放置指导_v0.1.md 的体检结论、
+# configs/contracts/siemens_swt40_rudong.yaml 的 materials 节、以及 data/raw/如东 实测)
+REQ_STATUS = [
+    ('1', '10分钟SCADA数据', '已在位', 'scada_10min(38 件 / 14.7 GB)+ scada_mdb 归档'),
+    ('2', '秒级SCADA高频数据', '部分', 'scada_1min 38 件在位;1 s/5 s 原生高频未见交付'),
+    ('3', '测风塔数据', '未到位', 'mast: FALSE —— 测风塔目录零交付(须催缴)'),
+    ('4', '功率曲线', '部分', '合同担保曲线为图片级(须描点提取)'),
+    ('5', '机型参数', '已在位', 'contracts/siemens_swt40_rudong.yaml meta + 理论功率'),
+    ('6', '机位分布数据', '已在位', '风机坐标.xlsx(38 台 + 升压站)'),
+    ('7', '状态、事件、故障、告警', '已在位', '故障报警 + 风机故障记录(代码表 2793 条)'),
+    ('8', '机舱CMS振动数据', '已在位', 'windcms 25,693 件 / 153.8 GB + 厂家报告'),
+    ('9', '机组统计数据/运行数据', '已在位', 'SCADA 统计 + sum 层(DailySummary)'),
+    ('10', '保护定值及控制参数', '部分', '控制参数由 M9 反演;定值表未成册交付'),
+    ('11', '机组故障录波数据', '未到位', 'fastlog 仅 4 台、10 Hz 事件快照'),
+    ('12', '可研报告及微观选址', '未到位', '目录未见交付(可研另有史料塔记录)'),
+    ('13', '风电场简介', '部分', '场站元信息散见合同与技术资料'),
+    ('14', '风电场坐标、机位布置图', '已在位', '坐标件在位;布置图含于技术资料'),
+    ('15', '风机技术协议及合同', '已在位', '西门子 4.0 技术资料 + 担保曲线附件'),
+    ('16', '并网至今生产报表', '部分', '日粒度发电账 555 天 + 全寿命月账(2016-06 起)'),
+    ('17', '周围已建风电场生产报表', '未到位', '未见交付(尾流先验缺外部对照)'),
+    ('18', '运维记录', '已在位', '检修记录 2025+2026 / 油样 2025'),
+    ('19', '大部件更换记录', '部分', '出保备件台账在位;更换台账不完整'),
+    ('20', '图纸、技术文件', '已在位', '西门子 4.0 技术资料(电气/液压/逻辑)'),
+    ('21', '设备参数', '部分', '主轴/齿轮箱/发电机型号与齿数部分在位,明细待补'),
+]
+
+
+def fig_dat_03():
+    """收资 21 项到位状态 —— **与《数据要求说明》§8.1 表同源**(解析那张表,图文不打架)。"""
+    from collections import Counter
+    rows = md_find_table(SRC_DIR / f'数据要求说明_观澜_{VERSION}.md', ('序号', '状态'), first_cell='1')
+    items = []
+    if rows and len(rows) >= 21:
+        for r in rows[:21]:
+            no, name, status = r[0], r[1], r[3] if len(r) > 3 else ''
+            if '已在位' in status:
+                st = '已在位'
+            elif '部分' in status:
+                st = '部分'
+            elif '未到位' in status:
+                st = '未到位'
+            else:
+                st = '未标'
+            note = r[5] if len(r) > 5 else ''
+            qual = '(依契约记载)' if '记载' in status else ''
+            items.append((no, name, st, qual + note[:34]))
+    else:
+        WARN.append('数据要求说明 §8.1 的 21 项表未解析到,图 fig-dat-03 用内置判定兜底')
+        items = REQ_STATUS
+    cnt = Counter(r[2] for r in items)
+    order = ['已在位', '部分', '未到位']
+    colors = {'已在位': '#5b8c4a', '部分': '#d9a441', '未到位': '#c0504d', '未标': '#b8b8b8'}
+    fig, axes = plt.subplots(1, 2, figsize=(11.0, 5.8), gridspec_kw={'width_ratios': [1, 1.6]})
+    a = axes[0]
+    vals = [cnt.get(k, 0) for k in order]
+    a.bar(order, vals, color=[colors[k] for k in order], width=0.55)
+    for i, v in enumerate(vals):
+        a.text(i, v, ' %d 项' % v, ha='center', va='bottom', fontsize=10)
+    a.set_ylim(0, max(vals) + 2)
+    a.set_title('21 类收资项到位状态(合计 %d 项)' % len(items), fontsize=10.5)
+    a.set_ylabel('项数', fontsize=9)
+    for s in ('top', 'right'):
+        a.spines[s].set_visible(False)
+    b = axes[1]
+    b.axis('off')
+    b.set_xlim(0, 10)
+    b.set_ylim(0, len(items))
+    for i, (no, name, st, note) in enumerate(items):
+        y = len(items) - 1 - i
+        b.add_patch(plt.Rectangle((0, y - 0.4), 0.9, 0.8, facecolor='#f0f0f0', edgecolor='none'))
+        b.text(0.45, y, no, ha='center', va='center', fontsize=8)
+        b.add_patch(plt.Rectangle((0.95, y - 0.4), 0.38, 0.8, facecolor=colors.get(st, '#b8b8b8'),
+                                  edgecolor='none'))
+        b.text(1.45, y, name[:16], fontsize=8.4, va='center')
+        b.text(5.3, y, note[:36], fontsize=7.6, va='center', color='#444444')
+    b.set_title('逐项判定(状态与缺口取自《数据要求说明》§8.1 表)', fontsize=10.5)
+    fig.suptitle('收资要求 v3 的 21 类数据在观澜的到位状态', fontsize=11.5)
+    save(fig, 'fig-dat-03-收资项覆盖.png')
+
+
+def fig_dat_04():
+    import pandas as pd
+    store = P.store('rudong')
+    out = []
+    for fn, label in (('temp_monthly.parquet', '温度月轨迹 temp_monthly'),
+                      ('alarms.parquet', '报警台账 alarms')):
+        f = store / fn
+        if not f.exists():
+            WARN.append('缺产物 %s' % f.relative_to(ROOT).as_posix())
+            continue
+        df = pd.read_parquet(f, columns=['month'] if fn.startswith('temp') else None)
+        col = 'month' if 'month' in df.columns else df.columns[0]
+        g = df.groupby(df[col].astype(str)).size()
+        out.append((label, g))
+    if not out:
+        return
+    months = sorted({m for _, g in out for m in g.index if re.match(r'^\d{4}-\d{2}', str(m))})
+    fig, ax = plt.subplots(figsize=(11.0, 4.6))
+    w = 0.4
+    import numpy as np
+    x = np.arange(len(months))
+    for i, (label, g) in enumerate(out):
+        vals = [int(g.get(m, 0)) for m in months]
+        ax.bar(x + (i - 0.5) * w, vals, width=w, label=label)
+    ax.set_xticks(x)
+    ax.set_xticklabels([m[2:] for m in months], rotation=90, fontsize=7.5)
+    ax.set_ylabel('行数 / 事件数', fontsize=9)
+    ax.legend(fontsize=9)
+    ax.set_title('关键数据的时间覆盖(依据 outputs/rudong/windscada 的 temp_monthly 与 alarms 实读)',
+                 fontsize=11)
+    for s in ('top', 'right'):
+        ax.spines[s].set_visible(False)
+    save(fig, 'fig-dat-04-时间覆盖.png')
+
+
+GROUPS = {
+    'req': [fig_req_01, fig_req_02, fig_req_03, fig_req_04],
+    'des': [fig_des_01, fig_des_02, fig_des_03, fig_des_04, fig_des_05, fig_des_06],
+    'dat': [fig_dat_01, fig_dat_02, fig_dat_03, fig_dat_04],
+}
+EXPECTED = ['fig-req-01-需求演进时间轴.png', 'fig-req-02-角色场景页面.png',
+            'fig-req-03-页面归口分布.png', 'fig-req-04-验收门分类.png',
+            'fig-des-01-总体架构.png', 'fig-des-02-端口与进程.png', 'fig-des-03-重算链.png',
+            'fig-des-04-判级与融合.png', 'fig-des-05-时间窗与pending.png', 'fig-des-06-质量门闭环.png',
+            'fig-dat-01-数据族体量.png', 'fig-dat-02-数据到产物.png', 'fig-dat-03-收资项覆盖.png',
+            'fig-dat-04-时间覆盖.png']
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--only', default=None, choices=sorted(GROUPS))
+    ap.add_argument('--refresh', action='store_true',
+                    help='重跑取证底稿(guanlan.py check / rebuild_all --dry-run)后再画图')
+    ap.add_argument('--check', action='store_true', help='只报缺哪些图')
+    a = ap.parse_args()
+    if a.refresh:
+        for p in (SRC_DIR / '_guanlan_check.txt', SRC_DIR / '_rebuild_plan.txt'):
+            p.unlink(missing_ok=True)
+    if a.check:
+        miss = [f for f in EXPECTED if not (FIG_DIR / f).exists()]
+        print('图表体检: %d/%d 在位' % (len(EXPECTED) - len(miss), len(EXPECTED)))
+        for f in miss:
+            print('  ✗', f)
+        return 0 if not miss else 5
+    groups = [a.only] if a.only else sorted(GROUPS)
+    for g in groups:
+        print('── %s 组 ──' % g)
+        for fn in GROUPS[g]:
+            try:
+                fn()
+            except Exception as e:  # noqa: BLE001
+                WARN.append('%s 失败: %s' % (fn.__name__, e))
+                print('  ✗', fn.__name__, e)
+    miss = [f for f in EXPECTED if not (FIG_DIR / f).exists()]
+    if WARN:
+        print('\n告警/缺件:')
+        for w in WARN:
+            print(' -', w)
+    print('\n图表: %d/%d 在位' % (len(EXPECTED) - len(miss), len(EXPECTED)))
+    return 0 if not miss else 5
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())

+ 6 - 2
scripts/raw_scan.py

@@ -228,13 +228,17 @@ def scan(raw_root: pathlib.Path, deep: bool = False, station_dir: pathlib.Path |
     fam[TECH_DIR] = fp
     known = {n for n, *_ in FAMILIES} | {TECH_DIR}
     unclassified = []
-    known_dirs = {str(station / n) for n, *_ in FAMILIES} | {str(raw_root / TECH_DIR)}
+    # ★2026-09-22 可移植性消缺(check_portability --runtime 报 ERROR): 原先用
+    #   `str(p).startswith(k + os.sep)` 拼系统分隔符判"在不在已知族目录下" —— 一是 os.sep 让代码
+    #   带上平台特征(换机部署核查报 ERROR),二是前缀匹配会把 `scada_10min_extra/` 误认成
+    #   `scada_10min/` 的子件。改成 pathlib 的祖先判定,语义更准且与平台无关。
+    known_dirs = {station / n for n, *_ in FAMILIES} | {raw_root / TECH_DIR}
     # ★未归类 = 任何**不在已知族目录里**的文件 (递归找, 不只看一层): raw 根下、场站目录下、以及
     #   场站目录里嵌套的新子目录(例如新来了 `CMS_202701` 这种整包目录) 都跑不掉。
     for p in (sorted(raw_root.rglob('*')) if raw_root.is_dir() else []):
         if not p.is_file():
             continue
-        if any(str(p).startswith(k + os.sep) for k in known_dirs):
+        if any(p == k or k in p.parents for k in known_dirs):
             continue
         if p.name in IGNORE_FILES:
             continue                       # 随包的放置说明件: 不是数据, 不报"未归类"

+ 24 - 1
src/version.py

@@ -19,7 +19,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.9.2'          # ★ 改版本只改这里
+VERSION = '2.10.0'         # ★ 改版本只改这里
 EDITION = 'offline-single-package'
 PACKAGE_STEM = 'app_guanlang'           # 交付包文件名前缀(用户令 2026-09-17)
 
@@ -49,6 +49,29 @@ BUMP_RULE = ('改动落在"解决方案/架构/核心功能" → 大 +1(中/
 #   level: major/minor/patch 表示这一版**相对上一版**是哪一级变化;legacy 表示该版用的是
 #   旧编号体系(0.x,未按本规则),仅作历史对账用。
 HISTORY: tuple[dict, ...] = (
+    dict(version='2.10.0', date='2026-09-22', level='minor',
+         title='交付文档三件(需求分析 / 设计说明 / 数据要求说明)与源代码化生成器',
+         note='非核心功能新增(交付物)⇒ 中版本 +1。用户令: 「整理观澜的需求/设计/数据接入,各写一份 Word 文档,'
+              '要求区分章节目录、文表图并茂、字体字号分类统一;数据要求说明结合现场《数据分析收资要求-v3.docx》」。'
+              '① 新增三份交付文档(落 `docs/`,文件名带本版本号): '
+              '`需求分析_观澜_2.10.0.docx`(需求来源与演进、角色场景、功能需求 FR、非功能需求 NFR、'
+              '页面与信息架构、验收门、需求跟踪矩阵)、'
+              '`设计说明_观澜_2.10.0.docx`(总体架构、目录与路径真源、重算链、判级与算法、时间窗口径、'
+              '服务与前端、本体与模型、运维编排、安装与版本、质量保证、安全与边界、可移植性)、'
+              '`数据要求说明_观澜_2.10.0.docx`(数据分类总表、逐类要求、核心测点/字段/单位/必须性、'
+              '质量与对齐、落位流程、收资要求 v3 的 21 项逐条对照、缺失与替代、核对锚点、面向现场的收资清单); '
+              '② 两份源件均**源代码化**(用户令「所有的计算均要形成观澜的源代码」): '
+              '`scripts/delivery_docs_build.py` 把 `docs/src/*.md` 渲染成 docx(Word 域目录 + 标题/正文/表格/图题'
+              '四级字体字号分类统一: 黑体标题 + 宋体正文小四 + 表格五号 + 图题五号居中, 页眉页脚页码, 附录排版规范); '
+              '`scripts/delivery_docs_figures.py` 生成 14 张插图, 每张图的数字都从真件取: `src/version.py` 的 HISTORY、'
+              '`configs/portal_pages.yaml`、`configs/serve.json`、`data/raw/<场>/**`、`outputs/<场>/**`、'
+              '并落 `guanlan.py check` 与 `rebuild_all.py --dry-run` 的实跑底稿到 `docs/src/_*.txt`(可复核); '
+              '③ 文档口径遵循用户令 §17: 含"窗"且指时间窗口写全"时间窗"、影响机组写"影响机组数"、'
+              '英文简写写"中文(英文简写)"(平均无故障间隔(MTBF)/平均停机间隔(MTBO)/单次停机时长(MDT)); '
+              '④ 不确定与缺件一律如实写(测风塔零交付、m5_cms_tcm 正本缺失由观澜自算件顶上、故障录波仅 4 台、'
+              '远端未装 Ollama),不编造; 每份文档附「编写依据」表逐章列出源文件; '
+              '⑤ `guanlan.py check` 增一行「交付文档三件 · v<版本>」: 校验三份 docx 在位**且文件名含当前版本号**、'
+              'markdown 源件与所引插图都在位 —— 否则升一次版本号, 旧文档名就悄悄过期(同名旧版会跟着进包)。'),
     dict(version='2.9.2', date='2026-09-22', level='patch',
          title='升级后验收补缺:版本/文档漂移有了自动拦截点(重算链增 ⑧d 版本记录门)',
          note='纯消缺 ⇒ 小版本 +1。检查: 远端 2.9.1 升级 + 重算完成后跑 `guanlan.py check`,'

Vissa filer visades inte eftersom för många filer har ändrats