Selaa lähdekoodia

2.9.1 人工测试 7 项消缺:描述口径/依据空白/问题页空/门户链接 404/闭环文案/Ollama 档位

- 1.1 时间窗(确实指时间窗的'窗'全部写全)+ 1.2 影响台→影响机组 + 1.3 英文简写改'中文(英文简写)',英文映射键同步
- 2 需要关注'查看完整依据'空白:行来自融合面链盘、依据只查 SCADA watch ⇒ 集合不同即空(WTG09 实测);
  改为按本行字段(部件/进度/卡在哪步/证据源)自组句,永不空
- 3/5 报警机组'该问题页'与'命中系统'链接传 slug 而接口按中文名取数 ⇒ 0 条 + 页面误写'无非常态';
  改为链接传中文名 + 服务端 sys_norm() 双向归一 + 页面区分'标识无法识别'与'本时间窗无异常'
- 4 返回观澜门户原写死 http://127.0.0.1:18084/观澜_…html(用户机器上是自己的 loopback)⇒ not found;
  改为根相对 '/' + 网关新增 data-abs='1' 豁免(否则根相对链接被前缀改写成 /detail/),普通链接行为不变
- 6 振动端到端闭环去掉 handoff 黑话,改专业通俗表述
- 7 configs/models.json escalate_qa 由未安装的 qwen3.8:27b 改为实装 qwen3:32b(否则 ask 升档必失败)
- 远端与本地各跑一遍 HTTP 抽样验证;版本记录/设计说明 §17 同步
zhouyang.xie 2 viikkoa sitten
vanhempi
commit
439e05ecae

+ 2 - 2
configs/models.json

@@ -3,7 +3,7 @@
   "runtime": "ollama", "endpoint": "http://127.0.0.1:11434",
   "tiers": {
     "default_qa":  {"model": "qwen3:8b",        "vram_gb": 6,  "note": "默认问答, think=false; 校闸不过自动升档一次"},
-    "escalate_qa": {"model": "qwen3.8:27b",     "vram_gb": 20, "note": "复杂题升档 (开发机实测六问 6/6, 复杂题最长 14.5 min)"},
+    "escalate_qa": {"model": "qwen3:32b",       "vram_gb": 22, "note": "复杂题升档(★2026-09-22 用户人工测试 7 实逮: 原写 qwen3.8:27b, 本机没这个 tag ⇒ 升档必失败、ask 最终 error; 已改为本机实装的 qwen3:32b)"},
     "review":      {"model": "deepseek-r1:14b", "vram_gb": 10, "note": "交叉审核初筛票 (非独立审级; 确诊/¥/上云仍需云端跨厂商双审)"},
     "embed":       {"model": "bge-m3",          "vram_gb": 2,  "note": "检索向量"}
   },
@@ -12,5 +12,5 @@
     "gpu_12_16gb":   ["default_qa", "review", "embed"],
     "cpu_only":      ["default_qa", "embed"]
   },
-  "pull_commands": ["ollama pull qwen3:8b", "ollama pull qwen3.8:27b", "ollama pull deepseek-r1:14b", "ollama pull bge-m3"]
+  "pull_commands": ["ollama pull qwen3:8b", "ollama pull qwen3:32b", "ollama pull deepseek-r1:14b", "ollama pull bge-m3"]
 }

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 3 - 3
docs/版本记录.md


+ 25 - 0
docs/系统设计说明.md

@@ -1195,3 +1195,28 @@ v2 顶栏两个日期输入框 + 「应用」(校验起 ≤ 止,非法拒绝
 所以统一口径:**进程缓存命中即回;未命中就起后台线程、本次先回 `sysmx_pending` / `building`,
 页面轮询再取**(6 s 一次,至多 40 次)。服务启动时后台预热 6 个预设窗(判级 + 默认窗镜头)。
 `pending` 期间页面**在脸上写明**"下面这几张卡还是上一份口径",绝不静默把旧口径当新窗。
+
+## 17. 描述口径与服务端页面链接(2026-09-22 人工测试消缺)
+
+### 17.1 三条写作口径(用户令)
+
+| 口径 | 要求 | 落地 |
+|---|---|---|
+| 时间窗 | 描述里含"窗"且**确实指时间窗**的,一律写全"时间窗" | 预设窗→预设时间窗、判级窗→判级时间窗、窗内→时间窗内、证据窗→证据时间窗、随所选窗→随所选时间窗…;**领域词保持原样**:天气窗/作业窗(检修作业时间)、文档预览窗、交互窗、振动观测窗 |
+| 影响机组 | 影响的是风电机组时写"影响机组" | 表头与说明里的"影响台/影响台数"→"影响机组/影响机组数"(含报告构建器);数据字段名 `影响台数` 暂保留(数据契约/随包快照兼容),显示层统一 |
+| 英文简写 | 用简写时必须"中文(英文简写)" | 平均停机间隔(MTBO)、平均无故障间隔(MTBF)、单次停机时长(MDT);英文映射(`i18n_en`)的键同步改,否则英文页回落中文 |
+
+> 全量清扫脚本见 `F:\temp\_h9\wording_audit.py`(按文件点名"裸窗/影响台/英文简写"),只改确实指时间窗/机组的,不误伤领域词。
+
+### 17.2 服务端页面的两个链接契约
+
+| 事 | 口径 |
+|---|---|
+| 系统标识 | `/problem/<台>/<系统>` 与 `/api/problem?sys=` 一律经 `windscada_serve.sys_norm()` **双向归一**(中文名 / 前端 slug `pitch` / 大小写变体都认)。认不出时页面写**"系统标识无法识别"**,**绝不**写"该系统在判级窗内无非常态"(对报警机组那是假陈述) |
+| 根链接(返回门户) | 网关会把组件页的根相对链接按前缀改写(`/` → `/detail/`),因此**组件页要指回网关根时必须在标签上标 `data-abs="1"`** —— `guanlan_gateway.rewrite()` 对这类 href 跳过前缀改写(哨兵法实现,普通链接行为不变,见其 docstring 与单测) |
+
+### 17.3 v2 页面的依据不得为空
+
+「需要关注」的卡片来自**融合面链盘**(`fus.链盘.rows`),而 SCADA 的 `watch` 只覆盖九系统多报警台;
+两者集合不同时旧的 `why[t]` 会取到 undefined ⇒「查看完整依据」展开是空白(实测 WTG09 齿轮箱)。
+现在缺依据时按**本行字段自组句**:`部件 · 链路走到第 k/n 步 · 卡在「某步」 · 证据源`,永不空。

+ 20 - 4
scripts/guanlan_gateway.py

@@ -63,15 +63,31 @@ MODEL_PARAM_PATHS = ("/api/ask", "/api/rpt_compose")
 
 
 def rewrite(body: str, prefix: str) -> str:
-    """组件页根相对 URL → 统一前缀. 只改 href/src/action/content 属性、url()、fetch/open 调用; 不动 // 开头的协议相对与 data:。"""
-    if not prefix:
-        pass
-    else:
+    """组件页根相对 URL → 统一前缀. 只改 href/src/action/content 属性、url()、fetch/open 调用; 不动 // 开头的协议相对与 data:。
+
+    ★2026-09-22 用户测试 4: 作者可在标签上标 `data-abs="1"` 声明**这条链接不要被前缀改写**
+    (组件页要指回**网关根**——如"返回观澜门户"——根相对写法 `/` 会被本函数改成 `/detail/`,
+    于是永远回不到门户)。做法: 先把这类 href 加哨兵前缀跳过改写,收尾再摘掉哨兵。
+    """
+    if prefix:
+        keep: list[str] = []
+
+        def _protect(url: str) -> str:
+            keep.append(url)
+            return f'href="@@KEEP{len(keep) - 1}@@"'
+
+        # 同一标签里 href 与 data-abs="1" 两种书写顺序都认(哨兵不含 '/',天然不被下面的前缀正则命中)
+        body = re.sub(r'href="([^"]*)"([^>]*?data-abs="1")',
+                      lambda m: _protect(m.group(1)) + m.group(2), body)
+        body = re.sub(r'(<[^>]*?data-abs="1"[^>]*?)href="([^"]*)"',
+                      lambda m: m.group(1) + _protect(m.group(2)), body)
         body = re.sub(r'((?:href|src|action|content)=["\'])/(?![/])', r"\1" + prefix + "/", body)
         body = re.sub(r"(url\(['\"]?)/(?![/])", r"\1" + prefix + "/", body)
         body = re.sub(r"((?:fetch|open|EventSource|WebSocket|assign|replace)\(\s*(?:['\"](?:GET|POST)['\"]\s*,\s*)?[`'\"])/(?![/])", r"\1" + prefix + "/", body)
         body = re.sub(r"((?<![\w.])location\s*=\s*[`'\"])/(?![/])", r"\1" + prefix + "/", body)  # location.href= 已被 href= 规则覆盖, 此处只管裸 location=
         body = re.sub(r"([`'\"])/api/", r"\1" + prefix + "/api/", body)  # 字符串常量拼 URL (如 '/api/ask_status' + qs) 不经 fetch( 直接出现 → 逮 22:21 ask_status 404
+        for i, url in enumerate(keep):                      # 摘哨兵: 恢复原样(不做任何前缀改写)
+            body = body.replace(f'href="@@KEEP{i}@@"', f'href="{url}"')
     for port, pre in PORT2PREFIX.items():
         body = re.sub(r"https?://(?:127\.0\.0\.1|localhost):%d(?=[/\"'` )<&?#]|$)" % port, pre or "", body)
     body = body.replace('href=""', 'href="/"')  # 18084 根裸链

+ 108 - 64
scripts/windscada_serve.py

@@ -3,7 +3,7 @@
 """windscada serve (M7c): 交互后端 — 工作台(全场)+分台页, 全部带时间窗.
 设计: ①台页只渲染"有问题的系统"的证据图(分册两型: A=判据线+摆幅标注, B=本台vs全场四分位带), 无问题不出图;
 ②台内交互查询: 任一契约通道×时间窗×聚合 即时出图; ③工作台: 系统分类问题(全系统)+故障统计梳理+SOP控制策略;
-④时间窗: 月度类数据真窗; 判级类(变桨/偏航/蓄能=近90日, 曲线=2025H2判别窗)固定窗如实标注.
+④时间窗: 月度类数据真窗; 判级类(变桨/偏航/蓄能=近90日, 曲线=2025H2判别窗)固定时间窗如实标注.
 端口 8033. 判级=筛查级/候选, P级归综合报告线."""
 import sys as _sys, pathlib as _plb
 _sys.path.insert(0, str(_plb.Path(__file__).resolve().parents[1]))
@@ -131,7 +131,7 @@ _KEEP_ZH = frozenset((
     # ★第三类判别用法, 本闸的扫描器 (tests/test_keep_zh_discriminators.py) 抓不到:
     #   不是比较也不是查表, 而是拼进 URL 路径当 ID (`/problem/${t}/${encodeURIComponent(x)}`)。
     #   syss 被翻成 'Yaw' 后, 浮层请求 sys=Yaw 永远查不到烤的是 sys=偏航 的快照键,
-    #   点开必是"本窗无记录"——页面不报错, 数据静默消失, 和 st/ostate 那两类同一后果不同机制。
+    #   点开必是"本时间窗无记录"——页面不报错, 数据静默消失, 和 st/ostate 那两类同一后果不同机制。
     #   2026-09-03 用户实测逮出 (Watch list → 系统名链接)。显示文字仍由 sysName(x) 客户端译。
     'syss',
 ))
@@ -755,7 +755,7 @@ def months_of(win):
 def win_range(win):
     """窗 → (起, 止) 日期串,**含两端**(用户令 2026-09-21:时间窗支持自定义起止日期)。
 
-    预设/逐月/月区间窗一律落到"该窗覆盖月的月首/月末",于是与 `months_of()` 同口径;
+    预设/逐月/月区间窗一律落到"该时间窗覆盖月的月首/月末",于是与 `months_of()` 同口径;
     日区间窗原样返回。日粒度件(停机事件/报警/日粒度判据)用它做**含端**过滤。
     """
     import re as _re
@@ -789,6 +789,31 @@ def span_of(win):
     return win_range(win)
 
 
+# 九系统的 slug(前端 hash 用)↔ 中文名(服务端取数用)。**必须双向可认**:
+# 2026-09-22 用户测试 3/5 实逮 —— v2 的「该问题页」「命中系统」链接过去传的是 slug(`pitch`),
+# 而 `/api/problem` 与页面按中文名取数 ⇒ 返回 0 条后页面还写"该系统在判级时间窗内无非常态"(对报警机组是**假陈述**)。
+SYS_SLUG = {'变桨': 'pitch', '偏航': 'yaw', '齿轮箱': 'gearbox', '发电机': 'generator',
+            '主轴承': 'main_bearing', '变流器': 'converter', '塔架与基础': 'tower',
+            '主控与传感网': 'control', '叶片与叶根': 'blade'}
+SYS_FROM_SLUG = {v: k for k, v in SYS_SLUG.items()}
+
+
+def sys_norm(x):
+    """系统标识归一:中文名 / 前端 slug / 大小写变体 → 规范中文名;认不出返回 None。"""
+    if not x:
+        return None
+    s = str(x).strip()
+    if s in SYS_SLUG:
+        return s
+    low = s.lower().replace('-', '_')
+    if low in SYS_FROM_SLUG:
+        return SYS_FROM_SLUG[low]
+    for zh, slug in SYS_SLUG.items():                 # 兜底: 去空格/全半角差异后再比
+        if s.replace(' ', '') == zh.replace(' ', '') or low == slug.replace('-', '_'):
+            return zh
+    return None
+
+
 # ── 按窗重算:后台算 + 缓存(用户令 2026-09-21)─────────────────────────────────
 # 判级矩阵 ≈25 s/窗、七镜头 ≈9~44 s/窗 —— 直接同步算会把 HTTP 请求挂几十秒(远端还要过网关,
 # 长请求有被掐的风险)。所以统一口径:**缓存命中即回;未命中就起后台线程,本次先回 pending,
@@ -940,7 +965,7 @@ def fig_alarm_bars(t, codes, title, win):
     if not len(g): return None
     cnt = g.groupby('month').size().reindex(ms).fillna(0)
     return dict(kind='bars', title=f'{t} {title} 月度条数', unit='条', months=ms,
-                vals=[int(v) for v in cnt], note='窗内合计 %d 条' % int(cnt.sum()))
+                vals=[int(v) for v in cnt], note='时间窗内合计 %d 条' % int(cnt.sum()))
 
 def fig_curve(t):
     b = _CACHE['bins']; g = b[b.turbine == t].sort_values('bin')
@@ -988,7 +1013,7 @@ def fig_sys_alarms(t, sysname, win):
     if not len(g): return None
     v = g.groupby('month').size().reindex(ms).fillna(0)
     top = g.groupby('code').size().sort_values(ascending=False).head(3)
-    note = '窗内合计 %d 条; top: ' % len(g) + '; '.join(f'{c} {i18n.alarm_label(c)[:12]}×{n}' for c, n in top.items())
+    note = '时间窗内合计 %d 条; top: ' % len(g) + '; '.join(f'{c} {i18n.alarm_label(c)[:12]}×{n}' for c, n in top.items())
     return dict(kind='bars', title=f'{t} {sysname}系统 报警月度条数', unit='条', months=ms,
                 vals=[int(x) for x in v], note=note)
 
@@ -1134,7 +1159,7 @@ def curve_view(win='2025H2'):
                                f"{'/'.join(t.replace('WTG','')+'#' for t,_ in outs) if outs else '无'}"
                                f" | 灰域=全场四分位, 逐档中位聚合, 已剥限电(只正常发电态)"
                                f" | 样本 {r['n_total']:,} 个10min点 / {r['n_turbines']} 台 / 最小档 {r['n_min_bin']:,}"
-                               f" | 窗 {r['win']} (随所选窗)"),
+                               f" | 时间窗 {r['win']}(随所选时间窗)"),
                          band=dict(q1=r['q1'], q3=r['q3'])))
         # 残差视图 (§4.6c④ per机必算同型残差): 绝对量尺度上四分位带只有几个像素, 残差面才看得见
         if outs:
@@ -1148,7 +1173,7 @@ def curve_view(win='2025H2'):
                              thresholds=[dict(v=a['门'], label=f"显著门 +{a['门']}{a['单位']}"), dict(v=-a['门'], label='')],
                              band=dict(q1=q1d, q3=q3d),
                              note=(f"虚线=物理绝对锚 ±{a['门']}{a['单位']} ({a['说明']}); 灰域=全场四分位残差带; 出带且过锚才算离群"
-                                   f" | 样本 {r['n_total']:,} 点 / {r['n_turbines']} 台 | 窗 {r['win']}")))
+                                   f" | 样本 {r['n_total']:,} 点 / {r['n_turbines']} 台 | 时间窗 {r['win']}")))
     # ---- 时序件: 控制参数月度 (M9b) + 偏航动态月度 (M12) ----
     def _ts(path, val, title, unit, xlab, picks=None, note='', thr=None):
         f = ST / path
@@ -1172,7 +1197,7 @@ def curve_view(win='2025H2'):
         _n = int(piv.notna().to_numpy().sum())
         _nt = int(piv.shape[1])
         note2 = (note + f" | 样本 {_n:,} 个月×台 / {_nt} 台 / {len(msx)} 个月"
-                        f" | 窗 {msx[0]}~{msx[-1]} (月度聚合, 随所选窗·按月取整)") if msx else note
+                        f" | 时间窗 {msx[0]}~{msx[-1]}(月度聚合,随所选时间窗·按月取整)") if msx else note
         return dict(kind='multiline', title=title, unit=unit, months=msx, series=series, xlab=xlab,
                     note=note2, thresholds=thr or [],
                     band=dict(q1=[None if v != v else round(float(v), 3) for v in piv.quantile(0.25, axis=1)],
@@ -1338,7 +1363,7 @@ def fleet_view(win):
                 h=round(float(hrs.get(k, 0)), 0)) for k in hrs.index]
     _A = float(_act.sum()); _L = float(loss.sum())
     _cap = len(CFG['turbines']) * 4.0 * float(hrs_tot) / max(len(CFG['turbines']), 1)
-    # 逐月能量 (2026-08-28 报表端口): 汇报要环比与趋势, 全窗聚合给不出。
+    # 逐月能量 (2026-08-28 报表端口): 汇报要环比与趋势, 全时间窗聚合给不出。
     # 残月覆盖率一并带出 — 报表里拿残月和整月比环比会读反 (故障月度图已踩过一次)。
     _em = []
     for _mm in ms:
@@ -1400,7 +1425,7 @@ def fleet_view(win):
                     系数=dict(wear=cap['classes'].get('wear_progressive', {}).get('coef'),
                             thermal=cap['classes'].get('thermal_acute', {}).get('coef'),
                             unknown=cap['classes'].get('unknown', {}).get('coef')))
-        # 矩阵补 报警/工单 两列 (2026-08-28 用户令) — 报警=窗内真窗计数+族分布; 工单=历史台账(窗见列头)
+        # 矩阵补 报警/工单 两列 (2026-08-28 用户令) — 报警=时间窗内真窗计数+族分布; 工单=历史台账(窗见列头)
         # 报警/工单两列只统计传动链三部件 (2026-08-28 用户令: 只说发电机/齿轮箱/主轴) —
         # 本矩阵每行是"该台传动链状态", 全场报警计数会把变桨/偏航/主控的量混进来 (316 条里绝大多数与传动链无关)
         _DT_PAT = '齿轮|齿箱|润滑油|油冷|滤芯|发电机|定子|绕组|滑环|主轴承|主轴|轴承'
@@ -1517,7 +1542,7 @@ def fleet_view(win):
                 dict(k='排期', st='done' if t == 'WTG29' else 'open',
                      v='情景A 2026-09' if t == 'WTG29' else '未排',
                      d='进检修排程沙盘 (风险×损失双轴)'),
-                # ★这一列一律"不可判", 不是偷懒: 工单台账止 2024-11 而判级窗在 2026,
+                # ★这一列一律"不可判", 不是偷懒: 工单台账止 2024-11 而判级时间窗在 2026,
                 # 台账里的历史工单不可能是针对本次问题的 ⇒ 用 n>0 判"已派工"会造出假的完成态。
                 dict(k='动作', st='unknown',
                      v=(f"台账 {n_wo} 单" if n_wo else '台账无'),
@@ -1566,9 +1591,9 @@ def fleet_view(win):
                 m9_pending=bool(m9_pending),
                 win_pending=bool(sysmx_pending or m9_pending),
                 sysmx_span=list(span_of(win)),
-                note=('判级轴(变桨/偏航/蓄能/温度)按所选窗重算;曲线按所选窗重算;'
+                note=('判级轴(变桨/偏航/蓄能/温度)按所选时间窗重算;曲线按所选时间窗重算;'
                       '故障统计/五态/温度月轨迹/停机台账=所选窗真窗'
-                      + ('(⚠ 判级矩阵正在按窗重算,下面系统卡暂为上一份口径,稍后自动刷新)' if sysmx_pending else '')))
+                      + ('(⚠ 判级矩阵正在按所选时间窗重算,下面系统卡暂为上一份口径,稍后自动刷新)' if sysmx_pending else '')))
 
 # ---------- 本体网页面 (ontology 的网页体现: 机制链/决策台/机制库/对象浏览器; 只读, 复用 MCP 纯函数) ----------
 def ontology_view():
@@ -1799,7 +1824,7 @@ def rpt_compose(need, canvas, model='qwen3:8b'):
         f"上网电量 {E.get('act')} MWh; 理论可发 {E.get('theo')}; 损失 {E.get('loss')} MWh; 损失率 {E.get('loss_pct')}%",
         f"等效满发 {E.get('eflh')} h; 时间可用率 {(fl.get('control') or {}).get('avail')}%",
         f"总停机 {R.get('总停机h')} h = 设备类 {R.get('设备类停机h')} + 外部类 {R.get('外部类停机h')}",
-        f"平均停机间隔 {M.get('MTBF_h')} h; 停机 {M.get('停机事件数')} 起; 单次平均 {M.get('MDT_h')} h",
+        f"平均停机间隔(MTBO){M.get('MTBF_h')} h; 停机 {M.get('停机事件数')} 起; 单次停机时长(MDT){M.get('MDT_h')} h",
         f"报警 {F.get('total')} 条 / {F.get('n_codes')} 个码",
         f"需行动机组 {len(BD.get('rows') or [])} 台; 卡点 {BD.get('stuck')}",
         # 占比直接给出: 不给模型就得自己算, 算对了也会被接地闸判为"未接地数字"(实测 31.4/46.6)
@@ -1984,7 +2009,7 @@ const devBar=(ch,dev,vd,raw)=>{
     <div class="small" style="color:var(--muted);margin-top:1px">${esc(ch||'')}</div>`};
 // 证据窗时间条: 固定 2026-01→09 轴, 块=claim 窗覆盖段 (跨行可比, 一眼看窗对不对得上)
 const winBar=(spans,raw)=>{
-  if(!spans||!spans.length) return `<span class="small" title="${esc(raw)}">未标窗</span>`;
+  if(!spans||!spans.length) return `<span class="small" title="${esc(raw)}">未标时间窗</span>`;
   const T0=Date.parse('2026-01-01'), T1=Date.parse('2026-09-01'), W=118;
   const px=d=>{const v=Date.parse(d); return 3+Math.max(0,Math.min(1,(v-T0)/(T1-T0)))*(W-6)};
   const blocks=spans.map(([a,b])=>{const x1=px(a), x2=px(b);
@@ -2001,7 +2026,7 @@ const oilPill=(last,fresh,raw)=>`<div title="${esc(raw)}" style="display:flex;al
 const JARGON=[[/正样本/g,'健康对照台'],[/可降(?![级])/g,'可降级观察'],[/记基线观察/g,'记基线·下轮复核'],
   [/CLAIM窗|claim窗/g,'证据窗'],[/位次([0-9]+)[/]([0-9]+)/g,'全场第$1名(共$2台)'],[/未列 [((]≠正常[^))]*[))]/g,'未收录 (≠已排除异常)'],
   [/温度候选[(]定向查[)]/g,'温度偏高·待定向排查'],[/最大偏/g,'偏差最大通道'],[/单次([0-9]+)s/g,'单次循环 $1 秒'],
-  [/窗内无样/g,'本窗未取样'],[/仲裁输入/g,'待仲裁输入'],[/无新证/g,'本轮无新证据'],[/降观察/g,'降为观察'],
+  [/时间窗内无样/g,'本时间窗未取样'],[/仲裁输入/g,'待仲裁输入'],[/无新证/g,'本轮无新证据'],[/降观察/g,'降为观察'],
   [/弱同向新证/g,'弱同向新证据'],[/传感器项/g,'测量链(仪器)问题'],[/份样/g,' 份油样'],[/NBM/g,'基线模型(NBM)']];
 // 表述规范单源 = windcms agent.py 的业主口径注解 (2026-08-28 用户令: 等级评价和表述一定要参照 windcms)。
 // 规范原文: 状态等级打头(优秀=未见异常保持常规监视 / 良好=轻微偏离记录观察 / 报警=存在需现场核实或处置的异常 /
@@ -2044,7 +2069,7 @@ const zh=t=>{let x=String(t==null?'':t); JARGON.forEach(([a,b])=>x=x.replace(a,b
 // 条总长 = 该台条数/全场最大 → 跨行可比 (原来段宽按台内占比算, 总和恒 100% 所以每行一样长, 量的信息全丢);
 // 段内比例 = 族构成。总长封顶 118px, 最短 6px 保证可见。
 const famBar=(fam,n,maxN)=>{
-  if(!n) return `<span class="small" style="color:var(--muted)">窗内无报警</span>`;
+  if(!n) return `<span class="small" style="color:var(--muted)">时间窗内无报警</span>`;
   const CO={'齿轮箱':'#8A6D3B','发电机':'#4A6FA5','主轴承':'#2E7D6B','变桨叶片':'#4A6FA5','偏航':'#2E7D6B','测风':'#7C5295','其他':'#5A6470'};
   const W=118, w=Math.max(6, Math.round(n/Math.max(maxN||n,1)*W));
   return `<div title="${n} 条 · 全场最多 ${maxN||n} 条" style="display:flex;height:9px;border-radius:3px;overflow:hidden;width:${w}px">`
@@ -2075,8 +2100,8 @@ const RPT_BLOCKS = {
   water:  {g:'gen',  nm:'能量瀑布',     ic:'▤', d:'理论可发 → 逐段扣损 → 实际上网, 用于定位电量流向'},
   emon:   {g:'gen',  nm:'逐月发电表',   ic:'▥', d:'逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率'},
   blame:  {g:'gen',  nm:'损失责任分账', ic:'◧', d:'交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答"谁的责任"'},
-  ram:    {g:'rel',  nm:'可靠性象限',   ic:'◈', d:'MTBF × MDT 部件象限, 分出"频发易修"与"罕发难修", 处置方向相反'},
-  parts:  {g:'rel',  nm:'部件停机表',   ic:'▤', d:'逐部件停机时长 / 事件数 / MTBF / MDT / 影响台数'},
+  ram:    {g:'rel',  nm:'可靠性象限',   ic:'◈', d:'平均无故障间隔(MTBF)× 单次停机时长(MDT)部件象限, 分出"频发易修"与"罕发难修", 处置方向相反'},
+  parts:  {g:'rel',  nm:'部件停机表',   ic:'▤', d:'逐部件停机时长 / 事件数 / 平均无故障间隔(MTBF)/ 单次停机时长(MDT)/ 影响机组数'},
   attrib: {g:'rel',  nm:'归因覆盖',     ic:'▬', d:'已归因 vs 无记录停机的占比 — 无记录不等于没有故障'},
   pareto: {g:'flt',  nm:'故障帕累托',   ic:'▮', d:'按条数降序 + 累计 80% 线, 找关键少数'},
   quad:   {g:'flt',  nm:'故障四象限',   ic:'◇', d:'条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级'},
@@ -2239,8 +2264,8 @@ function rptBlock(k, d){
     '工况态分解 + 限电命令面绑定判别; 平均出力=实发÷台时, 用作风资源代理量');
   if(k==='ram') return R?wrap('可靠性 × 可维修性', ramChart(R.表)+
     `<p class="small" style="color:var(--muted);margin-top:6px">设备类 ${R.设备类停机h}h / 外部类 ${R.外部类停机h}h (电网·风况·运维操作不计设备可靠性)</p>`,
-    'SCADA ≥1h 停机段, 不含调度令; 海上 MDT 含交通窗口'):'';
-  if(k==='parts') return R?wrap('部件停机', `<table><tr><th>部件</th><th class="num">停机h</th><th class="num">事件</th><th class="num">MTBF h</th><th class="num">MDT h</th><th class="num">影响台</th></tr>
+    'SCADA ≥1h 停机段, 不含调度令; 海上单次停机时长(MDT)含交通窗口'):'';
+  if(k==='parts') return R?wrap('部件停机', `<table><tr><th>部件</th><th class="num">停机h</th><th class="num">事件</th><th class="num">平均无故障间隔(MTBF,h)</th><th class="num">单次停机时长(MDT,h)</th><th class="num">影响机组</th></tr>
     ${R.表.slice(0,9).map(x=>`<tr><td class="small">${x.部件}${x.外部?' (外部)':''}</td><td class="num">${x.停机时长h}</td><td class="num">${x.事件数}</td>
     <td class="num">${x.MTBF_h||'—'}</td><td class="num">${x.MDT_h}</td><td class="num">${x.影响台数}</td></tr>`).join('')}</table>`,
     '归因=段起点±30min 首发码'):'';
@@ -2406,7 +2431,7 @@ function popChain(t){
     mini=`<div class="pop-mini">
       <div><b>振动结论</b><span class="badge" style="border-color:${LVC2(frow.级)};color:${LVC2(frow.级)}">${(frow.振动结论||'').slice(0,26)}</span>
         <div class="small" style="color:var(--muted);margin-top:4px">${frow.部件||''}</div></div>
-      <div><b>证据窗</b>${winBar(frow.窗区间,frow.claim_window)}</div>
+      <div><b>证据窗</b>${winBar(frow.时间窗区间,frow.claim_window)}</div>
       <div><b>温度轴 (fleet相对)</b>${devBar(frow.温度通道,frow.温度dev,frow.温度判,frow.温度轴)}</div>
       <div><b>油液时效</b>${oilPill(frow.油末样,frow.油新鲜,frow.油样轴)}</div></div>`;
   }
@@ -2460,7 +2485,7 @@ async function popPart(t,part){
   if(frow){
     mini.push(`<div><b>振动结论</b><span class="badge" style="border-color:${LVC2(frow.级)};color:${LVC2(frow.级)}">${frow.振动结论}</span>
       <div class="small" style="color:var(--muted);margin-top:4px">${frow.部件}</div></div>`);
-    mini.push(`<div><b>证据窗</b>${winBar(frow.窗区间,frow.claim_window)}</div>`);
+    mini.push(`<div><b>证据窗</b>${winBar(frow.时间窗区间,frow.claim_window)}</div>`);
     mini.push(`<div><b>温度轴 (fleet相对)</b>${devBar(frow.温度通道,frow.温度dev,frow.温度判,frow.温度轴)}</div>`);
     mini.push(`<div><b>油液时效</b>${oilPill(frow.油末样,frow.油新鲜,frow.油样轴)}</div>`);
   }
@@ -2471,7 +2496,7 @@ async function popPart(t,part){
     `<div style="margin-bottom:10px">${dtChainStates({[part.replace(/[((].*/,'')]:LVC2(p0&&p0['状态']==='报警'?'bad':'warn')},260,null)}</div>
      <div class="pop-mini">${mini.join('')}</div>`
     +(curves.length?curves.slice(0,2).map(f=>`<div style="margin-bottom:10px">${drawFig(f)}</div>`).join('')
-                   :`<p class="small" style="color:var(--muted)">该部件在本窗无 SCADA 曲线 (振动谱级证据走 windcms)</p>`)
+                   :`<p class="small" style="color:var(--muted)">该部件在本时间窗无 SCADA 曲线 (振动谱级证据走 windcms)</p>`)
     +`<p class="small" style="margin-top:8px"><a class="tl" href="/problem/${t}/${encodeURIComponent(sys)}">完整问题页 (全部曲线与工单史) →</a>
       &nbsp;·&nbsp;<a class="tl" href="/turbine/${t}">本台总览 →</a></p>`;
 }
@@ -3071,7 +3096,7 @@ const figWrap = (f, inner, note) => `<div class="card"${isL(f) ? ' style="grid-c
 // 右上=高频长时(重点治理) 左下=低频短时(本底)。点面积=影响台数, 分离单台刷屏与批次共性。
 function quadChart(items){
   const it=(items||[]).filter(x=>x&&x.n>0&&x.med>0);
-  if(it.length<3) return '<p class="small">窗内码数不足, 不作四象限</p>';
+  if(it.length<3) return '<p class="small">时间窗内码数不足, 不作四象限</p>';
   const W=720,H=330,ML=58,MR=16,MB=44,MT=16;
   const lx=x=>Math.log10(x), xs=it.map(d=>lx(d.n)), ys=it.map(d=>lx(d.med));
   const x0=Math.min(...xs)-0.15,x1=Math.max(...xs)+0.15,y0=Math.min(...ys)-0.15,y1=Math.max(...ys)+0.15;
@@ -3110,8 +3135,8 @@ function quadChart(items){
 }
 
 // 月度故障率图 (2026-08-28 审核逮的反向误读)。
-// 原图画"月度条数", 末月 1371 条看着是全窗最低=在改善; 但 2026-07 只有 6 天数据(覆盖 19.4%),
-// 按日均归一后是 228 条/日 = 全窗第二高, 是 6 月(75)的 3 倍 — 方向完全相反。
+// 原图画"月度条数", 末月 1371 条看着是全时间窗最低=在改善; 但 2026-07 只有 6 天数据(覆盖 19.4%),
+// 按日均归一后是 228 条/日 = 全时间窗第二高, 是 6 月(75)的 3 倍 — 方向完全相反。
 // ⇒ 残月绝不能与整月并列比较。柱=日均(可比), 灰斜纹=数据不满月, 数字标总条数(原始事实)。
 function rateChart(mo){
   const ms=mo.months, vs=mo.vals, cov=mo.cov||ms.map(()=>1), pd=mo.per_day||[];
@@ -3199,7 +3224,7 @@ function ramChart(tb){
     const c=ext?'var(--muted)':(hot?'var(--bad)':'var(--accent)');
     return `<circle cx="${PX(d.MTBF_h).toFixed(1)}" cy="${PY(d.MDT_h).toFixed(1)}" r="${r.toFixed(1)}"
       fill="${c}" fill-opacity="${ext?0.3:0.42}" stroke="${c}" stroke-width="1.2"
-      stroke-dasharray="${ext?'3 2':''}"><title>${d.部件}: MTBF ${d.MTBF_h}h · MDT ${d.MDT_h}h · 停机 ${d.停机时长h}h · ${d.事件数}起 · ${d.影响台数}台${ext?' · 外部类不计设备可靠性':''}</title></circle>
+      stroke-dasharray="${ext?'3 2':''}"><title>${d.部件}: 平均无故障间隔(MTBF)${d.MTBF_h}h · 单次停机时长(MDT)${d.MDT_h}h · 停机 ${d.停机时长h}h · ${d.事件数}起 · ${d.影响台数} 台机组${ext?' · 外部类不计设备可靠性':''}</title></circle>
       <text x="${(PX(d.MTBF_h)+r+4).toFixed(1)}" y="${(PY(d.MDT_h)+3.5).toFixed(1)}" font-size="9.5"
         fill="${ext?'var(--muted)':'currentColor'}">${d.部件.slice(0,7)}</text>`;}).join('');
   const qn=(x,y,a,t,sub)=>`<text x="${x}" y="${y}" font-size="10" fill="var(--muted)" text-anchor="${a}" opacity="0.9">${t}</text>
@@ -3222,7 +3247,7 @@ function ramChart(tb){
     ${pts}
     <text x="${(ML+(W-MR-ML)/2).toFixed(1)}" y="${H-8}" font-size="9.5" fill="var(--muted)" text-anchor="middle">平均无故障间隔 MTBF (h, 对数) →  越右越可靠</text>
     <text x="13" y="${(MT+(H-MB-MT)/2).toFixed(1)}" font-size="9.5" fill="var(--muted)" text-anchor="middle"
-      transform="rotate(-90 13 ${(MT+(H-MB-MT)/2).toFixed(1)})">单次停机时长 MDT (h) →  越上越难修</text>
+      transform="rotate(-90 13 ${(MT+(H-MB-MT)/2).toFixed(1)})">单次停机时长(MDT,h)→  越上越难修</text>
     <circle cx="${W-MR+16}" cy="${MT+40}" r="5" fill="var(--accent)" fill-opacity="0.42" stroke="var(--accent)"/>
     <text x="${W-MR+26}" y="${MT+44}" font-size="9" fill="var(--muted)">设备类</text>
     <circle cx="${W-MR+16}" cy="${MT+58}" r="5" fill="var(--muted)" fill-opacity="0.3" stroke="var(--muted)" stroke-dasharray="3 2"/>
@@ -3252,7 +3277,7 @@ function attribBar(tb, tot){
 // 看起来只是"这一页没这个内容"。reliability 的 bool 位取反 bug 就是这样活下来的。
 // ⇒ 出错时改为渲染一张醒目失败卡: 说清哪块没了、意味着什么缺失、错在哪。
 const BLOCKDESC = {
-  rel:  ['部件停机贡献与可靠性', '本页看不到部件级 MTBF/MDT 与停机归因'],
+  rel:  ['部件停机贡献与可靠性', '本页看不到部件级平均无故障间隔(MTBF)/单次停机时长(MDT)与停机归因'],
   m8:   ['停机事件化与故障季节性', '故障统计页缺停机归因与季节形态'],
   m9:   ['控制参数一致性', '发电性能页缺封顶分组与桨距调度差'],
   fus:  ['振动四源融合', '振动融合页缺逐台判级矩阵'],
@@ -3261,25 +3286,25 @@ function blockErr(key, err){
   const d = BLOCKDESC[key] || [key, ''];
   return `<div class="card mb" style="border-left:3px solid var(--bad)">
     <h3 style="color:var(--bad)">⚠ ${d[0]} — 未算出</h3>
-    <p class="small" style="margin-bottom:6px">${d[1]}。<b>非"本窗无数据", 属计算失败</b> — 下方数字不完整, 不要据此下结论。</p>
+    <p class="small" style="margin-bottom:6px">${d[1]}。<b>非"本时间窗无数据", 属计算失败</b> — 下方数字不完整, 不要据此下结论。</p>
     <pre class="small" style="white-space:pre-wrap;color:var(--muted);background:var(--bg);padding:8px 10px;border-radius:6px;margin:0">${String(err).slice(0,300)}</pre></div>`;
 }
 
 // 逐页窗声明 (2026-08-28 审核逮的交互欺骗)。
 // 顶部窗选择器对"发电性能"完全无效 — 那 19 张图是预计算产物, 窗在生成时钉死为 2025-07~2026-01;
-// 用户切到"2026年"图纹丝不动却毫无提示。"控制策略"曾更糟(工况态跟随+参数件固定窗并列),
+// 用户切到"2026年"图纹丝不动却毫无提示。"控制策略"曾更糟(工况态跟随+参数件固定时间窗并列),
 // 两个窗并列在同一页。窗对不齐 = claim窗≠证据窗, 是判据失效的头号来源, 必须写在脸上。
 const WINSPEC = {
   '发电性能': {mode:'fixed', win:'2025-07 ~ 2025-12',
                why:'限电前干净窗 — 2026 起交易策略主导, 曲线与控制参数含策略性少发不可作设备判据。本页含特性曲线与控制参数一致性两件, 同窗'},
-  // 2026-08-28 用户裁: 控制参数件(固定窗)迁入发电性能, "电量算账"只留跟随窗内容
+  // 2026-08-28 用户裁: 控制参数件(固定时间窗)迁入发电性能, "电量算账"只留跟随时间窗内容
   // → 每页只有一个窗制, 混合窗从根上消掉 (原来是本系统唯一的双窗页)
 };
 function winBanner(tab, curWin, extra){
   const w = WINSPEC[tab];
   if(!w) return `<div class="winb"><b>时间窗</b> ${curWin} · 本页全部数字随上方窗切换${extra?' | '+extra:''}</div>`;
   const c = w.mode==='fixed' ? 'var(--warn)' : 'var(--info)';
-  const tag = w.mode==='fixed' ? '固定窗 · 不随上方切换' : '本页含两个窗';
+  const tag = w.mode==='fixed' ? '固定时间窗 · 不随上方切换' : '本页含两个窗';
   return `<div class="winb" style="border-left-color:${c}">
     <b style="color:${c}">${tag}</b> ${w.win}
     <div class="small" style="color:var(--muted);margin-top:3px">${w.why}${extra?' | '+extra:''}</div></div>`;
@@ -3379,7 +3404,7 @@ function blameBar(e){
 // 原来只有两列排序表, 看不出前几项就占了大头。
 function paretoChart(items, unit, note){
   const it = (items || []).filter(x => x && x.v > 0);
-  if(!it.length) return '<p class="small">窗内无数据</p>';
+  if(!it.length) return '<p class="small">时间窗内无数据</p>';
   // 尺寸 (2026-08-28 排布优化): 原 720x260 + 旋转长标签, 两列并排时被缩到 0.60 倍 = 字高 6.5px 不可读。
   // 改紧凑版 520x236: x 轴只标码号(水平, 不旋转 → 底部留白 86→40), 全名进悬停与图注前三行。
   const W = 520, H = 236, ML = 46, MR = 40, MB = 40, MT = 14;
@@ -3486,7 +3511,7 @@ function snapOverview(d){
      + '<div class="kpi"><b>' + (K['关注台'] != null ? K['关注台'] : '—') + '</b>'
      + '<span>机组在跟踪清单上</span></div>'
      + '<div class="kpi"><b>' + (M['停机事件数'] != null ? M['停机事件数'] : '—') + '</b>'
-     + '<span>停机事件 (本窗)</span></div></div>';
+     + '<span>停机事件 (本时间窗)</span></div></div>';
   h += '<div style="font-size:12px;color:var(--muted);margin-top:14px;line-height:1.75">'
      + '<b>各屏回答什么</b><br>'
      + '电量算账 — 电去哪了, 每一度归到哪个原因<br>'
@@ -3854,7 +3879,7 @@ function drawFig(f){
 function ordersHtml(o){
   if(!o) return '';
   let h=`<div class="card mb"><h3>工单处理史 · 该系统 (处理 ${o.n} 次${o.窗?'; 窗 '+o.窗:''})</h3>`;
-  if(o.n===0){h+=`<p class="small">该窗内无工单记录。${o.note||''}</p></div>`; return h}
+  if(o.n===0){h+=`<p class="small">该时间窗内无工单记录。${o.note||''}</p></div>`; return h}
   const acts=Object.entries(o.动作统计||{}).map(([k,v])=>k+'×'+v).join(' / ');
   if(acts) h+=`<p class="small" style="margin-bottom:6px">处置动作统计: <b>${acts}</b></p>`;
   h+=`<table><tr><th style="width:92px">日期</th><th>故障/工作</th><th>排查发现</th><th>处置</th><th>类别</th></tr>`;
@@ -3921,7 +3946,7 @@ async function loadDbase(){
           ${r.hist_loop?` · <span style="color:var(--ok)">${r.hist_loop.replace_date} 有过闭环 ${r.hist_loop.ratio}×</span>`:''}</td></tr>`;});
     h+=`</table>
       <p class="small" style="color:var(--muted);margin-top:8px">
-      <b>「动作」一列全场都是不可判</b>, 不是偷懒: ${BD.wo_bound}, 而判级窗在 2026 —
+      <b>「动作」一列全场都是不可判</b>, 不是偷懒: ${BD.wo_bound}, 而判级时间窗在 2026 —
       台账里的历史工单不可能是针对本次问题的, 用"有没有工单"判已派工会造出假的完成态。要填这一列须向现场要 25~26 工单。</p>`;
     if(BD.closed&&BD.closed.length){
       h+=`<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--bg);border:1px solid var(--line)">
@@ -4186,7 +4211,7 @@ function render(){
     h+=`<div class="kpis">
       <div class="kpi ${d.control&&d.control.avail>=95?'ok':'warn'}"><b>${d.control?d.control.avail:'—'}%</b><span>时间可用率(不含调度令) · ${d.win}</span></div>`;
     if(M8) h+=`<div class="kpi"><b>${M8.停机总时长h}h</b><span>停机时长 | ${M8.停机事件数} 起 (${d.win})</span></div>
-      <div class="kpi"><b>${M8.MTBF_h}h</b><span>平均停机间隔MTBO | 剔除口径 ${M8.剔除口径.MTBF_h}h (非行业MTBF)</span></div>`;
+      <div class="kpi"><b>${M8.MTBF_h}h</b><span>平均停机间隔(MTBO)| 剔除口径 ${M8.剔除口径.MTBF_h}h(非行业 MTBF)</span></div>`;
     if(LS['停机']!=null) h+=`<div class="kpi"><b>${Math.round(LS['停机']).toLocaleString()}</b><span>停机损失 MWh${LS['停机(调度令)']!=null?` (另调度令 ${Math.round(LS['停机(调度令)']).toLocaleString()})`:''}</span></div>`;
     if(LS['限电·绑定']!=null) h+=`<div class="kpi"><b>${Math.round(LS['限电·绑定']).toLocaleString()}</b><span>限电·绑定损失 MWh</span></div>`;
     h+=`</div>`;
@@ -4251,7 +4276,7 @@ function render(){
     <p class="small" style="color:var(--muted);margin:-2px 0 8px">口径 ≥1h 停机段, 不含调度令 —
     与"电量算账"里的停机时长${(function(){var v=d.control.energy&&d.control.energy.items.filter(function(x){return x.k==='停机'}).map(function(x){return x.h})[0];return (v===undefined||v===null)?'':' '+v+'h'})()} 不同,
     差额是 <b>短于 1h 的停机段</b> (本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。</p>
-    <table><tr><th>部件</th><th class="barcell">停机时长</th><th class="num">事件</th><th class="num">MTBO h</th><th class="num">MDT h</th><th class="num">影响台</th><th>典型失效模式 (按时长)</th></tr>`;
+    <table><tr><th>部件</th><th class="barcell">停机时长</th><th class="num">事件</th><th class="num">平均停机间隔(MTBO,h)</th><th class="num">单次停机时长(MDT,h)</th><th class="num">影响机组</th><th>典型失效模式 (按时长)</th></tr>`;
     tb.forEach(r=>{
       const w=Math.round(r.停机时长h/maxH*72);
       const sev=r.部件==='无记录停机'?'var(--unknown)':r.外部?'var(--info)':(r.占停机>=0.06?'var(--bad)':r.占停机>=0.03?'var(--warn)':'var(--ok)');
@@ -4311,7 +4336,7 @@ function render(){
       <div style="color:var(--muted)">两轴不可互相替代: "候选"说的是可信度不是设备好坏; "良好"说的是设备状态不是证据强度。设备状态只取自振动线的判级栏这一个来源。</div>
     </div>`;
     h+=`<table style="table-layout:fixed;width:100%"><colgroup><col style="width:7%">${'<col style="width:15.5%">'.repeat(6)}</colgroup>
-    <tr><th>机组</th>${th('振动',F.列窗.振动,'不随本窗')}${th('温度',F.列窗.温度)}${th('润滑柱塞',F.列窗.润滑,'只展示')}${th('油液',F.列窗.油液)}${th('报警',F.列窗.报警||'')}${th('工单',F.列窗.工单||'')}</tr>`;
+    <tr><th>机组</th>${th('振动',F.列窗.振动,'不随本时间窗')}${th('温度',F.列窗.温度)}${th('润滑柱塞',F.列窗.润滑,'只展示')}${th('油液',F.列窗.油液)}${th('报警',F.列窗.报警||'')}${th('工单',F.列窗.工单||'')}</tr>`;
     const MAXAL = Math.max(1, ...F.矩阵.map(r=>(r.报警||{}).n||0));
     const MAXWO = Math.max(1, ...F.矩阵.map(r=>(r.工单||{}).n||0));
     F.矩阵.forEach(r=>{
@@ -4367,7 +4392,7 @@ function render(){
           ${al.rank?`<span class="small" style="color:var(--muted)"> · 第${al.rank}/${al.tot}</span>`:''}
           ${bu?`<div class="small" style="color:var(--unknown)">⚠ ${(bu.share*100).toFixed(0)}% 是码${bu.code}抖动</div>`:''}
           ${(al.rank&&al.rank<=10)||bu?famBar(al.fam,al.n,MAXAL):''}</td>
-        <td class="${(wo.items||[]).length?'tipw':''}" data-tip="${esc((wo.items||[]).map(x=>`${x.date} ${x.name} → ${x.act}${x.part?' '+x.part:''}`).join('\\n') || '窗内无传动链工单')}" style="min-width:110px;white-space:pre-line">
+        <td class="${(wo.items||[]).length?'tipw':''}" data-tip="${esc((wo.items||[]).map(x=>`${x.date} ${x.name} → ${x.act}${x.part?' '+x.part:''}`).join('\\n') || '时间窗内无传动链工单')}" style="min-width:110px;white-space:pre-line">
           ${wo.n?`<b class="small">${wo.n} 单</b>${wo.last?`<span class="small" style="color:var(--muted)"> · ${wo.last}</span>`:''}
             <div style="height:7px;border-radius:3px;background:var(--muted);opacity:.5;margin-top:4px;width:${Math.max(6,Math.round(wo.n/MAXWO*90))}px"></div>
             <div class="small" style="color:var(--muted);margin-top:3px">${(wo.acts||[]).slice(0,2).map(a=>a.k+'×'+a.n).join(' · ')}</div>`
@@ -4429,7 +4454,7 @@ function render(){
       <td style="min-width:220px">${dtMini(r.部件,sev,null,r.turbine)}</td>
       <td><span class="duo" style="--c:${C[r.设备状态]||'var(--muted)'}" title="${r.设备状态||'?'}: ${ST_NOTE[r.设备状态]||''} | 证据 ${r.证据状态||'?'}: ${EV_NOTE[r.证据状态]||''} | 振动线原文: ${esc(r.振动结论)}">
         <span class="st">${r.设备状态||'—'}</span><span class="ev">${plain(r.振动结论)}</span></span></td>
-      <td style="min-width:130px">${winBar(r.窗区间,r.claim_window)}</td>
+      <td style="min-width:130px">${winBar(r.时间窗区间,r.claim_window)}</td>
       <td class="small tipw" data-tip="振动线原文: ${esc(r.振动信号)}">${vibPlain(r.振动信号)}</td><td style="min-width:158px">${devBar(r.温度通道,r.温度dev,r.温度判,r.温度轴)}</td>
       <td style="min-width:112px">${oilPill(r.油末样,r.油新鲜,r.油样轴)}</td></tr>`});
     h+=`</table></div>`;
@@ -4441,7 +4466,7 @@ function render(){
       // 关键一句仍在工单卡("0 条=记录缺失非未修过")与汇报定制页第⑧章口径与数据边界。
       h+=`<div class="card mb"><p class="small" style="color:var(--muted)"><b>时间窗 ${d.win}</b> · 顶部选择器可换</p></div>`;
       for(const [comp,E] of Object.entries(F.事件)){
-        h+=`<div class="card mb"><h3>${comp} — 报警 ${E.报警.总条数} 条/${E.报警.台数} 台 · 工单 ${E.工单.总条数} 条/${E.工单.台数} 台 (窗内)</h3>`;
+        h+=`<div class="card mb"><h3>${comp} — 报警 ${E.报警.总条数} 条/${E.报警.台数} 台 · 工单 ${E.工单.总条数} 条/${E.工单.台数} 台 (时间窗内)</h3>`;
         if(E.报警.总条数>0){
           h+=`<table><tr><th>码</th><th>报警内容</th><th class="num">条数</th><th class="num">台数</th><th class="num">累计时长h</th></tr>`;
           E.报警.码统计.forEach(c=>{h+=`<tr><td>${c.code}</td><td class="small">${c.text}</td><td class="num">${c.条数}</td><td class="num">${c.台数}</td><td class="num">${c.时长h}</td></tr>`});
@@ -4449,12 +4474,12 @@ function render(){
           h+=`<details><summary class="small">最近 ${E.报警.近况.length} 条明细</summary><table><tr><th>机组</th><th>时间</th><th>码</th><th>内容</th><th class="num">时长min</th></tr>`;
           E.报警.近况.forEach(r=>{h+=`<tr><td>${r.t.replace('WTG','')}#</td><td class="small">${r.on}</td><td>${r.code}</td><td class="small">${r.text}</td><td class="num">${r.dur_min??'—'}</td></tr>`});
           h+=`</table></details>`;
-        } else h+=`<p class="small">窗内无该部件报警</p>`;
+        } else h+=`<p class="small">时间窗内无该部件报警</p>`;
         if(E.工单.总条数>0){
           h+=`<table style="margin-top:8px"><tr><th>机组</th><th>日期</th><th>故障名称</th><th>处理</th><th>原因</th></tr>`;
           E.工单.明细.forEach(r=>{h+=`<tr><td>${r.t.replace('WTG','')}#</td><td class="small">${r.date}</td><td class="small">${r.name}</td><td class="small">${r.act}</td><td class="small">${r.cause}</td></tr>`});
           h+=`</table>`;
-        } else h+=`<p class="small">窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)</p>`;
+        } else h+=`<p class="small">时间窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)</p>`;
         h+=`</div>`;
       }
     }
@@ -4592,7 +4617,7 @@ function render(){
   if(TAB==='故障统计'){
     const _pt=(d.faults.partial||[]);
     h+=`<div class="eyebrow">故障统计与可靠性</div>`
-      +winBanner('故障统计', d.win, _pt.length?`<span style="color:var(--warn)">窗内 ${_pt.join('/')} 数据不满月(覆盖 ${Math.round(d.faults.cov_min*100)}%), 月度图已按天归一</span>`:'');
+      +winBanner('故障统计', d.win, _pt.length?`<span style="color:var(--warn)">时间窗内 ${_pt.join('/')} 数据不满月(覆盖 ${Math.round(d.faults.cov_min*100)}%), 月度图已按天归一</span>`:'');
   }
   if(TAB==='电量算账') h+=`<div class="eyebrow">电量算账</div>`+winBanner('电量算账', d.win, '');
   if(TAB==='汇报定制'){ document.getElementById('root').innerHTML=renderReport(d);
@@ -4611,17 +4636,17 @@ function render(){
   h+=sec('报警面', `${F2.total.toLocaleString()} 条 · ${F2.n_codes} 个码 · 窗 ${d.win}`);
   h+=`<div class="card" style="grid-column:1/-1"><h3>月度故障率</h3>${rateChart(F2.monthly)}
     <p class="small" style="color:var(--muted);margin-top:6px">
-    <b>柱高=日均条数, 不是月总数</b> — ${_pm.length?`本窗 ${_pm.join('/')} 数据不满月(灰虚线柱), 直接比月总数会读反`:'各月覆盖完整'}。
-    ${_pm.length?`实测: ${_pm[0]} 总数最低但日均 ${F2.monthly.per_day[F2.monthly.months.indexOf(_pm[0])]} 条/日 = 全窗前列。`:''}</p></div>`;
+    <b>柱高=日均条数, 不是月总数</b> — ${_pm.length?`本时间窗 ${_pm.join('/')} 数据不满月(灰虚线柱), 直接比月总数会读反`:'各月覆盖完整'}。
+    ${_pm.length?`实测: ${_pm[0]} 总数最低但日均 ${F2.monthly.per_day[F2.monthly.months.indexOf(_pm[0])]} 条/日 = 全时间窗前列。`:''}</p></div>`;
   h+=`<div class="card"><h3>帕累托 · 按发生条数</h3>
     ${paretoChart(F2.pareto_n, ' 条', '条数=发生频次, 不反映影响程度 — 高频短时报警码(信号抖动或重复触发)排序靠前')}</div>`;
   h+=`<div class="card"><h3>帕累托 · 按持续时长</h3>
     ${paretoChart(F2.pareto_d, ' h', '时长=闩锁口径(复位−激活), 只作排序参考不作停机时长')}</div>`;
   h+=`<div class="card" style="grid-column:1/-1"><h3>故障模式四象限 · 条数 × 单次时长</h3>
     ${quadChart(F2.quad)}
-    <p class="small" style="color:var(--muted);margin-top:6px">窗内共 ${F2.n_codes} 个码, 图上为条数前 ${F2.quad.length} 个。
+    <p class="small" style="color:var(--muted);margin-top:6px">时间窗内共 ${F2.n_codes} 个码, 图上为条数前 ${F2.quad.length} 个。
     ${_pm.length?`<span style="color:var(--warn)">窗含不满月 ${_pm.join('/')} (覆盖 ${Math.round(F2.cov_min*100)}%), 条数为原始累计未按天归一。</span> `:''}
-    点面积表示影响台数, 大点为批次共性、小点为单台; 竖分界取条数中位, 横分界取 10min (一个SCADA采样格)。
+    点面积表示影响机组数, 大点为批次共性、小点为单台; 竖分界取条数中位, 横分界取 10min (一个SCADA采样格)。
     <b>右下角的码不要按条数排优先级</b> — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。</p></div>`;
   const maxT=Math.max(1,...F2.per_t.map(x=>x.v));
   h+=`<div class="card"><h3>台级分布 · 报警条数 top${F2.per_t.length}</h3>
@@ -4750,13 +4775,16 @@ PAGE_PROBLEM = """<!doctype html><html lang="zh" data-theme="light"><head><meta
 <div id="one">加载中…</div>
 <footer>单问题视图: 只呈现本台该系统的判据与证据图, 不带出其他系统</footer>
 <script>%JS%
-const T='%T%', SYS='%SYS%';
+const T='%T%', SYS='%SYS%', SYSRAW='%SYSRAW%', SYSOK=('%SYSOK%'==='1');
 async function reload(win){
   const r=await fetch('/api/problem?t='+T+'&sys='+encodeURIComponent(SYS)+'&win='+encodeURIComponent(win)); const d=await r.json();
   document.getElementById('winbox').innerHTML=winSel(win);
   let h='';
-  if(!d.problems.length){h='<div class="card"><p class="empty">该系统在判级窗内无非常态。</p></div>'}
-  d.problems.forEach(p=>{
+  // ★2026-09-22 用户测试 3/5: 三种"空"必须分清 —— ①系统标识认不出(链接传错)②本时间窗确实无非常态
+  //   ③接口报错。以前三者都落到"该系统在判级时间窗内无非常态",对报警机组就是**假陈述**。
+  if(!SYSOK){h=`<div class="card"><p class="empty">系统标识「${SYSRAW}」无法识别(应为本场九系统之一,如 变桨/偏航/齿轮箱/发电机…)。请从「部件问题 → 系统卡 → 报警机组」的链接进入。</p></div>`}
+  else if(!d.problems || !d.problems.length){h=`<div class="card"><p class="empty">${SYS} 在所选时间窗内无非常态。</p></div>`}
+  else d.problems.forEach(p=>{
     h+=`<div class="card mb"><h3>${p.sys} ${chip(p.状态)}</h3><p style="margin-bottom:8px">${p.依据}</p></div>`;
     if(p.figs.length){h+=`<div class="grid">`; p.figs.forEach(f=>{h+=drawFig(f)}); h+=`</div>`}
     h+=ordersHtml(p.orders);
@@ -4776,14 +4804,14 @@ PAGE_TURBINE = """<!doctype html><html lang="zh" data-theme="light"><head><meta
 <select id="qagg"><option value="med">月中位</option><option value="dutyd">月duty(秒/日)</option><option value="daily">逐日中位</option></select>
 <button onclick="runQ()">出图</button><span class="small" id="qmsg"></span></div>
 <div id="qfig"></div></div>
-<footer>边界: 叶片/塔架无SCADA轴 | 对风误差=wrap(风向−机舱位置) vane口径; 死区/速率需≤30s数据=INSUFFICIENT | 判级窗: 变桨/偏航/蓄能/温度判级=近90日固定, 曲线=2025H2; 图内月序=所选时间窗 | P级归综合报告线</footer>
+<footer>边界: 叶片/塔架无SCADA轴 | 对风误差=wrap(风向−机舱位置) vane口径; 死区/速率需≤30s数据=INSUFFICIENT | 判级时间窗: 变桨/偏航/蓄能/温度判级=近90日固定, 曲线=2025H2; 图内月序=所选时间窗 | P级归综合报告线</footer>
 <script>%JS%
 const T='%T%';let WIN='2026年';
 async function reload(win){WIN=win;
   const r=await fetch('/api/turbine?t='+T+'&win='+encodeURIComponent(win)); const d=await r.json();
   document.getElementById('winbox').innerHTML=winSel(win);
   let h='';
-  if(!d.problems.length) h='<div class="card"><p class="empty">各系统在判级窗内均无非常态 —— 可用下方即时查询查看任意契约通道。</p></div>';
+  if(!d.problems.length) h='<div class="card"><p class="empty">各系统在判级时间窗内均无非常态 —— 可用下方即时查询查看任意契约通道。</p></div>';
   d.problems.forEach(p=>{
     h+=`<div class="card mb"><h3>${p.sys} ${chip(p.状态)}</h3><p class="small" style="margin-bottom:8px">${p.依据}</p>`;
     if(p.figs.length){h+=`<div class="grid">`; p.figs.forEach(f=>{h+=drawFig(f)}); h+=`</div>`}
@@ -4807,7 +4835,14 @@ class H(BaseHTTPRequestHandler):
         if 'text/html' in ctype and not self.path.startswith('/v2'):   # 2026-09-06 用户批: /v2 = detail_style_baseline, 不注入 unified-review 样式/导航条 (基线令牌须等于门户)
             from src.windscada.design import GUANLAN_REVIEW_CSS
             text = body.decode('utf-8') if isinstance(body, bytes) else body
-            text = text.replace('</header>', '</header><nav class="action-path" aria-label="页面操作"><a href="http://127.0.0.1:18084/观澜_如东样板_门户_单文件.html#index" target="_top">← 返回观澜门户</a><a href="/">分析总览 / 重新选择</a></nav>', 1)
+            # ★2026-09-22 用户测试 4 实逮: 这里原来写死 `http://127.0.0.1:18084/观澜_如东样板_门户_单文件.html`
+            #   —— 用户从别的机器点「返回观澜门户」等于访问**自己机器的 loopback**,且那文件在服务端并不存在
+            #   ⇒ 网关回 {"err":"not found","path":"/观澜_…html"}。
+            #   修法: 用根相对 `/`(门户就在网关根)+ `data-abs="1"` 让**网关不要按组件前缀改写它**
+            #   (不加标记会被改写成 /detail/,永远回不到门户 —— 见 guanlan_gateway.rewrite 的说明)。
+            text = text.replace('</header>', '</header><nav class="action-path" aria-label="页面操作">'
+                                '<a href="/" data-abs="1" target="_top">← 返回观澜门户</a>'
+                                '<a href="/detail/v2" data-abs="1">分析总览 / 重新选择</a></nav>', 1)
             if '</head>' in text:
                 body = text.replace('</head>', '<style id="guanlan-unified-review-v1">' + GUANLAN_REVIEW_CSS + '</style></head>', 1)
         b = body.encode('utf-8') if isinstance(body, str) else body
@@ -4932,10 +4967,19 @@ class H(BaseHTTPRequestHandler):
             elif u.path.startswith('/problem/'):
                 import urllib.parse as _up
                 _, _, t, sysn = u.path.split('/', 3)
+                _raw = _up.unquote(sysn)
+                _zh = sys_norm(_raw)
+                # ★2026-09-22 用户测试 3/5: 认得出就换成规范中文名(页面/接口都按中文名取数),
+                #   认不出就把原文交给页面 —— 页面据此**如实说"系统标识无法识别"**,
+                #   绝不写成"该系统在判级时间窗内无非常态"(那对报警机组是假陈述)。
                 self._send(PAGE_PROBLEM.replace('%CSS%', BASE_CSS).replace('%JS%', CHART_JS)
-                           .replace('%T%', t).replace('%SYS%', _up.unquote(sysn)))
+                           .replace('%T%', t).replace('%SYS%', _zh or _raw)
+                           .replace('%SYSRAW%', _raw).replace('%SYSOK%', '1' if _zh else '0'))
             elif u.path == '/api/problem':
-                self._send(_jdump(dict(problems=single_problem(q['t'], q['sys'], win)), ensure_ascii=False), 'application/json; charset=utf-8')
+                _zh = sys_norm(q['sys']) or q['sys']
+                self._send(_jdump(dict(problems=single_problem(q['t'], _zh, win), sys=_zh,
+                                       sys_ok=bool(sys_norm(q['sys']))), ensure_ascii=False),
+                           'application/json; charset=utf-8')
             elif u.path == '/api/turbine':
                 self._send(_jdump(dict(problems=turbine_problems(q['t'], win)), ensure_ascii=False), 'application/json; charset=utf-8')
             elif u.path == '/api/query':
@@ -5033,7 +5077,7 @@ class H(BaseHTTPRequestHandler):
                     # ★系统名两种形态都要烤 (2026-09-01 实逮"点开是空浮层")。
                     #   systems 的分组键带**等级后缀** (`塔架与基础·良好`), 而页面上
                     #   `/problem/<t>/<sys>` 链接发出的是**裸系统名** (`塔架与基础`) ——
-                    #   于是 key 差一截, 取不到数, 浮层开了但里面是"本窗无记录"。
+                    #   于是 key 差一截, 取不到数, 浮层开了但里面是"本时间窗无记录"。
                     #   **这种失效最难发现: 浮层确实弹出来了, 看着像"这台这系统没数据",
                     #   而不像"key 错了"。** 两种形态都烤成同一份数据即可。
                     for _t, _sy in sorted(_pairs):
@@ -5109,7 +5153,7 @@ class H(BaseHTTPRequestHandler):
                         #   __NK 把参数**按字母排序**, 所以正确形态是 `?lang=en&sys=…&t=…&win=…`;
                         #   而拼接得到的是 `?sys=…&t=…&win=…&lang=en` —— 差一个顺序, 全部 miss。
                         #   后果极隐蔽: 十个标签页照常显示 (它们命中的是**不带 lang** 的那份),
-                        #   只有**点开下钻**才走带 lang 的 key ⇒ 浮层弹出来却是"本窗无记录",
+                        #   只有**点开下钻**才走带 lang 的 key ⇒ 浮层弹出来却是"本时间窗无记录",
                         #   看着像"这台这系统没数据", 不像 key 错了。用户报的"有些点不开"就是它。
                         def _with_lang(k):
                             base, _, q = k.partition('?')
@@ -5175,7 +5219,7 @@ class H(BaseHTTPRequestHandler):
                               #   所以回落到默认窗是**正确答案**而不是遮丑。
                               # ★同 src/windscada/ui/snapshot.py 的坑: 这里不能 encodeURIComponent —— k 与
                               #   __SNAP__ 键都是原文(未编码)中文, 编码后拼出的键谁都不匹配, 兜底静默失效
-                              #   (2026-09-03 实逮: 换窗点浮层报"本窗无记录", 默认窗其实有数据)。
+                              #   (2026-09-03 实逮: 换窗点浮层报"本时间窗无记录", 默认窗其实有数据)。
                               'var c=[k,k.replace(/&?win=[^&]*/,"").replace(/\\?$/,""),String(u).split("?")[0],'
                               'k.replace(/win=[^&]*/,"win="+(window.__DEFWIN||""))];'
                               'for(var i=0;i<c.length;i++){var d=window.__SNAP__[c[i]];'
@@ -5197,7 +5241,7 @@ class H(BaseHTTPRequestHandler):
                     #   开浮层, 图表复用共享的 drawFig, 与在线版看到的是同一批图。
                     # ★注入块里的文案要**自己**按语言选: 它加在 _en_page 之后,
                     #   英文化那一遍已经跑完了, 这里的中文字面量永远不会被翻译。
-                    #   实逮: 英文页浮层里弹出"本窗无记录"。
+                    #   实逮: 英文页浮层里弹出"本时间窗无记录"。
                     #   ('状态'/'依据' 是 JSON 键, '年' 是窗名数据值 —— 都不能动。)
                     inject += ('<script>(function(){var M=null;'
                                'function esc(s){return String(s==null?"":s).replace(/[&<>]/g,function(c){'
@@ -5220,7 +5264,7 @@ class H(BaseHTTPRequestHandler):
                                '+"<h3 style=\'margin:0 0 4px\'>"+esc(t)+(sys?" · "+esc(sys):"")+"</h3>"'
                                '+\'<button onclick="window.__CLOSEM()" style="background:none;border:1px solid \'' 
                                '+\'var(--line,#22303d);color:inherit;border-radius:6px;padding:2px 9px;cursor:pointer">×</button></div>\';'
-                               'if(!ps.length)h+=\'<p class="small">' + ('No records in this window' if _lang == 'en' else '本窗无记录') + '</p>\';'
+                               'if(!ps.length)h+=\'<p class="small">' + ('No records in this window' if _lang == 'en' else '本时间窗无记录') + '</p>\';'
                                'ps.forEach(function(p){h+=\'<div class="card mb"><b>\'+esc(p.sys)+\'</b> · \'+esc(p["状态"])'
                                '+\'<p class="small">\'+esc(p["依据"])+\'</p>\';'
                                'if(p.figs&&p.figs.length&&typeof drawFig==="function"){h+=\'<div class="grid">\';'

+ 43 - 7
src/ontology/fast_agent.py

@@ -344,7 +344,8 @@ LANG_EN = (
     'they appear. Every conclusion sentence must end with a source tag like [object/id].')
 
 
-BUDGET_S = {'qwen3:8b': 30, 'qwen3:30b-a3b': 30, 'qwen3.8:27b': 60}   # 用户令 2026-09-07: 复杂题最多 30 s; 其余档不设 (27B/r1 本来就慢, 属"离线慢审"用途)
+BUDGET_S = {'qwen3:8b': 30, 'qwen3:30b-a3b': 30, 'qwen3:32b': 120}   # 用户令 2026-09-07: 复杂题最多 30 s;
+# ★2026-09-22: 升档目标改为本机实装的 qwen3:32b(18.8 GB 权重, 单轮数分钟 ⇒ 给 120 s 预算, 属"离线慢审")。
 
 
 def answer(q, model='qwen3:8b', max_rounds=5, on_step=None, lang='zh', budget_s=None):
@@ -685,25 +686,60 @@ def answer(q, model='qwen3:8b', max_rounds=5, on_step=None, lang='zh', budget_s=
 # ── 自动升档 (用户令 2026-09-07 "复杂题 8B 拦了自动升 a3b 重答") ──
 # 8B 在全场类复杂题上会写退化稿 (实测 "无直接证据[对象id]"), 闸拦得对但用户拿不到答案; a3b (30B 参数 / 3B 激活) 单轮 ~20 s 且质量接近 27B.
 # 规则: 默认档被闸拦下 → 用升档目标重答一次; 升档也拦 → 照旧不出文, 但把两次拦截原因都带回.
-ESCALATE = {'qwen3:8b': 'qwen3.8:27b'}   # v16 实测 a3b 当升档目标不合格 (元话语/不引契约/2 拦); 27B 六问 6/6 准确, 升档只在 8B 被拦时走, 准确优先 (预算 60 s)
+ESCALATE = {'qwen3:8b': 'qwen3:32b'}   # ★2026-09-22 用户人工测试 7 实逮: 原写 qwen3.8:27b, 本机**没这个 tag**
+#   ⇒ 升档调用 digest=None、ask 终态 error(用户看到"复核中"卡住)。改为本机实装的 32B(18.8 GB 权重, 预算见 BUDGET_S)。
 
 
-def interim_text(q, first_gate=''):
+def installed_models(timeout=5):
+    """本机 Ollama 已装模型名集合(拿不到就返回空集 —— 绝不影响主流程)。"""
+    import json as _j
+    import urllib.request as _u
+    try:
+        with _u.urlopen('http://127.0.0.1:11434/api/tags', timeout=timeout) as r:
+            d = _j.loads(r.read().decode('utf-8'))
+        return {m.get('name', '') for m in (d.get('models') or [])}
+    except Exception:
+        return set()
+
+
+def escalate_target(model):
+    """升档目标解析: ①`configs/models.json` 的 escalate_qa(且**确已安装**)→ ②本文件表 → ③None。
+
+    为什么要"确已安装": 档位写了一个本机没装的模型时,升档必然失败,而用户只看到一句"复核中"。
+    配置可换模型,但换错必须**响亮回落**,不能静默把答案卡死。
+    """
+    try:
+        import json as _j
+        from src import paths as _P
+        _cand = (((_j.loads(_P.config('models.json').read_text(encoding='utf-8')).get('tiers') or {})
+                  .get('escalate_qa') or {}).get('model'))
+        inst = installed_models()
+        if _cand and (not inst or _cand in inst):
+            return _cand
+        if _cand:
+            print(f'[escalate] 配置档位 {_cand} 本机未安装, 回落到 {ESCALATE.get(model)}', flush=True)
+    except Exception:
+        pass
+    return ESCALATE.get(model)
+
+
+def interim_text(q, first_gate='', nxt=''):
     """先快后准 (用户裁 2026-09-07): 默认档被拦后 30 s 内先给用户看的"初答" —— 不含被拦的错稿, 只列契约里与本题最相关的结论 (机器直出, 契约来源),
-    并明说正在复核. 27B 出来后整段替换."""
+    并明说正在复核. 升档模型出来后整段替换。"""
     refs = contract_refs(q, k=3)
     lines = [f"- 【{c['verdict']}】{(c.get('display') or {}).get('title_zh', '')[:110]} [claim/{c['claim_id']}]" for c in refs]
-    return ('【初答 · 复核中】默认档 (8B) 的回答未通过校闸' + (f' ({first_gate[:60]})' if first_gate else '') + ', 已交更强模型 (27B) 复核, 约 1–3 分钟后本段会被替换。\n'
+    return ('【初答 · 复核中】默认档 (8B) 的回答未通过校闸' + (f' ({first_gate[:60]})' if first_gate else '')
+            + f', 已交更强模型 ({nxt or "升档档"}) 复核, 约 1–3 分钟后本段会被替换。\n'
             '契约里与本问题最相关的已审级结论 (机器直出, 非模型生成):\n' + ('\n'.join(lines) if lines else '- (契约里没有直接相关的结论)'))
 
 
 def answer_escalate(q, model='qwen3:8b', max_rounds=5, on_step=None, lang='zh', on_partial=None):
     r = answer(q, model, max_rounds=max_rounds, on_step=on_step, lang=lang)
-    nxt = ESCALATE.get(model)
+    nxt = escalate_target(model)
     if not r.get('blocked') or not nxt:
         r.setdefault('model', model); return r
     if on_partial:
-        try: on_partial(interim_text(q, r['gate']))
+        try: on_partial(interim_text(q, r['gate'], nxt))
         except Exception: pass
     if on_step:
         try: on_step('升档', f'{model} 校闸未过 → {nxt} 重答')

+ 31 - 1
src/version.py

@@ -19,7 +19,7 @@ import json
 import pathlib
 
 NAME = '观澜·如东样板 v2'
-VERSION = '2.9.0'          # ★ 改版本只改这里
+VERSION = '2.9.1'          # ★ 改版本只改这里
 EDITION = 'offline-single-package'
 PACKAGE_STEM = 'app_guanlang'           # 交付包文件名前缀(用户令 2026-09-17)
 
@@ -49,6 +49,36 @@ BUMP_RULE = ('改动落在"解决方案/架构/核心功能" → 大 +1(中/
 #   level: major/minor/patch 表示这一版**相对上一版**是哪一级变化;legacy 表示该版用的是
 #   旧编号体系(0.x,未按本规则),仅作历史对账用。
 HISTORY: tuple[dict, ...] = (
+    dict(version='2.9.1', date='2026-09-22', level='patch',
+         title='人工测试 7 项消缺(描述口径 + 依据空白 + 问题页空 + 门户链接 404 + 闭环文案 + Ollama 档位)',
+         note='纯消缺(无功能增减)⇒ 小版本 +1。逐项「检查→根因→解决→验证」: '
+              '① 描述口径: 「窗」确实指时间窗者一律写「时间窗」(预设窗→预设时间窗、判级窗→判级时间窗、'
+              '窗内→时间窗内、证据窗→证据时间窗、随所选窗→随所选时间窗…;天气窗/作业窗/预览窗/观测窗'
+              '属领域词,保持原样);「影响台」→「影响机组」(含报告构建器表头);英文简写改「中文(英文简写)」'
+              '(平均停机间隔(MTBO)/平均无故障间隔(MTBF)/单次停机时长(MDT)),英文映射键同步; '
+              '② 需要关注「查看完整依据」空白: 根因=行的来源是**融合面链盘**(d.fus.链盘.rows),依据却只从 '
+              'SCADA 的 d.watch 查 ⇒ 两边机组集合不同时 why[t] 为空(实测 WTG09 齿轮箱 bad 台)。'
+              '改为按本行字段自组句(部件/链路进度/卡在哪步/证据源),永不空; '
+              '③ 报警机组「该问题页」打开空白 + ⑤ 命中系统链接: 根因=链接传**slug**(pitch)而页面/接口按'
+              '**中文系统名**取数 ⇒ /api/problem 返回 0 条,页面还写"该系统在判级窗内无非常态"(对报警机组是假陈述)。'
+              '改为: 链接传中文名 + 服务端 `sys_norm()` 双向归一(slug/中文/大小写都认)+ 页面区分'
+              '"系统标识无法识别 / 本时间窗无异常"两者,绝不把认不出说成正常; '
+              '④ /turbine 页「返回观澜门户」→ 原写死 `http://127.0.0.1:18084/观澜_如东样板_门户_单文件.html`'
+              '(用户机器上等于访问自己的 loopback,且该文件服务端没有)⇒ 网关回 not found。'
+              '修法: 用根相对 `/`,并给**网关加 `data-abs="1"` 豁免**(作者显式声明某链接不按组件前缀改写,'
+              '否则根相对链接一律被改写成 /detail/… 永远回不到门户),普通链接前缀行为不变(有单测); '
+              '⑥ 振动「端到端闭环」文案: 去掉内部黑话 handoff,改为'
+              '「暂无端到端闭环证据:现场提交的振动数据包里没有「报警→检修→复测」的成对记录」; '
+              '⑦ 观澜↔本机 Ollama: 链路实测可用(探针 OK、/api/ask 提交成功、8B 出答但**未过校闸**→触发升档),'
+              '但升档目标**硬编码**在 `src/ontology/fast_agent.py` 的 `ESCALATE`(写的是本机没装的 `qwen3.8:27b`,'
+              '`configs/models.json` 只是同处笔误的另一份)⇒ 升档 digest=None、ask 终态 error(用户只看到"复核中")。'
+              '修法: ESCALATE 改 `qwen3:32b` + 新增 `installed_models()`/`escalate_target()` —— 升档目标按'
+              '「配置档位且**确已安装**」解析,没装则**响亮回落**并打日志,不再把答案静默卡死;'
+              '初答文案里的"27B"改为实际目标名;BUDGET_S 随档位更新(32B 给 120 s)。'
+              '验证: `escalate_target("qwen3:8b") → qwen3:32b`(本机实装 4 档实测);'
+              '另: 远端 106.120.102.238 与本地各跑一遍 HTTP 抽样(门户链接=/、slug 归一出 1 条问题、'
+              '文案含时间窗/影响机组/(MTBO)、闭环新文案、WTG09 依据不再空),网关改写 `data-abs` 豁免单测通过,'
+              'config_audit/pages_audit/反向审计/raw_scan selftest 全 rc=0,`guanlan.py check` 全绿'),
     dict(version='2.9.0', date='2026-09-21', level='minor',
          title='时间窗真正生效(用户令):「部件问题」与「发电性能」随所选窗重算,并新增自定义起止日期窗',
          note='功能增减改 ⇒ 中版本 +1。要点: '

+ 1438 - 1438
src/windscada/i18n_en.py

@@ -1,1438 +1,1438 @@
-# -*- coding: utf-8 -*-
-"""中→英 翻译层 (2026-08-28 用户令: 英文版系统, 给东南亚客户).
-
-不是通用机翻 —— 是这套系统的**领域词表**。做法上有三条硬约束:
-
-1. **最长匹配优先**。"报警级"必须先于"报警"匹配, 否则译出 "Alarm级";
-   "主轴承温度" 先于 "主轴承"。词表按 key 长度倒序编译成一条正则。
-2. **术语用行业通行译法, 不直译**。等效满发小时=EFLH 不是 "equivalent full hair";
-   限电=curtailment 不是 "power limit"; 定论=Confirmed 不是 "final verdict"。
-3. **数字与标识符不动**。台号 WTG29、报警码 3225、单位 MWh/kW/h 原样保留。
-
-覆盖策略: 先 UI 骨架(导航/标题/表头/按钮/指标名), 再判据与状态词, 再说明文字。
-未覆盖的中文会在 audit_untranslated() 里被列出来 —— 残留必须收敛到零, 因为
-给外方客户看的页面里出现半句中文, 比整页中文更糟。
-"""
-import re
-
-# ── 1 导航与页面骨架 ──
-NAV = {
-    '观澜': 'Guanlan', '如东海上': 'Offshore Fleet',
-    '风电智能分析决策': 'Wind Fleet Analytics and Decision Support',
-    '风电智能': 'wind fleet intelligence',
-    '总览': 'Overview', '电量算账': 'Energy Accounting', '部件问题': 'Component Issues',
-    '振动融合分析': 'Vibration Fusion', '发电性能': 'Performance Curves',
-    '故障统计': 'Fault Analytics', '检修决策': 'Maintenance Decisions',
-    '检修助手': 'Maintenance Assistant', '汇报定制': 'Report Builder', '系统维护': 'Data Sources',
-}
-
-# ── 2 设备状态与判级 (windcms 两套并行体系) ──
-STATE = {
-    '危险': 'Critical', '报警级': 'Alarm level', '报警': 'Alarm', '良好': 'Good', '优秀': 'Excellent',
-    '不可判': 'Indeterminate', '监测不可用': 'Monitoring unavailable', '无监测': 'Not monitored',
-    '定论·双模审': 'Confirmed · dual-model review', '定论': 'Confirmed',
-    '准定论·预警': 'Near-confirmed · warning', '准定论': 'Near-confirmed',
-    '预警·处置升急': 'Warning · escalate', '预警·处置规划': 'Warning · plan action', '预警': 'Warning',
-    '候选·监视(复发)': 'Candidate · watch (recurring)', '候选·跟踪': 'Candidate · tracking',
-    '候选观察': 'Candidate / watch', '候选': 'Candidate',
-    '销案(定论)': 'Closed (confirmed)', '销案正常': 'Closed · normal', '销案': 'Closed',
-    '监视(不开箱)': 'Watch (no teardown)', '监视': 'Watch', '观察(发电机侧)': 'Watch (generator side)',
-    '观察': 'Watch', '参考': 'Reference', '未列': 'Not covered', '未收录': 'not covered',
-    '正样本': 'Positive control', '仲裁输入': 'Arbitration input', '弱同向新证': 'Weak concurring evidence',
-    '本体无新证': 'No new evidence on component', '复发候选(两轴合并)': 'Recurrence candidate (two axes merged)',
-    '温度单轴保留': 'Temperature axis retained', '传感器项': 'Sensor issue', '可降': 'Downgradable',
-    '降观察': 'Downgraded to watch', '在升': 'rising', '未闭环': 'not closed', '已闭环': 'closed loop',
-    '筛查级': 'screening level', '双模审': 'dual-model review', '审级': 'review tier',
-    'lite审': 'lite review', '设备状态': 'Equipment state', '证据状态': 'Evidence state',
-}
-
-# ── 3 部件与系统 ──
-PART = {
-    '主轴承(后)+集中润滑泵': 'Main bearing (rear) + centralised lubrication pump',
-    '齿轮箱二级行星内齿圈': 'Gearbox 2nd-stage planetary ring gear',
-    '齿轮箱谱线级未映射到部件': 'Gearbox · spectral line not mapped to component',
-    '发电机引用同族见族主台': 'Generator · family reference, see lead unit',
-    '主轴承后集中润滑泵': 'Rear main-bearing lubrication pump',
-    '齿轮箱高速轴': 'Gearbox high-speed shaft', '齿轮箱中间轴': 'Gearbox intermediate shaft',
-    '主控与传感网': 'Main controller and sensor network', '变桨与液压': 'Pitch and hydraulics',
-    '塔架与基础': 'Tower and foundation', '塔架/结构': 'Tower / structure', '轮毂/叶片': 'Hub / blades',
-    '叶片与叶根': 'Blades and blade roots', '集中润滑泵': 'Centralised lubrication pump',
-    '电网/供电(外部)': 'Grid supply (external)', '风况(外部)': 'Wind conditions (external)',
-    '无记录停机': 'Unattributed downtime', '运维操作': 'OandM actions', '未归类': 'Unclassified',
-    '主轴承': 'Main bearing', '齿轮箱': 'Gearbox', '发电机': 'Generator', '变流器': 'Converter',
-    '变桨': 'Pitch', '偏航': 'Yaw', '叶片': 'Blade', '塔架': 'Tower', '主控': 'Main controller',
-    '轮毂': 'Hub', '油缸': 'Cylinder', '油泵': 'Pump', '测点': 'measurement point',
-}
-
-# ── 4 指标与口径 ──
-METRIC = {
-    '等效满发小时': 'Equivalent full-load hours', '时间可用率': 'Time-based availability',
-    '平均停机间隔': 'Mean time between outages', '上网电量': 'Net exported energy',
-    '理论可发': 'Gross available energy', '实际上网': 'Net exported', '损失率': 'Loss ratio',
-    '需行动机组': 'Units requiring action', '停机时长': 'Downtime hours', '停机事件数': 'Outage count',
-    '设备类停机': 'Equipment downtime', '外部类停机': 'External downtime', '总停机': 'Total downtime',
-    '影响台数': 'Units affected', '事件数': 'Events', '停机': 'Downtime', '损失': 'Loss',
-    '限电·绑定': 'Curtailment (bound)', '限电命令·未绑定': 'Curtailment order (unbound)',
-    '停机(调度令)': 'Dispatch-ordered stop', '低风待机': 'Low-wind standby', '正常发电': 'Normal production',
-    '限电': 'Curtailment', '工况态': 'Operating state', '占比': 'share', '时长': 'duration',
-    '条数': 'count', '次数': 'count', '台数': 'units', '全场': 'fleet', '本台': 'this unit',
-    '数据覆盖': 'data coverage', '不满月': 'partial month', '窗内': 'in window', '窗末': 'window end',
-    '证据窗': 'evidence window', '判级窗': 'grading window', '时间窗': 'Time window', '口径': 'basis',
-}
-
-# ── 5 动作与流程 ──
-ACTION = {
-    '从证据到验收的决策链': 'decision chain from evidence to verification',
-    '决策链进度盘': 'Decision chain status', '决策链': 'Decision chain',
-    '证据': 'Evidence', '机制': 'Mechanism', '判级': 'Grading', '排期': 'Scheduling',
-    '动作': 'Action', '验收': 'Verification', '卡在': 'held at', '下一步': 'Next step',
-    '抢修': 'Corrective', '预防性维护': 'Preventive', '故障树': 'Fault tree', '排程': 'Scheduling',
-    '工单': 'work order', '换件': 'replacement', '更换': 'replace', '检修': 'maintenance',
-    '闭环': 'closed loop', '端到端闭环': 'end-to-end closed loop', '派工': 'dispatch work',
-    '现场': 'site', '台账': 'ledger', '复测': 'retest', '核实': 'verify', '待审批': 'pending approval',
-}
-
-# ── 6 高频术语补齐 (2026-08-28 按渲染后残留统计倒排补, 长词在前靠正则长度排序保证) ──
-MORE = {
-    # 测量与证据面
-    '振动结论': 'vibration verdict', '振动信号': 'vibration signature', '振动线转录': 'from vibration line',
-    '振动线未收录': 'not covered by vibration line', '振动线': 'vibration line', '振动分析': 'Vibration analysis',
-    '振动': 'vibration', '温度通道': 'temperature channel', '温度轴': 'temperature axis',
-    '温度判': 'temperature verdict', '温度候选': 'temperature candidate', '温度': 'temperature',
-    '润滑柱塞': 'lubrication plunger', '润滑': 'lubrication', '油末样': 'last oil sample',
-    '油新鲜': 'oil sample fresh', '油样轴': 'oil sample axis', '油液': 'oil analysis',
-    '油脂水平低': 'grease level low', '油路与密封': 'oil circuit and seals', '末样': 'last sample',
-    '无样': 'no sample', '取样': 'sampling', '化验': 'laboratory analysis', '华标': 'lab',
-    # 窗与统计
-    '窗区间': 'window range', '判级窗': 'grading window', '单次循环时长': 'cycle duration',
-    '单次循环': 'cycle', '逐月': 'monthly', '月度': 'monthly', '现水平': 'current level',
-    '轨迹': 'trend', '中位': 'median', '样本': 'sample', '总条数': 'total events',
-    '记基线观察': 'baseline recorded, watching', '记基线': 'baseline recorded',
-    '窗至': 'window through', '未提供': 'not provided', '数据时点': 'data as of',
-    '个月': ' months', '天数': 'days', '占时': 'time share', '累计': 'cumulative',
-    # 机制与判据
-    '根因定到部件与失效模式': 'root cause identified to component and failure mode',
-    '取样把根因定到部件与失效模式': 'sample to identify root cause to component and failure mode',
-    '机制不清则排期与动作都无依据': 'without a mechanism, neither scheduling nor action has a basis',
-    '同一判据是否回落': 'whether the same criterion falls back',
-    '历史单不对应本次问题': 'historical orders do not correspond to this finding',
-    '本轮动作执行后复测': 'retest after this round of action',
-    '须向现场核实': 'requires site verification', '未定性': 'not characterised', '定性': 'characterisation',
-    '未排': 'not scheduled', '未验': 'not verified', '已达': 'complete', '全通': 'all clear',
-    '两者非同源': 'the two are not from the same source', '负面对照': 'negative control',
-    '代理轴': 'proxy axis', '判别器': 'discriminator', '判据': 'criterion', '判': 'verdict',
-    # 部件细分
-    '叶根螺栓': 'blade-root bolts', '换栓': 'bolt replacement', '螺栓断裂': 'bolt fracture',
-    '非螺栓断裂确诊': 'not a confirmed bolt fracture', '松动嫌疑': 'suspected loosening', '松动': 'loosening',
-    '塔筒湿度': 'tower humidity', '湿度高告警': 'high-humidity alarm', '蓄能与压力调节': 'accumulator and pressure control',
-    '蓄能': 'accumulator', '液压': 'hydraulics', '执行与位置反馈': 'actuation and position feedback',
-    '驱动': 'drive', '轴承与': 'bearing and', '轴承': 'bearing', '逆变器通讯错误': 'inverter communication error',
-    '点名该叶断裂': 'names this blade as fractured', '叶断裂': 'blade fracture',
-    # 报表与流程
-    '评估报告': 'assessment report', '分册另含': 'the volume also contains', '沙盘': 'scenario model',
-    '情景': 'Scenario', '排查': 'investigation', '处置': 'action', '步骤': 'steps',
-    '明细': 'detail', '综合': 'combined', '近况': 'recent status', '逐台情况': 'per-unit status',
-    '融合矩阵': 'fusion matrix', '矩阵': 'matrix', '链盘': 'chain board', '能力边界': 'capability limits',
-    '能力': 'capability', '色标': 'colour key', '收录台': 'units covered', '机制链': 'mechanism chain',
-    '用户令': 'per client request', '语义类': 'semantic class', '四维': 'four-source',
-    '风险': 'risk', '双轴': 'dual-axis', '外部': 'external', '记录': 'record', '源': 'source',
-    '表': 'table', '级': ' level', '窗': 'window', '次': '', '条': '', '台': ' units',
-    '第': 'No.', '月': '-', '日': '', '年': '', '前': 'fore', '后': 'aft', '侧': ' side',
-    '与': ' and ', '无': 'none', '优': ' OK', '单': '', '末': 'last', '进': 'into',
-    '部件': 'component', '机组': 'unit', '正常': 'normal', '本轮': 'this round',
-    '不清则': 'without it, ', '都无依据': 'there is no basis', '执行后': 'after execution',
-}
-def _assert_quote_free():
-    """译文不得含引号或反引号 (2026-08-28 活逮): 译文会被注入 JS 字符串与 HTML 属性,
-    一个撇号就让整块解析失败, 表现是"页面全空但每个 script 块单独检查都通过" — 极难定位。
-    构建期直接拦, 不留到运行时。"""
-    bad = {k: v for k, v in TERMS.items()
-           if isinstance(v, str) and any(c in v for c in ('\'', '"', '`', '\\', '&', '<', '>'))}
-    if bad:
-        raise ValueError(f'译文含引号/反斜杠, 会破坏注入上下文: {list(bad)[:5]}')
-
-
-GROUPS = [NAV, STATE, PART, METRIC, ACTION]
-TERMS = {}
-for g in GROUPS:
-    TERMS.update(g)
-TERMS.update(MORE)
-# ── 8 第一阶段批量 (2026-08-28, 按渲染后残留频次倒排, 长词优先写以免被短词截断) ──
-TERMS.update({
-    # 长复合词组先写 (正则按长度排序, 这里的顺序只影响可读性)
-    '出带且过锚才算离群': 'outlier only if outside the band and past the anchor',
-    '同型机群残差': 'residual vs same-type fleet', '物理绝对锚': 'physical absolute anchor',
-    '四分位残差带': 'inter-quartile residual band', '控制参数一致性': 'Control-parameter consistency',
-    '只作回测锚非现时': 'back-test anchor only, not current-state evidence',
-    '不足或监测中断': 'insufficient or monitoring interrupted',
-    '旁路过滤器齿': 'bypass filter · gearbox', '油压力高激活': 'oil pressure high · activated',
-    '油压力过高': 'oil pressure too high', '各油水位检': 'oil/water level checks',
-    '变压器绕组最高': 'transformer winding max', '变压器油温': 'transformer oil temperature',
-    '变压器室': 'transformer room', '集线器通信错误': 'hub communication error',
-    '同油温档带宽': 'bandwidth within the same oil-temperature bin',
-    '就地操作模式': 'local operation mode', '电网故障穿越': 'grid fault ride-through',
-    '转速传感器故障': 'speed sensor fault', '电池电压低': 'battery voltage low',
-    '电网停电': 'grid outage', '降功率运行': 'derated operation', '此速率触': 'at this rate triggers',
-    '测风故障计数': 'anemometer fault count', '风速计偏置': 'anemometer bias',
-    '测风计数': 'anemometer count', '采样时间线': 'sampling timeline',
-    '不随上方': 'independent of the selector above', '时可作现时': 'usable as current evidence when',
-    '轻微偏离': 'slight deviation', '新出现待': 'newly appeared, pending',
-    '基本确认': 'largely confirmed', '可继续使用': 'may remain in service',
-    '机械刹车': 'mechanical brake', '油鼓风机': 'oil blower', '柜排风': 'cabinet exhaust',
-    '塔振模块': 'tower vibration module', '电气滑环': 'electrical slip ring', '滑环故障': 'slip-ring fault',
-    '缸检超时': 'cylinder check timeout', '缸超时': 'cylinder timeout', '比例阀': 'proportional valve',
-    '通讯故障': 'communication fault', '油位过低': 'oil level too low', '油位低': 'oil level low',
-    '自动重启': 'auto restart', '执行中': 'in progress', '双温差': 'dual temperature difference',
-    '频发易修': 'frequent but quick to fix', '罕发难修': 'rare but slow to fix',
-    '等效满发': 'equivalent full-load', '叶尖速比': 'tip-speed ratio', '天归一': 'per-day normalised',
-    # 部件与测点
-    '高速轴': 'high-speed shaft', '中间轴': 'intermediate shaft', '中速轴': 'intermediate-speed shaft',
-    '行星': 'planetary', '齿圈': 'ring gear', '内圈': 'inner race', '外圈': 'outer race',
-    '滑环': 'slip ring', '转子': 'rotor', '定子': 'stator', '主轴': 'main shaft', '主承': 'main bearing',
-    '柱塞': 'plunger', '滤芯': 'filter element', '冷却器': 'cooler', '油管': 'oil line',
-    '分配器': 'distributor', '桨叶': 'blade', '传动链': 'drivetrain', '塔筒': 'tower',
-    '油温': 'oil temperature', '油样': 'oil sample', '总线': 'bus', '备件': 'spare parts',
-    '两端': 'both ends', '配对差': 'paired difference', '中速': 'intermediate speed',
-    '高速': 'high speed', '中间': 'intermediate',
-    # 判据与统计
-    '离群': 'outlier', '谱线': 'spectral line', '残差': 'residual', '灰域': 'shaded band',
-    '四分位': 'quartile', '逐档': 'per bin', '最小档': 'smallest bin', '显著门': 'significance threshold',
-    '归因': 'attribution', '观测面': 'observation face', '监测面': 'monitoring coverage',
-    '没测到': 'not detected', '宽带': 'broadband', '全净': 'entirely clean', '干净': 'clean',
-    '慢性': 'chronic', '持平': 'unchanged', '降至': 'fell to', '极差': 'range', '对数': 'log',
-    '基线': 'baseline', '同族': 'same family', '引用同族': 'family reference', '族主': 'family lead',
-    '新证': 'new evidence', '实测': 'measured', '实证': 'empirical evidence', '印证': 'corroboration',
-    '检出': 'detection', '缺陷': 'defect', '疲劳': 'fatigue', '断裂': 'fracture', '过热': 'overheating',
-    '欠发': 'under-performance', '故障率': 'fault rate', '可用率': 'availability', '象限': 'quadrant',
-    '降序': 'descending', '噪声': 'noise', '特征': 'characteristic', '命中': 'match', '分组': 'grouping',
-    '封顶': 'cap', '额定': 'rated', '功率': 'power', '风速': 'wind speed', '转速': 'rotor speed',
-    '曲线': 'curve', '对风': 'yaw alignment', '对风良': 'yaw alignment OK', '对风报': 'yaw alignment alarm',
-    '定向': 'orientation', '泄压': 'pressure relief', '加注': 'top-up', '紧固': 'tightening',
-    '平均偏': 'mean deviation', '偏离': 'deviation', '偏置': 'bias',
-    # 流程与界面
-    '徽章': 'badge', '全位': 'all positions', '固定': 'fixed', '切换': 'switch', '映射到': 'mapped to',
-    '映射': 'mapping', '聚合': 'aggregation', '典型': 'typical', '交叉': 'cross', '不参': 'excluded from',
-    '可对': 'matchable', '不足': 'insufficient', '状态': 'status', '本件为': 'this item is',
-    '可靠性': 'reliability', '重点值': 'key value', '宽限': 'grace', '边界': 'boundary',
-    '调度令': 'dispatch order', '调度': 'dispatch', '监控': 'monitoring', '时序': 'time series',
-    '用户裁': 'client decision', '用户逮': 'caught by client', '异常': 'anomaly', '悬停': 'hover',
-    '能看': 'can see', '失败': 'failed', '手册': 'manual', '虚线': 'dashed', '差异': 'difference',
-    '限制': 'limit', '问题': 'issue', '新发': 'newly found', '发电': 'generation', '设备类': 'equipment',
-    '设备': 'equipment', '融合': 'fusion', '输入': 'input', '本场': 'this site', '节点': 'node',
-    '决策': 'decision', '详情': 'detail', '业主': 'owner', '评估': 'assessment', '撤回': 'withdrawn',
-    '取消': 'cancelled', '定案': 'settled', '原文': 'source text', '清洁': 'clean', '重点': 'priority',
-    '分账': 'attribution', '作业': 'work', '原因': 'cause', '参数': 'parameter', '类别': 'category',
-    '本页': 'this page', '分钟': ' min', '结构': 'structure', '码统计': 'code statistics',
-    '取证': 'evidence retrieval', '哪些': 'which', '修复': 'repair', '保留': 'retained',
-    '警告': 'warning', '故障': 'fault', '运行': 'operating', '恢复': 'recovered', '事件': 'event',
-    '变化': 'change', '检测': 'detection', '强烈': 'strong', '等待': 'waiting', '跳闸': 'trip',
-    '传感器': 'sensor', '本地': 'local', '不是': 'not', '上网': 'exported', '指标': 'indicator',
-    '用户': 'client', '的异常': ' anomaly', '时间': 'time',
-})
-
-# ── 9 第二阶段批量 ──
-TERMS.update({
-    '正是算法当初的检出台': 'these are exactly the units the algorithm flagged',
-    '明显超出即分布非峰': 'a clear excess means the distribution is not single-peaked',
-    '选组件再点击移出': 'click a block again to remove it', '全程零外发': 'nothing leaves the site',
-    '考核的只有': 'the only item under equipment accountability is',
-    '交易策略主导': 'driven by trading strategy', '正态抽样的': 'for a normal sample, the',
-    '落在几离散': 'sit at a few discrete positions', '环内怎么坏的': 'how it fails inside',
-    '简图分工': 'schematic scope', '出脂计量': 'grease output metering', '喷淋回箱': 'spray return to tank',
-    '急性热失效': 'acute thermal failure', '散热风扇': 'cooling fan', '手动待机': 'manual standby',
-    '自动解缆': 'automatic cable untwist', '长停远': 'long stop, distant', '泵超时': 'pump timeout',
-    '压力偏高': 'pressure high', '油压偏高': 'oil pressure high', '器储压': 'accumulator charge',
-    '高频短时': 'frequent · short', '低频长时': 'rare · long', '高频长时': 'frequent · long',
-    '低频短时': 'rare · short', '重复触发': 'repeat triggering', '非硬件': 'not hardware',
-    '常坏且难修': 'frequent and slow to fix', '少发好修': 'rare and quick to fix',
-    '关键少数': 'the vital few', '点面积': 'bubble area', '圆面积': 'circle area',
-    '柱高': 'bar height', '纵界': 'vertical divider', '分界': 'divider', '本底': 'background',
-    '采样格': 'sampling interval', '模式四': 'mode quadrant', '逐段扣损': 'losses deducted step by step',
-    '两数都对': 'both figures are correct', '谁的': 'whose', '哪一': 'which',
-    '关键少数': 'the vital few', '同档差': 'within-bin difference', '尺子巡检': 'yardstick check',
-    '下期计划': 'Next-period plan', '季节性': 'seasonality', '控制策略': 'control strategy',
-    '需跟踪': 'under tracking', '首发码': 'first alarm code', '测量链': 'measurement chain',
-    '可维修性': 'maintainability', '帕累托': 'Pareto', '总结': 'summary', '分析': 'analysis',
-    '生成': 'generate', '控制': 'control', '桨距': 'pitch angle', '吊装': 'lifting',
-    '执行': 'execution', '版本': 'version', '审核': 'review', '盲区': 'blind spot',
-    '保持常规': 'keep routine', '记为': 'recorded as', '截图逮': 'caught in screenshot',
-    '间隔': 'interval', '交易': 'trading', '实际': 'actual', '要求': 'requirement',
-    '物理': 'physical', '电网': 'grid', '海上': 'offshore', '对比': 'comparison',
-    '建议': 'recommendation', '分母': 'denominator', '门槛': 'threshold', '相对': 'relative',
-    '发现': 'finding', '完整': 'complete', '计数': 'count', '本体': 'ontology', '命令': 'command',
-    '微视图': 'micro-view', '叶根': 'blade root', '解缆': 'cable untwist', '解剖图': 'anatomy',
-    '支路': 'branch', '油路': 'oil circuit', '冷却': 'cooling', '信号': 'signal', '趋势': 'trend',
-    '发生': 'occurrence', '平均': 'mean', '根因': 'root cause', '不作': 'not used as',
-    '顶部': 'top', '特性': 'characteristic', '期望': 'expected', '佐证': 'corroborating',
-    '满发': 'full load', '合计': 'total', '统计': 'statistics', '万元': ' k CNY',
-    # 2026-09-19 变桨面零位两口径 (用户令「停机段中位、满发段中位两者分列出」) 的用词
-    '满发段': 'full-load segment', '顺桨停机段': 'feathered stop segment', '顺桨段': 'feathered segment',
-    '顺桨': 'feathered', '并列参考': 'for reference',
-    '天气': 'weather', '对象': 'object', '读纪律': 'reading discipline', '分支': 'branch',
-    '失效': 'failure', '全程': 'full window', '入口': 'entry point', '不同': 'different',
-    '转录': 'transcribed', '强度': 'severity', '管理': 'management', '行动': 'action',
-    '内有': 'contains', '接线': 'wiring', '过多': 'excessive', '油位': 'oil level',
-    '允许': 'permitted', '度高': ' high', '以切入': 'to cut in', '不可能': 'not possible',
-    '冲击量': 'impact magnitude', '路径': 'path', '电压': 'voltage', '下限': 'lower limit',
-    '上限': 'upper limit', '远程': 'remote', '载体': 'carrier', '机舱风': 'nacelle anemometer',
-    '对照': 'control', '纯图形': 'graphic-only', '复核': 'cross-check', '偏高': 'high',
-    '严重': 'severe', '建议尽快': 'recommend prompt', '观测': 'observation', '缓升': 'slow rise',
-    '回落': 'fall back', '回归': 'return', '同批': 'same batch', '增益': 'gain',
-    '梯度': 'gradient', '周期主导': 'cycle-dominated', '急性': 'acute', '长期': 'long-term',
-    '总长': 'total length', '最大': 'max', '测风': 'wind measurement', '其他': 'other',
-    '显示': 'display', '支持': 'support', '范式': 'template', '完成': 'complete',
-    '情况': 'status', '周期': 'cycle', '周报': 'Weekly', '专题': 'Topic', '改为': 'changed to',
-    '三段': 'three segments', '基于': 'based on', '离线': 'offline', '看组件': 'view block',
-    '调整顺序': 'reorder', '环比': 'month-on-month', '理论': 'theoretical', '低于': 'below',
-    '分解': 'breakdown', '之差': 'difference', '比值': 'ratio', '命令期': 'command period',
-    '被压制': 'suppressed', '风况': 'wind conditions', '不计': 'excluded', '交通': 'access',
-    '影响': 'impact', '段点': 'segment point', '属重复触发': 'is repeat triggering',
-    '总数': 'total', '瓶颈': 'bottleneck', '一列': 'column', '标量': 'scalar', '引用': 'citation',
-    '上报版': 'Group-report version', '轻量': 'Light', '标准': 'Standard', '声明': 'declared',
-    '其余': 'remaining', '时效': 'currency', '电费': 'tariff', '修完': 'repaired',
-    '展示': 'presentation', '原来': 'previously', '该类': 'this class', '维护面': 'maintenance surface',
-    '更新': 'update', '技术依': 'technical basis', '归属': 'attribution', '定规范': 'specification',
-    '实机': 'as-built', '什么': 'what', '缺失非': 'missing, not', '修过': 'repaired before',
-    '卡撤': 'card removed', '点看': 'click to view', '确诊': 'confirmed diagnosis',
-    '扭缆': 'cable twist', '原则': 'principle', '滚子': 'roller', '标注': 'annotation',
-    '集中': 'centralised', '管路': 'piping', '往复': 'reciprocating', '脂桶': 'grease drum',
-    '主管路': 'main line', '冲击': 'impact', '铁谱': 'ferrography', '推动': 'drive',
-    '压力': 'pressure', '带宽': 'bandwidth', '列宽有限': 'column width limited',
-    '不可读': 'not legible', '双环': 'dual ring', '上下': 'top/bottom', '右下': 'bottom-right',
-    '抖动': 'chatter', '刷屏': 'flooding', '左上': 'top-left', '右上': 'top-right',
-    '治理': 'remediation', '左下': 'bottom-left', '维持': 'maintain', '类不计': 'class excluded',
-    '贡献': 'contribution', '页缺': 'page missing', '并列': 'side by side', '迁入': 'moved in',
-    '排布': 'layout', '工作': 'work', '早三': 'earliest three', '这三': 'these three',
-    '人员转运': 'personnel transfer', '语义本体': 'semantic ontology', '滚动': 'scroll',
-    '内容会外发': 'content is sent externally', '云端': 'Cloud', '物理闸': 'physical gate',
-    '同一': 'same', '只有': 'only', '没有': 'no', '里的': ' in ', '的六': ' six ',
-})
-
-# ── 10 第三阶段: 上屏长尾 (统计已剥 JS 注释, 只补真正会显示的) ──
-TERMS.update({
-    '选组件再点击移出': 'click a block again to remove it', '两数都对': 'both figures are correct',
-    '样龄过期': 'sample age expired', '有效监测': 'effective monitoring', '能量过剩': 'excess energy',
-    '绕组三相': 'winding three-phase', '通讯模块': 'communication module', '旁路阀': 'bypass valve',
-    '跟踪偏差': 'tracking deviation', '数加权': 'count-weighted', '同标尺': 'same scale',
-    '不随本': 'independent of this', '零外发': 'no external transmission', '加热带': 'heating band',
-    '通信线': 'communication line', '出力': 'output', '错误': 'error', '不等于': 'is not',
-    '分布': 'distribution', '持续': 'sustained', '通道': 'channel', '峰值': 'peak',
-    '时段': 'period', '稳定': 'stable', '复合': 'composite', '结论': 'conclusion',
-    '关注': 'watch', '稳态': 'steady-state', '瞬态': 'transient', '可信度': 'confidence',
-    '现状': 'current state', '波形': 'waveform', '内容': 'content', '发作': 'episode',
-    '如何': 'how', '联动': 'linked', '复位': 'reset', '激活': 'activated', '桨角': 'pitch angle',
-    '柜左': 'cabinet left', '限需': 'limit required', '塔振': 'tower vibration', '该叶': 'this blade',
-    '手动': 'manual', '电机': 'motor', '端别': 'end', '载荷': 'load', '缺采': 'sampling gap',
-    '不在': 'not in', '断供': 'supply interruption', '高位': 'elevated', '度夏': 'summer operation',
-    '磨损': 'wear', '不可': 'cannot', '不给等': 'no grade given', '有过一': 'has had one',
-    '的多': ' more', '不代': 'does not represent', '五态': 'five states', '平铺': 'flat layout',
-    '展现的': 'presented', '筛选': 'filter', '系数': 'coefficient', '计算隐藏': 'computation hidden',
-    '所选': 'selected', '九枚举': 'nine enumerated values', '好坏': 'good/bad',
-    '两轴不可互相替代': 'the two axes are not interchangeable', '明确定': 'explicitly defined',
-    '截断的': 'truncated', '记忆': 'memory', '实心点': 'solid point', '季节': 'season',
-    '尺子': 'yardstick', '数量': 'count', '推理': 'reasoning', '全部': 'all', '分开': 'separate',
-    '剖面': 'section', '剖面符号': 'section symbol', '竖矩形': 'vertical rectangle',
-    '一眼看出': 'shows at a glance', '偏差': 'deviation', '长度即幅度': 'length equals magnitude',
-    '跨行可比': 'comparable across rows', '端口': 'port', '打开': 'open', '最高': 'highest',
-    '提示': 'hint', '模式归类': 'mode classification', '拆检': 'teardown inspection',
-    '内窥': 'borescope', '靠内窥镜': 'via borescope', '轻度损伤': 'minor damage',
-    '轻微损伤': 'slight damage', '中等持续': 'moderate sustained', '点蚀在': 'pitting at',
-    '当时的': 'at the time', '同样没看出': 'likewise not detected', '缺口': 'gap',
-    '西门子': 'OEM', '非链路静默': 'not link silence', '风能利用系数': 'power coefficient Cp',
-    '转矩': 'torque', '分册': 'volume', '取每': 'take each', '高位稳定值': 'upper stable value',
-    '阶跃': 'step change', '配置变更': 'configuration change', '清洗': 'cleansing',
-    '行程': 'travel', '泵压': 'pump pressure', '切入段': 'cut-in segment', '语义色': 'semantic colour',
-    '现值': 'current value', '返服': 'return to service', '对不对得上': 'whether they match',
-    '胶囊': 'pill', '只作回测锚': 'back-test anchor only', '术语去黑话': 'plain-language terms',
-    '翻译': 'translation', '黑话': 'jargon', '内部': 'internal', '缩写': 'abbreviation',
-    '括注': 'parenthetical', '健康': 'healthy', '排除': 'excluded', '降为': 'downgraded to',
-    '仪器': 'instrument', '规范': 'specification', '打头': 'leading', '即可': 'is sufficient',
-    '保持架': 'cage', '时域转': 'time-domain to', '有效值': 'RMS', '倍轴频': '× shaft frequency',
-    '轴频': 'shaft frequency', '分位': 'percentile', '宽频带总量': 'broadband total',
-    '阶跃上升': 'step rise', '缓慢上升': 'slow rise', '回到常态': 'back to normal',
-    '该批主': 'batch lead', '性组': ' group', '沿链': 'along the chain', '最高点': 'peak point',
-    '成分主导': 'component-dominated', '突发': 'sudden', '主要': 'main', '位于': 'located at',
-    '族分布堆叠': 'family distribution stack', '族分布': 'family distribution', '段宽': 'segment width',
-    '总和恒': 'sum constant', '段内比例': 'within-segment share', '族构成': 'family composition',
-    '最短': 'shortest', '保证可': 'ensures', '最多': 'at most', '汇报端口': 'reporting portal',
-    '客户汇报': 'client reporting', '定制化': 'customised', '决定': 'determines',
-    '六核心数': 'six headline figures', '用大字直出': 'shown in large type', '下沉到': 'pushed down to',
-    '一屏也拿得到结论': 'the first screen already carries the conclusion', '每数字带': 'each figure carries',
-    '结论可回溯到': 'conclusions trace back to', '组件': 'block', '主题': 'theme',
-    '收尾': 'closing', '能量瀑布': 'energy waterfall', '用于定位电量流向': 'locates where energy went',
-    '回答': 'answers', '分出': 'separates', '方向相反': 'opposite directions', '区分': 'distinguishes',
-    '排序会误定': 'ranking would mis-set', '的历史例证': ' historical evidence',
-    '证明这链是通的': 'proves the chain works end to end', '下期': 'next period',
-    '推出的': 'derived', '套话': 'boilerplate', '性质': 'nature', '常驻': 'always on',
-    '补充文字结论': 'adds a written conclusion', '供汇报使用': 'for reporting use',
-    '空白': 'blank', '默认空': 'empty by default', '都去掉': 'all removed', '组合': 'combination',
-    '一张图': 'one chart', '就是一块': 'is one block', '退化为': 'reduced to',
-    '客户看到的是负担': 'the client sees a burden', '长度天然由客户': 'length is set by the client',
-    '交互界面设计一下': 'interaction design', '区不浮动': 'fixed, not floating',
-    '的叙事顺序分五组': 'grouped in five by report narrative order',
-})
-
-# ── 11 第四阶段: 整句界面文案 (按 HTML 标签切分后的中文段, 标签本身不入表) ──
-TERMS.update({
-    '(本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。':
-        ' (this page needs contiguous segments for reliability metrics; the energy account bills state by state). Both figures are correct and must not be subtracted from one another.',
-    '短于 1h 的停机段': 'stops shorter than 1 h',
-    '差额是': 'the difference is ',
-    '限电前干净窗 — 2026 起交易策略主导, 曲线与控制参数含策略性少发不可作设备判据。本页含特性曲线与控制参数一致性两件, 同窗':
-        'Pre-curtailment clean window — from 2026 trading strategy dominates, so curves and control parameters contain strategic under-generation and cannot serve as equipment criteria. This page holds both characteristic curves and control-parameter consistency, on the same window.',
-    '由另一本地模型基于': 'Reviewed by a second local model against ',
-    '同一份证据': 'the same evidence',
-    '复核上述答案。溯源核验仅校验数字与机组号是否越界, 本项校验结论是否有证据支撑。':
-        '. The traceability check only verifies that figures and unit numbers stay within the evidence; this check asks whether the conclusion is supported at all.',
-    '右下角的码不要按条数排优先级': 'Do not rank the bottom-right codes by event count',
-    ' — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。':
-        ' — they are repeat triggering, not hardware faults. The two Pareto charts above are this figure projected onto each axis.',
-    '总览内嵌页在快照版中不可用 (它由系统实时渲染)。本快照的分析内容在上方各标签页内, 数据已随快照固化。':
-        'The embedded overview is unavailable in the snapshot (it is rendered live by the system). The analysis in this snapshot is in the tabs above, with data frozen at capture time.',
-    '海上机组 MDT 普遍偏高是': 'Offshore MDT is generally inflated by ',
-    '交通窗口': 'vessel access windows',
-    '决定的, 不等于维修本身慢 — 跨场比 MDT 须同为海上。':
-        ', not by slow repair work — comparing MDT across sites requires both to be offshore.',
-    '这是两个指标的设计差异不是错, 但两者不可直接相乘或互推。低风待机对损失的贡献仅 118 MWh(0.5%)':
-        'This is a design difference between two indicators, not an error, but they cannot be multiplied or derived from one another. Low-wind standby contributes only 118 MWh (0.5%) of losses.',
-    '不是物理判据: 正常数据下必然通过。它的作用是当损失被高估到不可能时(台账数字虚高曾把损失写成超过全停上限)立刻拦下':
-        'Not a physical criterion: with sound data it always passes. Its purpose is to stop an impossible loss figure at once — an inflated ledger once recorded a loss larger than a full fleet stop.',
-    '故障前能看见、换件后能看见恢复': 'visible before failure and visible as recovery after replacement',
-    ', 这条链是通的; 闭环成功的台会从盘面消失。':
-        ' — the chain works end to end, and units that close the loop drop off this board.',
-    '点面积=影响台数(大=批次共性, 小=单台); 横界=条数中位, 纵界=10min(一个SCADA采样格)。':
-        'Bubble area = units affected (large = fleet-wide pattern, small = single unit); the vertical divider is the count median, the horizontal one 10 minutes — one SCADA sampling interval.',
-    '明显超出即分布非单峰': 'a clear excess means the distribution is not single-peaked',
-    ' — 与左图的分组互为印证; 落在期望附近的轴(额定桨角)则是连续分布。':
-        ' — corroborating the grouping on the left; an axis near the expected value (rated pitch angle) is continuously distributed.',
-    '四个观测面是': 'The four observation faces are ',
-    '同一条机制链的四个面': 'four faces of a single mechanism chain',
-    ', 不算四个独立源 — 加权时不可当多源印证。':
-        ', not four independent sources — they must not be weighted as multi-source corroboration.',
-    '要走六步。这张盘不讲某一台的故事, 用于确认': 'Six steps in all. This board tracks the fleet, not one unit; it confirms ',
-    '当前需处置机组、各自所处环节、后续动作': 'which units need action, the step each is held at, and what comes next',
-    '不成立 — 前 72h 有报警不等于该报警与本段停机有因果 (双模审 2026-08-28 逮)。':
-        'does not hold — an alarm within the preceding 72 h does not make it the cause of this stop (caught in dual-model review, 2026-08-28).',
-    '本地模型依据需求选取组件, 并基于实测数据生成运行总结; 数据不出本地':
-        'A local model selects blocks from your request and writes an operations summary from measured data; nothing leaves this machine',
-    '可靠性口径 ≥1h 段, 电量账为逐条工况态 — 两个数都对, 不可相减':
-        'reliability counts stops ≥1 h; the energy account bills state by state — both are correct and must not be subtracted',
-    '统计期 / 残月 / 工单窗 / 判级性质 — 建议常驻, 没有边界的数字堆不能作汇报依据':
-        'Reporting period / partial months / work-order window / nature of the grading — recommended always on; a set of figures without its boundaries cannot serve as a reporting basis',
-    '六个核心数: 上网电量 / 等效满发 / 可用率 / 损失率 / 停机间隔 / 需行动台':
-        'Six headline figures: net exported / equivalent full-load hours / availability / loss ratio / mean time between outages / units requiring action',
-    '两侧不是两个独立源, 是同一条机制链的两端。':
-        'The two sides are not independent sources; they are two ends of one mechanism chain.',
-    '工况态分解; 损失=同工况理论出力与实测之差。量级自检为防虚高的下限闸, 不是风资源判据':
-        'Operating-state decomposition; loss = theoretical output under the same conditions minus measured. The magnitude self-check is a floor gate against inflation, not a wind-resource criterion',
-    '限电属交易与调度侧, 2026 起交易策略主导, 少发未必是弃风。':
-        'Curtailment sits with trading and dispatch; from 2026 trading strategy dominates, so reduced generation is not necessarily spilled wind.',
-    '由振动/温度/润滑/油液四源融合得出; 行动等级由综合报告出具':
-        'derived by fusing vibration, temperature, lubrication and oil analysis; action levels are issued by the consolidated report',
-    ' — 损失六成在交易与调度侧, 设备侧改善空间有限':
-        ' — six tenths of losses sit with trading and dispatch; the scope for equipment-side improvement is limited',
-    ' — 台账止 2024-11, 本期检修执行与闭环无法自动判定':
-        ' — the ledger ends 2024-11, so maintenance execution and loop closure for this period cannot be determined automatically',
-    '这是配置/版本差异的形状; 磨损或工况引起的会是连续散布。两轴各自成组且分组不同台':
-        'This is the shape of a configuration or version difference; wear or operating conditions would scatter continuously. The two axes group independently and the groups hold different units',
-    '文档预览窗 — 从上面的组件库点选内容, 此处显示报告预览':
-        'Document preview — select blocks from the library above and the report appears here',
-    '条数=发生频次, 不反映影响程度 — 高频短时报警码(信号抖动或重复触发)排序靠前':
-        'Count = frequency, not severity — high-frequency short-duration codes (signal chatter or repeat triggering) rank first',
-    '格子颜色 = 设备状态 (业主口径, 与 windcms 报告同一套):':
-        'Cell colour = equipment state (owner convention, same set as the assessment report):',
-    '海上作业天气窗 · 近12月可作业日 (公开再分析统计, 非派工依据)':
-        'Offshore weather windows · workable days over the last 12 months (public reanalysis statistics, not a dispatch basis)',
-    '排期沙盘 · 四情景损失对比 (万元, 电价区间中值; 建议级待人裁)':
-        'Scheduling model · four-scenario loss comparison (k CNY, mid-point of the tariff range; advisory, pending human decision)',
-    '主轴承集中润滑链: 润滑泵经管路到分配器, 分配器柱塞往复把脂定量送到前后主轴承':
-        'Main-bearing centralised lubrication: the pump feeds a distributor through piping, and the distributor plungers meter grease to the fore and aft main bearings',
-    '变桨系统: 蓄能器储压经比例阀推动油缸, 油缸驱动变桨轴承转动桨叶; 滑环传电与信号':
-        'Pitch system: accumulator pressure drives the cylinder through a proportional valve, the cylinder turns the pitch bearing and blade; the slip ring carries power and signals',
-    '过热类失效沿油路查(冷却/滤芯), 磨损类沿齿轮线查(振动/铁谱)。':
-        'Overheating failures are traced along the oil circuit (cooling, filters); wear failures along the gear line (vibration, ferrography).',
-    '执行侧看桨距跟踪偏差, 三叶不一致指向单支路。':
-        'On the actuation side look at pitch tracking deviation; disagreement between the three blades points to a single branch.',
-    '观测: 蓄能带宽/停机泄压 · 桨距跟踪偏差 · 三叶零位一致性 · 轴承润滑脂':
-        'Observed: accumulator bandwidth / shutdown pressure relief · pitch tracking deviation · three-blade zero consistency · bearing grease',
-    '窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)':
-        'No work order for this component in the window (order data ends 2024-11, so any 2026 window is necessarily empty — a data boundary)',
-    '每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。':
-        'Each source states: current coverage / last updated / agreed location / ingest command / update frequency / owner.',
-    '转录自振动线, ': 'Transcribed from the vibration line, ',
-    '不随上方时间窗变': 'independent of the time window above',
-    '; 报警/工单/温度列才跟随所选窗。': '; only the alarm, work-order and temperature columns follow the selected window.',
-    '本码无手册处置条目 (手册覆盖本场 161/322 码) — 需查图纸或联系厂商支持。':
-        'No manual procedure for this code (the manual covers 161 of 322 codes on this site) — consult drawings or contact OEM support.',
-    '非厂商绑定 — 现场以铭牌与图纸复核; 参数 ID 与告警码是不同命名空间, 未按同号关联。':
-        'Not vendor-locked — verify on site against nameplates and drawings; parameter IDs and alarm codes are separate namespaces and are not linked by number.',
-    '状态管理判级, 不代表确诊数量; 判据/谱线/计算过程不在本页 — 深潜走底部独立系统。':
-        'State-management grading; it is not a count of confirmed diagnoses. Criteria, spectral lines and computation are not on this page — use the dedicated system below for depth.',
-    '语义本体 · 对象/机制/决策/约束 (Palantir 思路轻量自建; 判读纪律=写入闸, 违反写不进库)':
-        'Semantic ontology · objects / mechanisms / decisions / constraints (lightweight, Palantir-style; the reading discipline is enforced as a write gate — violations cannot enter the store)',
-    '检修决策 · ontology 展现的窗口 (语义本体 + dsh 编排; 推理无界, 结论受闸)':
-        'Maintenance decisions · the ontology view (semantic ontology with orchestration; reasoning is open, conclusions are gated)',
-    '融合 · 结构=reviewed vibration evidence and SCADA context=vibration evidence package (source-backed record, evidence review only; no rating is assigned here)':
-        'Fusion · reviewed vibration evidence and SCADA context',
-    '判读纪律: 六个观测量=一条机制链 (C3 源数按观测面), 禁写多源印证; 判级只在 L1/审级产生, 本体转录带审级。':
-        'Reading discipline: six observed quantities form one mechanism chain (independent sources are counted by observation face); do not claim multi-source corroboration. Grades originate only in the analysis layer, and the ontology transcribes them with their review tier.',
-})
-
-# ── 12 第五阶段: 四维状态简写与模板碎片 (逐台判据描述由"模板+数值"拼成, 模板词入表即可覆盖) ──
-TERMS.update({
-    # 四维评级的单字简写 — 逐台描述里出现最密, 不收就满屏碎字
-    '蓄能与压力调节': 'accumulator and pressure control', '变桨轴承与轮毂润滑': 'pitch bearing and hub lubrication',
-    '执行与位置反馈': 'actuation and position feedback', '油路与密封': 'oil circuit and seals',
-    '四维': 'four axes', '停机泄压': 'shutdown pressure decay', '蓄能缸检超时': 'accumulator cylinder check timeout',
-    '同油温档带宽': 'bandwidth in the same oil-temperature bin', '液压油温': 'hydraulic oil temperature',
-    '曲线判风速': 'curve-derived wind speed', '绕组三相极差': 'winding three-phase range',
-    '仪表/接线候选': 'instrumentation or wiring candidate', '记基线观': 'baseline recorded, watching',
-    '配对差发电机轴承': 'paired difference, generator bearing', '配对差': 'paired difference',
-    '驱动端': 'drive end', '非驱动端': 'non-drive end', '两端': 'both ends',
-    '未映射到部件': 'not mapped to a component', '未映射': 'not mapped', '谱线级': 'spectral-line level',
-    '引用同族': 'family reference', '见族主台': 'see family lead unit', '振动线转录': 'transcribed from vibration line',
-    '监测面未覆盖': 'monitoring does not cover it', '没测到': 'not measured',
-    '机械刹车温度': 'mechanical brake temperature', '塔筒振动': 'tower vibration',
-    '强烈振动停机': 'severe-vibration stop', '叶片振动检测': 'blade vibration detection',
-    '无有效监测': 'no effective monitoring', '不给等级': 'no grade assigned',
-    '湿度高告警': 'high-humidity alarm', '通讯故障': 'communication fault',
-    '工单点名该叶断裂': 'work order names this blade as fractured', '换栓': 'bolt replacement',
-    '全批疲劳磨粒最高': 'highest fatigue particle count in the batch', '批内最高': 'highest in batch',
-    '重点值': 'key values', '疲劳': 'fatigue', '磨粒': 'wear particles',
-    '端别映射': 'end mapping', '未列台': 'unit not covered', '未列≠正常': 'not covered is not the same as healthy',
-    '缺采': 'sampling gap', '前置': 'prerequisite', '未验': 'not verified',
-    '慢性载荷冲击': 'chronic load impact', '行星级周期主导': 'planetary-stage cycle dominated',
-    '梯度': 'gradient', '排名路径': 'ranking path', '持续路径': 'persistence path',
-    '渐进磨损': 'progressive wear', '点蚀': 'pitting', '剥落': 'spalling',
-    '标量冲击量': 'scalar impact magnitude', '可检出率': 'detection rate',
-    '高温跑圈': 'high-temperature race creep', '轴损': 'shaft damage', '烧蚀': 'burn damage',
-    '实证不可见': 'not empirically visible', '改看速度量或温度轴': 'use velocity or the temperature axis instead',
-    '分布正中间': 'mid-distribution', '拆检前': 'before teardown',
-    '因侧': 'cause side', '果侧': 'effect side', '因果时序闭合': 'cause-effect timing closes',
-    '机制定性窗无关': 'mechanism characterisation is window-independent',
-    '恶化态是窗相关判断': 'deterioration is a window-dependent judgement',
-    '润滑断供': 'lubrication starvation', '泵本体': 'pump body', '柱塞两段式失效': 'two-stage plunger failure',
-    '温度长期高位': 'sustained high temperature', '振动间歇尖峰': 'intermittent vibration spikes',
-    '无归因停机': 'unattributed downtime', '长停远窗': 'long stop, distant window',
-    '非链路静默': 'not link silence', '前6h无报警': 'no alarm in the preceding 6 h',
-    '镜头口径': 'lens basis', '直测干净量': 'directly measured clean quantity',
-    '只判形状': 'shape only', '绝对达成率须现场测风': 'absolute performance requires on-site anemometry',
-    '禁眼估': 'no eyeballing', '逐档中位聚合': 'per-bin median aggregation',
-    '已剥限电': 'curtailment removed', '只正常发电态': 'normal production only',
-    '全场四分位': 'fleet inter-quartile', '四分位残差带': 'inter-quartile residual band',
-    '出带且过锚才算离群': 'an outlier must leave the band and pass the anchor',
-    '物理绝对锚': 'physical absolute anchor', '显著门': 'significance gate',
-    '个月×台': ' unit-months', '月×台': ' unit-months', '月度聚合': 'monthly aggregation',
-    '不随上方窗切换': 'independent of the window selector above',
-    '两源时间几乎不重叠': 'the two sources barely overlap in time',
-    '集团模板无本场记录': 'group template, no records for this site',
-    '负面对照': 'negative control', '判据未乱报': 'the criterion did not misfire',
-    '全场极齐': 'uniform across the fleet', '无泵压异常': 'no pump-pressure anomaly',
-    '活动量代理': 'activity proxy', '非真磨损量': 'not true wear', '与状态位次数互核': 'cross-checked against status-bit counts',
-    '非发作型': 'not episodic', '机舱位置通道拖偏': 'nacelle position channel drift',
-    '数据质量': 'data quality', '非对风故障': 'not a yaw fault',
-    '参数级分组见': 'parameter-level grouping, see ', '阶跃=配置变更': 'a step is a configuration change',
-    '平移=工况': 'a shift is an operating condition', '两轴独立分组': 'the two axes group independently',
-    '润滑柱塞各台设定本就不同': 'plunger settings differ by unit by design',
-    '所以此列只展示不判级': 'so this column is shown without grading',
-    '只有已定案的机制链才标色': 'only settled mechanism chains are coloured',
-    '末次取样距今超': 'last sampled more than ', '此列看的是': 'this column shows ',
-    '同一失效机制的多个观测面': 'multiple observation faces of one failure mechanism',
-    '不是': ' are not ', '多源印证': 'multi-source corroboration',
-    '融合时禁按源数加权': 'fusion must not weight by source count',
-    '级小修全年窗宽': 'minor repairs have a year-round window',
-    '吊装级受台风季约束': 'lifting work is constrained by typhoon season',
-    '色深=可作业占比': 'colour depth is the workable share',
-    '限值=行业常规': 'limits are industry norms', '非本场合同值': 'not this site contract values',
-    '报': 'alarm', '良': 'good', '优': 'OK', '中位': 'median', '全场': 'fleet',
-    '窗至': 'window to', '判': 'graded ', '共': 'total ', '段': ' segments',
-    '轴': ' axis', '页': ' page', '类': ' class', '面': ' face', '点': ' point',
-    '看': 'see ', '卡': 'held', '该': 'this ', '依': 'basis', '九': 'nine ',
-    '多': 'more', '禁': 'do not ', '四': 'four ', '一': 'one ', '没': 'no ',
-    '在': 'in ', '上': 'upper ', '当': 'when ', '时': ' at ', '现': 'current ',
-    '至': 'to ', '只': 'only ', '待': 'pending ', '各': 'each ', '秒': ' s',
-    '别': 'end', '达': 'reached', '倍': '×', '比': 'ratio', '角': ' angle',
-    '态': ' state', '峰': 'peak', '律': ' law', '门': 'gate', '三': 'three ',
-    '度': ' degree', '警': 'alarm', '可': 'can ', '数': 'count', '松': 'loose',
-    '观': 'watch', '油': 'oil', '齿': 'gear', '口': 'basis', '反馈': 'feedback',
-    '从': 'from ', '到': 'to ', '向': 'to ', '要': 'need ', '有': 'has ',
-    '而': 'while ', '并': 'and ', '用': 'use ', '非': 'not ', '法': 'cannot ',
-    '止于': 'ends at ', '调取': 'obtain', '走完': 'complete', '万': 'k',
-    '长': 'long', '大': 'large', '新': 'new', '产': 'produce', '换': 'replace',
-    '补': 'add', '跑': 'run', '放到': 'place in ', '内': 'within', '线': ' line',
-    '场': ' site', '码': ' code', '季': 'quarterly', '半年': 'semi-annual', '半值': 'half of the ',
-    '提供': 'provided', '需': 'requires ', '实为': 'actually ', '注意': 'note ',
-    '至今': 'to date', '机构': 'institution', '两家': 'two providers', '到货': 'delivery',
-    '重跑': 'rerun', '必重跑下游': 'downstream must be rerun', '的底座': ' as the base',
-    '随运营': 'as operations proceed', '到位': 'in place', '自动刷新': 'auto refresh',
-    '怎么做': 'how to', '衍生': 'derived', '如东风场': 'site', '风机': 'turbine',
-    '约定位置': 'agreed location', '摄入命令': 'ingest command', '更新频率': 'update frequency',
-    '责任方': 'owner', '当前覆盖': 'current coverage', '末次更新': 'last updated',
-})
-
-# ── 13 收尾: 维护端口与剩余整句 ──
-TERMS.update({
-    '要补Data 就把新文件放到': 'to add data, place the new file in ',
-    '要补数据就把新文件放到': 'to add data, place the new file in ',
-    '再跑': ', then run ', '就把新文件放到': ', place the new file in ',
-    '随运营滚动 — 新数据到位跑 ingest, 图谱与判级自动刷新':
-        'rolls with operations — once new data is in place, run ingest and the graph and grading refresh automatically',
-    '相对稳定 — 换厂商资料/换版本/参数变更时才动, 改动需审':
-        'relatively stable — touched only when vendor documents, versions or parameters change, and changes require review',
-    '注意度总': 'note that the total ', '核过确': 'verified as ', '六类': 'six classes',
-    '当前止': 'currently ends at ', '缺口需现场补': 'the gap must be filled by site',
-    '新一轮到货重跑': 'rerun when the next batch arrives',
-    '振动线出件 → fusion 自动读': 'issued by the vibration line, read automatically by fusion',
-    '在升(末三窗/早三窗≥1.3)与换件闭环两类实证入图谱':
-        'rising trend (last-three vs first-three windows) and replacement closed-loop evidence enter the graph',
-    '补前这类信息只在链盘里, 图谱检索不到':
-        'before this, such information lived only on the chain board and was not retrievable from the graph',
-    '每台六步进度/停滞环节/下一步 + 全场瓶颈汇总':
-        'six-step progress, stalled step and next action per unit, plus a fleet bottleneck summary',
-    '补前只在运行期内存里, 图谱检索不到':
-        'before this it existed only in runtime memory and was not retrievable from the graph',
-    '判级或工单更新后': 'after a grading or work-order update', '振动线每轮出件后': 'after each vibration-line issue',
-    '换厂商资料时': 'when vendor documents change', '换版本时': 'when the version changes',
-    '参数变更时': 'when parameters change', '补充资料时': 'when documents are added',
-    '随码表/工单更新': 'with code-table and work-order updates', '月/事件驱动': 'monthly / event-driven',
-    '事件驱动': 'event-driven', '需 windscada 服务在跑': 'requires the service to be running',
-    '产物存在': 'artefact present', '数据源': 'data source', '摄入': 'ingest',
-    '现场提供': 'site provided', '化验机构': 'laboratory', '振动线': 'vibration line',
-    '图谱': 'knowledge graph', '本表': 'this table', '月表': 'monthly table',
-    '数据边界': 'data boundary', '空≠无检修': 'empty does not mean no maintenance',
-    '实机参数表': 'as-built parameter table', '判定规范': 'decision specification',
-    '技术资料索引': 'technical document index', '图纸/原理': 'drawings and principles',
-    '失效模式与原因树': 'failure modes and cause tree', '报警码表与处置手册': 'alarm code table and procedures',
-    '预防性维护': 'preventive maintenance', '检修工单台账': 'maintenance work order ledger',
-    '油液化验': 'oil analysis', '报警事件': 'alarm events', '月度衍生': 'monthly derived',
-    '趋势与闭环证据': 'trend and closed-loop evidence', '决策链进度': 'decision chain progress',
-    '振动评估报告': 'vibration assessment report',
-})
-
-# ── 14 末轮: 决策/振动/曲线三页残留 ──
-TERMS.update({
-    '一台机组从': 'A unit runs from ', '发现异常': 'anomaly detected', '修完验收': 'repaired and verified',
-    '这张盘不讲某一台的故事': 'this board tracks the fleet, not one unit',
-    '瓶颈集中在一步': 'the bottleneck sits at a single step',
-    '不是': 'is not ', '再多看点数据能解的': 'something more data can solve',
-    '核实': 'verify', '向现场调取': 'obtain from site', '台账止于': 'ledger ends at ',
-    '系统无法判定是否已派工': 'the system cannot tell whether work was dispatched',
-    '非未执行': 'not the same as not done', '数据不足不可判': 'insufficient data, indeterminate',
-    '这不是': 'this is not ', '偷懒': 'laziness', '历史单不对应本次问题': 'historical orders do not match this finding',
-    '会造出假的完成态': 'would fabricate a completed state', '要填这一列': 'to fill this column',
-    '须向现场要': 'requires site to provide ', '已走完全程并退出盘面': 'completed the full path and left the board',
-    '端到端闭环实证': 'end-to-end closed-loop evidence', '立即修': 'repair now',
-    '推迟': 'defer', '拖到失效': 'run to failure', '大部件成本': 'major component cost',
-    '作业级': 'work class', '修理窗': 'repair window', '停机损失': 'downtime loss',
-    '电费口径': 'tariff basis', '风险敞口': 'risk exposure', '等待月数': 'months waited',
-    '随板块常显': 'always shown with the panel', '各列取数窗口不同': 'columns draw on different windows',
-    '两格同时发红': 'both cells turn red', '不等于同时发生': 'does not mean they happened together',
-    '不可当共时印证': 'must not be taken as simultaneous corroboration',
-    '每格右侧曲线': 'the curve on the right of each cell', '逐月记忆轨迹': 'monthly trend memory',
-    '实心点': 'solid point', '现窗': 'current window', '快照会丢形态': 'a snapshot loses the shape',
-    '才看得出': 'only then can one see ', '塌': 'collapse', '回升': 'recovery', '翻转': 'reversal',
-    '阴性不构成证据': 'a negative does not constitute evidence', '在知盲区上': 'over a known blind spot',
-    '同理': 'likewise', '两条判读纪律': 'two reading rules', '随板块': 'with the panel',
-    '格子颜色': 'cell colour', '徽章': 'badge', '业主口径': 'owner convention',
-    '可信度': 'confidence', '九枚举': 'nine values', '两轴不可互相替代': 'the two axes are not interchangeable',
-    '说的是': 'refers to ', '设定本就不同': 'settings differ by design', '此列只展示不判级': 'this column is shown without grading',
-    '落位': 'position', '圆内数字': 'the number inside each circle', '连成一片': 'form a continuum',
-    '成因须配置台账': 'the cause requires the configuration ledger', '档下的': ' within the bin',
-    '标定偏移': 'calibration offset', '正态抽样': 'normal sampling', '自身的分辨力': 'its own resolving power',
-    '削峰': 'peak shaving', '饱和点': 'saturation point', '三叶': 'three-blade',
-    '集距': 'collective pitch', '不平衡': 'imbalance', '齿比': 'gear ratio',
-    '两轴独立分组': 'the two axes group independently', '三组': 'three groups',
-    '专项锚': 'dedicated anchor', '只出当期电量账': 'issues only the current-period energy account',
-    '数字随上方窗切换': 'figures follow the window selector above',
-    '唯一进设备考核的一段': 'the only segment under equipment accountability',
-    '命令与实际功率对得上': 'command and actual power agree',
-    '功率未被压制': 'power was not suppressed', '按责任归属': 'by accountability',
-    '限电与调度令': 'curtailment and dispatch orders', '不进设备责任': 'not equipment accountability',
-    '正常发电欠发与低风待机': 'under-generation in production and low-wind standby',
-    '能量守恒核': 'energy conservation check', '全额定上限': 'all-rated ceiling',
-    '量级自检': 'magnitude self-check', '最大一笔是限电不是故障': 'the largest item is curtailment, not faults',
-    '限电命令面占比': 'curtailment command coverage', '移至独立板块': 'moved to a separate panel',
-    '交付九分册一一对应': 'maps one-to-one onto the nine delivery volumes',
-    '无 SCADA 轴': 'no SCADA axis', '分册蓝本': 'volume basis', '巡检': 'inspection',
-    '独立页': 'dedicated page', '点机组号': 'click a unit number', '看全场': 'view fleet',
-    '项占': ' items account for ', '直接比': 'comparing directly', '会读反': 'reads backwards',
-    '最低但均': 'lowest yet averages ', '灰柱': 'grey bar', '本图': 'this figure',
-    '量排': ' ranked by volume', '点击加入报告': 'click to add to the report',
-    '再点击移出': 'click again to remove', '画布里已有': 'the canvas holds ',
-    '悬停查看组件说明': 'hover for a description', '右侧': 'on the right', '调整顺序': 'to reorder',
-})
-
-# ── 15 收尾末轮 ──
-TERMS.update({
-    '六步进度': 'six-step progress', '实线': 'solid line', '虚线': 'dashed line',
-    '能解的': 'can solve', '有过': 'previously had ', '是针对本': 'addresses this ',
-    '尺度': 'scale', '跨两个数量级': 'spanning two orders of magnitude', '弱源': 'weak source',
-    '早修成本被': 'the cost of repairing early is ', '稀释': 'diluted',
-    '晚修成本被': 'the cost of repairing late is ', '放大': 'amplified',
-    '窗口与船期': 'access windows and vessel scheduling', '船期': 'vessel scheduling',
-    '总表': 'master table', '自动翻绿': 'turns green automatically', '会话': 'session',
-    '五级来源': 'five-level source', '取最新版不写死日期': 'always takes the latest version, no hard-coded date',
-    '定谳链': 'settled-verdict chain', '以上': ' and above', '两轴': 'two axes',
-    '取严合并': 'merged on the stricter of the two', '厂商资料': 'vendor documents',
-    '现场反馈修正': 'site feedback corrections', '它是判据的地基': 'it underpins the criteria',
-    '变更须过审': 'changes require review', '运行期计算': 'computed at runtime',
-    '两类': 'two classes', '入图谱': 'into the knowledge graph', '厂商/技术': 'vendor / technical',
-    '分步处置': 'step-by-step procedure', '点击查看': 'click to view', '点击': 'click ',
-    '在已知盲区上': 'over a known blind spot', '见列头': 'see column headers',
-    '同时发生': 'simultaneous', '才看得出': 'only then is it visible: ',
-    '类机制签名': 'mechanism-class signature', '签名': 'signature',
-    '落在几个离散位置上': 'sit at a few discrete positions', '离散位置': 'discrete positions',
-    '偏差达中位的': 'deviation reaches the median by ', '正态期望': 'normal expectation',
-    '筛查': 'screening', '固定判别窗': 'fixed discrimination window',
-    '风速计偏高': 'anemometer reads high', '残差带': 'residual band',
-    '可辨门': 'discrimination gate', '标定漂移': 'calibration drift',
-    '判别窗': 'discrimination window', '一致': 'consistent', '刷屏': 'flooding',
-    '数量级': 'orders of magnitude', '中值': 'mid-value', '地基': 'foundation',
-    '取严': 'take the stricter', '合并': 'merge', '列头': 'column header',
-    '知盲区': 'known blind spot', '离散': 'discrete', '期望': 'expectation',
-    '计高': 'reads high', '偏高': 'reads high', '带': ' band', '门': ' gate',
-    '过审': 'review', '取最新版': 'latest version', '不写死': 'not hard-coded',
-    '期计算': ' computed', '五级': 'five-level', '来源': 'source',
-})
-
-# ── 16 地道化: 高可见句子整条重写 + 中文标点 ──
-# 用户令: 要英文技术文档的自然表达, 不是逐词替换出来的中式英语。
-# 逐词替换必然产出中文语序 ("bar height=均count, is not -total"), 高可见文案必须整句重写。
-PUNCT = {
-    '。': '. ', ',': ', ', '、': ', ', ';': '; ', ':': ': ',
-    '(': ' (', ')': ') ',
-    # 「」《》 在英文里没有对应形态, 且引号会被闸拦 (会破坏注入上下文) — 去掉即可, 语义不丢
-    '「': ' ', '」': ' ', '《': ' ', '》': ' ',
-    '★': '* ', '⇒': ' leads to ', '~': '~', '·': ' · ', '|': ' | ',
-}
-TERMS.update(PUNCT)
-TERMS.update({
-    # 窗声明
-    '本页全部数字随上方窗切换': 'all figures on this page follow the window selector above',
-    '本页含两个窗': 'this page uses two different windows',
-    '固定窗 · 不随上方切换': 'fixed window — independent of the selector above',
-    '窗内 2026-07 数据不满月': '2026-07 is a partial month',
-    '月度图已按天归一': 'monthly figures are normalised per day',
-    # 能量账
-    '理论可发 (实际+损失)': 'gross available (net exported + losses)',
-    '实际上网 · 等效满发': 'net exported · equivalent full-load hours',
-    '能量守恒核 · 理论 < 全额定上限': 'energy conservation check — gross available below the all-rated ceiling',
-    '限电 (命令与实际功率对得上)': 'curtailment (command matches actual power)',
-    '限电命令期 · 功率未被压制': 'curtailment order in force, power not suppressed',
-    '故障与检修停机': 'faults and maintenance downtime',
-    '正常发电时的欠发': 'under-generation while producing',
-    # 故障统计
-    '柱高=日均条数, 不是月总数': 'bars show events per day, not the monthly total',
-    '本窗 2026-07 数据不满月(灰虚线柱), 直接比月总数会读反':
-        '2026-07 is a partial month (grey dashed bar) — comparing raw monthly totals would reverse the reading',
-    '实测: 2026-07 总数最低但日均': 'measured: 2026-07 has the lowest total but the second-highest daily rate at ',
-    '条/日 = 全窗前列': ' events per day',
-    '关键少数: 前': 'the vital few — the top ', '项占 80%': ' items account for 80%',
-    '共 12 项, 合计': 'of 12 codes, ', '前三': 'top three',
-    '条数=发生频次, 不反映影响程度': 'count is frequency, not severity',
-    # 决策链
-    '一台机组从「发现异常」到「修完验收」要走六步': 'A unit moves through six steps, from anomaly detected to repaired and verified',
-    '要补数据就把新文件放到「约定位置」再跑「摄入命令」': 'To add data, place the file in the stated location and run its ingest command',
-    '这张盘不讲某一台的故事, 用于确认': 'This board tracks the fleet rather than a single unit, showing ',
-    '已达': 'complete', '数据不足不可判': 'insufficient data', '未达': 'not reached',
-    '瓶颈集中在一步, 不是「再多看点数据」能解的':
-        'the bottleneck sits at one step and more data will not clear it',
-    '的下一步': ' — next step: ',
-    '定性: 现场/取样把根因定到部件与失效模式':
-        'characterise on site or by sampling: identify the root cause down to component and failure mode',
-    '核实: 向现场调取 2025~2026 工单': 'obtain 2025–26 work orders from site',
-    # 维护端口
-    '数据维护面': 'Data sources', '个源': ' registered',
-    '每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。':
-        'Each source lists coverage, last update, location, ingest command, refresh cadence and owner. ',
-    '要补数据就把新文件放到「约定位置」再跑「摄入命令」。':
-        'To add data, place the file in the stated location and run its ingest command. ',
-    '随运营滚动 — 新数据到位跑摄入, 图谱与判级自动刷新':
-        'Rolls with operations: once new data lands, run the ingest command and the graph and grading refresh automatically',
-    '相对稳定 — 换厂商资料或现场反馈修正时才动; 它是判据的地基, 变更须过审':
-        'Relatively stable: changed only when vendor documents or site feedback require it. These underpin the criteria, so changes go through review',
-    '怎么补数据': 'How to add data', '当前覆盖': 'Coverage', '末次更新': 'Last updated',
-    '更新频率': 'Cadence', '责任方': 'Owner', '数据层': 'Data layer', '机理层': 'Domain layer',
-    '放到': 'place in ', '跑': 'run ',
-})
-
-# ── 17 精修: 用源码里的准确原文入表 (之前凭印象写 key, 有细微出入所以没命中) ──
-TERMS.update({
-    '随运营滚动 — 新数据到位后跑摄入, 本体与判级自动刷新':
-        'Rolls with operations: when new data lands, run its ingest command and the ontology and grading refresh automatically',
-    '要补数据就把新文件放到': 'To add data, place the file in the ', '再跑': ' and run the ',
-    '约定位置': 'stated location', '摄入命令': 'ingest command',
-    '数据线': 'analytics team', '现场': 'site', '振动线': 'vibration team',
-    '化验机构': 'laboratory', '厂商/技术': 'vendor / engineering',
-    'windscada → 图谱': 'analytics to knowledge graph', '振动线 → 图谱': 'vibration team to knowledge graph',
-    # 常见中式残片
-    '总table': 'master table', '注意度': 'note that ', '实为': 'is in fact ',
-    '级事件': '-level event', '不work order': 'is not a work order',
-    '六类source': 'six source classes', '核过确': 'verified as ', '需site补': 'requires site to supply ',
-    '新一round到货aft rerun': 'rerun when the next batch arrives',
-    '自动翻绿': 'turns green automatically', '两家': 'two providers',
-    '取严合and': 'merged on the stricter of the two', '两 axis': 'two axes',
-    '来source': 'source', '五 level': 'five levels', '自动取最new版': 'always takes the latest version',
-    '不写死期': 'no hard-coded date', '以upper': ' and above',
-    '会话': 'session', '期计算': ' computed at runtime', '两 class': 'two classes',
-    '分 steps action': 'step-by-step procedures', '分steps': 'step-by-step',
-})
-
-# ── 7 量词/连接词/动词 (最后补, 都是短词; 长词已在前面命中所以不会被截断) ──
-TERMS.update({
-    '非行业': 'not the industry-standard ', '真窗': 'actual window', '真': 'actual ',
-    '近90日固定': 'last 90 days, fixed', '所选时间窗': 'selected time window',
-    '判级窗': 'Grading window', '五态': 'five states', '温度月轨迹': 'monthly temperature trend',
-    '剔除口径': 'excl. basis', '剔除': 'excluding', '不含调度令': 'excl. dispatch orders',
-    '不含': 'excl. ', '含': 'incl. ', '另计': 'counted separately', '另': 'also ',
-    '起': '', '个': '', '处': '', '份': '', '轮': ' round', '步': ' steps',
-    '共': 'total ', '约': 'approx. ', '仅': 'only ', '已': '', '未': 'not ',
-    '按': 'by ', '见': 'see ', '据': 'per ', '经': 'via ', '含义': 'meaning',
-    '全真': 'all verified', '引用id数': 'cited ids', '溯源核验': 'Traceability check',
-    '用时': 'elapsed', '模型': 'model', '推理中': 'reasoning', '空闲': 'idle',
-    '提问': 'Ask', '查询': 'Query', '查': 'Go', '常查': 'Common', '报警码': 'Alarm code',
-    '失效模式': 'failure mode', '手册分步处置': 'manual step-by-step procedure',
-    '历史工单': 'historical work orders', '年度任务': 'annual task', '系统': 'system',
-    '本体确定性查询': 'Deterministic ontology query',
-    '结果': 'result', '手册与工单source-backed record': 'verbatim from manuals and work orders',
-    '不经模型': 'no model involved', '预设': 'Preset', '章节': 'section', '受众': 'Audience',
-    '场站内部': 'Internal', '上报集团': 'For group reporting', '全量': 'full', '脱敏': 'redacted',
-    '打印': 'Print', '存': 'Save as', '导出': 'Export', '组件库': 'Block library',
-    '画布': 'canvas', '文档预览': 'Document preview', '报告': 'report', '运行分析': 'Operations Analysis',
-    '统计期': 'Reporting period', '数据源': 'Data sources', '出件': 'issued',
-    '覆盖': 'coverage', '末次更新': 'last updated', '位置': 'location', '摄入': 'ingest',
-    '责任': 'owner', '频率': 'frequency', '说明': 'note', '存在': 'present',
-    '数据': 'Data', '机理': 'Domain model', '层': 'layer', '名': 'name',
-})
-
-_RE = None
-
-
-def _compile():
-    global _RE
-    keys = sorted(TERMS, key=len, reverse=True)      # 最长匹配优先
-    _RE = re.compile('|'.join(re.escape(k) for k in keys))
-    return _RE
-
-
-# 单字量词只在数字后才是量词: ' 行' 当 key 会切进 行动等级/行星级 (实测切成 "rows 动等
-# level"), 加前导空格也锁不住 "/ 行动"。词表是纯字符串匹配, 表达不了上下文条件, 故后处理。
-_RE_ROWS = re.compile(r'(?<=\d)\s*行(?![\w\u4e00-\u9fff])')
-# 量词 '条' 同理: 数字后面是量词该译, 但收成单字条目会切碎 条件 / 条款 / 一条链。
-_RE_CNT = re.compile(r'(?<=\d)\s*条(?![\w\u4e00-\u9fff])')
-
-
-def translate(s):
-    """替换已知术语; 未知中文原样留下, 由 audit_untranslated 收口。
-
-    ★词间补空格 (2026-08-28 实测): 中文没有词界, 两个相邻中文词各自被替换后英文会直接粘连 —
-    "分析"+"决策" → "analysisdecision"。替换时看后一个字符, 若替换结果以字母结尾且下一个
-    字符也是字母/中文, 补一个空格。这只缓解粘连, 治不了"部分覆盖"本身: 未译的中文词
-    仍会夹在英文中间 ("风电智能analysis"), 所以覆盖率必须推到接近满值才可交付。
-    """
-    if not isinstance(s, str) or not s:
-        return s
-    r = _RE or _compile()
-
-    def _sub(m):
-        v = TERMS[m.group(0)]
-        nxt = s[m.end():m.end() + 1]
-        if v and v[-1].isascii() and v[-1].isalnum() and nxt and (nxt.isalnum() or nxt == '(' or '\u4e00' <= nxt <= '\u9fff'):
-            return v + ' '
-        return v
-    out = _RE_CNT.sub(' events', _RE_ROWS.sub(' rows', r.sub(_sub, s)))
-    # 数字上下文量词: 只有紧跟数字时才是量词, 收成单字条目会切碎 次数/第一/年度/个别/台账
-    out = _RE_TIMES.sub(' times ', out)
-    out = _RE_UNITS.sub(' units ', out)
-    out = _RE_YEAR.sub('', out)
-    out = _RE_ITEMS.sub(' ', out)
-    out = _RE_STEP.sub('No.', out)
-    out = _RE_STEPN.sub(' steps ', out)
-    out = _RE_TOTAL.sub('total ', out)
-    out = _RE_IN_N.sub('in ', out)
-    # 上面几条一律补尾空格防粘连 (实测 "6 stepshandling"), 再压掉行内多余空格。
-    # 只压 空格/tab, 不碰换行 —— 产物里有靠换行分段的文本。
-    out = re.sub(r'(?<=\S)[ \t]{2,}', ' ', out)
-    # ★混杂回退 (2026-08-30 用户在检修助手排查步骤里逮到): 词表对长尾技术文本
-    # (手册排查步骤、告警处置) 不可能穷举, 部分覆盖会把整句切成中英混杂 ——
-    # "更换机侧BCU控制器" → "replace 机侧BCUcontrol 器", 读不通且显得系统劣质。
-    # 判据不靠列举而靠自适应: 译完仍留下**过半**原有中文 ⇒ 这段根本没覆盖,
-    # 整段退回原文。完整的中文至少是句能读的话, 客户看得出这是未翻译区。
-    zh0 = sum(1 for c in s if '\u4e00' <= c <= '\u9fff')
-    if zh0 >= 6:
-        zh1 = sum(1 for c in out if '\u4e00' <= c <= '\u9fff')
-        if zh1 / zh0 > 0.5:
-            return s
-    return out
-
-
-def tr_deep(o):
-    if isinstance(o, str):
-        return translate(o)
-    if isinstance(o, dict):
-        return {translate(k) if isinstance(k, str) else k: tr_deep(v) for k, v in o.items()}
-    if isinstance(o, list):
-        return [tr_deep(v) for v in o]
-    return o
-
-
-def audit_untranslated(text, top=40):
-    """列出仍未译的中文片段, 按出现次数排 — 迭代补词表用。"""
-    import collections
-    frags = re.findall(r'[一-鿿]+', text or '')
-    return collections.Counter(frags).most_common(top)
-
-
-_assert_quote_free()
-
-# ── 18 图表说明与告警码 ────────────────────────────────────────────
-# 来源: 单文件版下钻浮层。这批文案此前从未进过残留统计 —— 统计只走了十个 tab 的
-# 页面文本, 而浮层要点击才出现。口径漏了一层, 补测 15 个浮层扫出 45 词 629 字。
-# 取值全部从预烤 JSON 的 fig.note / fig.unit / fig.title 抓原文, 不凭印象写 key。
-TERMS.update({
-    # 图例整句 (整句先于逐词匹配, 避免拼出中文语序)
-    '灰带=全场四分位区间, 深线=全场中位, 红线=本台':
-        'Grey band = fleet interquartile range; dark line = fleet median; red line = this unit',
-    '灰带=全场四分位区间': 'Grey band = fleet interquartile range',
-    '振动判级交叉轴: 温度=同机制果侧观测(观测面≠源数)':
-        'Vibration grading cross-check: temperature is an effect-side observation of the same '
-        'mechanism (an observation face is not an independent source)',
-    '数据=TCM 六窗标量': 'Data: TCM six-window scalars',
-    '窗内合计': 'Total in window',
-    '判据:': 'Criterion:',
-    # 统计与图元
-    '稳健离群度': 'robust outlier score', '六窗比': 'six-window ratio',
-    '末窗/首窗': 'last window / first window', '六窗标量': 'six-window scalars',
-    '四分位区间': 'interquartile range', '全场中位': 'fleet median',
-    '灰带': 'grey band', '深线': 'dark line', '红线': 'red line',
-    '均值': 'mean', '稳健': 'robust', '档带宽': 'band width', '档': 'band',
-    '月样本中位': 'monthly sample median', '月样本': 'monthly sample',
-    '产物转录': 'transcribed from analysis outputs', '源数': 'source count',
-    '上升': 'rising', '定向查': 'targeted check', '报警门': 'alarm gate',
-    # 量与单位
-    '秒/日': 's/day', '泵': 'pump', '制动': 'brake',
-    '月度条数': 'monthly count', '报警月度条数': 'alarm count per month',
-    # 测点与系统
-    '偏航润滑泵运行时长': 'yaw lubrication pump runtime',
-    '桨叶零位偏差': 'blade zero-position deviation',
-    '齿轮箱中速轴温度': 'gearbox IMS bearing temperature',
-    '齿轮箱高速轴温度': 'gearbox HSS bearing temperature',
-    '变压器绕组最高温度': 'transformer winding peak temperature',
-    '主控与传感网系统': 'main controller and sensor network',
-    '蓄能失效候选': 'accumulator failure candidate', '蓄能行为': 'accumulator behaviour',
-    '振动分析摘要': 'vibration analysis summary', '振动标量': 'vibration scalars',
-    '偏航系统': 'yaw system', '变桨系统': 'pitch system', '偏航失败': 'yaw failure',
-    '发电机侧': 'generator side', '发电态': 'generating', '风传感': 'wind sensing',
-    '变桨分册': 'pitch volume',
-    # 告警码名称 (top: ... 里出现)
-    '自动解缆停机': 'automatic cable untwist stop',
-    '偏航制动液压压力异常': 'yaw brake hydraulic pressure fault',
-    '偏航失败-等待': 'yaw failure - waiting', '偏航失败-停止': 'yaw failure - stop',
-    '偏航电机过热': 'yaw motor overheat',
-    '偏航变流器警告过多': 'excessive yaw converter warnings',
-    '变流器降功率运行(DG)': 'converter derated operation (DG)',
-    '变流器跳闸(自动重启)': 'converter trip (auto-restart)',
-    'ABB变流器PU通讯故障': 'ABB converter PU communication fault',
-    'UPS电池电压低': 'UPS battery voltage low',
-    'FT1超声波风速计故障': 'FT1 ultrasonic anemometer fault',
-    '液压监控·轮毂(变桨)压': 'hydraulic monitoring - hub (pitch) pressure',
-    '变桨轴承润滑(执行中)': 'pitch bearing lubrication (running)',
-    '变桨泵运行超时停机': 'pitch pump runtime timeout stop',
-    '液压油温异常': 'hydraulic oil temperature fault',
-    '液压油位异常': 'hydraulic oil level fault',
-    '轮毂部件松动': 'hub component loose',
-    '风速计偏高候选(A类)': 'anemometer reads-high candidate (class A)',
-    '风速计偏置': 'anemometer bias',
-})
-_assert_quote_free()
-
-# '条' 原映射空串, 使 "窗内合计 40 条; top:" 译成 "Total in window 40 ; top:" — 英文这里
-# 需要一个名词把数字接住, 空串留下悬空的分号。'次'/'起' 保持省略 (英文计数惯例不同)。
-TERMS['条'] = 'events'
-_assert_quote_free()
-
-# ── 19 图例硬编码文案 ──────────────────────────────────────────────
-# 这批不在 fig.note 里, 是 drawFig 渲染时拼进 SVG/HTML 的固定图例 —— 从源码 grep 会淹没在
-# Python 注释里, 故改从浮层 DOM 的文本节点直接抓, 只有 11 个串。key 保留 &lt; 原形:
-# hook 拦的是 innerHTML 写入前的字符串, 那时实体还没解码。
-TERMS.update({
-    '条=本台/全场中位 (竖线=1× 基准, 右端封顶 2.5×); 色按 z (稳健离群度): 绿&lt;2 · 橙2~3 · 红≥3':
-        'Bar = this unit / fleet median (vertical line = 1x reference, right end capped at 2.5x); '
-        'colour by z (robust outlier score): green under 2 · orange 2 to 3 · red 3 and above',
-    '谱级深潜与逐窗趋势 → 振动融合板块·振动分析入口 (windcms)':
-        'Spectrum-level deep dive and per-window trend → Vibration Fusion section · '
-        'vibration analysis entry point (windcms)',
-    '峭度': 'kurtosis', 'ISO烈度': 'ISO severity', '烈度': 'severity',
-    '速度有效值': 'velocity RMS', '零位偏差': 'zero-position deviation', '零位': 'zero position',
-    '竖线': 'vertical line', '右端封顶': 'right end capped at', '封顶': 'capped',
-    '色按': 'colour by', '基准': 'reference',
-    '绿': 'green', '橙': 'orange', '红': 'red',
-    '谱级深潜': 'spectrum-level deep dive', '深潜': 'deep dive',
-    '逐窗趋势': 'per-window trend', '逐窗': 'per-window', '板块': 'section',
-    # 测点与工况 (浮层标题)
-    '(发电态P>500kW)': '(generating, P over 500 kW)', '·均值 月度': '· mean, monthly',
-    '本台 vs 全场': 'this unit vs fleet', '记基线观察': 'baseline recorded, monitoring',
-    '温度候选(定向查)': 'temperature candidate (targeted check)',
-    '发电机驱动端(DE)轴承温度': 'generator drive-end (DE) bearing temperature',
-    '发电机非驱动端(NDE)轴承温度': 'generator non-drive-end (NDE) bearing temperature',
-    '后主轴承温度(齿轮箱侧)': 'rear main bearing temperature (gearbox side)',
-    '液压油温': 'hydraulic oil temperature',
-    '(发电机侧)': '(generator side)', '(转子侧)': '(rotor side)',
-    '(功率 0-1600 kW 档)': '(power band 0-1600 kW)',
-    '(功率 1601-2000 kW 档)': '(power band 1601-2000 kW)',
-    # 告警码 (第二批)
-    '齿轮油温度警告': 'gear oil temperature warning', '齿轮油温度高': 'gear oil temperature high',
-    '高速轴发电机侧齿轮轴承温': 'HSS generator-side gear bearing temperature',
-    'FT1超声波风速计通讯故': 'FT1 ultrasonic anemometer communication fault',
-    '一次风速计故障': 'primary anemometer fault', '一次风向标失效': 'primary wind vane failure',
-})
-_assert_quote_free()
-
-# 上面那次单字回归测试顺带暴露的既有裂缝 (单字 '进' 把 "进行" 切成 "into 行")
-TERMS.update({'进行': 'carry out', '行动等级': 'action level', '等级': 'level'})
-_assert_quote_free()
-
-# ── 20 删除汉字单字条目 ────────────────────────────────────────────
-# 2026-08-30 用户在检修助手的排查步骤里逮到: "检查机侧BCU控制器" 被译成
-# "检Go 机 side BCUcontrol 器"。根因是词表里有约 110 个**汉字单字**条目
-# (查→Go / 卡→held / 侧→side / 换→replace / 上→upper / 步→steps ...):
-# 它们在词表收录了的长词里被最长匹配保护住, 一旦碰到**未收录的长词**就把词切碎。
-#
-# 这类长尾技术文本(手册排查步骤、告警处置)不可能穷举收词, 所以靠"补长词"堵不住。
-# 判断: **半译比不译更糟** —— "检Go 机 side" 读不通且显得系统劣质,
-# 而完整的 "检查机侧" 至少是句能读的话, 客户看得出这是未翻译区而非系统出错。
-# 故删除所有汉字单字, 只保留标点与符号映射 (那些无歧义)。
-_CJK1 = [k for k in TERMS if len(k) == 1 and '一' <= k <= '鿿']
-for _k in _CJK1:
-    del TERMS[_k]
-_RE = None          # 词表变了, 强制重编译
-
-# 单字按钮的收尾: 删除汉字单字条目后, 界面上孤立的单字按钮(查/看)不再被译。
-# 解法不是把单字加回来(那会重新切碎长词), 而是把按钮文案本身改成双字词。
-TERMS.update({'查询': 'Query', '查看': 'View', '常用': 'Common', '未知告警码': 'Unknown alarm code'})
-_RE = None
-
-# ── 21 告警码链模板与码名 ──────────────────────────────────────────
-# 用户: 还有很多字没改过来。实测预烤 JSON 里含中文的串 6131 个 —— 全译出 3021,
-# 整段回退 878 (23918 字), 部分残留 2232 (14166 字)。残留高度集中在**模板化**的
-# 告警码链与失效模式链上, 所以补模板词的边际收益远高于补零散长句。
-TERMS.update({
-    # 模板 (每条告警码链都出现, 频次见注释)
-    '告警类型': 'alarm type',            # 120
-    '码表故障原因': 'code-table fault cause',  # 60
-    '本场报警': 'site alarms',           # 130
-    '预防性任务': 'preventive task', '预防链': 'preventive chain',
-    '系统级': 'system-level', '按实证': 'by observed', '出现量排序': 'ranked by volume',
-    '未归属': 'unassigned', '气动': 'pneumatic', '液压/气动失效': 'hydraulic or pneumatic failure',
-    '码表未收录': 'not in code table',
-    # 告警码名称 — 偏航
-    '触发偏航限位传感器': 'yaw limit sensor triggered',
-    '偏航圈数值超出范围': 'yaw turn count out of range',
-    '偏航润滑油脂油位低': 'yaw grease level low',
-    '偏航轴承润滑脂油位低': 'yaw bearing grease level low',
-    '偏航轴承的润滑不可能': 'yaw bearing lubrication not possible',
-    '偏航刹车压力传感器错误': 'yaw brake pressure sensor error',
-    '偏航齿盘压力传感器错误': 'yaw ring-gear pressure sensor error',
-    '偏航刹车压力错误': 'yaw brake pressure error',
-    '偏航齿盘压力错误': 'yaw ring-gear pressure error',
-    '太多的偏航变流器警告': 'too many yaw converter warnings',
-    '太多偏航刹车': 'too many yaw brake applications',
-    '强制偏航一圈': 'forced yaw one turn',
-    '无润滑时,偏航轴承': 'no lubrication, yaw bearing',
-    '无润滑,偏航系统': 'no lubrication, yaw system',
-    # 风传感
-    '风向标阻塞-冰冻': 'wind vane blocked - iced', '风向标阻塞': 'wind vane blocked',
-    '有效性-风速低': 'validity - low wind speed',
-    # 塔架
-    '塔筒紧停开关故障': 'tower emergency-stop switch fault',
-    '检测到塔筒振动频率变化': 'tower vibration frequency shift detected',
-    '机舱紧急开关故障': 'nacelle emergency switch fault',
-    '触发应急开关': 'emergency switch triggered',
-    '叶轮不平衡': 'rotor imbalance',
-    # 变压器
-    '变压器散热器断路器跳开': 'transformer radiator breaker tripped',
-    '变压器油温高': 'transformer oil temperature high',
-    '变压器油位过低': 'transformer oil level too low',
-    '变压器油压力过高': 'transformer oil pressure too high',
-    '变压器油泵跳开': 'transformer oil pump tripped',
-    # 运行与控制
-    '由于功率提升延迟引起的停机': 'stop due to power ramp-up delay',
-    '最大转速等待时间超时': 'maximum speed wait timeout',
-    '在本地操作中的风机': 'turbine in local control',
-    '逾时直流回路充电': 'DC-link charging timeout',
-    '避雷器压敏电阻误差': 'surge arrester varistor error',
-    '变流器冷却机模式': 'converter cooler mode',
-    '软件更新停机': 'software update stop', '手动空转停机': 'manual idling stop',
-    '主制动器切出': 'main brake cut out', '功率控制误差': 'power control error',
-    '启动失败': 'start failure',
-})
-_RE = None
-
-# ── 22 高频残留收口 ────────────────────────────────────────────────
-# 按"部分残留串里的未译片段频次"定优先级 (比逐条读产物高效): 1091 种片段里
-# 前 55 种覆盖了大部分残留串。单字量词不做词条 (会切碎长词), 一律走数字上下文正则。
-_RE_TIMES = re.compile(r'(?<=\d)\s*次(?!数|序|要|级)')
-_RE_UNITS = re.compile(r'(?<=\d)\s*台(?!账|风|阶)')
-_RE_YEAR  = re.compile(r'(?<=\d)\s*年(?![\w一-鿿])')
-_RE_ITEMS = re.compile(r'(?<=\d)\s*个(?!别|人|体|案|性)')
-_RE_STEP  = re.compile(r'第\s*(?=\d)')
-_RE_STEPN = re.compile(r'(?<=\d)\s*步(?!骤)')
-_RE_TOTAL = re.compile(r'共\s*(?=\d)')
-_RE_IN_N  = re.compile(r'分\s*(?=\d)')   # 分 6 步 → in 6 steps; 挡不住的 分析/分级 是双字词, 词表先命中
-# '个' 后面允许跟中文 (5 个模块), 但挡掉 个别/个人/个体/个案/个性 这些真词
-TERMS.update({
-    # 高频双字/多字 (数字 = 出现在多少个残留串里)
-    '检查': 'check',            # 175
-    '资料': 'documentation',    # 160
-    '处理': 'handling',         # 127
-    '培训课件': 'training material',  # 125
-    '变频器': 'frequency converter',  # 92
-    '油脂': 'grease',           # 75
-    '是否': 'whether',          # 72
-    '线路': 'circuit',          # 59
-    '技术资料': 'technical documentation',  # 52
-    '停滞于': 'stalled at',      # 50
-    '软件': 'software', '模块': 'module', '卡钳': 'caliper', '年度': 'annual',
-    '任务': 'task', '编码器': 'encoder', '涉码': 'codes involved', '密封': 'seal',
-    '回转支承的安装': 'slewing bearing installation', '维修和': 'maintenance and',
-    '指南': 'guide', '清理': 'cleaning', '螺栓': 'bolt', '征兆码': 'symptom code',
-    '六步': 'six steps', '相关': 'related', '各': 'each ',
-    # 常见搭配 (双字以上才收, 避免切碎)
-    '检查项': 'check item', '处理步骤': 'handling steps', '检查内容': 'what to check',
-    '维修': 'maintenance', '安装': 'installation', '更换': 'replacement',
-    '紧固': 'tightening', '润滑': 'lubrication', '清洗': 'cleaning',
-    '目视': 'visual', '测量': 'measurement', '记录': 'record',
-})
-_RE = None
-
-# 量词+名词在英文里是一个词: 中文「38 台机组」= 38 turbines, 量词要被吞掉而不是各译各的
-# (放宽 lookahead 后实测出 "38 unitsunit" 这种重复)。词表先于数字正则执行, 故收在这里生效。
-TERMS.update({
-    '台机组': 'turbines', '台风机': 'turbines', '台机': 'turbines',
-    '个失效模式': 'failure modes', '个模块': 'modules', '个部件': 'components',
-    '步处理': 'handling steps', '次动作': 'actions', '个系统': 'systems',
-    '条告警': 'alarms', '项任务': 'tasks', '类失效模式': 'failure-mode classes',
-})
-_RE = None
-
-# ── 23 领域名词与判据措辞收口 ──────────────────────────────────────
-# 排名口径修正后的 top: 先过脱敏 (文档名/业主名客户看不到, 不占补词预算),
-# 再只看"部分残留" (回退段落里的词词表其实已收)。剩下的才是真缺口。
-TERMS.update({
-    # 判据与推断措辞
-    '树深两层不外推': 'tree depth two layers, no extrapolation beyond',
-    '定位仍需图纸与': 'localisation still requires drawings and',
-    '需查图纸或厂商支持': 'requires drawings or vendor support',
-    '需查图纸或厂商': 'requires drawings or vendor',
-    '本码无手册处置条目': 'no handling entry for this code in the manual',
-    '手册覆盖本场': 'manual coverage for this site',
-    '非我方推断': 'not our inference', '因果来自': 'causality from', '归纳自': 'derived from',
-    '方向归一': 'direction normalised', '轴同时非常态': 'axes abnormal simultaneously',
-    '技术依据': 'technical basis', '窗止': 'window ends',
-    # 电气与部件
-    '支撑示意图': 'support diagram', '电气接口图': 'electrical interface diagram',
-    '原理图': 'schematic', '图纸': 'drawing', '主控图纸': 'main controller drawing',
-    '吊具图纸': 'lifting gear drawing', '开关柜': 'switchgear', '环网柜通讯': 'ring main unit comms',
-    '供电元件': 'power supply component', '变流元件': 'converter component',
-    '散热片': 'heat sink', '散热': 'cooling', '通风': 'ventilation', '板卡': 'board',
-    '超声波': 'ultrasonic', '刹车': 'brake', '气象站': 'met station',
-    '各部件资料': 'per-component documentation',
-    '故障逻辑及处理': 'fault logic and handling',
-    # 运维措辞
-    '技改': 'retrofit', '出质保整改': 'end-of-warranty rectification',
-    '全年维护': 'annual maintenance', '供给': 'supply', '走到': 'reaches',
-    '完成三个叶片防雷导通测试': 'complete lightning-continuity test on all three blades',
-})
-_RE = None
-
-TERMS.update({'厂商支持': 'vendor support', '厂商': 'vendor', '主控': 'main controller'})
-_RE = None
-
-# ── 24 界面文案系统性英文化 (第一批) ────────────────────────────────
-# 用户令: 把系统整个重新生成一遍, 不要打补丁式翻译。
-# 做法不是改 serve.py 源码 —— 那里的中文串同时是 UI 文案与**数据键**
-# (d['停机'] 之类取值用的), 源码替换会让取值落空。改为把 UI 文案一次性补全进词表,
-# 显示层因此全英文, 逻辑层的键不动。以下按出现频次批量补, 不再逐条追补。
-TERMS.update({
-    # 判据与结论措辞
-    '依据': 'basis', '判别': 'discriminator', '单位': 'unit', '外部类': 'external class',
-    '倍中位': '× median', '当前在升': 'rising metrics awaiting confirmation', '待现场核实': 'field confirmation required',
-    '仅记录': 'recorded only', '未见异常': 'no anomaly found', '证据窗末': 'evidence window ends',
-    '存在需现场核实或处置的异常': 'anomalies requiring field confirmation or action',
-    '纪律': 'discipline', '可用数据': 'available data', '一级指标': 'primary indicator',
-    '级归综合报告线': 'escalated to the consolidated report line',
-    '进设备考核的只有': 'counted against equipment performance:',
-    '限电属交易与调度侧': 'curtailment belongs to trading and dispatch, not equipment',
-    # 部件与测点
-    '主轴承前': 'main bearing front', '主轴承后': 'main bearing rear', '齿轮': 'gear', '齿箱': 'gearbox',
-    '行星级': 'planetary stage', '一级行星': 'first planetary stage', '行星架': 'planet carrier',
-    '高速轴转子侧': 'HSS rotor side', '传动链三部件': 'three drivetrain components',
-    '润滑油': 'lubricating oil', '油冷': 'oil cooling', '绕组': 'winding',
-    '元器件名称': 'component name', '设备侧': 'equipment side', '运行侧': 'operations side',
-    # 时窗与统计
-    '列窗': 'column window', '日期': 'date', '近90日': 'last 90 days', '近30日': 'last 30 days',
-    '月中位': 'monthly median', '逐日中位': 'daily median', '单次时长': 'duration per event',
-    '停机总时长h': 'total downtime h', '占停机': 'share of downtime', '停机段': 'downtime segment',
-    '日均条数': 'events per day', '条/日': 'events/day', '影响台': 'units affected',
-    '台级top': 'unit-level top', '全场四分位区间': 'fleet interquartile range',
-    '月duty(秒/日)': 'monthly duty (s/day)',
-    # 界面动作与状态
-    '有问题的系统': 'systems with findings', '无问题不出图': 'no chart when nothing is flagged',
-    '任一契约通道': 'any contracted channel', '工作台': 'workspace', '附录': 'appendix',
-    '故障名称': 'fault name', '故障四象限': 'fault quadrant', '安全风险预测': 'safety risk outlook',
-    '特性曲线多镜头': 'performance curve, multiple views', '一级显示': 'top-level view',
-    '问题为空': 'question is empty', '启动': 'start', '卡在哪一步': 'where it stalled',
-    '已选组件再次点击移出': 'click a selected component again to remove it',
-    '已取消': 'withdrawn', '阴性': 'negative',
-    # 模型档位说明
-    '单台状态查询与快速核证': 'single-unit status and quick verification',
-    '多台横向比对与机制推断': 'cross-unit comparison and mechanism inference',
-    '复杂推理与交叉审核': 'complex reasoning and cross-review',
-    '台内即时查询': 'in-unit instant query',
-    '一个SCADA采样格': 'one SCADA sampling interval',
-    '个码': ' codes',
-})
-_RE = None
-
-
-# ── 全局一致替换 (原生英文生成) ────────────────────────────────────
-# 2026-08-31 用户令: 不要打补丁式翻译, 整个系统重新生成一遍。
-#
-# 运行时 hook 的本质是"中文系统外面套一层英文": 它只译写入 DOM 的那一刻的文本,
-# JS 逻辑与 JSON 数据键仍是中文, 于是长尾永远补不完, 且源码里满是中文。
-# 改为**生成时对整页做全局替换** —— 关键在"全局一致":
-#   onclick="TAB='总览'" 与 if(TAB==='总览') 同时变成 'Overview', 比较照样成立;
-#   __SNAP__ 里的键 "停机" 与取值处 d['停机'] 同时变成 'Downtime', 取值照样命中。
-# 逻辑不破的前提是**同一张表、同一次替换、不做任何上下文判断**。
-#
-# 与 translate() 的区别: 这里不做混杂回退 —— 整页替换时"译完还剩中文"是常态
-# (长尾未覆盖), 回退会把整页退回中文。
-# ── 永不翻译的标识符 ──────────────────────────────────────────────
-# 这些中文串在系统里同时充当**数据键**: 后端构造 d['列窗']=... , 前端再 d.列窗 取回。
-# 译了其中任何一侧, 取值就落空 —— 2026-08-31 用户实报"报告点不开、选项点不开"的根因。
-# 它们在界面上的英文化由前端渲染层负责, 不在这一层做。
-NEVER_TRANSLATE = {
-    '报警', '列窗', '级', '工单', '链盘', '振动结论', '矩阵', '能力', '设备状态',
-    '总条数', '定论', '预警', '停机', '停机(调度令)', '限电·绑定', '主轴承后', '主轴承前',
-    '换件闭环 (检出→换件→恢复)', '状态', '天数', '依据', '表', '部件', '停机时长h',
-    '外部', '故障', '类别', '日期', '原因', '排查', '处置', '占停机',
-}
-
-
-def translate_all(s):
-    """对整页文本做最长匹配的全局一致替换; 不回退、不做上下文判断。"""
-    if not isinstance(s, str) or not s:
-        return s
-    r = _RE or _compile()
-
-    def _sub(m):
-        w = m.group(0)
-        if w in NEVER_TRANSLATE:      # 同时充当数据键 → 原样保留
-            return w
-        v = TERMS[w]
-        nxt = s[m.end():m.end() + 1]
-        if v and v[-1].isascii() and v[-1].isalnum() and nxt and (
-                nxt.isalnum() or nxt == '(' or '\u4e00' <= nxt <= '\u9fff'):
-            return v + ' '
-        return v
-    out = r.sub(_sub, s)
-    out = _RE_CNT.sub(' events', _RE_ROWS.sub(' rows', out))
-    out = _RE_TIMES.sub(' times ', out)
-    out = _RE_UNITS.sub(' units ', out)
-    out = _RE_YEAR.sub('', out)
-    out = _RE_ITEMS.sub(' ', out)
-    out = _RE_STEP.sub('No.', out)
-    out = _RE_STEPN.sub(' steps ', out)
-    out = _RE_TOTAL.sub('total ', out)
-    out = _RE_IN_N.sub('in ', out)
-    return re.sub(r'(?<=\S)[ \t]{2,}', ' ', out)
-
-
-# ── 源码级英文化 (保护数据键) ──────────────────────────────────────
-# translate_all 无差别替换会炸: `${r.停机时长h}` → `${r.Downtime hours h}`,
-# 属性名带空格即 JS 语法错误 (2026-08-31 实测 237 处风险点)。
-#
-# 正确的切分不是"UI 文案 vs 数据键"这种语义判断 (自动判不准), 而是**语法位置**:
-#   .中文        属性访问 —— 键, 不译 (数据里的键也是中文, 两边必须一致)
-#   ['中文']     下标访问 —— 同上
-#   "中文":      对象字面量/JSON 的键 —— 同上
-# 其余位置的中文都是要显示的文本, 一律译。
-#
-# 比较值不在保护之列且必须不保护: TAB==='总览' 与 onclick="TAB='总览'" 同时被译成
-# 'Overview', 比较照样成立 —— 一致性来自"同一张表替换所有出现处"。
-_PROT = [
-    re.compile(r'\.[一-鿿][一-鿿A-Za-z0-9_]*'),           # .属性
-    re.compile(r'\[\s*[\'"][^\'"]*[一-鿿][^\'"]*[\'"]\s*\]'),  # ['键']
-    re.compile(r'[\'"][一-鿿][^\'"]{0,40}[\'"]\s*:'),   # "键":
-    # Python 关键字参数名 dict(倍中位=...) —— 译了就是非法标识符 (实逮 × median=)。
-    # 限定在 ( 或 , 之后, 避免误伤字符串里的 "灰带=全场四分位区间" 这类显示文案。
-    re.compile(r'(?<=[(,])\s*[一-鿿][一-鿿A-Za-z0-9_]*(?=\s*=(?!=))'),
-    # 多行调用的续行: 关键字参数落在行首, 前面既不是 ( 也不是 , (逐行处理看不到上一行)
-    re.compile(r'^[一-鿿][一-鿿A-Za-z0-9_]*(?=\s*=(?!=))'),
-    # 中文函数名/变量名定义
-    re.compile(r'(?<=def )[一-鿿][一-鿿A-Za-z0-9_]*'),
-]
-
-
-def to_english_source(src):
-    """把一份含中文的页面源码转成英文, 保留数据键不动。"""
-    if not isinstance(src, str) or not src:
-        return src
-    keep = []
-
-    def hide(m):
-        keep.append(m.group(0))
-        return '\x00%d\x00' % (len(keep) - 1)
-
-    for rx in _PROT:
-        src = rx.sub(hide, src)
-    src = translate_all(src)
-    return re.sub(r'\x00(\d+)\x00', lambda m: keep[int(m.group(1))], src)
-
-# ── 25 显示文案整句原生英文 ────────────────────────────────────────
-# 用户: 别再用"改中文成英文"的方式, 要让英文在生成时就是原生文案。
-# 逐词映射拼出来的句子读着像翻译 (中文语序 + 术语堆叠); 这一批按**整句**写英文技术文档
-# 的表达, 整句条目在最长匹配下优先于任何词级条目, 所以它们直接决定成品读感。
-TERMS.update({
-    # 状态与提示
-    '载入中…': 'Loading…', '加载中…': 'Loading…', '进度': 'Progress',
-    '本地模型编排中…': 'Local model working…',
-    '未标窗': 'window not stated', '窗内无报警': 'no alarms in this window',
-    '← 卡在这里': '← stalled here', '(+约 30 秒)': '(+ approx. 30 s)',
-    '轻量 8B · 约 10 秒': 'Light 8B · approx. 10 s',
-    '标准 27B · 约 30 秒': 'Standard 27B · approx. 30 s',
-    '打印 / 存 PDF': 'Print / Save as PDF',
-    '部件与证据': 'Component and evidence',
-    '换前': 'before replacement', '换后': 'after replacement',
-    '补机制定性': 'mechanism characterisation pending',
-    '油液采样节奏': 'Oil sampling cadence', '限电侧': 'Curtailment side',
-    '在升·待处置对账': 'Rising and pending action — reconciliation',
-    # 链接
-    '该问题页 →': 'Open this issue →',
-    '该台完整问题页 →': 'Full issue page for this unit →',
-    '完整问题页 (全部曲线与工单史) →': 'Full issue page — all curves and work-order history →',
-    # 整句 (决定读感的部分)
-    '只显示有问题的系统; 无问题不出图':
-        'Only systems with findings are shown; nothing is charted when nothing is flagged',
-    '按各台最严重系统状态着色 · 悬停看台号':
-        "Coloured by each unit's most severe system state · hover for the unit",
-    '点击加入报告, 再次点击移出 · 画布里已有':
-        'Click to add to the report, click again to remove · already on the canvas',
-    '还没有生成 — 在上面的交互窗输入报告要求。':
-        'Nothing generated yet — state what the report should cover in the panel above.',
-    '上报版 — 判据门槛与特征频率已脱敏':
-        'Reporting version — criterion thresholds and characteristic frequencies redacted',
-    '上报版隐去判据门槛与特征频率':
-        'the reporting version omits criterion thresholds and characteristic frequencies',
-    '系统失效模式按实证量排 + 部件级原因':
-        'System failure modes ranked by observed volume, with component-level causes',
-    '— 瓶颈集中在一步, 不是"再多看点数据"能解的':
-        '— the bottleneck sits at a single step; more data will not clear it',
-    '"无记录停机"不是没有故障, 是没归上因。':
-        'Unattributed downtime does not mean there was no fault — it means the cause was never assigned.',
-    '右下角报警码属重复触发, 不应按条数排定优先级。':
-        'Codes in the lower right are repeat triggers, not faults; ranking them by count would misdirect the work.',
-    '限电不是"低风时段收到的空令"':
-        'Curtailment is not an empty instruction issued during low wind',
-    '可用率与损失率分母不同': 'availability and loss rate rest on different denominators',
-    '这几台正是算法当初的检出台 ⇒ 故障前能看见、换件后能看见恢复。':
-        'These are the units the method flagged at the time — visible before the failure, '
-        'and visible recovering after replacement.',
-    '该部件在本窗无 SCADA 曲线 (振动谱级证据走 windcms)':
-        'No SCADA curve for this component in this window; spectral evidence is handled in the CMS module',
-    '— 台账止 2024-11, 本期检修执行与闭环无法自动判定':
-        '— the ledger ends 2024-11, so execution and closure in the current period cannot be determined automatically',
-    '台账 2020~2024-11, 本报告不对本期检修执行下结论':
-        'Ledger covers 2020 to 2024-11; this report draws no conclusion about maintenance execution in the current period',
-    '— 四源里油液长期 stale, 只能作回测锚':
-        '— of the four sources, oil is chronically stale and serves only as a retrospective anchor',
-    '— 损失六成在交易与调度侧, 设备侧改善空间有限':
-        '— six tenths of the loss sits with trading and dispatch; the equipment side has limited headroom',
-    '(证据: 已定案)': '(evidence: established)',
-    '(证据: 待现场核实 / 已记为基线下窗观察)':
-        '(evidence: field confirmation required / recorded as baseline, watch next window)',
-    '(传动链: 主承前·主承后·齿轮箱·发电机两端)':
-        '(drivetrain: main bearing front and rear, gearbox, both generator ends)',
-    '(主承前·主承后·齿轮箱三级·发电机DE/NDE; 悬停看逐件等级)':
-        '(main bearing front and rear, three gearbox stages, generator DE and NDE; hover for per-component grade)',
-    '回收 25~26 工单': 'recover the 2025–26 work orders',
-    # ★含场名的标题必须脱敏
-    '如东海上风电场 · 运行分析报告': 'Offshore wind farm · operations analysis report',
-})
-_RE = None
-
-# ── 26 工单与检修记录文本 ──────────────────────────────────────────
-# 残留的大头不是界面文案而是**数据内容** —— 工单描述、巡检项、消缺记录。
-# 数据库里存的就是中文, 英文版没有第二份数据源, 所以只能靠词表覆盖。
-# 按可消字数排序批量补 (top 60 覆盖了大半)。
-TERMS.update({
-    '为该台该': "for this unit's", '轴无非常态': 'no abnormality on this axis',
-    '日常': 'routine', '缺失': 'missing', '损坏': 'damage', '定检': 'scheduled inspection',
-    '监测': 'monitoring', '消除': 'cleared', '消缺': 'defect clearance', '新增': 'added',
-    '卫生': 'cleaning', '机舱卫生': 'nacelle cleaning', '机舱底部卫生': 'nacelle floor cleaning',
-    '机舱': 'nacelle', '风扇': 'fan', '逆变器': 'inverter', '通讯': 'communication',
-    '震动': 'vibration', '告警': 'alarm',
-    # 检修项目
-    '防雷导通测试': 'lightning continuity test', '导通测试': 'continuity test',
-    '完成三个叶片防雷导通测试': 'lightning continuity test completed on all three blades',
-    '光纤整改及升降机维护': 'fibre rectification and lift maintenance',
-    '光纤改造': 'fibre upgrade', '升降机维护': 'lift maintenance',
-    '垫片加装': 'shim installation', '水扩容': 'coolant top-up',
-    '接地漏电': 'earth leakage', '平台盖板': 'platform cover',
-    '主断路器': 'main breaker', '集油瓶': 'oil collection bottle',
-    '不间断电源旁路': 'UPS bypass', '号五防钥匙': 'interlock key',
-    '松散物体': 'loose objects', '到松散物体': 'loose objects found',
-    '到有松散物体': 'loose objects present',
-    # 位置描述
-    '机侧第一个': 'machine side, first', '机侧第三个': 'machine side, third',
-    '机侧第五个': 'machine side, fifth', '网侧第一个': 'grid side, first',
-    # 数据来源与口径
-    '数值按需逐份解析': 'values parsed per report as required',
-    '非第三方独立源': 'not an independent third-party source',
-})
-_RE = None
-
-# ── 27 报告块库标签 ────────────────────────────────────────────────
-# 用户截图逮到块库满屏中英混杂 (monthly generation 表 / 部件停机表 / 故障Pareto / basis 与boundary)。
-# 这些是前端硬编码标签, 但其中的 故障/表/与 等词同时是数据键, 落在 NEVER_TRANSLATE 里,
-# 于是词级替换绕不过去。解法: 收**整句**条目 —— 最长匹配优先, 整句先命中就不会再拆到单词层。
-TERMS.update({
-    '逐月发电表': 'Monthly generation table',
-    '逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率':
-        'Monthly export, loss, loss rate and equivalent full-load hours, with data coverage',
-    '交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答':
-        'Trading and dispatch, equipment, operations — the three-way split, answering',
-    'MTBF × MDT 部件象限, 分出': 'MTBF against MDT by component, separating',
-    '部件停机表': 'Component downtime table',
-    '逐部件停机时长 / 事件数 / MTBF / MDT / 影响台数':
-        'Downtime, event count, MTBF, MDT and units affected, per component',
-    '已归因 vs 无记录停机的占比 — 无记录不等于没有故障':
-        'Attributed versus unattributed downtime — unattributed does not mean fault-free',
-    '故障帕累托': 'Fault Pareto',
-    '按条数降序 + 累计 80% 线, 找关键少数':
-        'Ranked by count with a cumulative 80% line, to find the vital few',
-    '条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级':
-        'Count against duration per event, separating repeat triggers from real downtime — '
-        'ranking by count alone misdirects priority',
-    '按天归一的日均条数, 数据不满月单独标注':
-        'Events per day, normalised; partial months flagged separately',
-    '需跟踪台的部件 / 证据源 / 六步进度 / 卡在哪一步':
-        'Units under tracking: component, evidence source, six-step progress, and where it stalled',
-    '证据→机制→判级→排期→动作→验收 六步的瓶颈分布':
-        'Where the six steps stall: evidence, mechanism, grading, scheduling, action, acceptance',
-    '检出 → 换件 → 复测回落的历史例证, 证明这条链是通的':
-        'Detection, replacement, retest — historical cases showing the chain closes',
-    '由数据推出的行动项, 不是套话':
-        'Action items derived from the data, not boilerplate',
-    '口径与边界': 'Basis and boundaries',
-    '本地模型基于实测数据生成的运行总结 — 为图表补充文字结论, 供汇报使用':
-        'An operating summary written by the local model from measured data — a written '
-        'conclusion to accompany the charts',
-    '先输入报告要求': 'State what the report should cover',
-    '挑组件 + 写总结, 全程零外发':
-        'selecting blocks and writing the summary — nothing leaves the site',
-    '月度运营分析': 'Monthly operations review', '周报': 'Weekly', '设备专题': 'Equipment topic',
-    '年度总结': 'Annual review', '块库': 'Block library',
-    '点击加入报告, 再次点击移出 · 画布里已有': 'Click to add, click again to remove · on the canvas:',
-})
-_RE = None
+# -*- coding: utf-8 -*-
+"""中→英 翻译层 (2026-08-28 用户令: 英文版系统, 给东南亚客户).
+
+不是通用机翻 —— 是这套系统的**领域词表**。做法上有三条硬约束:
+
+1. **最长匹配优先**。"报警级"必须先于"报警"匹配, 否则译出 "Alarm级";
+   "主轴承温度" 先于 "主轴承"。词表按 key 长度倒序编译成一条正则。
+2. **术语用行业通行译法, 不直译**。等效满发小时=EFLH 不是 "equivalent full hair";
+   限电=curtailment 不是 "power limit"; 定论=Confirmed 不是 "final verdict"。
+3. **数字与标识符不动**。台号 WTG29、报警码 3225、单位 MWh/kW/h 原样保留。
+
+覆盖策略: 先 UI 骨架(导航/标题/表头/按钮/指标名), 再判据与状态词, 再说明文字。
+未覆盖的中文会在 audit_untranslated() 里被列出来 —— 残留必须收敛到零, 因为
+给外方客户看的页面里出现半句中文, 比整页中文更糟。
+"""
+import re
+
+# ── 1 导航与页面骨架 ──
+NAV = {
+    '观澜': 'Guanlan', '如东海上': 'Offshore Fleet',
+    '风电智能分析决策': 'Wind Fleet Analytics and Decision Support',
+    '风电智能': 'wind fleet intelligence',
+    '总览': 'Overview', '电量算账': 'Energy Accounting', '部件问题': 'Component Issues',
+    '振动融合分析': 'Vibration Fusion', '发电性能': 'Performance Curves',
+    '故障统计': 'Fault Analytics', '检修决策': 'Maintenance Decisions',
+    '检修助手': 'Maintenance Assistant', '汇报定制': 'Report Builder', '系统维护': 'Data Sources',
+}
+
+# ── 2 设备状态与判级 (windcms 两套并行体系) ──
+STATE = {
+    '危险': 'Critical', '报警级': 'Alarm level', '报警': 'Alarm', '良好': 'Good', '优秀': 'Excellent',
+    '不可判': 'Indeterminate', '监测不可用': 'Monitoring unavailable', '无监测': 'Not monitored',
+    '定论·双模审': 'Confirmed · dual-model review', '定论': 'Confirmed',
+    '准定论·预警': 'Near-confirmed · warning', '准定论': 'Near-confirmed',
+    '预警·处置升急': 'Warning · escalate', '预警·处置规划': 'Warning · plan action', '预警': 'Warning',
+    '候选·监视(复发)': 'Candidate · watch (recurring)', '候选·跟踪': 'Candidate · tracking',
+    '候选观察': 'Candidate / watch', '候选': 'Candidate',
+    '销案(定论)': 'Closed (confirmed)', '销案正常': 'Closed · normal', '销案': 'Closed',
+    '监视(不开箱)': 'Watch (no teardown)', '监视': 'Watch', '观察(发电机侧)': 'Watch (generator side)',
+    '观察': 'Watch', '参考': 'Reference', '未列': 'Not covered', '未收录': 'not covered',
+    '正样本': 'Positive control', '仲裁输入': 'Arbitration input', '弱同向新证': 'Weak concurring evidence',
+    '本体无新证': 'No new evidence on component', '复发候选(两轴合并)': 'Recurrence candidate (two axes merged)',
+    '温度单轴保留': 'Temperature axis retained', '传感器项': 'Sensor issue', '可降': 'Downgradable',
+    '降观察': 'Downgraded to watch', '在升': 'rising', '未闭环': 'not closed', '已闭环': 'closed loop',
+    '筛查级': 'screening level', '双模审': 'dual-model review', '审级': 'review tier',
+    'lite审': 'lite review', '设备状态': 'Equipment state', '证据状态': 'Evidence state',
+}
+
+# ── 3 部件与系统 ──
+PART = {
+    '主轴承(后)+集中润滑泵': 'Main bearing (rear) + centralised lubrication pump',
+    '齿轮箱二级行星内齿圈': 'Gearbox 2nd-stage planetary ring gear',
+    '齿轮箱谱线级未映射到部件': 'Gearbox · spectral line not mapped to component',
+    '发电机引用同族见族主台': 'Generator · family reference, see lead unit',
+    '主轴承后集中润滑泵': 'Rear main-bearing lubrication pump',
+    '齿轮箱高速轴': 'Gearbox high-speed shaft', '齿轮箱中间轴': 'Gearbox intermediate shaft',
+    '主控与传感网': 'Main controller and sensor network', '变桨与液压': 'Pitch and hydraulics',
+    '塔架与基础': 'Tower and foundation', '塔架/结构': 'Tower / structure', '轮毂/叶片': 'Hub / blades',
+    '叶片与叶根': 'Blades and blade roots', '集中润滑泵': 'Centralised lubrication pump',
+    '电网/供电(外部)': 'Grid supply (external)', '风况(外部)': 'Wind conditions (external)',
+    '无记录停机': 'Unattributed downtime', '运维操作': 'OandM actions', '未归类': 'Unclassified',
+    '主轴承': 'Main bearing', '齿轮箱': 'Gearbox', '发电机': 'Generator', '变流器': 'Converter',
+    '变桨': 'Pitch', '偏航': 'Yaw', '叶片': 'Blade', '塔架': 'Tower', '主控': 'Main controller',
+    '轮毂': 'Hub', '油缸': 'Cylinder', '油泵': 'Pump', '测点': 'measurement point',
+}
+
+# ── 4 指标与口径 ──
+METRIC = {
+    '等效满发小时': 'Equivalent full-load hours', '时间可用率': 'Time-based availability',
+    '平均停机间隔': 'Mean time between outages', '上网电量': 'Net exported energy',
+    '理论可发': 'Gross available energy', '实际上网': 'Net exported', '损失率': 'Loss ratio',
+    '需行动机组': 'Units requiring action', '停机时长': 'Downtime hours', '停机事件数': 'Outage count',
+    '设备类停机': 'Equipment downtime', '外部类停机': 'External downtime', '总停机': 'Total downtime',
+    '影响机组数': 'Units affected', '事件数': 'Events', '停机': 'Downtime', '损失': 'Loss',
+    '限电·绑定': 'Curtailment (bound)', '限电命令·未绑定': 'Curtailment order (unbound)',
+    '停机(调度令)': 'Dispatch-ordered stop', '低风待机': 'Low-wind standby', '正常发电': 'Normal production',
+    '限电': 'Curtailment', '工况态': 'Operating state', '占比': 'share', '时长': 'duration',
+    '条数': 'count', '次数': 'count', '台数': 'units', '全场': 'fleet', '本台': 'this unit',
+    '数据覆盖': 'data coverage', '不满月': 'partial month', '窗内': 'in window', '窗末': 'window end',
+    '证据窗': 'evidence window', '判级时间窗': 'grading time window', '时间窗': 'Time window', '口径': 'basis',
+}
+
+# ── 5 动作与流程 ──
+ACTION = {
+    '从证据到验收的决策链': 'decision chain from evidence to verification',
+    '决策链进度盘': 'Decision chain status', '决策链': 'Decision chain',
+    '证据': 'Evidence', '机制': 'Mechanism', '判级': 'Grading', '排期': 'Scheduling',
+    '动作': 'Action', '验收': 'Verification', '卡在': 'held at', '下一步': 'Next step',
+    '抢修': 'Corrective', '预防性维护': 'Preventive', '故障树': 'Fault tree', '排程': 'Scheduling',
+    '工单': 'work order', '换件': 'replacement', '更换': 'replace', '检修': 'maintenance',
+    '闭环': 'closed loop', '端到端闭环': 'end-to-end closed loop', '派工': 'dispatch work',
+    '现场': 'site', '台账': 'ledger', '复测': 'retest', '核实': 'verify', '待审批': 'pending approval',
+}
+
+# ── 6 高频术语补齐 (2026-08-28 按渲染后残留统计倒排补, 长词在前靠正则长度排序保证) ──
+MORE = {
+    # 测量与证据面
+    '振动结论': 'vibration verdict', '振动信号': 'vibration signature', '振动线转录': 'from vibration line',
+    '振动线未收录': 'not covered by vibration line', '振动线': 'vibration line', '振动分析': 'Vibration analysis',
+    '振动': 'vibration', '温度通道': 'temperature channel', '温度轴': 'temperature axis',
+    '温度判': 'temperature verdict', '温度候选': 'temperature candidate', '温度': 'temperature',
+    '润滑柱塞': 'lubrication plunger', '润滑': 'lubrication', '油末样': 'last oil sample',
+    '油新鲜': 'oil sample fresh', '油样轴': 'oil sample axis', '油液': 'oil analysis',
+    '油脂水平低': 'grease level low', '油路与密封': 'oil circuit and seals', '末样': 'last sample',
+    '无样': 'no sample', '取样': 'sampling', '化验': 'laboratory analysis', '华标': 'lab',
+    # 窗与统计
+    '时间窗区间': 'time-window range', '判级时间窗': 'grading time window', '单次循环时长': 'cycle duration',
+    '单次循环': 'cycle', '逐月': 'monthly', '月度': 'monthly', '现水平': 'current level',
+    '轨迹': 'trend', '中位': 'median', '样本': 'sample', '总条数': 'total events',
+    '记基线观察': 'baseline recorded, watching', '记基线': 'baseline recorded',
+    '窗至': 'window through', '未提供': 'not provided', '数据时点': 'data as of',
+    '个月': ' months', '天数': 'days', '占时': 'time share', '累计': 'cumulative',
+    # 机制与判据
+    '根因定到部件与失效模式': 'root cause identified to component and failure mode',
+    '取样把根因定到部件与失效模式': 'sample to identify root cause to component and failure mode',
+    '机制不清则排期与动作都无依据': 'without a mechanism, neither scheduling nor action has a basis',
+    '同一判据是否回落': 'whether the same criterion falls back',
+    '历史单不对应本次问题': 'historical orders do not correspond to this finding',
+    '本轮动作执行后复测': 'retest after this round of action',
+    '须向现场核实': 'requires site verification', '未定性': 'not characterised', '定性': 'characterisation',
+    '未排': 'not scheduled', '未验': 'not verified', '已达': 'complete', '全通': 'all clear',
+    '两者非同源': 'the two are not from the same source', '负面对照': 'negative control',
+    '代理轴': 'proxy axis', '判别器': 'discriminator', '判据': 'criterion', '判': 'verdict',
+    # 部件细分
+    '叶根螺栓': 'blade-root bolts', '换栓': 'bolt replacement', '螺栓断裂': 'bolt fracture',
+    '非螺栓断裂确诊': 'not a confirmed bolt fracture', '松动嫌疑': 'suspected loosening', '松动': 'loosening',
+    '塔筒湿度': 'tower humidity', '湿度高告警': 'high-humidity alarm', '蓄能与压力调节': 'accumulator and pressure control',
+    '蓄能': 'accumulator', '液压': 'hydraulics', '执行与位置反馈': 'actuation and position feedback',
+    '驱动': 'drive', '轴承与': 'bearing and', '轴承': 'bearing', '逆变器通讯错误': 'inverter communication error',
+    '点名该叶断裂': 'names this blade as fractured', '叶断裂': 'blade fracture',
+    # 报表与流程
+    '评估报告': 'assessment report', '分册另含': 'the volume also contains', '沙盘': 'scenario model',
+    '情景': 'Scenario', '排查': 'investigation', '处置': 'action', '步骤': 'steps',
+    '明细': 'detail', '综合': 'combined', '近况': 'recent status', '逐台情况': 'per-unit status',
+    '融合矩阵': 'fusion matrix', '矩阵': 'matrix', '链盘': 'chain board', '能力边界': 'capability limits',
+    '能力': 'capability', '色标': 'colour key', '收录台': 'units covered', '机制链': 'mechanism chain',
+    '用户令': 'per client request', '语义类': 'semantic class', '四维': 'four-source',
+    '风险': 'risk', '双轴': 'dual-axis', '外部': 'external', '记录': 'record', '源': 'source',
+    '表': 'table', '级': ' level', '窗': 'window', '次': '', '条': '', '台': ' units',
+    '第': 'No.', '月': '-', '日': '', '年': '', '前': 'fore', '后': 'aft', '侧': ' side',
+    '与': ' and ', '无': 'none', '优': ' OK', '单': '', '末': 'last', '进': 'into',
+    '部件': 'component', '机组': 'unit', '正常': 'normal', '本轮': 'this round',
+    '不清则': 'without it, ', '都无依据': 'there is no basis', '执行后': 'after execution',
+}
+def _assert_quote_free():
+    """译文不得含引号或反引号 (2026-08-28 活逮): 译文会被注入 JS 字符串与 HTML 属性,
+    一个撇号就让整块解析失败, 表现是"页面全空但每个 script 块单独检查都通过" — 极难定位。
+    构建期直接拦, 不留到运行时。"""
+    bad = {k: v for k, v in TERMS.items()
+           if isinstance(v, str) and any(c in v for c in ('\'', '"', '`', '\\', '&', '<', '>'))}
+    if bad:
+        raise ValueError(f'译文含引号/反斜杠, 会破坏注入上下文: {list(bad)[:5]}')
+
+
+GROUPS = [NAV, STATE, PART, METRIC, ACTION]
+TERMS = {}
+for g in GROUPS:
+    TERMS.update(g)
+TERMS.update(MORE)
+# ── 8 第一阶段批量 (2026-08-28, 按渲染后残留频次倒排, 长词优先写以免被短词截断) ──
+TERMS.update({
+    # 长复合词组先写 (正则按长度排序, 这里的顺序只影响可读性)
+    '出带且过锚才算离群': 'outlier only if outside the band and past the anchor',
+    '同型机群残差': 'residual vs same-type fleet', '物理绝对锚': 'physical absolute anchor',
+    '四分位残差带': 'inter-quartile residual band', '控制参数一致性': 'Control-parameter consistency',
+    '只作回测锚非现时': 'back-test anchor only, not current-state evidence',
+    '不足或监测中断': 'insufficient or monitoring interrupted',
+    '旁路过滤器齿': 'bypass filter · gearbox', '油压力高激活': 'oil pressure high · activated',
+    '油压力过高': 'oil pressure too high', '各油水位检': 'oil/water level checks',
+    '变压器绕组最高': 'transformer winding max', '变压器油温': 'transformer oil temperature',
+    '变压器室': 'transformer room', '集线器通信错误': 'hub communication error',
+    '同油温档带宽': 'bandwidth within the same oil-temperature bin',
+    '就地操作模式': 'local operation mode', '电网故障穿越': 'grid fault ride-through',
+    '转速传感器故障': 'speed sensor fault', '电池电压低': 'battery voltage low',
+    '电网停电': 'grid outage', '降功率运行': 'derated operation', '此速率触': 'at this rate triggers',
+    '测风故障计数': 'anemometer fault count', '风速计偏置': 'anemometer bias',
+    '测风计数': 'anemometer count', '采样时间线': 'sampling timeline',
+    '不随上方': 'independent of the selector above', '时可作现时': 'usable as current evidence when',
+    '轻微偏离': 'slight deviation', '新出现待': 'newly appeared, pending',
+    '基本确认': 'largely confirmed', '可继续使用': 'may remain in service',
+    '机械刹车': 'mechanical brake', '油鼓风机': 'oil blower', '柜排风': 'cabinet exhaust',
+    '塔振模块': 'tower vibration module', '电气滑环': 'electrical slip ring', '滑环故障': 'slip-ring fault',
+    '缸检超时': 'cylinder check timeout', '缸超时': 'cylinder timeout', '比例阀': 'proportional valve',
+    '通讯故障': 'communication fault', '油位过低': 'oil level too low', '油位低': 'oil level low',
+    '自动重启': 'auto restart', '执行中': 'in progress', '双温差': 'dual temperature difference',
+    '频发易修': 'frequent but quick to fix', '罕发难修': 'rare but slow to fix',
+    '等效满发': 'equivalent full-load', '叶尖速比': 'tip-speed ratio', '天归一': 'per-day normalised',
+    # 部件与测点
+    '高速轴': 'high-speed shaft', '中间轴': 'intermediate shaft', '中速轴': 'intermediate-speed shaft',
+    '行星': 'planetary', '齿圈': 'ring gear', '内圈': 'inner race', '外圈': 'outer race',
+    '滑环': 'slip ring', '转子': 'rotor', '定子': 'stator', '主轴': 'main shaft', '主承': 'main bearing',
+    '柱塞': 'plunger', '滤芯': 'filter element', '冷却器': 'cooler', '油管': 'oil line',
+    '分配器': 'distributor', '桨叶': 'blade', '传动链': 'drivetrain', '塔筒': 'tower',
+    '油温': 'oil temperature', '油样': 'oil sample', '总线': 'bus', '备件': 'spare parts',
+    '两端': 'both ends', '配对差': 'paired difference', '中速': 'intermediate speed',
+    '高速': 'high speed', '中间': 'intermediate',
+    # 判据与统计
+    '离群': 'outlier', '谱线': 'spectral line', '残差': 'residual', '灰域': 'shaded band',
+    '四分位': 'quartile', '逐档': 'per bin', '最小档': 'smallest bin', '显著门': 'significance threshold',
+    '归因': 'attribution', '观测面': 'observation face', '监测面': 'monitoring coverage',
+    '没测到': 'not detected', '宽带': 'broadband', '全净': 'entirely clean', '干净': 'clean',
+    '慢性': 'chronic', '持平': 'unchanged', '降至': 'fell to', '极差': 'range', '对数': 'log',
+    '基线': 'baseline', '同族': 'same family', '引用同族': 'family reference', '族主': 'family lead',
+    '新证': 'new evidence', '实测': 'measured', '实证': 'empirical evidence', '印证': 'corroboration',
+    '检出': 'detection', '缺陷': 'defect', '疲劳': 'fatigue', '断裂': 'fracture', '过热': 'overheating',
+    '欠发': 'under-performance', '故障率': 'fault rate', '可用率': 'availability', '象限': 'quadrant',
+    '降序': 'descending', '噪声': 'noise', '特征': 'characteristic', '命中': 'match', '分组': 'grouping',
+    '封顶': 'cap', '额定': 'rated', '功率': 'power', '风速': 'wind speed', '转速': 'rotor speed',
+    '曲线': 'curve', '对风': 'yaw alignment', '对风良': 'yaw alignment OK', '对风报': 'yaw alignment alarm',
+    '定向': 'orientation', '泄压': 'pressure relief', '加注': 'top-up', '紧固': 'tightening',
+    '平均偏': 'mean deviation', '偏离': 'deviation', '偏置': 'bias',
+    # 流程与界面
+    '徽章': 'badge', '全位': 'all positions', '固定': 'fixed', '切换': 'switch', '映射到': 'mapped to',
+    '映射': 'mapping', '聚合': 'aggregation', '典型': 'typical', '交叉': 'cross', '不参': 'excluded from',
+    '可对': 'matchable', '不足': 'insufficient', '状态': 'status', '本件为': 'this item is',
+    '可靠性': 'reliability', '重点值': 'key value', '宽限': 'grace', '边界': 'boundary',
+    '调度令': 'dispatch order', '调度': 'dispatch', '监控': 'monitoring', '时序': 'time series',
+    '用户裁': 'client decision', '用户逮': 'caught by client', '异常': 'anomaly', '悬停': 'hover',
+    '能看': 'can see', '失败': 'failed', '手册': 'manual', '虚线': 'dashed', '差异': 'difference',
+    '限制': 'limit', '问题': 'issue', '新发': 'newly found', '发电': 'generation', '设备类': 'equipment',
+    '设备': 'equipment', '融合': 'fusion', '输入': 'input', '本场': 'this site', '节点': 'node',
+    '决策': 'decision', '详情': 'detail', '业主': 'owner', '评估': 'assessment', '撤回': 'withdrawn',
+    '取消': 'cancelled', '定案': 'settled', '原文': 'source text', '清洁': 'clean', '重点': 'priority',
+    '分账': 'attribution', '作业': 'work', '原因': 'cause', '参数': 'parameter', '类别': 'category',
+    '本页': 'this page', '分钟': ' min', '结构': 'structure', '码统计': 'code statistics',
+    '取证': 'evidence retrieval', '哪些': 'which', '修复': 'repair', '保留': 'retained',
+    '警告': 'warning', '故障': 'fault', '运行': 'operating', '恢复': 'recovered', '事件': 'event',
+    '变化': 'change', '检测': 'detection', '强烈': 'strong', '等待': 'waiting', '跳闸': 'trip',
+    '传感器': 'sensor', '本地': 'local', '不是': 'not', '上网': 'exported', '指标': 'indicator',
+    '用户': 'client', '的异常': ' anomaly', '时间': 'time',
+})
+
+# ── 9 第二阶段批量 ──
+TERMS.update({
+    '正是算法当初的检出台': 'these are exactly the units the algorithm flagged',
+    '明显超出即分布非峰': 'a clear excess means the distribution is not single-peaked',
+    '选组件再点击移出': 'click a block again to remove it', '全程零外发': 'nothing leaves the site',
+    '考核的只有': 'the only item under equipment accountability is',
+    '交易策略主导': 'driven by trading strategy', '正态抽样的': 'for a normal sample, the',
+    '落在几离散': 'sit at a few discrete positions', '环内怎么坏的': 'how it fails inside',
+    '简图分工': 'schematic scope', '出脂计量': 'grease output metering', '喷淋回箱': 'spray return to tank',
+    '急性热失效': 'acute thermal failure', '散热风扇': 'cooling fan', '手动待机': 'manual standby',
+    '自动解缆': 'automatic cable untwist', '长停远': 'long stop, distant', '泵超时': 'pump timeout',
+    '压力偏高': 'pressure high', '油压偏高': 'oil pressure high', '器储压': 'accumulator charge',
+    '高频短时': 'frequent · short', '低频长时': 'rare · long', '高频长时': 'frequent · long',
+    '低频短时': 'rare · short', '重复触发': 'repeat triggering', '非硬件': 'not hardware',
+    '常坏且难修': 'frequent and slow to fix', '少发好修': 'rare and quick to fix',
+    '关键少数': 'the vital few', '点面积': 'bubble area', '圆面积': 'circle area',
+    '柱高': 'bar height', '纵界': 'vertical divider', '分界': 'divider', '本底': 'background',
+    '采样格': 'sampling interval', '模式四': 'mode quadrant', '逐段扣损': 'losses deducted step by step',
+    '两数都对': 'both figures are correct', '谁的': 'whose', '哪一': 'which',
+    '关键少数': 'the vital few', '同档差': 'within-bin difference', '尺子巡检': 'yardstick check',
+    '下期计划': 'Next-period plan', '季节性': 'seasonality', '控制策略': 'control strategy',
+    '需跟踪': 'under tracking', '首发码': 'first alarm code', '测量链': 'measurement chain',
+    '可维修性': 'maintainability', '帕累托': 'Pareto', '总结': 'summary', '分析': 'analysis',
+    '生成': 'generate', '控制': 'control', '桨距': 'pitch angle', '吊装': 'lifting',
+    '执行': 'execution', '版本': 'version', '审核': 'review', '盲区': 'blind spot',
+    '保持常规': 'keep routine', '记为': 'recorded as', '截图逮': 'caught in screenshot',
+    '间隔': 'interval', '交易': 'trading', '实际': 'actual', '要求': 'requirement',
+    '物理': 'physical', '电网': 'grid', '海上': 'offshore', '对比': 'comparison',
+    '建议': 'recommendation', '分母': 'denominator', '门槛': 'threshold', '相对': 'relative',
+    '发现': 'finding', '完整': 'complete', '计数': 'count', '本体': 'ontology', '命令': 'command',
+    '微视图': 'micro-view', '叶根': 'blade root', '解缆': 'cable untwist', '解剖图': 'anatomy',
+    '支路': 'branch', '油路': 'oil circuit', '冷却': 'cooling', '信号': 'signal', '趋势': 'trend',
+    '发生': 'occurrence', '平均': 'mean', '根因': 'root cause', '不作': 'not used as',
+    '顶部': 'top', '特性': 'characteristic', '期望': 'expected', '佐证': 'corroborating',
+    '满发': 'full load', '合计': 'total', '统计': 'statistics', '万元': ' k CNY',
+    # 2026-09-19 变桨面零位两口径 (用户令「停机段中位、满发段中位两者分列出」) 的用词
+    '满发段': 'full-load segment', '顺桨停机段': 'feathered stop segment', '顺桨段': 'feathered segment',
+    '顺桨': 'feathered', '并列参考': 'for reference',
+    '天气': 'weather', '对象': 'object', '读纪律': 'reading discipline', '分支': 'branch',
+    '失效': 'failure', '全程': 'full window', '入口': 'entry point', '不同': 'different',
+    '转录': 'transcribed', '强度': 'severity', '管理': 'management', '行动': 'action',
+    '内有': 'contains', '接线': 'wiring', '过多': 'excessive', '油位': 'oil level',
+    '允许': 'permitted', '度高': ' high', '以切入': 'to cut in', '不可能': 'not possible',
+    '冲击量': 'impact magnitude', '路径': 'path', '电压': 'voltage', '下限': 'lower limit',
+    '上限': 'upper limit', '远程': 'remote', '载体': 'carrier', '机舱风': 'nacelle anemometer',
+    '对照': 'control', '纯图形': 'graphic-only', '复核': 'cross-check', '偏高': 'high',
+    '严重': 'severe', '建议尽快': 'recommend prompt', '观测': 'observation', '缓升': 'slow rise',
+    '回落': 'fall back', '回归': 'return', '同批': 'same batch', '增益': 'gain',
+    '梯度': 'gradient', '周期主导': 'cycle-dominated', '急性': 'acute', '长期': 'long-term',
+    '总长': 'total length', '最大': 'max', '测风': 'wind measurement', '其他': 'other',
+    '显示': 'display', '支持': 'support', '范式': 'template', '完成': 'complete',
+    '情况': 'status', '周期': 'cycle', '周报': 'Weekly', '专题': 'Topic', '改为': 'changed to',
+    '三段': 'three segments', '基于': 'based on', '离线': 'offline', '看组件': 'view block',
+    '调整顺序': 'reorder', '环比': 'month-on-month', '理论': 'theoretical', '低于': 'below',
+    '分解': 'breakdown', '之差': 'difference', '比值': 'ratio', '命令期': 'command period',
+    '被压制': 'suppressed', '风况': 'wind conditions', '不计': 'excluded', '交通': 'access',
+    '影响': 'impact', '段点': 'segment point', '属重复触发': 'is repeat triggering',
+    '总数': 'total', '瓶颈': 'bottleneck', '一列': 'column', '标量': 'scalar', '引用': 'citation',
+    '上报版': 'Group-report version', '轻量': 'Light', '标准': 'Standard', '声明': 'declared',
+    '其余': 'remaining', '时效': 'currency', '电费': 'tariff', '修完': 'repaired',
+    '展示': 'presentation', '原来': 'previously', '该类': 'this class', '维护面': 'maintenance surface',
+    '更新': 'update', '技术依': 'technical basis', '归属': 'attribution', '定规范': 'specification',
+    '实机': 'as-built', '什么': 'what', '缺失非': 'missing, not', '修过': 'repaired before',
+    '卡撤': 'card removed', '点看': 'click to view', '确诊': 'confirmed diagnosis',
+    '扭缆': 'cable twist', '原则': 'principle', '滚子': 'roller', '标注': 'annotation',
+    '集中': 'centralised', '管路': 'piping', '往复': 'reciprocating', '脂桶': 'grease drum',
+    '主管路': 'main line', '冲击': 'impact', '铁谱': 'ferrography', '推动': 'drive',
+    '压力': 'pressure', '带宽': 'bandwidth', '列宽有限': 'column width limited',
+    '不可读': 'not legible', '双环': 'dual ring', '上下': 'top/bottom', '右下': 'bottom-right',
+    '抖动': 'chatter', '刷屏': 'flooding', '左上': 'top-left', '右上': 'top-right',
+    '治理': 'remediation', '左下': 'bottom-left', '维持': 'maintain', '类不计': 'class excluded',
+    '贡献': 'contribution', '页缺': 'page missing', '并列': 'side by side', '迁入': 'moved in',
+    '排布': 'layout', '工作': 'work', '早三': 'earliest three', '这三': 'these three',
+    '人员转运': 'personnel transfer', '语义本体': 'semantic ontology', '滚动': 'scroll',
+    '内容会外发': 'content is sent externally', '云端': 'Cloud', '物理闸': 'physical gate',
+    '同一': 'same', '只有': 'only', '没有': 'no', '里的': ' in ', '的六': ' six ',
+})
+
+# ── 10 第三阶段: 上屏长尾 (统计已剥 JS 注释, 只补真正会显示的) ──
+TERMS.update({
+    '选组件再点击移出': 'click a block again to remove it', '两数都对': 'both figures are correct',
+    '样龄过期': 'sample age expired', '有效监测': 'effective monitoring', '能量过剩': 'excess energy',
+    '绕组三相': 'winding three-phase', '通讯模块': 'communication module', '旁路阀': 'bypass valve',
+    '跟踪偏差': 'tracking deviation', '数加权': 'count-weighted', '同标尺': 'same scale',
+    '不随本': 'independent of this', '零外发': 'no external transmission', '加热带': 'heating band',
+    '通信线': 'communication line', '出力': 'output', '错误': 'error', '不等于': 'is not',
+    '分布': 'distribution', '持续': 'sustained', '通道': 'channel', '峰值': 'peak',
+    '时段': 'period', '稳定': 'stable', '复合': 'composite', '结论': 'conclusion',
+    '关注': 'watch', '稳态': 'steady-state', '瞬态': 'transient', '可信度': 'confidence',
+    '现状': 'current state', '波形': 'waveform', '内容': 'content', '发作': 'episode',
+    '如何': 'how', '联动': 'linked', '复位': 'reset', '激活': 'activated', '桨角': 'pitch angle',
+    '柜左': 'cabinet left', '限需': 'limit required', '塔振': 'tower vibration', '该叶': 'this blade',
+    '手动': 'manual', '电机': 'motor', '端别': 'end', '载荷': 'load', '缺采': 'sampling gap',
+    '不在': 'not in', '断供': 'supply interruption', '高位': 'elevated', '度夏': 'summer operation',
+    '磨损': 'wear', '不可': 'cannot', '不给等': 'no grade given', '有过一': 'has had one',
+    '的多': ' more', '不代': 'does not represent', '五态': 'five states', '平铺': 'flat layout',
+    '展现的': 'presented', '筛选': 'filter', '系数': 'coefficient', '计算隐藏': 'computation hidden',
+    '所选': 'selected', '九枚举': 'nine enumerated values', '好坏': 'good/bad',
+    '两轴不可互相替代': 'the two axes are not interchangeable', '明确定': 'explicitly defined',
+    '截断的': 'truncated', '记忆': 'memory', '实心点': 'solid point', '季节': 'season',
+    '尺子': 'yardstick', '数量': 'count', '推理': 'reasoning', '全部': 'all', '分开': 'separate',
+    '剖面': 'section', '剖面符号': 'section symbol', '竖矩形': 'vertical rectangle',
+    '一眼看出': 'shows at a glance', '偏差': 'deviation', '长度即幅度': 'length equals magnitude',
+    '跨行可比': 'comparable across rows', '端口': 'port', '打开': 'open', '最高': 'highest',
+    '提示': 'hint', '模式归类': 'mode classification', '拆检': 'teardown inspection',
+    '内窥': 'borescope', '靠内窥镜': 'via borescope', '轻度损伤': 'minor damage',
+    '轻微损伤': 'slight damage', '中等持续': 'moderate sustained', '点蚀在': 'pitting at',
+    '当时的': 'at the time', '同样没看出': 'likewise not detected', '缺口': 'gap',
+    '西门子': 'OEM', '非链路静默': 'not link silence', '风能利用系数': 'power coefficient Cp',
+    '转矩': 'torque', '分册': 'volume', '取每': 'take each', '高位稳定值': 'upper stable value',
+    '阶跃': 'step change', '配置变更': 'configuration change', '清洗': 'cleansing',
+    '行程': 'travel', '泵压': 'pump pressure', '切入段': 'cut-in segment', '语义色': 'semantic colour',
+    '现值': 'current value', '返服': 'return to service', '对不对得上': 'whether they match',
+    '胶囊': 'pill', '只作回测锚': 'back-test anchor only', '术语去黑话': 'plain-language terms',
+    '翻译': 'translation', '黑话': 'jargon', '内部': 'internal', '缩写': 'abbreviation',
+    '括注': 'parenthetical', '健康': 'healthy', '排除': 'excluded', '降为': 'downgraded to',
+    '仪器': 'instrument', '规范': 'specification', '打头': 'leading', '即可': 'is sufficient',
+    '保持架': 'cage', '时域转': 'time-domain to', '有效值': 'RMS', '倍轴频': '× shaft frequency',
+    '轴频': 'shaft frequency', '分位': 'percentile', '宽频带总量': 'broadband total',
+    '阶跃上升': 'step rise', '缓慢上升': 'slow rise', '回到常态': 'back to normal',
+    '该批主': 'batch lead', '性组': ' group', '沿链': 'along the chain', '最高点': 'peak point',
+    '成分主导': 'component-dominated', '突发': 'sudden', '主要': 'main', '位于': 'located at',
+    '族分布堆叠': 'family distribution stack', '族分布': 'family distribution', '段宽': 'segment width',
+    '总和恒': 'sum constant', '段内比例': 'within-segment share', '族构成': 'family composition',
+    '最短': 'shortest', '保证可': 'ensures', '最多': 'at most', '汇报端口': 'reporting portal',
+    '客户汇报': 'client reporting', '定制化': 'customised', '决定': 'determines',
+    '六核心数': 'six headline figures', '用大字直出': 'shown in large type', '下沉到': 'pushed down to',
+    '一屏也拿得到结论': 'the first screen already carries the conclusion', '每数字带': 'each figure carries',
+    '结论可回溯到': 'conclusions trace back to', '组件': 'block', '主题': 'theme',
+    '收尾': 'closing', '能量瀑布': 'energy waterfall', '用于定位电量流向': 'locates where energy went',
+    '回答': 'answers', '分出': 'separates', '方向相反': 'opposite directions', '区分': 'distinguishes',
+    '排序会误定': 'ranking would mis-set', '的历史例证': ' historical evidence',
+    '证明这链是通的': 'proves the chain works end to end', '下期': 'next period',
+    '推出的': 'derived', '套话': 'boilerplate', '性质': 'nature', '常驻': 'always on',
+    '补充文字结论': 'adds a written conclusion', '供汇报使用': 'for reporting use',
+    '空白': 'blank', '默认空': 'empty by default', '都去掉': 'all removed', '组合': 'combination',
+    '一张图': 'one chart', '就是一块': 'is one block', '退化为': 'reduced to',
+    '客户看到的是负担': 'the client sees a burden', '长度天然由客户': 'length is set by the client',
+    '交互界面设计一下': 'interaction design', '区不浮动': 'fixed, not floating',
+    '的叙事顺序分五组': 'grouped in five by report narrative order',
+})
+
+# ── 11 第四阶段: 整句界面文案 (按 HTML 标签切分后的中文段, 标签本身不入表) ──
+TERMS.update({
+    '(本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。':
+        ' (this page needs contiguous segments for reliability metrics; the energy account bills state by state). Both figures are correct and must not be subtracted from one another.',
+    '短于 1h 的停机段': 'stops shorter than 1 h',
+    '差额是': 'the difference is ',
+    '限电前干净窗 — 2026 起交易策略主导, 曲线与控制参数含策略性少发不可作设备判据。本页含特性曲线与控制参数一致性两件, 同窗':
+        'Pre-curtailment clean window — from 2026 trading strategy dominates, so curves and control parameters contain strategic under-generation and cannot serve as equipment criteria. This page holds both characteristic curves and control-parameter consistency, on the same window.',
+    '由另一本地模型基于': 'Reviewed by a second local model against ',
+    '同一份证据': 'the same evidence',
+    '复核上述答案。溯源核验仅校验数字与机组号是否越界, 本项校验结论是否有证据支撑。':
+        '. The traceability check only verifies that figures and unit numbers stay within the evidence; this check asks whether the conclusion is supported at all.',
+    '右下角的码不要按条数排优先级': 'Do not rank the bottom-right codes by event count',
+    ' — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。':
+        ' — they are repeat triggering, not hardware faults. The two Pareto charts above are this figure projected onto each axis.',
+    '总览内嵌页在快照版中不可用 (它由系统实时渲染)。本快照的分析内容在上方各标签页内, 数据已随快照固化。':
+        'The embedded overview is unavailable in the snapshot (it is rendered live by the system). The analysis in this snapshot is in the tabs above, with data frozen at capture time.',
+    '海上机组 MDT 普遍偏高是': 'Offshore MDT is generally inflated by ',
+    '交通窗口': 'vessel access windows',
+    '决定的, 不等于维修本身慢 — 跨场比 MDT 须同为海上。':
+        ', not by slow repair work — comparing MDT across sites requires both to be offshore.',
+    '这是两个指标的设计差异不是错, 但两者不可直接相乘或互推。低风待机对损失的贡献仅 118 MWh(0.5%)':
+        'This is a design difference between two indicators, not an error, but they cannot be multiplied or derived from one another. Low-wind standby contributes only 118 MWh (0.5%) of losses.',
+    '不是物理判据: 正常数据下必然通过。它的作用是当损失被高估到不可能时(台账数字虚高曾把损失写成超过全停上限)立刻拦下':
+        'Not a physical criterion: with sound data it always passes. Its purpose is to stop an impossible loss figure at once — an inflated ledger once recorded a loss larger than a full fleet stop.',
+    '故障前能看见、换件后能看见恢复': 'visible before failure and visible as recovery after replacement',
+    ', 这条链是通的; 闭环成功的台会从盘面消失。':
+        ' — the chain works end to end, and units that close the loop drop off this board.',
+    '点面积=影响台数(大=批次共性, 小=单台); 横界=条数中位, 纵界=10min(一个SCADA采样格)。':
+        'Bubble area = units affected (large = fleet-wide pattern, small = single unit); the vertical divider is the count median, the horizontal one 10 minutes — one SCADA sampling interval.',
+    '明显超出即分布非单峰': 'a clear excess means the distribution is not single-peaked',
+    ' — 与左图的分组互为印证; 落在期望附近的轴(额定桨角)则是连续分布。':
+        ' — corroborating the grouping on the left; an axis near the expected value (rated pitch angle) is continuously distributed.',
+    '四个观测面是': 'The four observation faces are ',
+    '同一条机制链的四个面': 'four faces of a single mechanism chain',
+    ', 不算四个独立源 — 加权时不可当多源印证。':
+        ', not four independent sources — they must not be weighted as multi-source corroboration.',
+    '要走六步。这张盘不讲某一台的故事, 用于确认': 'Six steps in all. This board tracks the fleet, not one unit; it confirms ',
+    '当前需处置机组、各自所处环节、后续动作': 'which units need action, the step each is held at, and what comes next',
+    '不成立 — 前 72h 有报警不等于该报警与本段停机有因果 (双模审 2026-08-28 逮)。':
+        'does not hold — an alarm within the preceding 72 h does not make it the cause of this stop (caught in dual-model review, 2026-08-28).',
+    '本地模型依据需求选取组件, 并基于实测数据生成运行总结; 数据不出本地':
+        'A local model selects blocks from your request and writes an operations summary from measured data; nothing leaves this machine',
+    '可靠性口径 ≥1h 段, 电量账为逐条工况态 — 两个数都对, 不可相减':
+        'reliability counts stops ≥1 h; the energy account bills state by state — both are correct and must not be subtracted',
+    '统计期 / 残月 / 工单窗 / 判级性质 — 建议常驻, 没有边界的数字堆不能作汇报依据':
+        'Reporting period / partial months / work-order window / nature of the grading — recommended always on; a set of figures without its boundaries cannot serve as a reporting basis',
+    '六个核心数: 上网电量 / 等效满发 / 可用率 / 损失率 / 停机间隔 / 需行动台':
+        'Six headline figures: net exported / equivalent full-load hours / availability / loss ratio / mean time between outages / units requiring action',
+    '两侧不是两个独立源, 是同一条机制链的两端。':
+        'The two sides are not independent sources; they are two ends of one mechanism chain.',
+    '工况态分解; 损失=同工况理论出力与实测之差。量级自检为防虚高的下限闸, 不是风资源判据':
+        'Operating-state decomposition; loss = theoretical output under the same conditions minus measured. The magnitude self-check is a floor gate against inflation, not a wind-resource criterion',
+    '限电属交易与调度侧, 2026 起交易策略主导, 少发未必是弃风。':
+        'Curtailment sits with trading and dispatch; from 2026 trading strategy dominates, so reduced generation is not necessarily spilled wind.',
+    '由振动/温度/润滑/油液四源融合得出; 行动等级由综合报告出具':
+        'derived by fusing vibration, temperature, lubrication and oil analysis; action levels are issued by the consolidated report',
+    ' — 损失六成在交易与调度侧, 设备侧改善空间有限':
+        ' — six tenths of losses sit with trading and dispatch; the scope for equipment-side improvement is limited',
+    ' — 台账止 2024-11, 本期检修执行与闭环无法自动判定':
+        ' — the ledger ends 2024-11, so maintenance execution and loop closure for this period cannot be determined automatically',
+    '这是配置/版本差异的形状; 磨损或工况引起的会是连续散布。两轴各自成组且分组不同台':
+        'This is the shape of a configuration or version difference; wear or operating conditions would scatter continuously. The two axes group independently and the groups hold different units',
+    '文档预览窗 — 从上面的组件库点选内容, 此处显示报告预览':
+        'Document preview — select blocks from the library above and the report appears here',
+    '条数=发生频次, 不反映影响程度 — 高频短时报警码(信号抖动或重复触发)排序靠前':
+        'Count = frequency, not severity — high-frequency short-duration codes (signal chatter or repeat triggering) rank first',
+    '格子颜色 = 设备状态 (业主口径, 与 windcms 报告同一套):':
+        'Cell colour = equipment state (owner convention, same set as the assessment report):',
+    '海上作业天气窗 · 近12月可作业日 (公开再分析统计, 非派工依据)':
+        'Offshore weather windows · workable days over the last 12 months (public reanalysis statistics, not a dispatch basis)',
+    '排期沙盘 · 四情景损失对比 (万元, 电价区间中值; 建议级待人裁)':
+        'Scheduling model · four-scenario loss comparison (k CNY, mid-point of the tariff range; advisory, pending human decision)',
+    '主轴承集中润滑链: 润滑泵经管路到分配器, 分配器柱塞往复把脂定量送到前后主轴承':
+        'Main-bearing centralised lubrication: the pump feeds a distributor through piping, and the distributor plungers meter grease to the fore and aft main bearings',
+    '变桨系统: 蓄能器储压经比例阀推动油缸, 油缸驱动变桨轴承转动桨叶; 滑环传电与信号':
+        'Pitch system: accumulator pressure drives the cylinder through a proportional valve, the cylinder turns the pitch bearing and blade; the slip ring carries power and signals',
+    '过热类失效沿油路查(冷却/滤芯), 磨损类沿齿轮线查(振动/铁谱)。':
+        'Overheating failures are traced along the oil circuit (cooling, filters); wear failures along the gear line (vibration, ferrography).',
+    '执行侧看桨距跟踪偏差, 三叶不一致指向单支路。':
+        'On the actuation side look at pitch tracking deviation; disagreement between the three blades points to a single branch.',
+    '观测: 蓄能带宽/停机泄压 · 桨距跟踪偏差 · 三叶零位一致性 · 轴承润滑脂':
+        'Observed: accumulator bandwidth / shutdown pressure relief · pitch tracking deviation · three-blade zero consistency · bearing grease',
+    '窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)':
+        'No work order for this component in the window (order data ends 2024-11, so any 2026 window is necessarily empty — a data boundary)',
+    '每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。':
+        'Each source states: current coverage / last updated / agreed location / ingest command / update frequency / owner.',
+    '转录自振动线, ': 'Transcribed from the vibration line, ',
+    '不随上方时间窗变': 'independent of the time window above',
+    '; 报警/工单/温度列才跟随所选窗。': '; only the alarm, work-order and temperature columns follow the selected window.',
+    '本码无手册处置条目 (手册覆盖本场 161/322 码) — 需查图纸或联系厂商支持。':
+        'No manual procedure for this code (the manual covers 161 of 322 codes on this site) — consult drawings or contact OEM support.',
+    '非厂商绑定 — 现场以铭牌与图纸复核; 参数 ID 与告警码是不同命名空间, 未按同号关联。':
+        'Not vendor-locked — verify on site against nameplates and drawings; parameter IDs and alarm codes are separate namespaces and are not linked by number.',
+    '状态管理判级, 不代表确诊数量; 判据/谱线/计算过程不在本页 — 深潜走底部独立系统。':
+        'State-management grading; it is not a count of confirmed diagnoses. Criteria, spectral lines and computation are not on this page — use the dedicated system below for depth.',
+    '语义本体 · 对象/机制/决策/约束 (Palantir 思路轻量自建; 判读纪律=写入闸, 违反写不进库)':
+        'Semantic ontology · objects / mechanisms / decisions / constraints (lightweight, Palantir-style; the reading discipline is enforced as a write gate — violations cannot enter the store)',
+    '检修决策 · ontology 展现的窗口 (语义本体 + dsh 编排; 推理无界, 结论受闸)':
+        'Maintenance decisions · the ontology view (semantic ontology with orchestration; reasoning is open, conclusions are gated)',
+    '融合 · 结构=reviewed vibration evidence and SCADA context=vibration evidence package (source-backed record, evidence review only; no rating is assigned here)':
+        'Fusion · reviewed vibration evidence and SCADA context',
+    '判读纪律: 六个观测量=一条机制链 (C3 源数按观测面), 禁写多源印证; 判级只在 L1/审级产生, 本体转录带审级。':
+        'Reading discipline: six observed quantities form one mechanism chain (independent sources are counted by observation face); do not claim multi-source corroboration. Grades originate only in the analysis layer, and the ontology transcribes them with their review tier.',
+})
+
+# ── 12 第五阶段: 四维状态简写与模板碎片 (逐台判据描述由"模板+数值"拼成, 模板词入表即可覆盖) ──
+TERMS.update({
+    # 四维评级的单字简写 — 逐台描述里出现最密, 不收就满屏碎字
+    '蓄能与压力调节': 'accumulator and pressure control', '变桨轴承与轮毂润滑': 'pitch bearing and hub lubrication',
+    '执行与位置反馈': 'actuation and position feedback', '油路与密封': 'oil circuit and seals',
+    '四维': 'four axes', '停机泄压': 'shutdown pressure decay', '蓄能缸检超时': 'accumulator cylinder check timeout',
+    '同油温档带宽': 'bandwidth in the same oil-temperature bin', '液压油温': 'hydraulic oil temperature',
+    '曲线判风速': 'curve-derived wind speed', '绕组三相极差': 'winding three-phase range',
+    '仪表/接线候选': 'instrumentation or wiring candidate', '记基线观': 'baseline recorded, watching',
+    '配对差发电机轴承': 'paired difference, generator bearing', '配对差': 'paired difference',
+    '驱动端': 'drive end', '非驱动端': 'non-drive end', '两端': 'both ends',
+    '未映射到部件': 'not mapped to a component', '未映射': 'not mapped', '谱线级': 'spectral-line level',
+    '引用同族': 'family reference', '见族主台': 'see family lead unit', '振动线转录': 'transcribed from vibration line',
+    '监测面未覆盖': 'monitoring does not cover it', '没测到': 'not measured',
+    '机械刹车温度': 'mechanical brake temperature', '塔筒振动': 'tower vibration',
+    '强烈振动停机': 'severe-vibration stop', '叶片振动检测': 'blade vibration detection',
+    '无有效监测': 'no effective monitoring', '不给等级': 'no grade assigned',
+    '湿度高告警': 'high-humidity alarm', '通讯故障': 'communication fault',
+    '工单点名该叶断裂': 'work order names this blade as fractured', '换栓': 'bolt replacement',
+    '全批疲劳磨粒最高': 'highest fatigue particle count in the batch', '批内最高': 'highest in batch',
+    '重点值': 'key values', '疲劳': 'fatigue', '磨粒': 'wear particles',
+    '端别映射': 'end mapping', '未列台': 'unit not covered', '未列≠正常': 'not covered is not the same as healthy',
+    '缺采': 'sampling gap', '前置': 'prerequisite', '未验': 'not verified',
+    '慢性载荷冲击': 'chronic load impact', '行星级周期主导': 'planetary-stage cycle dominated',
+    '梯度': 'gradient', '排名路径': 'ranking path', '持续路径': 'persistence path',
+    '渐进磨损': 'progressive wear', '点蚀': 'pitting', '剥落': 'spalling',
+    '标量冲击量': 'scalar impact magnitude', '可检出率': 'detection rate',
+    '高温跑圈': 'high-temperature race creep', '轴损': 'shaft damage', '烧蚀': 'burn damage',
+    '实证不可见': 'not empirically visible', '改看速度量或温度轴': 'use velocity or the temperature axis instead',
+    '分布正中间': 'mid-distribution', '拆检前': 'before teardown',
+    '因侧': 'cause side', '果侧': 'effect side', '因果时序闭合': 'cause-effect timing closes',
+    '机制定性窗无关': 'mechanism characterisation is window-independent',
+    '恶化态是窗相关判断': 'deterioration is a window-dependent judgement',
+    '润滑断供': 'lubrication starvation', '泵本体': 'pump body', '柱塞两段式失效': 'two-stage plunger failure',
+    '温度长期高位': 'sustained high temperature', '振动间歇尖峰': 'intermittent vibration spikes',
+    '无归因停机': 'unattributed downtime', '长停远窗': 'long stop, distant window',
+    '非链路静默': 'not link silence', '前6h无报警': 'no alarm in the preceding 6 h',
+    '镜头口径': 'lens basis', '直测干净量': 'directly measured clean quantity',
+    '只判形状': 'shape only', '绝对达成率须现场测风': 'absolute performance requires on-site anemometry',
+    '禁眼估': 'no eyeballing', '逐档中位聚合': 'per-bin median aggregation',
+    '已剥限电': 'curtailment removed', '只正常发电态': 'normal production only',
+    '全场四分位': 'fleet inter-quartile', '四分位残差带': 'inter-quartile residual band',
+    '出带且过锚才算离群': 'an outlier must leave the band and pass the anchor',
+    '物理绝对锚': 'physical absolute anchor', '显著门': 'significance gate',
+    '个月×台': ' unit-months', '月×台': ' unit-months', '月度聚合': 'monthly aggregation',
+    '不随上方窗切换': 'independent of the window selector above',
+    '两源时间几乎不重叠': 'the two sources barely overlap in time',
+    '集团模板无本场记录': 'group template, no records for this site',
+    '负面对照': 'negative control', '判据未乱报': 'the criterion did not misfire',
+    '全场极齐': 'uniform across the fleet', '无泵压异常': 'no pump-pressure anomaly',
+    '活动量代理': 'activity proxy', '非真磨损量': 'not true wear', '与状态位次数互核': 'cross-checked against status-bit counts',
+    '非发作型': 'not episodic', '机舱位置通道拖偏': 'nacelle position channel drift',
+    '数据质量': 'data quality', '非对风故障': 'not a yaw fault',
+    '参数级分组见': 'parameter-level grouping, see ', '阶跃=配置变更': 'a step is a configuration change',
+    '平移=工况': 'a shift is an operating condition', '两轴独立分组': 'the two axes group independently',
+    '润滑柱塞各台设定本就不同': 'plunger settings differ by unit by design',
+    '所以此列只展示不判级': 'so this column is shown without grading',
+    '只有已定案的机制链才标色': 'only settled mechanism chains are coloured',
+    '末次取样距今超': 'last sampled more than ', '此列看的是': 'this column shows ',
+    '同一失效机制的多个观测面': 'multiple observation faces of one failure mechanism',
+    '不是': ' are not ', '多源印证': 'multi-source corroboration',
+    '融合时禁按源数加权': 'fusion must not weight by source count',
+    '级小修全年窗宽': 'minor repairs have a year-round window',
+    '吊装级受台风季约束': 'lifting work is constrained by typhoon season',
+    '色深=可作业占比': 'colour depth is the workable share',
+    '限值=行业常规': 'limits are industry norms', '非本场合同值': 'not this site contract values',
+    '报': 'alarm', '良': 'good', '优': 'OK', '中位': 'median', '全场': 'fleet',
+    '窗至': 'window to', '判': 'graded ', '共': 'total ', '段': ' segments',
+    '轴': ' axis', '页': ' page', '类': ' class', '面': ' face', '点': ' point',
+    '看': 'see ', '卡': 'held', '该': 'this ', '依': 'basis', '九': 'nine ',
+    '多': 'more', '禁': 'do not ', '四': 'four ', '一': 'one ', '没': 'no ',
+    '在': 'in ', '上': 'upper ', '当': 'when ', '时': ' at ', '现': 'current ',
+    '至': 'to ', '只': 'only ', '待': 'pending ', '各': 'each ', '秒': ' s',
+    '别': 'end', '达': 'reached', '倍': '×', '比': 'ratio', '角': ' angle',
+    '态': ' state', '峰': 'peak', '律': ' law', '门': 'gate', '三': 'three ',
+    '度': ' degree', '警': 'alarm', '可': 'can ', '数': 'count', '松': 'loose',
+    '观': 'watch', '油': 'oil', '齿': 'gear', '口': 'basis', '反馈': 'feedback',
+    '从': 'from ', '到': 'to ', '向': 'to ', '要': 'need ', '有': 'has ',
+    '而': 'while ', '并': 'and ', '用': 'use ', '非': 'not ', '法': 'cannot ',
+    '止于': 'ends at ', '调取': 'obtain', '走完': 'complete', '万': 'k',
+    '长': 'long', '大': 'large', '新': 'new', '产': 'produce', '换': 'replace',
+    '补': 'add', '跑': 'run', '放到': 'place in ', '内': 'within', '线': ' line',
+    '场': ' site', '码': ' code', '季': 'quarterly', '半年': 'semi-annual', '半值': 'half of the ',
+    '提供': 'provided', '需': 'requires ', '实为': 'actually ', '注意': 'note ',
+    '至今': 'to date', '机构': 'institution', '两家': 'two providers', '到货': 'delivery',
+    '重跑': 'rerun', '必重跑下游': 'downstream must be rerun', '的底座': ' as the base',
+    '随运营': 'as operations proceed', '到位': 'in place', '自动刷新': 'auto refresh',
+    '怎么做': 'how to', '衍生': 'derived', '如东风场': 'site', '风机': 'turbine',
+    '约定位置': 'agreed location', '摄入命令': 'ingest command', '更新频率': 'update frequency',
+    '责任方': 'owner', '当前覆盖': 'current coverage', '末次更新': 'last updated',
+})
+
+# ── 13 收尾: 维护端口与剩余整句 ──
+TERMS.update({
+    '要补Data 就把新文件放到': 'to add data, place the new file in ',
+    '要补数据就把新文件放到': 'to add data, place the new file in ',
+    '再跑': ', then run ', '就把新文件放到': ', place the new file in ',
+    '随运营滚动 — 新数据到位跑 ingest, 图谱与判级自动刷新':
+        'rolls with operations — once new data is in place, run ingest and the graph and grading refresh automatically',
+    '相对稳定 — 换厂商资料/换版本/参数变更时才动, 改动需审':
+        'relatively stable — touched only when vendor documents, versions or parameters change, and changes require review',
+    '注意度总': 'note that the total ', '核过确': 'verified as ', '六类': 'six classes',
+    '当前止': 'currently ends at ', '缺口需现场补': 'the gap must be filled by site',
+    '新一轮到货重跑': 'rerun when the next batch arrives',
+    '振动线出件 → fusion 自动读': 'issued by the vibration line, read automatically by fusion',
+    '在升(末三窗/早三窗≥1.3)与换件闭环两类实证入图谱':
+        'rising trend (last-three vs first-three windows) and replacement closed-loop evidence enter the graph',
+    '补前这类信息只在链盘里, 图谱检索不到':
+        'before this, such information lived only on the chain board and was not retrievable from the graph',
+    '每台六步进度/停滞环节/下一步 + 全场瓶颈汇总':
+        'six-step progress, stalled step and next action per unit, plus a fleet bottleneck summary',
+    '补前只在运行期内存里, 图谱检索不到':
+        'before this it existed only in runtime memory and was not retrievable from the graph',
+    '判级或工单更新后': 'after a grading or work-order update', '振动线每轮出件后': 'after each vibration-line issue',
+    '换厂商资料时': 'when vendor documents change', '换版本时': 'when the version changes',
+    '参数变更时': 'when parameters change', '补充资料时': 'when documents are added',
+    '随码表/工单更新': 'with code-table and work-order updates', '月/事件驱动': 'monthly / event-driven',
+    '事件驱动': 'event-driven', '需 windscada 服务在跑': 'requires the service to be running',
+    '产物存在': 'artefact present', '数据源': 'data source', '摄入': 'ingest',
+    '现场提供': 'site provided', '化验机构': 'laboratory', '振动线': 'vibration line',
+    '图谱': 'knowledge graph', '本表': 'this table', '月表': 'monthly table',
+    '数据边界': 'data boundary', '空≠无检修': 'empty does not mean no maintenance',
+    '实机参数表': 'as-built parameter table', '判定规范': 'decision specification',
+    '技术资料索引': 'technical document index', '图纸/原理': 'drawings and principles',
+    '失效模式与原因树': 'failure modes and cause tree', '报警码表与处置手册': 'alarm code table and procedures',
+    '预防性维护': 'preventive maintenance', '检修工单台账': 'maintenance work order ledger',
+    '油液化验': 'oil analysis', '报警事件': 'alarm events', '月度衍生': 'monthly derived',
+    '趋势与闭环证据': 'trend and closed-loop evidence', '决策链进度': 'decision chain progress',
+    '振动评估报告': 'vibration assessment report',
+})
+
+# ── 14 末轮: 决策/振动/曲线三页残留 ──
+TERMS.update({
+    '一台机组从': 'A unit runs from ', '发现异常': 'anomaly detected', '修完验收': 'repaired and verified',
+    '这张盘不讲某一台的故事': 'this board tracks the fleet, not one unit',
+    '瓶颈集中在一步': 'the bottleneck sits at a single step',
+    '不是': 'is not ', '再多看点数据能解的': 'something more data can solve',
+    '核实': 'verify', '向现场调取': 'obtain from site', '台账止于': 'ledger ends at ',
+    '系统无法判定是否已派工': 'the system cannot tell whether work was dispatched',
+    '非未执行': 'not the same as not done', '数据不足不可判': 'insufficient data, indeterminate',
+    '这不是': 'this is not ', '偷懒': 'laziness', '历史单不对应本次问题': 'historical orders do not match this finding',
+    '会造出假的完成态': 'would fabricate a completed state', '要填这一列': 'to fill this column',
+    '须向现场要': 'requires site to provide ', '已走完全程并退出盘面': 'completed the full path and left the board',
+    '端到端闭环实证': 'end-to-end closed-loop evidence', '立即修': 'repair now',
+    '推迟': 'defer', '拖到失效': 'run to failure', '大部件成本': 'major component cost',
+    '作业级': 'work class', '修理窗': 'repair window', '停机损失': 'downtime loss',
+    '电费口径': 'tariff basis', '风险敞口': 'risk exposure', '等待月数': 'months waited',
+    '随板块常显': 'always shown with the panel', '各列取数窗口不同': 'columns draw on different windows',
+    '两格同时发红': 'both cells turn red', '不等于同时发生': 'does not mean they happened together',
+    '不可当共时印证': 'must not be taken as simultaneous corroboration',
+    '每格右侧曲线': 'the curve on the right of each cell', '逐月记忆轨迹': 'monthly trend memory',
+    '实心点': 'solid point', '现窗': 'current window', '快照会丢形态': 'a snapshot loses the shape',
+    '才看得出': 'only then can one see ', '塌': 'collapse', '回升': 'recovery', '翻转': 'reversal',
+    '阴性不构成证据': 'a negative does not constitute evidence', '在知盲区上': 'over a known blind spot',
+    '同理': 'likewise', '两条判读纪律': 'two reading rules', '随板块': 'with the panel',
+    '格子颜色': 'cell colour', '徽章': 'badge', '业主口径': 'owner convention',
+    '可信度': 'confidence', '九枚举': 'nine values', '两轴不可互相替代': 'the two axes are not interchangeable',
+    '说的是': 'refers to ', '设定本就不同': 'settings differ by design', '此列只展示不判级': 'this column is shown without grading',
+    '落位': 'position', '圆内数字': 'the number inside each circle', '连成一片': 'form a continuum',
+    '成因须配置台账': 'the cause requires the configuration ledger', '档下的': ' within the bin',
+    '标定偏移': 'calibration offset', '正态抽样': 'normal sampling', '自身的分辨力': 'its own resolving power',
+    '削峰': 'peak shaving', '饱和点': 'saturation point', '三叶': 'three-blade',
+    '集距': 'collective pitch', '不平衡': 'imbalance', '齿比': 'gear ratio',
+    '两轴独立分组': 'the two axes group independently', '三组': 'three groups',
+    '专项锚': 'dedicated anchor', '只出当期电量账': 'issues only the current-period energy account',
+    '数字随上方窗切换': 'figures follow the window selector above',
+    '唯一进设备考核的一段': 'the only segment under equipment accountability',
+    '命令与实际功率对得上': 'command and actual power agree',
+    '功率未被压制': 'power was not suppressed', '按责任归属': 'by accountability',
+    '限电与调度令': 'curtailment and dispatch orders', '不进设备责任': 'not equipment accountability',
+    '正常发电欠发与低风待机': 'under-generation in production and low-wind standby',
+    '能量守恒核': 'energy conservation check', '全额定上限': 'all-rated ceiling',
+    '量级自检': 'magnitude self-check', '最大一笔是限电不是故障': 'the largest item is curtailment, not faults',
+    '限电命令面占比': 'curtailment command coverage', '移至独立板块': 'moved to a separate panel',
+    '交付九分册一一对应': 'maps one-to-one onto the nine delivery volumes',
+    '无 SCADA 轴': 'no SCADA axis', '分册蓝本': 'volume basis', '巡检': 'inspection',
+    '独立页': 'dedicated page', '点机组号': 'click a unit number', '看全场': 'view fleet',
+    '项占': ' items account for ', '直接比': 'comparing directly', '会读反': 'reads backwards',
+    '最低但均': 'lowest yet averages ', '灰柱': 'grey bar', '本图': 'this figure',
+    '量排': ' ranked by volume', '点击加入报告': 'click to add to the report',
+    '再点击移出': 'click again to remove', '画布里已有': 'the canvas holds ',
+    '悬停查看组件说明': 'hover for a description', '右侧': 'on the right', '调整顺序': 'to reorder',
+})
+
+# ── 15 收尾末轮 ──
+TERMS.update({
+    '六步进度': 'six-step progress', '实线': 'solid line', '虚线': 'dashed line',
+    '能解的': 'can solve', '有过': 'previously had ', '是针对本': 'addresses this ',
+    '尺度': 'scale', '跨两个数量级': 'spanning two orders of magnitude', '弱源': 'weak source',
+    '早修成本被': 'the cost of repairing early is ', '稀释': 'diluted',
+    '晚修成本被': 'the cost of repairing late is ', '放大': 'amplified',
+    '窗口与船期': 'access windows and vessel scheduling', '船期': 'vessel scheduling',
+    '总表': 'master table', '自动翻绿': 'turns green automatically', '会话': 'session',
+    '五级来源': 'five-level source', '取最新版不写死日期': 'always takes the latest version, no hard-coded date',
+    '定谳链': 'settled-verdict chain', '以上': ' and above', '两轴': 'two axes',
+    '取严合并': 'merged on the stricter of the two', '厂商资料': 'vendor documents',
+    '现场反馈修正': 'site feedback corrections', '它是判据的地基': 'it underpins the criteria',
+    '变更须过审': 'changes require review', '运行期计算': 'computed at runtime',
+    '两类': 'two classes', '入图谱': 'into the knowledge graph', '厂商/技术': 'vendor / technical',
+    '分步处置': 'step-by-step procedure', '点击查看': 'click to view', '点击': 'click ',
+    '在已知盲区上': 'over a known blind spot', '见列头': 'see column headers',
+    '同时发生': 'simultaneous', '才看得出': 'only then is it visible: ',
+    '类机制签名': 'mechanism-class signature', '签名': 'signature',
+    '落在几个离散位置上': 'sit at a few discrete positions', '离散位置': 'discrete positions',
+    '偏差达中位的': 'deviation reaches the median by ', '正态期望': 'normal expectation',
+    '筛查': 'screening', '固定判别窗': 'fixed discrimination window',
+    '风速计偏高': 'anemometer reads high', '残差带': 'residual band',
+    '可辨门': 'discrimination gate', '标定漂移': 'calibration drift',
+    '判别窗': 'discrimination window', '一致': 'consistent', '刷屏': 'flooding',
+    '数量级': 'orders of magnitude', '中值': 'mid-value', '地基': 'foundation',
+    '取严': 'take the stricter', '合并': 'merge', '列头': 'column header',
+    '知盲区': 'known blind spot', '离散': 'discrete', '期望': 'expectation',
+    '计高': 'reads high', '偏高': 'reads high', '带': ' band', '门': ' gate',
+    '过审': 'review', '取最新版': 'latest version', '不写死': 'not hard-coded',
+    '期计算': ' computed', '五级': 'five-level', '来源': 'source',
+})
+
+# ── 16 地道化: 高可见句子整条重写 + 中文标点 ──
+# 用户令: 要英文技术文档的自然表达, 不是逐词替换出来的中式英语。
+# 逐词替换必然产出中文语序 ("bar height=均count, is not -total"), 高可见文案必须整句重写。
+PUNCT = {
+    '。': '. ', ',': ', ', '、': ', ', ';': '; ', ':': ': ',
+    '(': ' (', ')': ') ',
+    # 「」《》 在英文里没有对应形态, 且引号会被闸拦 (会破坏注入上下文) — 去掉即可, 语义不丢
+    '「': ' ', '」': ' ', '《': ' ', '》': ' ',
+    '★': '* ', '⇒': ' leads to ', '~': '~', '·': ' · ', '|': ' | ',
+}
+TERMS.update(PUNCT)
+TERMS.update({
+    # 窗声明
+    '本页全部数字随上方窗切换': 'all figures on this page follow the window selector above',
+    '本页含两个窗': 'this page uses two different windows',
+    '固定窗 · 不随上方切换': 'fixed window — independent of the selector above',
+    '窗内 2026-07 数据不满月': '2026-07 is a partial month',
+    '月度图已按天归一': 'monthly figures are normalised per day',
+    # 能量账
+    '理论可发 (实际+损失)': 'gross available (net exported + losses)',
+    '实际上网 · 等效满发': 'net exported · equivalent full-load hours',
+    '能量守恒核 · 理论 < 全额定上限': 'energy conservation check — gross available below the all-rated ceiling',
+    '限电 (命令与实际功率对得上)': 'curtailment (command matches actual power)',
+    '限电命令期 · 功率未被压制': 'curtailment order in force, power not suppressed',
+    '故障与检修停机': 'faults and maintenance downtime',
+    '正常发电时的欠发': 'under-generation while producing',
+    # 故障统计
+    '柱高=日均条数, 不是月总数': 'bars show events per day, not the monthly total',
+    '本窗 2026-07 数据不满月(灰虚线柱), 直接比月总数会读反':
+        '2026-07 is a partial month (grey dashed bar) — comparing raw monthly totals would reverse the reading',
+    '实测: 2026-07 总数最低但日均': 'measured: 2026-07 has the lowest total but the second-highest daily rate at ',
+    '条/日 = 全窗前列': ' events per day',
+    '关键少数: 前': 'the vital few — the top ', '项占 80%': ' items account for 80%',
+    '共 12 项, 合计': 'of 12 codes, ', '前三': 'top three',
+    '条数=发生频次, 不反映影响程度': 'count is frequency, not severity',
+    # 决策链
+    '一台机组从「发现异常」到「修完验收」要走六步': 'A unit moves through six steps, from anomaly detected to repaired and verified',
+    '要补数据就把新文件放到「约定位置」再跑「摄入命令」': 'To add data, place the file in the stated location and run its ingest command',
+    '这张盘不讲某一台的故事, 用于确认': 'This board tracks the fleet rather than a single unit, showing ',
+    '已达': 'complete', '数据不足不可判': 'insufficient data', '未达': 'not reached',
+    '瓶颈集中在一步, 不是「再多看点数据」能解的':
+        'the bottleneck sits at one step and more data will not clear it',
+    '的下一步': ' — next step: ',
+    '定性: 现场/取样把根因定到部件与失效模式':
+        'characterise on site or by sampling: identify the root cause down to component and failure mode',
+    '核实: 向现场调取 2025~2026 工单': 'obtain 2025–26 work orders from site',
+    # 维护端口
+    '数据维护面': 'Data sources', '个源': ' registered',
+    '每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。':
+        'Each source lists coverage, last update, location, ingest command, refresh cadence and owner. ',
+    '要补数据就把新文件放到「约定位置」再跑「摄入命令」。':
+        'To add data, place the file in the stated location and run its ingest command. ',
+    '随运营滚动 — 新数据到位跑摄入, 图谱与判级自动刷新':
+        'Rolls with operations: once new data lands, run the ingest command and the graph and grading refresh automatically',
+    '相对稳定 — 换厂商资料或现场反馈修正时才动; 它是判据的地基, 变更须过审':
+        'Relatively stable: changed only when vendor documents or site feedback require it. These underpin the criteria, so changes go through review',
+    '怎么补数据': 'How to add data', '当前覆盖': 'Coverage', '末次更新': 'Last updated',
+    '更新频率': 'Cadence', '责任方': 'Owner', '数据层': 'Data layer', '机理层': 'Domain layer',
+    '放到': 'place in ', '跑': 'run ',
+})
+
+# ── 17 精修: 用源码里的准确原文入表 (之前凭印象写 key, 有细微出入所以没命中) ──
+TERMS.update({
+    '随运营滚动 — 新数据到位后跑摄入, 本体与判级自动刷新':
+        'Rolls with operations: when new data lands, run its ingest command and the ontology and grading refresh automatically',
+    '要补数据就把新文件放到': 'To add data, place the file in the ', '再跑': ' and run the ',
+    '约定位置': 'stated location', '摄入命令': 'ingest command',
+    '数据线': 'analytics team', '现场': 'site', '振动线': 'vibration team',
+    '化验机构': 'laboratory', '厂商/技术': 'vendor / engineering',
+    'windscada → 图谱': 'analytics to knowledge graph', '振动线 → 图谱': 'vibration team to knowledge graph',
+    # 常见中式残片
+    '总table': 'master table', '注意度': 'note that ', '实为': 'is in fact ',
+    '级事件': '-level event', '不work order': 'is not a work order',
+    '六类source': 'six source classes', '核过确': 'verified as ', '需site补': 'requires site to supply ',
+    '新一round到货aft rerun': 'rerun when the next batch arrives',
+    '自动翻绿': 'turns green automatically', '两家': 'two providers',
+    '取严合and': 'merged on the stricter of the two', '两 axis': 'two axes',
+    '来source': 'source', '五 level': 'five levels', '自动取最new版': 'always takes the latest version',
+    '不写死期': 'no hard-coded date', '以upper': ' and above',
+    '会话': 'session', '期计算': ' computed at runtime', '两 class': 'two classes',
+    '分 steps action': 'step-by-step procedures', '分steps': 'step-by-step',
+})
+
+# ── 7 量词/连接词/动词 (最后补, 都是短词; 长词已在前面命中所以不会被截断) ──
+TERMS.update({
+    '非行业': 'not the industry-standard ', '真窗': 'actual window', '真': 'actual ',
+    '近90日固定': 'last 90 days, fixed', '所选时间窗': 'selected time window',
+    '判级窗': 'Grading window', '五态': 'five states', '温度月轨迹': 'monthly temperature trend',
+    '剔除口径': 'excl. basis', '剔除': 'excluding', '不含调度令': 'excl. dispatch orders',
+    '不含': 'excl. ', '含': 'incl. ', '另计': 'counted separately', '另': 'also ',
+    '起': '', '个': '', '处': '', '份': '', '轮': ' round', '步': ' steps',
+    '共': 'total ', '约': 'approx. ', '仅': 'only ', '已': '', '未': 'not ',
+    '按': 'by ', '见': 'see ', '据': 'per ', '经': 'via ', '含义': 'meaning',
+    '全真': 'all verified', '引用id数': 'cited ids', '溯源核验': 'Traceability check',
+    '用时': 'elapsed', '模型': 'model', '推理中': 'reasoning', '空闲': 'idle',
+    '提问': 'Ask', '查询': 'Query', '查': 'Go', '常查': 'Common', '报警码': 'Alarm code',
+    '失效模式': 'failure mode', '手册分步处置': 'manual step-by-step procedure',
+    '历史工单': 'historical work orders', '年度任务': 'annual task', '系统': 'system',
+    '本体确定性查询': 'Deterministic ontology query',
+    '结果': 'result', '手册与工单source-backed record': 'verbatim from manuals and work orders',
+    '不经模型': 'no model involved', '预设': 'Preset', '章节': 'section', '受众': 'Audience',
+    '场站内部': 'Internal', '上报集团': 'For group reporting', '全量': 'full', '脱敏': 'redacted',
+    '打印': 'Print', '存': 'Save as', '导出': 'Export', '组件库': 'Block library',
+    '画布': 'canvas', '文档预览': 'Document preview', '报告': 'report', '运行分析': 'Operations Analysis',
+    '统计期': 'Reporting period', '数据源': 'Data sources', '出件': 'issued',
+    '覆盖': 'coverage', '末次更新': 'last updated', '位置': 'location', '摄入': 'ingest',
+    '责任': 'owner', '频率': 'frequency', '说明': 'note', '存在': 'present',
+    '数据': 'Data', '机理': 'Domain model', '层': 'layer', '名': 'name',
+})
+
+_RE = None
+
+
+def _compile():
+    global _RE
+    keys = sorted(TERMS, key=len, reverse=True)      # 最长匹配优先
+    _RE = re.compile('|'.join(re.escape(k) for k in keys))
+    return _RE
+
+
+# 单字量词只在数字后才是量词: ' 行' 当 key 会切进 行动等级/行星级 (实测切成 "rows 动等
+# level"), 加前导空格也锁不住 "/ 行动"。词表是纯字符串匹配, 表达不了上下文条件, 故后处理。
+_RE_ROWS = re.compile(r'(?<=\d)\s*行(?![\w\u4e00-\u9fff])')
+# 量词 '条' 同理: 数字后面是量词该译, 但收成单字条目会切碎 条件 / 条款 / 一条链。
+_RE_CNT = re.compile(r'(?<=\d)\s*条(?![\w\u4e00-\u9fff])')
+
+
+def translate(s):
+    """替换已知术语; 未知中文原样留下, 由 audit_untranslated 收口。
+
+    ★词间补空格 (2026-08-28 实测): 中文没有词界, 两个相邻中文词各自被替换后英文会直接粘连 —
+    "分析"+"决策" → "analysisdecision"。替换时看后一个字符, 若替换结果以字母结尾且下一个
+    字符也是字母/中文, 补一个空格。这只缓解粘连, 治不了"部分覆盖"本身: 未译的中文词
+    仍会夹在英文中间 ("风电智能analysis"), 所以覆盖率必须推到接近满值才可交付。
+    """
+    if not isinstance(s, str) or not s:
+        return s
+    r = _RE or _compile()
+
+    def _sub(m):
+        v = TERMS[m.group(0)]
+        nxt = s[m.end():m.end() + 1]
+        if v and v[-1].isascii() and v[-1].isalnum() and nxt and (nxt.isalnum() or nxt == '(' or '\u4e00' <= nxt <= '\u9fff'):
+            return v + ' '
+        return v
+    out = _RE_CNT.sub(' events', _RE_ROWS.sub(' rows', r.sub(_sub, s)))
+    # 数字上下文量词: 只有紧跟数字时才是量词, 收成单字条目会切碎 次数/第一/年度/个别/台账
+    out = _RE_TIMES.sub(' times ', out)
+    out = _RE_UNITS.sub(' units ', out)
+    out = _RE_YEAR.sub('', out)
+    out = _RE_ITEMS.sub(' ', out)
+    out = _RE_STEP.sub('No.', out)
+    out = _RE_STEPN.sub(' steps ', out)
+    out = _RE_TOTAL.sub('total ', out)
+    out = _RE_IN_N.sub('in ', out)
+    # 上面几条一律补尾空格防粘连 (实测 "6 stepshandling"), 再压掉行内多余空格。
+    # 只压 空格/tab, 不碰换行 —— 产物里有靠换行分段的文本。
+    out = re.sub(r'(?<=\S)[ \t]{2,}', ' ', out)
+    # ★混杂回退 (2026-08-30 用户在检修助手排查步骤里逮到): 词表对长尾技术文本
+    # (手册排查步骤、告警处置) 不可能穷举, 部分覆盖会把整句切成中英混杂 ——
+    # "更换机侧BCU控制器" → "replace 机侧BCUcontrol 器", 读不通且显得系统劣质。
+    # 判据不靠列举而靠自适应: 译完仍留下**过半**原有中文 ⇒ 这段根本没覆盖,
+    # 整段退回原文。完整的中文至少是句能读的话, 客户看得出这是未翻译区。
+    zh0 = sum(1 for c in s if '\u4e00' <= c <= '\u9fff')
+    if zh0 >= 6:
+        zh1 = sum(1 for c in out if '\u4e00' <= c <= '\u9fff')
+        if zh1 / zh0 > 0.5:
+            return s
+    return out
+
+
+def tr_deep(o):
+    if isinstance(o, str):
+        return translate(o)
+    if isinstance(o, dict):
+        return {translate(k) if isinstance(k, str) else k: tr_deep(v) for k, v in o.items()}
+    if isinstance(o, list):
+        return [tr_deep(v) for v in o]
+    return o
+
+
+def audit_untranslated(text, top=40):
+    """列出仍未译的中文片段, 按出现次数排 — 迭代补词表用。"""
+    import collections
+    frags = re.findall(r'[一-鿿]+', text or '')
+    return collections.Counter(frags).most_common(top)
+
+
+_assert_quote_free()
+
+# ── 18 图表说明与告警码 ────────────────────────────────────────────
+# 来源: 单文件版下钻浮层。这批文案此前从未进过残留统计 —— 统计只走了十个 tab 的
+# 页面文本, 而浮层要点击才出现。口径漏了一层, 补测 15 个浮层扫出 45 词 629 字。
+# 取值全部从预烤 JSON 的 fig.note / fig.unit / fig.title 抓原文, 不凭印象写 key。
+TERMS.update({
+    # 图例整句 (整句先于逐词匹配, 避免拼出中文语序)
+    '灰带=全场四分位区间, 深线=全场中位, 红线=本台':
+        'Grey band = fleet interquartile range; dark line = fleet median; red line = this unit',
+    '灰带=全场四分位区间': 'Grey band = fleet interquartile range',
+    '振动判级交叉轴: 温度=同机制果侧观测(观测面≠源数)':
+        'Vibration grading cross-check: temperature is an effect-side observation of the same '
+        'mechanism (an observation face is not an independent source)',
+    '数据=TCM 六窗标量': 'Data: TCM six-window scalars',
+    '窗内合计': 'Total in window',
+    '判据:': 'Criterion:',
+    # 统计与图元
+    '稳健离群度': 'robust outlier score', '六窗比': 'six-window ratio',
+    '末窗/首窗': 'last window / first window', '六窗标量': 'six-window scalars',
+    '四分位区间': 'interquartile range', '全场中位': 'fleet median',
+    '灰带': 'grey band', '深线': 'dark line', '红线': 'red line',
+    '均值': 'mean', '稳健': 'robust', '档带宽': 'band width', '档': 'band',
+    '月样本中位': 'monthly sample median', '月样本': 'monthly sample',
+    '产物转录': 'transcribed from analysis outputs', '源数': 'source count',
+    '上升': 'rising', '定向查': 'targeted check', '报警门': 'alarm gate',
+    # 量与单位
+    '秒/日': 's/day', '泵': 'pump', '制动': 'brake',
+    '月度条数': 'monthly count', '报警月度条数': 'alarm count per month',
+    # 测点与系统
+    '偏航润滑泵运行时长': 'yaw lubrication pump runtime',
+    '桨叶零位偏差': 'blade zero-position deviation',
+    '齿轮箱中速轴温度': 'gearbox IMS bearing temperature',
+    '齿轮箱高速轴温度': 'gearbox HSS bearing temperature',
+    '变压器绕组最高温度': 'transformer winding peak temperature',
+    '主控与传感网系统': 'main controller and sensor network',
+    '蓄能失效候选': 'accumulator failure candidate', '蓄能行为': 'accumulator behaviour',
+    '振动分析摘要': 'vibration analysis summary', '振动标量': 'vibration scalars',
+    '偏航系统': 'yaw system', '变桨系统': 'pitch system', '偏航失败': 'yaw failure',
+    '发电机侧': 'generator side', '发电态': 'generating', '风传感': 'wind sensing',
+    '变桨分册': 'pitch volume',
+    # 告警码名称 (top: ... 里出现)
+    '自动解缆停机': 'automatic cable untwist stop',
+    '偏航制动液压压力异常': 'yaw brake hydraulic pressure fault',
+    '偏航失败-等待': 'yaw failure - waiting', '偏航失败-停止': 'yaw failure - stop',
+    '偏航电机过热': 'yaw motor overheat',
+    '偏航变流器警告过多': 'excessive yaw converter warnings',
+    '变流器降功率运行(DG)': 'converter derated operation (DG)',
+    '变流器跳闸(自动重启)': 'converter trip (auto-restart)',
+    'ABB变流器PU通讯故障': 'ABB converter PU communication fault',
+    'UPS电池电压低': 'UPS battery voltage low',
+    'FT1超声波风速计故障': 'FT1 ultrasonic anemometer fault',
+    '液压监控·轮毂(变桨)压': 'hydraulic monitoring - hub (pitch) pressure',
+    '变桨轴承润滑(执行中)': 'pitch bearing lubrication (running)',
+    '变桨泵运行超时停机': 'pitch pump runtime timeout stop',
+    '液压油温异常': 'hydraulic oil temperature fault',
+    '液压油位异常': 'hydraulic oil level fault',
+    '轮毂部件松动': 'hub component loose',
+    '风速计偏高候选(A类)': 'anemometer reads-high candidate (class A)',
+    '风速计偏置': 'anemometer bias',
+})
+_assert_quote_free()
+
+# '条' 原映射空串, 使 "窗内合计 40 条; top:" 译成 "Total in window 40 ; top:" — 英文这里
+# 需要一个名词把数字接住, 空串留下悬空的分号。'次'/'起' 保持省略 (英文计数惯例不同)。
+TERMS['条'] = 'events'
+_assert_quote_free()
+
+# ── 19 图例硬编码文案 ──────────────────────────────────────────────
+# 这批不在 fig.note 里, 是 drawFig 渲染时拼进 SVG/HTML 的固定图例 —— 从源码 grep 会淹没在
+# Python 注释里, 故改从浮层 DOM 的文本节点直接抓, 只有 11 个串。key 保留 &lt; 原形:
+# hook 拦的是 innerHTML 写入前的字符串, 那时实体还没解码。
+TERMS.update({
+    '条=本台/全场中位 (竖线=1× 基准, 右端封顶 2.5×); 色按 z (稳健离群度): 绿&lt;2 · 橙2~3 · 红≥3':
+        'Bar = this unit / fleet median (vertical line = 1x reference, right end capped at 2.5x); '
+        'colour by z (robust outlier score): green under 2 · orange 2 to 3 · red 3 and above',
+    '谱级深潜与逐窗趋势 → 振动融合板块·振动分析入口 (windcms)':
+        'Spectrum-level deep dive and per-window trend → Vibration Fusion section · '
+        'vibration analysis entry point (windcms)',
+    '峭度': 'kurtosis', 'ISO烈度': 'ISO severity', '烈度': 'severity',
+    '速度有效值': 'velocity RMS', '零位偏差': 'zero-position deviation', '零位': 'zero position',
+    '竖线': 'vertical line', '右端封顶': 'right end capped at', '封顶': 'capped',
+    '色按': 'colour by', '基准': 'reference',
+    '绿': 'green', '橙': 'orange', '红': 'red',
+    '谱级深潜': 'spectrum-level deep dive', '深潜': 'deep dive',
+    '逐窗趋势': 'per-window trend', '逐窗': 'per-window', '板块': 'section',
+    # 测点与工况 (浮层标题)
+    '(发电态P>500kW)': '(generating, P over 500 kW)', '·均值 月度': '· mean, monthly',
+    '本台 vs 全场': 'this unit vs fleet', '记基线观察': 'baseline recorded, monitoring',
+    '温度候选(定向查)': 'temperature candidate (targeted check)',
+    '发电机驱动端(DE)轴承温度': 'generator drive-end (DE) bearing temperature',
+    '发电机非驱动端(NDE)轴承温度': 'generator non-drive-end (NDE) bearing temperature',
+    '后主轴承温度(齿轮箱侧)': 'rear main bearing temperature (gearbox side)',
+    '液压油温': 'hydraulic oil temperature',
+    '(发电机侧)': '(generator side)', '(转子侧)': '(rotor side)',
+    '(功率 0-1600 kW 档)': '(power band 0-1600 kW)',
+    '(功率 1601-2000 kW 档)': '(power band 1601-2000 kW)',
+    # 告警码 (第二批)
+    '齿轮油温度警告': 'gear oil temperature warning', '齿轮油温度高': 'gear oil temperature high',
+    '高速轴发电机侧齿轮轴承温': 'HSS generator-side gear bearing temperature',
+    'FT1超声波风速计通讯故': 'FT1 ultrasonic anemometer communication fault',
+    '一次风速计故障': 'primary anemometer fault', '一次风向标失效': 'primary wind vane failure',
+})
+_assert_quote_free()
+
+# 上面那次单字回归测试顺带暴露的既有裂缝 (单字 '进' 把 "进行" 切成 "into 行")
+TERMS.update({'进行': 'carry out', '行动等级': 'action level', '等级': 'level'})
+_assert_quote_free()
+
+# ── 20 删除汉字单字条目 ────────────────────────────────────────────
+# 2026-08-30 用户在检修助手的排查步骤里逮到: "检查机侧BCU控制器" 被译成
+# "检Go 机 side BCUcontrol 器"。根因是词表里有约 110 个**汉字单字**条目
+# (查→Go / 卡→held / 侧→side / 换→replace / 上→upper / 步→steps ...):
+# 它们在词表收录了的长词里被最长匹配保护住, 一旦碰到**未收录的长词**就把词切碎。
+#
+# 这类长尾技术文本(手册排查步骤、告警处置)不可能穷举收词, 所以靠"补长词"堵不住。
+# 判断: **半译比不译更糟** —— "检Go 机 side" 读不通且显得系统劣质,
+# 而完整的 "检查机侧" 至少是句能读的话, 客户看得出这是未翻译区而非系统出错。
+# 故删除所有汉字单字, 只保留标点与符号映射 (那些无歧义)。
+_CJK1 = [k for k in TERMS if len(k) == 1 and '一' <= k <= '鿿']
+for _k in _CJK1:
+    del TERMS[_k]
+_RE = None          # 词表变了, 强制重编译
+
+# 单字按钮的收尾: 删除汉字单字条目后, 界面上孤立的单字按钮(查/看)不再被译。
+# 解法不是把单字加回来(那会重新切碎长词), 而是把按钮文案本身改成双字词。
+TERMS.update({'查询': 'Query', '查看': 'View', '常用': 'Common', '未知告警码': 'Unknown alarm code'})
+_RE = None
+
+# ── 21 告警码链模板与码名 ──────────────────────────────────────────
+# 用户: 还有很多字没改过来。实测预烤 JSON 里含中文的串 6131 个 —— 全译出 3021,
+# 整段回退 878 (23918 字), 部分残留 2232 (14166 字)。残留高度集中在**模板化**的
+# 告警码链与失效模式链上, 所以补模板词的边际收益远高于补零散长句。
+TERMS.update({
+    # 模板 (每条告警码链都出现, 频次见注释)
+    '告警类型': 'alarm type',            # 120
+    '码表故障原因': 'code-table fault cause',  # 60
+    '本场报警': 'site alarms',           # 130
+    '预防性任务': 'preventive task', '预防链': 'preventive chain',
+    '系统级': 'system-level', '按实证': 'by observed', '出现量排序': 'ranked by volume',
+    '未归属': 'unassigned', '气动': 'pneumatic', '液压/气动失效': 'hydraulic or pneumatic failure',
+    '码表未收录': 'not in code table',
+    # 告警码名称 — 偏航
+    '触发偏航限位传感器': 'yaw limit sensor triggered',
+    '偏航圈数值超出范围': 'yaw turn count out of range',
+    '偏航润滑油脂油位低': 'yaw grease level low',
+    '偏航轴承润滑脂油位低': 'yaw bearing grease level low',
+    '偏航轴承的润滑不可能': 'yaw bearing lubrication not possible',
+    '偏航刹车压力传感器错误': 'yaw brake pressure sensor error',
+    '偏航齿盘压力传感器错误': 'yaw ring-gear pressure sensor error',
+    '偏航刹车压力错误': 'yaw brake pressure error',
+    '偏航齿盘压力错误': 'yaw ring-gear pressure error',
+    '太多的偏航变流器警告': 'too many yaw converter warnings',
+    '太多偏航刹车': 'too many yaw brake applications',
+    '强制偏航一圈': 'forced yaw one turn',
+    '无润滑时,偏航轴承': 'no lubrication, yaw bearing',
+    '无润滑,偏航系统': 'no lubrication, yaw system',
+    # 风传感
+    '风向标阻塞-冰冻': 'wind vane blocked - iced', '风向标阻塞': 'wind vane blocked',
+    '有效性-风速低': 'validity - low wind speed',
+    # 塔架
+    '塔筒紧停开关故障': 'tower emergency-stop switch fault',
+    '检测到塔筒振动频率变化': 'tower vibration frequency shift detected',
+    '机舱紧急开关故障': 'nacelle emergency switch fault',
+    '触发应急开关': 'emergency switch triggered',
+    '叶轮不平衡': 'rotor imbalance',
+    # 变压器
+    '变压器散热器断路器跳开': 'transformer radiator breaker tripped',
+    '变压器油温高': 'transformer oil temperature high',
+    '变压器油位过低': 'transformer oil level too low',
+    '变压器油压力过高': 'transformer oil pressure too high',
+    '变压器油泵跳开': 'transformer oil pump tripped',
+    # 运行与控制
+    '由于功率提升延迟引起的停机': 'stop due to power ramp-up delay',
+    '最大转速等待时间超时': 'maximum speed wait timeout',
+    '在本地操作中的风机': 'turbine in local control',
+    '逾时直流回路充电': 'DC-link charging timeout',
+    '避雷器压敏电阻误差': 'surge arrester varistor error',
+    '变流器冷却机模式': 'converter cooler mode',
+    '软件更新停机': 'software update stop', '手动空转停机': 'manual idling stop',
+    '主制动器切出': 'main brake cut out', '功率控制误差': 'power control error',
+    '启动失败': 'start failure',
+})
+_RE = None
+
+# ── 22 高频残留收口 ────────────────────────────────────────────────
+# 按"部分残留串里的未译片段频次"定优先级 (比逐条读产物高效): 1091 种片段里
+# 前 55 种覆盖了大部分残留串。单字量词不做词条 (会切碎长词), 一律走数字上下文正则。
+_RE_TIMES = re.compile(r'(?<=\d)\s*次(?!数|序|要|级)')
+_RE_UNITS = re.compile(r'(?<=\d)\s*台(?!账|风|阶)')
+_RE_YEAR  = re.compile(r'(?<=\d)\s*年(?![\w一-鿿])')
+_RE_ITEMS = re.compile(r'(?<=\d)\s*个(?!别|人|体|案|性)')
+_RE_STEP  = re.compile(r'第\s*(?=\d)')
+_RE_STEPN = re.compile(r'(?<=\d)\s*步(?!骤)')
+_RE_TOTAL = re.compile(r'共\s*(?=\d)')
+_RE_IN_N  = re.compile(r'分\s*(?=\d)')   # 分 6 步 → in 6 steps; 挡不住的 分析/分级 是双字词, 词表先命中
+# '个' 后面允许跟中文 (5 个模块), 但挡掉 个别/个人/个体/个案/个性 这些真词
+TERMS.update({
+    # 高频双字/多字 (数字 = 出现在多少个残留串里)
+    '检查': 'check',            # 175
+    '资料': 'documentation',    # 160
+    '处理': 'handling',         # 127
+    '培训课件': 'training material',  # 125
+    '变频器': 'frequency converter',  # 92
+    '油脂': 'grease',           # 75
+    '是否': 'whether',          # 72
+    '线路': 'circuit',          # 59
+    '技术资料': 'technical documentation',  # 52
+    '停滞于': 'stalled at',      # 50
+    '软件': 'software', '模块': 'module', '卡钳': 'caliper', '年度': 'annual',
+    '任务': 'task', '编码器': 'encoder', '涉码': 'codes involved', '密封': 'seal',
+    '回转支承的安装': 'slewing bearing installation', '维修和': 'maintenance and',
+    '指南': 'guide', '清理': 'cleaning', '螺栓': 'bolt', '征兆码': 'symptom code',
+    '六步': 'six steps', '相关': 'related', '各': 'each ',
+    # 常见搭配 (双字以上才收, 避免切碎)
+    '检查项': 'check item', '处理步骤': 'handling steps', '检查内容': 'what to check',
+    '维修': 'maintenance', '安装': 'installation', '更换': 'replacement',
+    '紧固': 'tightening', '润滑': 'lubrication', '清洗': 'cleaning',
+    '目视': 'visual', '测量': 'measurement', '记录': 'record',
+})
+_RE = None
+
+# 量词+名词在英文里是一个词: 中文「38 台机组」= 38 turbines, 量词要被吞掉而不是各译各的
+# (放宽 lookahead 后实测出 "38 unitsunit" 这种重复)。词表先于数字正则执行, 故收在这里生效。
+TERMS.update({
+    '台机组': 'turbines', '台风机': 'turbines', '台机': 'turbines',
+    '个失效模式': 'failure modes', '个模块': 'modules', '个部件': 'components',
+    '步处理': 'handling steps', '次动作': 'actions', '个系统': 'systems',
+    '条告警': 'alarms', '项任务': 'tasks', '类失效模式': 'failure-mode classes',
+})
+_RE = None
+
+# ── 23 领域名词与判据措辞收口 ──────────────────────────────────────
+# 排名口径修正后的 top: 先过脱敏 (文档名/业主名客户看不到, 不占补词预算),
+# 再只看"部分残留" (回退段落里的词词表其实已收)。剩下的才是真缺口。
+TERMS.update({
+    # 判据与推断措辞
+    '树深两层不外推': 'tree depth two layers, no extrapolation beyond',
+    '定位仍需图纸与': 'localisation still requires drawings and',
+    '需查图纸或厂商支持': 'requires drawings or vendor support',
+    '需查图纸或厂商': 'requires drawings or vendor',
+    '本码无手册处置条目': 'no handling entry for this code in the manual',
+    '手册覆盖本场': 'manual coverage for this site',
+    '非我方推断': 'not our inference', '因果来自': 'causality from', '归纳自': 'derived from',
+    '方向归一': 'direction normalised', '轴同时非常态': 'axes abnormal simultaneously',
+    '技术依据': 'technical basis', '窗止': 'window ends',
+    # 电气与部件
+    '支撑示意图': 'support diagram', '电气接口图': 'electrical interface diagram',
+    '原理图': 'schematic', '图纸': 'drawing', '主控图纸': 'main controller drawing',
+    '吊具图纸': 'lifting gear drawing', '开关柜': 'switchgear', '环网柜通讯': 'ring main unit comms',
+    '供电元件': 'power supply component', '变流元件': 'converter component',
+    '散热片': 'heat sink', '散热': 'cooling', '通风': 'ventilation', '板卡': 'board',
+    '超声波': 'ultrasonic', '刹车': 'brake', '气象站': 'met station',
+    '各部件资料': 'per-component documentation',
+    '故障逻辑及处理': 'fault logic and handling',
+    # 运维措辞
+    '技改': 'retrofit', '出质保整改': 'end-of-warranty rectification',
+    '全年维护': 'annual maintenance', '供给': 'supply', '走到': 'reaches',
+    '完成三个叶片防雷导通测试': 'complete lightning-continuity test on all three blades',
+})
+_RE = None
+
+TERMS.update({'厂商支持': 'vendor support', '厂商': 'vendor', '主控': 'main controller'})
+_RE = None
+
+# ── 24 界面文案系统性英文化 (第一批) ────────────────────────────────
+# 用户令: 把系统整个重新生成一遍, 不要打补丁式翻译。
+# 做法不是改 serve.py 源码 —— 那里的中文串同时是 UI 文案与**数据键**
+# (d['停机'] 之类取值用的), 源码替换会让取值落空。改为把 UI 文案一次性补全进词表,
+# 显示层因此全英文, 逻辑层的键不动。以下按出现频次批量补, 不再逐条追补。
+TERMS.update({
+    # 判据与结论措辞
+    '依据': 'basis', '判别': 'discriminator', '单位': 'unit', '外部类': 'external class',
+    '倍中位': '× median', '当前在升': 'rising metrics awaiting confirmation', '待现场核实': 'field confirmation required',
+    '仅记录': 'recorded only', '未见异常': 'no anomaly found', '证据窗末': 'evidence window ends',
+    '存在需现场核实或处置的异常': 'anomalies requiring field confirmation or action',
+    '纪律': 'discipline', '可用数据': 'available data', '一级指标': 'primary indicator',
+    '级归综合报告线': 'escalated to the consolidated report line',
+    '进设备考核的只有': 'counted against equipment performance:',
+    '限电属交易与调度侧': 'curtailment belongs to trading and dispatch, not equipment',
+    # 部件与测点
+    '主轴承前': 'main bearing front', '主轴承后': 'main bearing rear', '齿轮': 'gear', '齿箱': 'gearbox',
+    '行星级': 'planetary stage', '一级行星': 'first planetary stage', '行星架': 'planet carrier',
+    '高速轴转子侧': 'HSS rotor side', '传动链三部件': 'three drivetrain components',
+    '润滑油': 'lubricating oil', '油冷': 'oil cooling', '绕组': 'winding',
+    '元器件名称': 'component name', '设备侧': 'equipment side', '运行侧': 'operations side',
+    # 时窗与统计
+    '列窗': 'column window', '日期': 'date', '近90日': 'last 90 days', '近30日': 'last 30 days',
+    '月中位': 'monthly median', '逐日中位': 'daily median', '单次时长': 'duration per event',
+    '停机总时长h': 'total downtime h', '占停机': 'share of downtime', '停机段': 'downtime segment',
+    '日均条数': 'events per day', '条/日': 'events/day', '影响台': 'units affected',
+    '台级top': 'unit-level top', '全场四分位区间': 'fleet interquartile range',
+    '月duty(秒/日)': 'monthly duty (s/day)',
+    # 界面动作与状态
+    '有问题的系统': 'systems with findings', '无问题不出图': 'no chart when nothing is flagged',
+    '任一契约通道': 'any contracted channel', '工作台': 'workspace', '附录': 'appendix',
+    '故障名称': 'fault name', '故障四象限': 'fault quadrant', '安全风险预测': 'safety risk outlook',
+    '特性曲线多镜头': 'performance curve, multiple views', '一级显示': 'top-level view',
+    '问题为空': 'question is empty', '启动': 'start', '卡在哪一步': 'where it stalled',
+    '已选组件再次点击移出': 'click a selected component again to remove it',
+    '已取消': 'withdrawn', '阴性': 'negative',
+    # 模型档位说明
+    '单台状态查询与快速核证': 'single-unit status and quick verification',
+    '多台横向比对与机制推断': 'cross-unit comparison and mechanism inference',
+    '复杂推理与交叉审核': 'complex reasoning and cross-review',
+    '台内即时查询': 'in-unit instant query',
+    '一个SCADA采样格': 'one SCADA sampling interval',
+    '个码': ' codes',
+})
+_RE = None
+
+
+# ── 全局一致替换 (原生英文生成) ────────────────────────────────────
+# 2026-08-31 用户令: 不要打补丁式翻译, 整个系统重新生成一遍。
+#
+# 运行时 hook 的本质是"中文系统外面套一层英文": 它只译写入 DOM 的那一刻的文本,
+# JS 逻辑与 JSON 数据键仍是中文, 于是长尾永远补不完, 且源码里满是中文。
+# 改为**生成时对整页做全局替换** —— 关键在"全局一致":
+#   onclick="TAB='总览'" 与 if(TAB==='总览') 同时变成 'Overview', 比较照样成立;
+#   __SNAP__ 里的键 "停机" 与取值处 d['停机'] 同时变成 'Downtime', 取值照样命中。
+# 逻辑不破的前提是**同一张表、同一次替换、不做任何上下文判断**。
+#
+# 与 translate() 的区别: 这里不做混杂回退 —— 整页替换时"译完还剩中文"是常态
+# (长尾未覆盖), 回退会把整页退回中文。
+# ── 永不翻译的标识符 ──────────────────────────────────────────────
+# 这些中文串在系统里同时充当**数据键**: 后端构造 d['列窗']=... , 前端再 d.列窗 取回。
+# 译了其中任何一侧, 取值就落空 —— 2026-08-31 用户实报"报告点不开、选项点不开"的根因。
+# 它们在界面上的英文化由前端渲染层负责, 不在这一层做。
+NEVER_TRANSLATE = {
+    '报警', '列窗', '级', '工单', '链盘', '振动结论', '矩阵', '能力', '设备状态',
+    '总条数', '定论', '预警', '停机', '停机(调度令)', '限电·绑定', '主轴承后', '主轴承前',
+    '换件闭环 (检出→换件→恢复)', '状态', '天数', '依据', '表', '部件', '停机时长h',
+    '外部', '故障', '类别', '日期', '原因', '排查', '处置', '占停机',
+}
+
+
+def translate_all(s):
+    """对整页文本做最长匹配的全局一致替换; 不回退、不做上下文判断。"""
+    if not isinstance(s, str) or not s:
+        return s
+    r = _RE or _compile()
+
+    def _sub(m):
+        w = m.group(0)
+        if w in NEVER_TRANSLATE:      # 同时充当数据键 → 原样保留
+            return w
+        v = TERMS[w]
+        nxt = s[m.end():m.end() + 1]
+        if v and v[-1].isascii() and v[-1].isalnum() and nxt and (
+                nxt.isalnum() or nxt == '(' or '\u4e00' <= nxt <= '\u9fff'):
+            return v + ' '
+        return v
+    out = r.sub(_sub, s)
+    out = _RE_CNT.sub(' events', _RE_ROWS.sub(' rows', out))
+    out = _RE_TIMES.sub(' times ', out)
+    out = _RE_UNITS.sub(' units ', out)
+    out = _RE_YEAR.sub('', out)
+    out = _RE_ITEMS.sub(' ', out)
+    out = _RE_STEP.sub('No.', out)
+    out = _RE_STEPN.sub(' steps ', out)
+    out = _RE_TOTAL.sub('total ', out)
+    out = _RE_IN_N.sub('in ', out)
+    return re.sub(r'(?<=\S)[ \t]{2,}', ' ', out)
+
+
+# ── 源码级英文化 (保护数据键) ──────────────────────────────────────
+# translate_all 无差别替换会炸: `${r.停机时长h}` → `${r.Downtime hours h}`,
+# 属性名带空格即 JS 语法错误 (2026-08-31 实测 237 处风险点)。
+#
+# 正确的切分不是"UI 文案 vs 数据键"这种语义判断 (自动判不准), 而是**语法位置**:
+#   .中文        属性访问 —— 键, 不译 (数据里的键也是中文, 两边必须一致)
+#   ['中文']     下标访问 —— 同上
+#   "中文":      对象字面量/JSON 的键 —— 同上
+# 其余位置的中文都是要显示的文本, 一律译。
+#
+# 比较值不在保护之列且必须不保护: TAB==='总览' 与 onclick="TAB='总览'" 同时被译成
+# 'Overview', 比较照样成立 —— 一致性来自"同一张表替换所有出现处"。
+_PROT = [
+    re.compile(r'\.[一-鿿][一-鿿A-Za-z0-9_]*'),           # .属性
+    re.compile(r'\[\s*[\'"][^\'"]*[一-鿿][^\'"]*[\'"]\s*\]'),  # ['键']
+    re.compile(r'[\'"][一-鿿][^\'"]{0,40}[\'"]\s*:'),   # "键":
+    # Python 关键字参数名 dict(倍中位=...) —— 译了就是非法标识符 (实逮 × median=)。
+    # 限定在 ( 或 , 之后, 避免误伤字符串里的 "灰带=全场四分位区间" 这类显示文案。
+    re.compile(r'(?<=[(,])\s*[一-鿿][一-鿿A-Za-z0-9_]*(?=\s*=(?!=))'),
+    # 多行调用的续行: 关键字参数落在行首, 前面既不是 ( 也不是 , (逐行处理看不到上一行)
+    re.compile(r'^[一-鿿][一-鿿A-Za-z0-9_]*(?=\s*=(?!=))'),
+    # 中文函数名/变量名定义
+    re.compile(r'(?<=def )[一-鿿][一-鿿A-Za-z0-9_]*'),
+]
+
+
+def to_english_source(src):
+    """把一份含中文的页面源码转成英文, 保留数据键不动。"""
+    if not isinstance(src, str) or not src:
+        return src
+    keep = []
+
+    def hide(m):
+        keep.append(m.group(0))
+        return '\x00%d\x00' % (len(keep) - 1)
+
+    for rx in _PROT:
+        src = rx.sub(hide, src)
+    src = translate_all(src)
+    return re.sub(r'\x00(\d+)\x00', lambda m: keep[int(m.group(1))], src)
+
+# ── 25 显示文案整句原生英文 ────────────────────────────────────────
+# 用户: 别再用"改中文成英文"的方式, 要让英文在生成时就是原生文案。
+# 逐词映射拼出来的句子读着像翻译 (中文语序 + 术语堆叠); 这一批按**整句**写英文技术文档
+# 的表达, 整句条目在最长匹配下优先于任何词级条目, 所以它们直接决定成品读感。
+TERMS.update({
+    # 状态与提示
+    '载入中…': 'Loading…', '加载中…': 'Loading…', '进度': 'Progress',
+    '本地模型编排中…': 'Local model working…',
+    '未标时间窗': 'time window not stated', '时间窗内无报警': 'no alarms in this time window',
+    '← 卡在这里': '← stalled here', '(+约 30 秒)': '(+ approx. 30 s)',
+    '轻量 8B · 约 10 秒': 'Light 8B · approx. 10 s',
+    '标准 27B · 约 30 秒': 'Standard 27B · approx. 30 s',
+    '打印 / 存 PDF': 'Print / Save as PDF',
+    '部件与证据': 'Component and evidence',
+    '换前': 'before replacement', '换后': 'after replacement',
+    '补机制定性': 'mechanism characterisation pending',
+    '油液采样节奏': 'Oil sampling cadence', '限电侧': 'Curtailment side',
+    '在升·待处置对账': 'Rising and pending action — reconciliation',
+    # 链接
+    '该问题页 →': 'Open this issue →',
+    '该台完整问题页 →': 'Full issue page for this unit →',
+    '完整问题页 (全部曲线与工单史) →': 'Full issue page — all curves and work-order history →',
+    # 整句 (决定读感的部分)
+    '只显示有问题的系统; 无问题不出图':
+        'Only systems with findings are shown; nothing is charted when nothing is flagged',
+    '按各台最严重系统状态着色 · 悬停看台号':
+        "Coloured by each unit's most severe system state · hover for the unit",
+    '点击加入报告, 再次点击移出 · 画布里已有':
+        'Click to add to the report, click again to remove · already on the canvas',
+    '还没有生成 — 在上面的交互窗输入报告要求。':
+        'Nothing generated yet — state what the report should cover in the panel above.',
+    '上报版 — 判据门槛与特征频率已脱敏':
+        'Reporting version — criterion thresholds and characteristic frequencies redacted',
+    '上报版隐去判据门槛与特征频率':
+        'the reporting version omits criterion thresholds and characteristic frequencies',
+    '系统失效模式按实证量排 + 部件级原因':
+        'System failure modes ranked by observed volume, with component-level causes',
+    '— 瓶颈集中在一步, 不是"再多看点数据"能解的':
+        '— the bottleneck sits at a single step; more data will not clear it',
+    '"无记录停机"不是没有故障, 是没归上因。':
+        'Unattributed downtime does not mean there was no fault — it means the cause was never assigned.',
+    '右下角报警码属重复触发, 不应按条数排定优先级。':
+        'Codes in the lower right are repeat triggers, not faults; ranking them by count would misdirect the work.',
+    '限电不是"低风时段收到的空令"':
+        'Curtailment is not an empty instruction issued during low wind',
+    '可用率与损失率分母不同': 'availability and loss rate rest on different denominators',
+    '这几台正是算法当初的检出台 ⇒ 故障前能看见、换件后能看见恢复。':
+        'These are the units the method flagged at the time — visible before the failure, '
+        'and visible recovering after replacement.',
+    '该部件在本窗无 SCADA 曲线 (振动谱级证据走 windcms)':
+        'No SCADA curve for this component in this window; spectral evidence is handled in the CMS module',
+    '— 台账止 2024-11, 本期检修执行与闭环无法自动判定':
+        '— the ledger ends 2024-11, so execution and closure in the current period cannot be determined automatically',
+    '台账 2020~2024-11, 本报告不对本期检修执行下结论':
+        'Ledger covers 2020 to 2024-11; this report draws no conclusion about maintenance execution in the current period',
+    '— 四源里油液长期 stale, 只能作回测锚':
+        '— of the four sources, oil is chronically stale and serves only as a retrospective anchor',
+    '— 损失六成在交易与调度侧, 设备侧改善空间有限':
+        '— six tenths of the loss sits with trading and dispatch; the equipment side has limited headroom',
+    '(证据: 已定案)': '(evidence: established)',
+    '(证据: 待现场核实 / 已记为基线下窗观察)':
+        '(evidence: field confirmation required / recorded as baseline, watch next window)',
+    '(传动链: 主承前·主承后·齿轮箱·发电机两端)':
+        '(drivetrain: main bearing front and rear, gearbox, both generator ends)',
+    '(主承前·主承后·齿轮箱三级·发电机DE/NDE; 悬停看逐件等级)':
+        '(main bearing front and rear, three gearbox stages, generator DE and NDE; hover for per-component grade)',
+    '回收 25~26 工单': 'recover the 2025–26 work orders',
+    # ★含场名的标题必须脱敏
+    '如东海上风电场 · 运行分析报告': 'Offshore wind farm · operations analysis report',
+})
+_RE = None
+
+# ── 26 工单与检修记录文本 ──────────────────────────────────────────
+# 残留的大头不是界面文案而是**数据内容** —— 工单描述、巡检项、消缺记录。
+# 数据库里存的就是中文, 英文版没有第二份数据源, 所以只能靠词表覆盖。
+# 按可消字数排序批量补 (top 60 覆盖了大半)。
+TERMS.update({
+    '为该台该': "for this unit's", '轴无非常态': 'no abnormality on this axis',
+    '日常': 'routine', '缺失': 'missing', '损坏': 'damage', '定检': 'scheduled inspection',
+    '监测': 'monitoring', '消除': 'cleared', '消缺': 'defect clearance', '新增': 'added',
+    '卫生': 'cleaning', '机舱卫生': 'nacelle cleaning', '机舱底部卫生': 'nacelle floor cleaning',
+    '机舱': 'nacelle', '风扇': 'fan', '逆变器': 'inverter', '通讯': 'communication',
+    '震动': 'vibration', '告警': 'alarm',
+    # 检修项目
+    '防雷导通测试': 'lightning continuity test', '导通测试': 'continuity test',
+    '完成三个叶片防雷导通测试': 'lightning continuity test completed on all three blades',
+    '光纤整改及升降机维护': 'fibre rectification and lift maintenance',
+    '光纤改造': 'fibre upgrade', '升降机维护': 'lift maintenance',
+    '垫片加装': 'shim installation', '水扩容': 'coolant top-up',
+    '接地漏电': 'earth leakage', '平台盖板': 'platform cover',
+    '主断路器': 'main breaker', '集油瓶': 'oil collection bottle',
+    '不间断电源旁路': 'UPS bypass', '号五防钥匙': 'interlock key',
+    '松散物体': 'loose objects', '到松散物体': 'loose objects found',
+    '到有松散物体': 'loose objects present',
+    # 位置描述
+    '机侧第一个': 'machine side, first', '机侧第三个': 'machine side, third',
+    '机侧第五个': 'machine side, fifth', '网侧第一个': 'grid side, first',
+    # 数据来源与口径
+    '数值按需逐份解析': 'values parsed per report as required',
+    '非第三方独立源': 'not an independent third-party source',
+})
+_RE = None
+
+# ── 27 报告块库标签 ────────────────────────────────────────────────
+# 用户截图逮到块库满屏中英混杂 (monthly generation 表 / 部件停机表 / 故障Pareto / basis 与boundary)。
+# 这些是前端硬编码标签, 但其中的 故障/表/与 等词同时是数据键, 落在 NEVER_TRANSLATE 里,
+# 于是词级替换绕不过去。解法: 收**整句**条目 —— 最长匹配优先, 整句先命中就不会再拆到单词层。
+TERMS.update({
+    '逐月发电表': 'Monthly generation table',
+    '逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率':
+        'Monthly export, loss, loss rate and equivalent full-load hours, with data coverage',
+    '交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答':
+        'Trading and dispatch, equipment, operations — the three-way split, answering',
+    'MTBF × MDT 部件象限, 分出': 'MTBF against MDT by component, separating',
+    '部件停机表': 'Component downtime table',
+    '逐部件停机时长 / 事件数 / 平均无故障间隔(MTBF)/ 单次停机时长(MDT)/ 影响机组数':
+        'Downtime, event count, MTBF, MDT and units affected, per component',
+    '已归因 vs 无记录停机的占比 — 无记录不等于没有故障':
+        'Attributed versus unattributed downtime — unattributed does not mean fault-free',
+    '故障帕累托': 'Fault Pareto',
+    '按条数降序 + 累计 80% 线, 找关键少数':
+        'Ranked by count with a cumulative 80% line, to find the vital few',
+    '条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级':
+        'Count against duration per event, separating repeat triggers from real downtime — '
+        'ranking by count alone misdirects priority',
+    '按天归一的日均条数, 数据不满月单独标注':
+        'Events per day, normalised; partial months flagged separately',
+    '需跟踪台的部件 / 证据源 / 六步进度 / 卡在哪一步':
+        'Units under tracking: component, evidence source, six-step progress, and where it stalled',
+    '证据→机制→判级→排期→动作→验收 六步的瓶颈分布':
+        'Where the six steps stall: evidence, mechanism, grading, scheduling, action, acceptance',
+    '检出 → 换件 → 复测回落的历史例证, 证明这条链是通的':
+        'Detection, replacement, retest — historical cases showing the chain closes',
+    '由数据推出的行动项, 不是套话':
+        'Action items derived from the data, not boilerplate',
+    '口径与边界': 'Basis and boundaries',
+    '本地模型基于实测数据生成的运行总结 — 为图表补充文字结论, 供汇报使用':
+        'An operating summary written by the local model from measured data — a written '
+        'conclusion to accompany the charts',
+    '先输入报告要求': 'State what the report should cover',
+    '挑组件 + 写总结, 全程零外发':
+        'selecting blocks and writing the summary — nothing leaves the site',
+    '月度运营分析': 'Monthly operations review', '周报': 'Weekly', '设备专题': 'Equipment topic',
+    '年度总结': 'Annual review', '块库': 'Block library',
+    '点击加入报告, 再次点击移出 · 画布里已有': 'Click to add, click again to remove · on the canvas:',
+})
+_RE = None

+ 1045 - 1035
src/windscada/lang.py

@@ -1,1035 +1,1045 @@
-# -*- coding: utf-8 -*-
-"""双语语言包 —— 一条文案一个 key, 中英并排。
-
-**为什么重做**: 旧办法是"中文页面 + 三层事后翻译" (源层 ui_en.apply / API 投影 _proj /
-显示层 MutationObserver)。那条路的症状不是一个个 bug, 是架构的必然产物:
-
-  · 6.6 万处 [translation pending] —— 查不到就留个标记, 靠显示层再藏起来
-  · placeholder / title / aria-label 翻不到 —— 它们是**属性**, 文本节点遍历扫不着
-  · 重渲染后回退中文 —— 翻译是一次性覆盖, DOM 重建就没了
-  · 正则保护三次翻车 (见 ui_en.py 顶部注释) —— 跨越把整段 HTML 藏起来, 且**静默**
-
-本模块把它反过来: **页面按语言渲染, 不存在"事后翻译"这一步**。缺英文在**构建期**
-就是硬错误 (check() 报出), 而不是到了用户眼前才变成一处中文残留。
-
-## 约定
-
-1. key 用**语义名**不用序号 —— `kpi.availability` 而非 `k017`。序号在增删条目时会漂,
-   而漂了之后是静默错位 (甲的文案落到乙的位置), 正是最难查的一类。
-2. 占位符用**命名式** `{n}` 不用位置式 `%s` —— 中英语序不同, 位置式无法重排。
-   例: ('{n} 条 · {m} 个码', '{n} alarms across {m} codes')
-3. 两语的占位符集合必须**完全一致** —— check() 硬闸。英文漏掉一个 {n} 是经典缺陷:
-   渲染出来不报错, 只是那个数字凭空消失了。
-4. 中文是**正本**。英文缺失 = 构建期 FAIL; 中文缺失 = key 不存在, 同样 FAIL。
-"""
-from __future__ import annotations
-
-import re
-
-_PH = re.compile(r'\{([a-zA-Z_][a-zA-Z0-9_]*)\}')
-PUBLIC_COPY_BANNED_EN = [
-    'handoff v2',
-    'handoff v2.2',
-    'vibration-line design v1',
-    'structure from vibration-line design',
-    'transcribed verbatim',
-    'verbatim transcription',
-    'source text transcribed',
-    'raw source transcription',
-    'fusion issues no ratings',
-    'fusion does not assign grades',
-    'issues no grades',
-    'hardest evidence',
-    'strongest evidence that the detection chain closes',
-    'no work order to match',
-    'cannot be matched to work orders',
-    'currently rising',
-    'blocked at',
-    'caveat',
-    'chinese originals',
-    'original record (chinese)',
-    'transcribed as-is',
-    'in the handoff',
-    'causality is transcribed',
-    'ratings are transcribed',
-    'transcribed from the windcms report',
-    'transcribed from the vibration line',
-    'owner basis',
-    'characteristic frequencies',
-    'characteristic-frequency',
-    'characteristic frequency',
-    'spectra',
-    'waveforms',
-    'bpfi',
-    'bpfo',
-    'ftf',
-    'bsf',
-]
-
-
-# ══════════════════════════════════════════════════════════════════
-#  语言包本体: key -> (中文, English)
-#  英文来源标注: [收割] = 取自 ui_en.PHRASES (厂商对译表/历次人工校订积累),
-#              [新] = 本次新写。收割优先 —— 那批译文经过用户逐轮点检, 比重译可靠。
-# ══════════════════════════════════════════════════════════════════
-
-L: dict[str, tuple[str, str]] = {}
-
-
-def _add(group: dict[str, tuple[str, str]]) -> None:
-    dup = set(group) & set(L)
-    if dup:
-        raise KeyError('语言包 key 重复: %s' % sorted(dup))
-    L.update(group)
-
-
-# ── 外壳 ──────────────────────────────────────────────────────────
-_add({
-    'app.title':      ('观澜 · 风电数据分析与决策支持', 'Guanlan · Wind Analytics and Decision Support'),
-    'app.brand':      ('观澜', 'Guanlan'),
-    'app.subtitle':   ('海上风电场', 'Offshore Fleet'),
-    'app.build':      ('最新完成构建', 'Latest completed build'),
-    'app.lang_zh':    ('中文', 'Chinese'),
-    'app.lang_en':    ('英文', 'English'),
-})
-
-# ── 页签 (英文全部 [收割]) ─────────────────────────────────────────
-_add({
-    'tab.overview':    ('总览', 'Overview'),
-    'tab.energy':      ('电量算账', 'Energy Accounting'),
-    'tab.component':   ('部件问题', 'Component Issues'),
-    'tab.vibration':   ('振动融合分析', 'Vibration Fusion'),
-    'tab.generation':  ('发电性能', 'Generation Performance'),
-    'tab.fault':       ('故障统计', 'Fault Statistics'),
-    'tab.decision':    ('检修决策', 'Maintenance Decisions'),
-    'tab.assistant':   ('检修助手', 'Maintenance Assistant'),
-    'tab.report':      ('汇报定制', 'Report Builder'),
-    'tab.system':      ('系统维护', 'System Maintenance'),
-    'tab.ontology':    ('本体', 'Ontology'),
-})
-
-# ── 通用动作与状态 ────────────────────────────────────────────────
-_add({
-    'ui.query':        ('查询', 'Query'),
-    'ui.loading':      ('载入中…', 'Loading…'),
-    'ui.empty':        ('无数据', 'No data'),
-    'ui.error':        ('出错了', 'Something went wrong'),
-    'ui.retry':        ('重试', 'Retry'),
-    'ui.expand':       ('展开', 'Expand'),
-    'ui.collapse':     ('收起', 'Collapse'),
-    'ui.all':          ('全部', 'All'),
-    'ui.unit':         ('机组', 'Unit'),
-    'ui.units':        ('机组', 'Units'),
-    'ui.system':       ('系统', 'System'),
-    'ui.date':         ('日期', 'Date'),
-    'ui.count':        ('条数', 'Count'),
-    'ui.not_recorded': ('台账未填', 'Not recorded in ledger'),
-})
-
-# ── 检修助手 (含此前 placeholder 漏译那条) ─────────────────────────
-_add({
-    'asst.head':        ('检修助手 — 确定性本体查询',
-                         'Maintenance Assistant — field guidance with reviewed English'),
-    'asst.head_note':   ('结果逐字取自手册与工单, 不经模型',
-                         'Grounded in manuals, fault records and work orders, with source references retained for traceability'),
-    'asst.corrective':  ('抢修', 'Corrective'),
-    'asst.corrective_flow': ('报警码 → 失效模式 → 手册分步处置 → 历史工单',
-                             'Alarm code → failure mode → manual step-by-step procedure → historical work orders'),
-    # ★这一条就是线上那处中文残留 —— placeholder 是属性, 旧显示层翻不到。
-    #   在新架构里它和别的文案没有区别: 一个 key, 两种语言, 渲染时选一个。
-    'asst.code_placeholder': ('报警码, 如 3225 / 3130 / 13902',
-                              'Alarm code, e.g. 3225 / 3130 / 13902'),
-    'asst.common':      ('常见', 'Common'),
-    'asst.preventive':  ('预防性维护', 'Preventive maintenance'),
-    'asst.preventive_flow': ('系统 → 失效模式 → 年度任务 → 作业指导步骤',
-                             'System → failure mode → annual task → work-instruction steps'),
-    'asst.faulttree':   ('故障树', 'Fault tree'),
-    'asst.faulttree_flow': ('按证据量排序的系统失效模式 + 部件级成因',
-                            'System failure modes ranked by evidence volume, with component-level cause'),
-})
-
-# ── 九系统 (交付分册口径; 与本体 component/fleet/* 同名) ────────────
-_add({
-    'sys.blade':        ('叶片与叶根', 'Blade and blade root'),
-    'sys.pitch':        ('变桨', 'Pitch'),
-    'sys.yaw':          ('偏航', 'Yaw'),
-    'sys.gearbox':      ('齿轮箱', 'Gearbox'),
-    'sys.generator':    ('发电机', 'Generator'),
-    'sys.main_bearing': ('主轴承', 'Main bearing'),
-    'sys.converter':    ('变流器', 'Converter'),
-    'sys.tower':        ('塔架与基础', 'Tower and foundation'),
-    'sys.control':      ('主控与传感网', 'Control and sensor network'),
-})
-
-# ── 设备状态 (业主口径, 与 windcms 报告同一套) ──────────────────────
-_add({
-    'st.bad':          ('危险', 'Critical'),
-    'st.warn':         ('报警', 'Alarm'),
-    'st.unk':          ('不可判', 'Undeterminable'),
-    'st.ok':           ('良好', 'Good'),
-    'st.excellent':    ('优秀', 'Excellent'),
-    'st.ok_excellent': ('良好 / 优秀', 'Good / Excellent'),
-})
-
-# ── 证据级 (SOP §0.2 六枚举) —— 盖成印 ─────────────────────────────
-_add({
-    'tier.confirmed':    ('定论', 'Confirmed'),
-    'tier.near':         ('准定论·预警', 'Near-confirmed · warning'),
-    'tier.candidate':    ('候选', 'Candidate'),
-    'tier.reference':    ('参考', 'Reference'),
-    'tier.insufficient': ('不足', 'Insufficient'),
-    'tier.withdrawn':    ('撤回', 'Withdrawn'),
-    # 印面短形 (卡上空间小; 全称进悬停)
-    'tier.confirmed.s':    ('定论', 'Conf.'),
-    'tier.near.s':         ('准定论', 'Near'),
-    'tier.candidate.s':    ('候选', 'Cand.'),
-    'tier.reference.s':    ('参考', 'Ref.'),
-    'tier.insufficient.s': ('不足', 'Insuf.'),
-    'tier.withdrawn.s':    ('撤回', 'Withd.'),
-})
-
-# ── 审级 (SOP §6.1 RV-1) ─────────────────────────────────────────
-_add({
-    'rv.dual':  ('双模型审', 'DUAL-MODEL'),
-    'rv.advci': ('AdvCI', 'ADVCI'),
-    'rv.lite':  ('lite', 'LITE'),
-    'rv.none':  ('未审', 'UNREVIEWED'),
-})
-
-# ── 总览 (设计基准 docs/design/guanlan_v2.html) ───────────────────
-_add({
-    'ov.attention':      ('需要关注', 'Needs attention'),
-    # 融合面缺件时的说明(2026-09-19 用户报「需要关注/全场状态下面没有计算输出」后补):
-    # 缺件就该说清"缺哪件、怎么补",不让两片空白被读成"没有需要关注的台"。
-    'ov.fus_gap_hint':   ('这一面(需要关注 / 全场状态)取数来自融合面;缺件时如实为空,不从交付包补齐。'
-                          '在本机放好原始件后跑一次重算即会由观澜自己的生成端补上'
-                          '(scripts/rudong_fusion_handoff.py,见 docs 的「源代码化清单」)。',
-                          'This block (needs attention / fleet status) draws on the fusion layer. '
-                          'When that product is missing it stays honestly empty rather than being filled from a delivery package; '
-                          'after placing the raw data, one recompute run rebuilds it from source (scripts/rudong_fusion_handoff.py).'),
-    'ov.attention_n':    ('{n} 台', '{n} units'),
-    'ov.ranked_by':      ('按 设备状态 × 证据量 排序', 'Ranked by equipment state × evidence weight'),
-    'ov.strip':          ('全场状态', 'Fleet status'),
-    'ov.strip_hint':     ('一格一台 · 点格定位', 'One cell per unit · click to locate'),
-    'ov.quiet':          ('其余 {n} 台未进入优先关注清单;设备状态请查看全场状态评估。',
-                          'The other {n} units are outside the priority list. Refer to fleet status for their equipment assessments.'),
-    'ov.quiet_all':      ('当前优先关注清单为空;{n} 台机组的设备状态请查看全场状态评估。', 'The priority list is empty. Refer to fleet status for the assessments of all {n} units.'),
-    'ov.caveat':         ('限定', 'Follow-up'),
-    'ov.systems':        ('九系统', 'Nine systems'),
-    'ov.systems_hint':   ('点系统看逐台依据', 'Open a system for per-unit evidence'),
-    'ov.illustrative':   ('示意数据', 'Illustrative'),
-    'ui.open':           ('查看 →', 'Open →'),
-    'ui.back_to':        ('← 返回{tab}', '← Back to {tab}'),
-    'ui.back_portal':    ('← 返回门户', '← Back to portal'),
-    'rpt.facts.title': ('可交付级结论 · 契约生成', 'Deliverable-grade conclusions · from facts contract'),
-    'rpt.facts.sub': ('{n} 条, 契约 {sha}; 数据/时间窗/结论只经契约更新', '{n} claims, contract {sha}; data / windows / verdicts change only via the contract'),
-    'rpt.facts.verdict': ('裁决', 'Verdict'), 'rpt.facts.text': ('结论', 'Conclusion'), 'rpt.facts.sys': ('系统', 'System'), 'rpt.facts.agg': ('聚合层', 'Aggregation'),
-    'ui.reselect':       ('重新选择机组 / 测点', 'Reselect unit / point'),
-})
-
-# ── KPI ───────────────────────────────────────────────────────────
-_add({
-    'kpi.avail':        ('时间可用率 (不含调度令) · {win}', 'Time-based availability (excl. dispatch) · {win}'),
-    'kpi.downtime':     ('停机时长 · {n} 起', 'Downtime · {n} events'),
-    'kpi.mtbo':         ('平均停机间隔 MTBO (非行业 MTBF)', 'Mean time between outages (not industry MTBF)'),
-    'kpi.loss':         ('停机电量损失 · {win}', 'Downtime energy loss · {win}'),
-    'kpi.loss_dispatch':('另调度令 {n}', 'plus {n} under dispatch orders'),
-    'kpi.curtail':      ('限电·绑定 电量损失', 'Curtailment (bound) energy loss'),
-    'kpi.alarm_units':  ('九系统有报警的台数 / 全场', 'Units with a system-level alarm / fleet'),
-    'kpi.alarm_units.hint': ('口径: 九系统 (叶片与叶根/变桨/偏航/齿轮箱/发电机/主轴承/变流器/塔架与基础/主控与传感网) 中任一被判「报警」即计一台。'
-                             '与下方「全场状态」格的口径不同 —— 那里按每台 振动/温度/润滑/油液/报警/工单 六列取最严一级, 所以两处数字可以不同, 不是矛盾。',
-                            'Caliber: a unit counts if ANY of the nine systems is rated "alarm". The fleet strip below uses a different axis '
-                            '(worst of vibration/temperature/lubrication/oil/alarm/work-order per unit), so the two numbers can differ by design.'),
-})
-
-# ── 证据级补全: API 实际发出的枚举 (fus.色标) 一条不落 ──────────────
-_add({
-    'tier.candidate_new':      ('候选·新发', 'Candidate · new'),
-    'tier.candidate_baseline': ('候选·记基线', 'Candidate · baseline'),
-    'tier.normal':             ('正常', 'Normal'),
-    'tier.candidate_new.s':      ('候选·新发', 'Cand. new'),
-    'tier.candidate_baseline.s': ('候选·记基线', 'Cand. base'),
-    'tier.normal.s':             ('正常', 'Normal'),
-})
-
-# ── 链盘六步 (fus.链盘.rows[].steps[].k) ─────────────────────────
-_add({
-    'step.evidence':   ('证据', 'Evidence'),
-    'step.site_records': ('现场记录', 'site records'),
-    'step.mechanism':  ('机制', 'Mechanism'),
-    'step.rating':     ('判级', 'Rating'),
-    'step.schedule':   ('排期', 'Schedule'),
-    'step.action':     ('动作', 'Action'),
-    'step.acceptance': ('验收', 'Acceptance'),
-    'step.stuck_at':   ('卡在「{step}」', 'Pending {step}'),
-})
-
-# ── 时间窗预设 ────────────────────────────────────────────────────
-_add({
-    'win.label':   ('时间窗', 'Window'),
-    'win.presets': ('预设窗', 'Presets'),
-    'win.months':  ('按月', 'By month'),
-    # ★自定义起止日期窗(用户令 2026-09-21):含两端;月度件按月取整,日粒度件按日精确
-    'win.custom':      ('自定义', 'Custom'),
-    'win.custom_hint': ('自定义起止日期(含两端)。日粒度件(判级/停机台账/曲线)按日精确;'
-                        '月度聚合件按所跨月份取整,图上会写明。',
-                        'Custom inclusive date range. Day-resolution items (grading, stop ledger, curves) are exact; '
-                        'monthly aggregates round to the spanned months and say so on the chart.'),
-    'win.from':        ('开始日期(含)', 'Start date (inclusive)'),
-    'win.to':          ('结束日期(含)', 'End date (inclusive)'),
-    'win.apply':       ('应用', 'Apply'),
-    'win.need_both':   ('请把开始与结束日期都选上', 'Pick both start and end dates'),
-    'win.bad_order':   ('开始日期不能晚于结束日期', 'Start date must not be after the end date'),
-    'win.custom_sel':  ('自定义 {a} ~ {b}', 'Custom {a} to {b}'),
-    'cmp.grading_pending': ('正在按所选时间窗重算判级…', 'Recomputing grading for the selected window…'),
-    'cmp.grading_pending_why': ('窗 {span}:变桨/偏航/蓄能/温度四个判级面正按该窗重算(首次约 25 秒),'
-                                '下面的系统卡暂时还是上一份口径 —— 算完会自动刷新,不会静默混用。',
-                                'Window {span}: the four grading axes (pitch / yaw / accumulator / temperature) '
-                                'are being recomputed for it (about 25 s the first time). The cards below still show '
-                                'the previous grading until it lands, then refresh automatically — no silent mixing.'),
-    'win.30d':     ('近30日', 'Last 30 days'),
-    'win.90d':     ('近90日', 'Last 90 days'),
-    'win.2026':    ('2026年', '2026'),
-    'win.2026H1':  ('2026H1', '2026 H1'),
-    'win.2025H2':  ('2025H2', '2025 H2'),
-    'win.all':     ('全程', 'Full record'),
-})
-
-# ── 迁移期 ────────────────────────────────────────────────────────
-_add({
-    'ui.not_ported':   ('本页尚未迁入新版。', 'This page has not been ported to the new build yet.'),
-    'ui.open_classic': ('在经典版打开 →', 'Open in classic build →'),
-    'ui.data_hidden':  ('({n} 处数据文本尚无英文,需复核)', '({n} data strings still require English translation review)'),
-    # 手册/工单**原文**与"字段"不同: 隐藏它等于把处置步骤整个抹掉。按原文转录并标注 (2026-09-03 取舍, 可推翻)。
-    'ui.verbatim':      ('原始记录 (中文)', 'Translation review required'),
-    'ui.verbatim_note': ('{n} 处为手册/工单原文, 无英文译本, 按原文转录。',
-                         '{n} source passages require translation review and are withheld from the public English view.'),
-})
-
-# ── 检修助手 · 结果渲染 ────────────────────────────────────────────
-_add({
-    'asst.querying':     ('查询中…', 'Querying…'),
-    'asst.query_failed': ('查询失败: {e}', 'Query failed: {e}'),
-    'asst.identity':     ('系统 {sys} · 本场 {n} 条', 'System {sys} · {n} site alarms'),
-    'asst.alarm_type':   ('告警类型 {t}', 'alarm type {t}'),
-    'asst.unassigned':   ('未归属', 'unassigned'),
-    'asst.no_name':      ('(码表未收录)', '(not in the code table)'),
-    'asst.failmodes':    ('指向的失效模式', 'Failure modes indicated'),
-    'asst.fm_counts':    ('该类本场报警 {a} 条 / 工单 {w} 条', '{a} site alarms / {w} work orders in this class'),
-    'asst.steps':        ('现场处置 {n} 步', 'Field procedure · {n} steps'),
-    'asst.steps_note':   ('节点部件 = 手册原文元件位号 (确定); 判定规范 = 实机参数表按物理量匹配的候选判据, 非厂商绑定 — 现场以铭牌与图纸复核。',
-                          "Node components are the manual's own designators (definite); acceptance specs are candidate criteria matched from the live parameter table by physical quantity, not vendor-bound — verify against nameplate and drawings on site."),
-    'asst.step_n':       ('第 {n} 步', 'Step {n}'),
-    'asst.qty':          ('量: {q}', 'quantity: {q}'),
-    'asst.tags':         ('节点部件', 'Node components'),
-    'asst.nodes':        ('应检节点 · {c}', 'Nodes to check · {c}'),
-    'asst.nodes_note':   ('手册此步未给位号 → 按回路构成展开, 逐点查', 'no designator given for this step → expanded by circuit composition, check point by point'),
-    'asst.node':         ('节点', 'Node'),
-    'asst.check':        ('查什么', 'What to check'),
-    'asst.known_tags':   ('该系统手册出现过的位号 (对图纸时参考):', "Designators seen in this system's manual (for drawing cross-reference):"),
-    'asst.specs':        ('判定规范 (实机参数表)', 'Acceptance specs (live parameter table)'),
-    'asst.spec_no':      ('参数号', 'Param'),
-    'asst.spec_name':    ('名称', 'Name'),
-    'asst.spec_val':     ('整定值', 'Setting'),
-    'asst.spec_range':   ('允许范围', 'Range'),
-    'asst.drawings':     ('对应图纸', 'Related drawings'),
-    'asst.no_steps':     ('本码无手册处置条目 (手册覆盖本场 161/322 码) — 需查图纸或联系厂商支持。',
-                          'No manual procedure for this code (the manual covers 161 of 322 site codes) — consult drawings or vendor support.'),
-    'asst.wo_head':      ('现场实际怎么修的 ({sys} 历史工单 {n} 条, 台账 2020~2024)',
-                          'What was actually done on site ({sys}: {n} historical work orders, ledger 2020–2024)'),
-    'asst.wo_fault':     ('故障', 'Fault'),
-    'asst.wo_action':    ('处置', 'Action'),
-    'asst.wo_note':      ('工单窗止 2024-11-21; 2025–26 现场未提供 ⇒ "0 条" 是记录缺失非未修过。',
-                          'Work-order window ends 2024-11-21; 2025–26 not provided by the site, so "0" means missing records, not no repair.'),
-    'asst.tree_head':    ('{sys} 故障树 · {n} 个失效模式', '{sys} fault tree · {n} failure modes'),
-    'asst.tree_note':    ('一层 = 系统 × 失效类 (归纳自本场码表与工单) · 二层 = 手册排查方向归一 + 工单原因计数; 条长按实证量 (报警条数 + 工单 × 10), 因果来自手册与工单原文非推断, 树深两层不外推。',
-                          'Level 1 groups site systems by failure class; level 2 merges manual troubleshooting directions with work-order cause counts. Bar length reflects available evidence (alarms + work orders × 10), and the tree stops at two levels.'),
-    'asst.tree_counts':  ('本场报警 {a} 条 · 工单 {w} 条', '{a} site alarms · {w} work orders'),
-    'asst.tree_codes':   ('征兆码 {c}', 'symptom codes {c}'),
-    'asst.causes':       ('← 可能原因 (手册排查方向 + 工单实证)', '← Possible causes (manual directions + work-order evidence)'),
-    'asst.cause_counts': ('手册 {r} 次 · 工单 {w} 次 · 涉码 {c}', 'manual ×{r} · work orders ×{w} · codes {c}'),
-    'asst.no_causes':    ('(手册无该类排查条目 — 原因层待补)', '(no troubleshooting entries in the manual for this class — cause layer pending)'),
-    'asst.tree_empty':   ('该系统无失效模式记录', 'No failure modes recorded for this system'),
-    'asst.prev_head':    ('{sys} 预防链 · 失效模式 {f} 类 · 预防性任务 {n} 项', '{sys} preventive chain · {f} failure-mode classes · {n} preventive tasks'),
-    'asst.prev_fm':      ('失效模式 (按本场报警量)', 'Failure modes (by site alarm volume)'),
-    'asst.prev_tasks':   ('年度任务', 'Annual tasks'),
-    'asst.interval':     ('周期 {i}', 'interval {i}'),
-    'asst.wi_steps':     ('WI {n} 步', 'WI · {n} steps'),
-    'asst.prev_empty':   ('(该系统无匹配的年度维护条目 — 维护作业指导书按位置代号编排, 未必逐系统对齐)',
-                          '(no annual maintenance entries matched to this system — the work-instruction book is organised by location code and does not map one-to-one onto systems)'),
-})
-
-# ── 检修决策 · 本体问答 (模型参与的板块) ────────────────────────────
-_add({
-    'dec.head':        ('检修决策 · 本体问答 (推理无界, 结论受闸)',
-                        'Maintenance decisions · ontology Q&A (unbounded reasoning, gated conclusions)'),
-    'dec.classic':     ('维护标准 / 维护框架 / 数据质量三块尚未迁入新版 —',
-                        'The maintenance-standard, framework and data-quality blocks are not ported yet —'),
-    'ask.common_q':    ('{cat} · 常用问题 (点即问, 或下方自由输入)', '{cat} · common questions (click to ask, or type below)'),
-    'ask.grounding':   ('答案句句带 [对象id] 溯源 + 接地闸核验; 判级只转述本体原文带审级, 模型零判级权。',
-                        'Every answer carries an [object id] and uses the reviewed evidence layer; model responses cannot create or change equipment ratings.'),
-    'ask.placeholder': ('自由提问 ({cat})', 'Ask anything ({cat})'),
-    'ask.model':       ('模型', 'Model'),
-    'ask.no_models':   ('无可用模型', 'No models available'),
-    'ask.refs.title':  ('契约引用 ({n} 条, 契约 {sha}) — 按问题/回答匹配的可交付结论, 引文来自 qa_refs', 'Contract citations ({n}, contract {sha}) — deliverable claims matched to the question/answer; cites from qa_refs'),
-    'ask.refs.none':   ('无匹配契约条目 (契约 {sha}): 本回答未被结构化事实契约背书, 按模型输出对待', 'No matching contract claim (contract {sha}): this answer is not backed by the facts contract; treat as model output'),
-    'ask.local_down':  ('本机模型未启动 (Ollama 127.0.0.1:11434 未响应); 离线版不转云', 'Local model not running (Ollama 127.0.0.1:11434 unreachable); offline build never falls back to cloud'),
-    'ask.partial':     ('初答 · 27B 复核中', 'Quick answer · 27B refining'),
-    'ask.refined':     ('已复核 (默认档 {m} 未过校闸, 由更强模型重答)', 'Refined (default tier {m} failed the gate; re-answered by the stronger model)'),
-    'ask.xrev':        ('交叉审核', 'Cross-review'),
-    'ask.xrev_note':   ('(DeepSeek 14B 互审, +约 1 分钟)', '(cross-checked by DeepSeek 14B, + about 1 min)'),
-    'ask.xrev_title':  ('由另一模型基于同一份证据复核; 同一模型自审无法构成独立校验',
-                        'A second model reviews on the same evidence; self-review by one model is not independent'),
-    'ask.go':          ('提问', 'Ask'),
-    'ask.idle':        ('空闲 — 点常用问题或输入后提问。串行一次一问。', 'Idle — click a common question or type one. One question at a time.'),
-    'ask.empty':       ('问题为空', 'The question is empty'),
-    'ask.running':     ('推理中…', 'Reasoning…'),
-    'ask.elapsed':     ('{s} 秒', '{s} s'),
-    'ask.question':    ('问题: {q}', 'Question: {q}'),
-    'ask.verify_ok':   ('溯源核验 {n}/{n} 全真', 'Traceability check {n}/{n} verified'),
-    'ask.verify_bad':  ('溯源核验未全过 (引用 {n})', 'Traceability check incomplete ({n} references)'),
-    'ask.done_meta':   ('模型 {m} · 用时 {s} 秒 · {n} 步', 'model {m} · {s} s · {n} steps'),
-    'ask.review':      ('交叉审核 · {m}', 'Cross-review · {m}'),
-    'ask.review_note': ('由另一模型基于同一份证据复核上述答案。溯源核验仅校验数字与机组号是否越界, 本项校验结论是否有证据支撑。',
-                        'A second model reviewed the answer on the same evidence. The traceability check only tests whether numbers and unit ids are in range; this review tests whether conclusions are supported by evidence.'),
-    'ask.review_err':  ('审核未完成: {e}', 'Review incomplete: {e}'),
-    'ask.error':       ('出错: {e}', 'Error: {e}'),
-    'ask.offline':     ('离线版: 模型问答需连接系统。', 'Offline copy: the model Q&A needs a live connection to the system.'),
-    'ask.en_qa_failed':('英文质检未通过: 已拦截这次模型回答, 请重新提问。',
-                        'English QA failed: this model answer was withheld. Please ask again.'),
-})
-
-# ── 模型档位说明 (id 固定集合; 未知 id 一律按"会外发"处理 —— 宁可说重不说轻) ──
-_add({
-    'mdl.local':          ('本地', 'local'),
-    'mdl.cloud':          ('云端', 'cloud'),
-    'mdl.zero_outbound':  ('全程零外发', 'Nothing leaves the machine'),
-    'mdl.outbound_warn':  ('内容会外发到模型厂商, 场名与台号已去标识化', 'Content is sent to the model vendor; site name and unit ids are de-identified'),
-    'mdl.unknown_warn':   ('未知档位, 按内容会外发处理', 'Unknown tier — treated as sending content externally'),
-    'mdl.qwen3:8b.nm':        ('轻量 8B · 默认', 'Light 8B · default'),
-    'mdl.qwen3:8b.t':         ('约 30 到 60 秒', 'about 30–60 s'),
-    'mdl.qwen3.8:27b.nm':     ('标准 27B · 复杂题', 'Standard 27B · complex'),
-    'mdl.qwen3.8:27b.t':      ('约 2 到 4 分钟', 'about 2–4 min'),
-    'mdl.qwen3:30b-a3b.nm':   ('快 30B (a3b)', 'Fast 30B (a3b)'),
-    'mdl.qwen3:30b-a3b.t':    ('约 20 到 60 秒', 'about 20–60 s'),
-    'mdl.deepseek-r1:14b.nm': ('本机 DeepSeek 14B · 审核档', 'Local DeepSeek 14B · reviewer'),
-    'mdl.deepseek-r1:14b.t':  ('约 1 到 3 分钟', 'about 1–3 min'),
-    'mdl.deepseek-r1:32b.nm': ('本机 DeepSeek 32B', 'Local DeepSeek 32B'),
-    'mdl.deepseek-r1:32b.t':  ('约 3 到 8 分钟', 'about 3–8 min'),
-    'mdl.deepseek-chat.nm':   ('DeepSeek', 'DeepSeek'),
-    'mdl.deepseek-chat.t':    ('约 3 到 10 秒', 'about 3–10 s'),
-    'mdl.kimi-fast.nm':       ('Kimi', 'Kimi'),
-    'mdl.kimi-fast.t':        ('约 5 到 20 秒', 'about 5–20 s'),
-    'mdl.qwen-plus.nm':       ('通义千问', 'Qwen Plus'),
-    'mdl.qwen-plus.t':        ('约 5 到 20 秒', 'about 5–20 s'),
-    'mdl.deepseek-v4-pro.nm': ('云端 DS', 'DeepSeek Pro'),
-    'mdl.deepseek-v4-pro.t':  ('约 2 到 6 分钟', 'about 2–6 min'),
-})
-
-# ── 问答四类 (API 的 cat 参数取中文名, 前端按 id 映射) ───────────────
-_add({
-    'cat.equip':      ('设备类', 'Equipment'),
-    'cat.equip.hint': ('设备状态 / 机制 / 判级 — 从机组档案、对象查询与机制检索取证',
-                       'Equipment state / mechanism / rating — evidence from unit profiles, object queries and mechanism search'),
-    'cat.equip.q1':   ('29号机组现在什么状态、为什么、该怎么办?', 'What is the state of WTG29, why, and what should be done?'),
-    'cat.equip.q2':   ('全场哪些机组有报警级或定论级判级?逐台列出并注明审级', 'Which units carry alarm-level or confirmed-tier ratings? List each with its review level.'),
-    'cat.equip.q3':   ('失效机制库里有哪些机制族?各涉及哪些机组、状态如何?', 'Which mechanism families are in the failure-mechanism library, which units do they involve, and in what state?'),
-    'cat.sched':      ('检修排程决策', 'Maintenance scheduling'),
-    'cat.sched.hint': ('排期沙盘 / 天气窗 / 验收树 — ont_scenario / ont_get 取证',
-                       'Scheduling sandbox / weather windows / acceptance tree — evidence via ont_scenario / ont_get'),
-    'cat.sched.q1':   ('29号修泵什么时候做最合适?对比各情景的损失与风险', 'When is the best time to repair the pump on WTG29? Compare loss and risk across scenarios.'),
-    'cat.sched.q2':   ('未来几个月哪些月份适合安排CTV检修和吊装作业?依据历史可作业窗', 'Which of the coming months suit CTV maintenance and lifting work, based on historical workable windows?'),
-    'cat.sched.q3':   ('29号修好之后按什么判据验收?三分支分别是什么条件?', 'After the repair on WTG29, what acceptance criteria apply, and what are the three branch conditions?'),
-    'cat.loss':       ('损失分析', 'Loss analysis'),
-    'cat.loss.hint':  ('当前 3 个月损失 / 未来损失 — ont_loss / ont_scenario 取证, 金额带电价区间口径',
-                       'Current 3-month and forward-looking loss estimates — values are shown as tariff ranges'),
-    'cat.loss.q1':    ('近3个月全场损失结构如何?分状态和分机组说, 电量与金额都给', 'What is the fleet loss structure over the last 3 months? By state and by unit, energy and value.'),
-    'cat.loss.q2':    ('29号如果拖到主轴承失效, 未来损失量级是多少?与现在修对比', 'If WTG29 runs to main-bearing failure, what is the future loss magnitude versus repairing now?'),
-    'cat.loss.q3':    ('限电常态化对检修机会成本有什么影响?', 'How does persistent curtailment change the opportunity cost of maintenance?'),
-    'cat.risk':       ('安全风险预测', 'Safety-risk outlook'),
-    'cat.risk.hint':  ('风险敞口 / 劣化趋势 / 盲区 — ont_risk / ont_mechanism_search 取证, 未列 ≠ 正常',
-                       'Risk exposure / degradation trend / blind spots — evidence via ont_risk / ont_mechanism_search; unlisted ≠ normal'),
-    'cat.risk.q1':    ('当前哪些机组风险在上升需要重点盯?联动监视读数如何?', 'Which units have rising risk and need close watching? What do the linked monitoring readings show?'),
-    'cat.risk.q2':    ('监测有哪些盲区?哪些"没报警"不等于正常?', 'Where are the monitoring blind spots? Which "no alarm" cases are not the same as normal?'),
-    'cat.risk.q3':    ('29号轴承正在承受金属接触, 风险敞口怎么评估?', "Unit 29's bearing is under metal-to-metal contact; how is the risk exposure assessed?"),
-})
-
-# ── 图元 (charts.js) ──────────────────────────────────────────────
-_add({
-    'ch.quad.few':     ('窗内码数不足, 不作四象限', 'Too few codes in the window for a quadrant plot'),
-    'ch.quad.q1':      ('低频长时 · 硬故障', 'Rare, long · hard faults'),      'ch.quad.q1s': ('备件与检修窗', 'spares and maintenance window'),
-    'ch.quad.q2':      ('高频长时 · 重点治理', 'Frequent, long · priority'),  'ch.quad.q2s': ('优先派工', 'dispatch first'),
-    'ch.quad.q3':      ('低频短时 · 本底', 'Rare, short · background'),        'ch.quad.q3s': ('不单独处置', 'no separate action'),
-    'ch.quad.q4':      ('高频短时 · 重复触发', 'Frequent, short · re-triggering'), 'ch.quad.q4s': ('调整参数或屏蔽, 非硬件故障', 'tune or mask; not hardware'),
-    'ch.quad.xlab':    ('发生条数 (对数)', 'Occurrences (log)'),
-    'ch.quad.ylab':    ('单次中位时长 (对数)', 'Median duration per event (log)'),
-    'ch.quad.tip':     ('{n} 条 · 单次中位 {med}s · 累计 {h}h · {nt} 台', '{n} events · median {med}s · total {h}h · {nt} units'),
-    'ch.rate.tip':     ('{m}: {v} 条/日 (总 {n} 条, 数据覆盖 {cov}%)', '{m}: {v}/day ({n} total, {cov}% coverage)'),
-    'ch.rate.month':   ('{m}月', '{m}'),
-    'ch.rate.n':       ('{n}条', '{n}'),
-    'ch.rate.days':    ('仅{d}天', '{d} d only'),
-    'ch.rate.ylab':    ('条/日', 'per day'),
-    'ch.wf.theo':      ('理论可发', 'Theoretical'),
-    'ch.wf.act':       ('实际上网', 'Delivered'),
-    'ch.wf.tip':       ('{nm}: 损失 {loss} MWh, 占理论 {pct}%, 时长 {h}h', '{nm}: loss {loss} MWh, {pct}% of theoretical, {h} h'),
-    'loss.curtail_bound':   ('限电 (命令与实际功率对得上)', 'Curtailment (command matches actual power)'),
-    'loss.curtail_unbound': ('限电命令期 · 功率未被压制', 'Curtailment command · power not suppressed'),
-    'loss.dispatch':        ('调度令停机', 'Dispatch-order outage'),
-    'loss.stop':            ('故障与检修停机', 'Fault and maintenance outage'),
-    'loss.normal':          ('正常发电时的欠发', 'Under-production while generating'),
-    'loss.lowwind':         ('低风待机', 'Low-wind standby'),
-    'ch.ram.few':      ('部件数不足, 不作象限', 'Too few components for a quadrant plot'),
-    'ch.ram.q1':       ('常坏且难修', 'Frequent and hard to fix'),  'ch.ram.q1s': ('最高优先级', 'highest priority'),
-    'ch.ram.q2':       ('罕发难修', 'Rare and hard to fix'),        'ch.ram.q2s': ('备件与作业窗', 'spares and work windows'),
-    'ch.ram.q3':       ('频发易修', 'Frequent and easy to fix'),    'ch.ram.q3s': ('查根因, 换件无用', 'find root cause; replacement will not help'),
-    'ch.ram.q4':       ('少发好修', 'Rare and easy to fix'),        'ch.ram.q4s': ('维持', 'maintain'),
-    'ch.ram.xlab':     ('平均无故障间隔 MTBF (h, 对数) →  越右越可靠', 'MTBF (h, log) →  more reliable to the right'),
-    'ch.ram.ylab':     ('单次停机时长 MDT (h) →  越上越难修', 'MDT (h) →  harder to repair upward'),
-    'ch.ram.equip':    ('设备类', 'Equipment'),
-    'ch.ram.ext':      ('外部类', 'External'),
-    'ch.ram.area':     ('圆面积 = 累计停机时长', 'Area = total downtime'),
-    'ch.ram.median':   ('虚线 = 本场中位分界', 'Dashed = site median split'),
-    'ch.ram.tip':      ('{p}: MTBF {mtbf}h · MDT {mdt}h · 停机 {h}h · {n}起 · {u}台', '{p}: MTBF {mtbf}h · MDT {mdt}h · downtime {h}h · {n} events · {u} units'),
-    'ch.ram.tip_ext':  (' · 外部类不计设备可靠性', ' · external, excluded from equipment reliability'),
-    'ch.attrib.ok':    ('已归因到部件 {h}h ({p}%)', 'Attributed to a component {h}h ({p}%)'),
-    'ch.attrib.none':  ('无记录 {h}h ({p}%)', 'Unrecorded {h}h ({p}%)'),
-    'ch.attrib.uncls': ('未归类 {h}h', 'Unclassified {h}h'),
-    'ch.pareto.none':  ('窗内无数据', 'No data in the window'),
-    'ch.pareto.cum80': ('累计 80%', 'cumulative 80%'),
-    'ch.pareto.cumtip':('{k} · 累计 {p}%', '{k} · cumulative {p}%'),
-    'ch.pareto.xlab':  ('报警码 (悬停看全名)', 'Alarm code (hover for full name)'),
-    'ch.pareto.key':   ('关键少数: 前 {k} 项占 80%', 'Vital few: the first {k} items make 80%'),
-    'ch.pareto.total': ('共 {n} 项, 合计 {tot}{unit}', '{n} items, total {tot}{unit}'),
-    'ch.pareto.top3':  ('前三: ', 'Top three: '),
-    'ch.blame.n1':     ('交易与调度侧', 'Market and dispatch'),   'ch.blame.d1': ('限电与调度令, 不进设备责任', 'curtailment and dispatch orders; not equipment responsibility'),
-    'ch.blame.n2':     ('设备侧', 'Equipment'),                   'ch.blame.d2': ('故障与检修停机, 唯一进设备考核', 'fault and maintenance outages; the only part assessed against equipment'),
-    'ch.blame.n3':     ('运行侧', 'Operations'),                  'ch.blame.d3': ('正常发电欠发与低风待机', 'under-production while generating, and low-wind standby'),
-    'ch.blame.title':  ('损失 {tot} MWh 按责任归属', 'Loss {tot} MWh by responsibility'),
-    'unit.h':          ('h', 'h'), 'unit.n': (' 条', ' events'), 'unit.units': ('台', 'units'),
-})
-
-# ── 汇报定制 ──────────────────────────────────────────────────────
-_add({
-    'rpt.eyebrow':      ('汇报定制 · 画布', 'Report builder · canvas'),
-    'rpt.presets':      ('预设', 'Presets'),
-    'rpt.preset.month': ('月度运行分析', 'Monthly operations'), 'rpt.preset.week': ('周报', 'Weekly'),
-    'rpt.preset.topic': ('设备专题', 'Equipment topic'),       'rpt.preset.year': ('年度总结', 'Annual summary'),
-    'rpt.preset.blank': ('空白画布', 'Blank canvas'),
-    'rpt.aud_in':       ('场站内部 (全量)', 'Site internal (full)'),
-    'rpt.aud_up':       ('上报集团 (脱敏)', 'Group report (de-identified)'),
-    'rpt.print':        ('打印 / 存 PDF', 'Print / save PDF'),
-    'rpt.export_docx':  ('导出 Word', 'Export Word'),
-    'rpt.export_xlsx':  ('导出 Excel', 'Export Excel'),
-    'rpt.up_note':      ('上报版 — 判据门槛与特征频率已脱敏', 'Group version — criterion thresholds and detailed diagnostic markers are redacted'),
-    'rpt.library':      ('组件库', 'Component library'),
-    'rpt.library_note': ('点击加入报告, 再次点击移出 · 画布里已有 {n} 块', 'Click to add, click again to remove · {n} blocks on the canvas'),
-    'rpt.hint_idle':    ('悬停查看组件说明 · 已选组件再次点击移出 · 右侧 ↑↓ 调整顺序', 'Hover for a description · click a selected block to remove · ↑↓ to reorder'),
-    'rpt.need_head':    ('输入报告要求', 'Describe the report you need'),
-    'rpt.need_hint':    ('模型依据需求选取组件, 并基于实测数据生成运行总结', 'The model picks components for the request and writes a summary from measured data'),
-    'rpt.local_note':   ('; 数据不出本地', '; data stays local'),
-    'rpt.cloud_note':   ('; 内容会外发到模型厂商', '; content is sent to the model vendor'),
-    'rpt.need_ph':      ('例如: 上报集团月报, 侧重设备可靠性与重点机组', 'e.g. monthly report for the group, focused on equipment reliability and key units'),
-    'rpt.compose':      ('生成', 'Compose'),
-    'rpt.no_models':    ('本部署未配置任何模型密钥, 编排不可用', 'No model keys configured in this deployment; composing is unavailable'),
-    'rpt.no_compose':   ('本部署未启用编排接口, 画布仍可手动拼装', 'The compose endpoint is not enabled here; the canvas can still be assembled by hand'),
-    'rpt.need_empty':   ('先输入报告要求', 'Enter the report request first'),
-    'rpt.composing':    ('模型编排中… 挑组件 + 写总结', 'Composing… picking components and writing the summary'),
-    'rpt.compose_fail': ('生成失败: {e}', 'Compose failed: {e}'),
-    'rpt.composed':     ('已按需求编排 {n} 块{narr} · {m} 用时 {s}s', 'Composed {n} blocks{narr} · {m} in {s}s'),
-    'rpt.narr_plus':    (' + 一段运行总结', ' + a narrative summary'),
-    'rpt.ungrounded_n': ('{n} 个数字待核', '{n} numbers to verify'),
-    'rpt.grounded':     ('数字全部接地', 'all numbers grounded'),
-    'rpt.empty_canvas': ('画布是空的 — 先从组件库选内容', 'The canvas is empty — pick content from the library first'),
-    'rpt.preview_empty':('文档预览窗 — 从上面的组件库点选内容, 此处显示报告预览', 'Document preview — pick components above to see the report here'),
-    'rpt.paper_title':  ('运行分析报告', 'Operations analysis report'),
-    'rpt.paper_sub':    ('统计期 {win} | {n} 台', 'Period {win} | {n} units'),
-    'rpt.up_tag':       ('上报版', 'group version'),
-    'rpt.chapters':     ('{n} 个章节', '{n} sections'),
-    'rpt.foot_src':     ('数据源 SCADA 10min · 报警事件 · 工单台账 · CMS 振动 · 油液化验', 'Sources: SCADA 10-min · alarm events · work-order ledger · CMS vibration · oil analysis'),
-    'rpt.foot_asof':    ('出件 {d}', 'issued {d}'),
-    'rpt.foot_trace':   ('结论可回溯至本体对象', 'Conclusions trace back to ontology objects'),
-    'rpt.foot_deid':    ('判据门槛与特征频率已脱敏', 'thresholds and detailed diagnostic markers redacted'),
-    'rpt.caliber_lbl':  ('口径: ', 'Basis: '),
-    'rpt.g.gen': ('发电', 'Generation'), 'rpt.g.rel': ('可靠性', 'Reliability'), 'rpt.g.flt': ('故障', 'Faults'),
-    'rpt.g.unit': ('机组', 'Units'), 'rpt.g.end': ('收尾', 'Closing'),
-    'rpt.b.kpi.nm': ('一级指标', 'Headline KPIs'),     'rpt.b.kpi.d': ('六个核心数: 上网电量 / 等效满发 / 可用率 / 损失率 / 停机间隔 / 需行动台', 'Six core numbers: delivered energy / full-load hours / availability / loss rate / MTBO / units needing action'),
-    'rpt.b.water.nm': ('能量瀑布', 'Energy waterfall'), 'rpt.b.water.d': ('理论可发 → 逐段扣损 → 实际上网, 用于定位电量流向', 'Theoretical → losses by segment → delivered; where the energy went'),
-    'rpt.b.emon.nm': ('逐月发电表', 'Monthly table'),  'rpt.b.emon.d': ('逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率', 'Monthly delivered / loss / loss rate / full-load hours, with data coverage'),
-    'rpt.b.blame.nm': ('损失责任分账', 'Loss by responsibility'), 'rpt.b.blame.d': ('交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答"谁的责任"', 'Market-dispatch / equipment / operations shares — who is responsible'),
-    'rpt.b.ram.nm': ('可靠性象限', 'Reliability quadrant'), 'rpt.b.ram.d': ('MTBF × MDT 部件象限, 分出"频发易修"与"罕发难修", 处置方向相反', 'MTBF × MDT by component; frequent-easy vs rare-hard call for opposite actions'),
-    'rpt.b.parts.nm': ('部件停机表', 'Downtime by component'), 'rpt.b.parts.d': ('逐部件停机时长 / 事件数 / MTBF / MDT / 影响台数', 'Per component: downtime / events / MTBF / MDT / units affected'),
-    'rpt.b.attrib.nm': ('归因覆盖', 'Attribution coverage'), 'rpt.b.attrib.d': ('已归因 vs 无记录停机的占比 — 无记录不等于没有故障', 'Attributed vs unrecorded downtime — unrecorded is not the same as no fault'),
-    'rpt.b.pareto.nm': ('故障帕累托', 'Fault Pareto'),    'rpt.b.pareto.d': ('按条数降序 + 累计 80% 线, 找关键少数', 'Descending counts with the 80% line; the vital few'),
-    'rpt.b.quad.nm': ('故障四象限', 'Fault quadrants'),  'rpt.b.quad.d': ('条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级', 'Count × duration separates re-triggering from real outages; ranking by count alone misleads'),
-    'rpt.b.frate.nm': ('月度故障率', 'Monthly fault rate'), 'rpt.b.frate.d': ('按天归一的日均条数, 数据不满月单独标注', 'Daily-normalised counts; partial months flagged'),
-    'rpt.b.units.nm': ('重点机组', 'Key units'),         'rpt.b.units.d': ('需跟踪台的部件 / 证据源 / 六步进度 / 卡在哪一步', 'Tracked units: component / evidence sources / six-step progress / where blocked'),
-    'rpt.b.chain.nm': ('决策链进度', 'Decision-chain progress'), 'rpt.b.chain.d': ('证据→机制→判级→排期→动作→验收 六步的瓶颈分布', 'Bottlenecks across evidence → mechanism → rating → schedule → action → acceptance'),
-    'rpt.b.loop.nm': ('闭环实证', 'Closed-loop evidence'), 'rpt.b.loop.d': ('检出 → 换件 → 复测回落的历史例证, 证明这条链是通的', 'Detected → replaced → re-measured recovery; proof the chain works'),
-    'rpt.b.plan.nm': ('下期建议', 'Next-period actions'),  'rpt.b.plan.d': ('由数据推出的行动项, 不是套话', 'Actions derived from data, not boilerplate'),
-    'rpt.b.caliber.nm': ('口径与边界', 'Basis and limits'), 'rpt.b.caliber.d': ('统计期 / 残月 / 工单窗 / 判级性质 — 建议常驻, 没有边界的数字堆不能作汇报依据', 'Period / partial months / work-order window / nature of ratings — keep it in; numbers without limits are not a basis for reporting'),
-    'rpt.b.narr.nm': ('运行总结', 'Narrative summary'),   'rpt.b.narr.d': ('模型基于实测数据生成的运行总结 — 为图表补充文字结论, 供汇报使用', 'A model-written summary from measured data — text conclusions to accompany the charts'),
-    # 块内文案
-    'rpt.kpi.act':       ('上网电量', 'Delivered energy'),      'rpt.kpi.theo': ('理论可发 {v}', 'theoretical {v}'),
-    'rpt.kpi.mom':       ('环比 {v}%', 'MoM {v}%'),              'rpt.kpi.mom_na': ('环比不可比(末月不满)', 'MoM not comparable (partial last month)'),
-    'rpt.kpi.eflh':      ('等效满发小时', 'Full-load hours'),   'rpt.kpi.annual': ('年化约 {v} h', 'annualised ≈ {v} h'),
-    'rpt.kpi.avail':     ('时间可用率', 'Time-based availability'), 'rpt.kpi.avail_n': ('不含调度令与低风待机', 'excl. dispatch orders and low-wind standby'),
-    'rpt.kpi.loss':      ('损失率', 'Loss rate'),               'rpt.kpi.mtbo': ('平均停机间隔', 'Mean time between outages'),
-    'rpt.kpi.mtbo_n':    ('停机 {n} 起 · 单次均 {h}h', '{n} outages · mean {h}h each'),
-    'rpt.kpi.action':    ('需行动机组', 'Units needing action'), 'rpt.kpi.action_n': ('定论 {a} · 预警 {b} · 候选 {c}', 'confirmed {a} · warning {b} · candidate {c}'),
-    'rpt.water.title':   ('发电完成情况', 'Generation outcome'),
-    'rpt.water.note':    ('理论可发 {theo} = 上网 {act} + 损失 {loss} MWh ({pct}%)', 'Theoretical {theo} = delivered {act} + loss {loss} MWh ({pct}%)'),
-    'rpt.water.cap_ok':  ('量级自检: 理论低于全额定上限 {cap} MWh ✔', 'Magnitude check: theoretical is below the full-rated cap {cap} MWh ✔'),
-    'rpt.water.cap_bad': ('量级自检: 理论 ≥ 全额定上限 {cap} MWh ✗ 数字不可信', 'Magnitude check: theoretical ≥ full-rated cap {cap} MWh ✗ figures not credible'),
-    'rpt.water.cal':     ('SCADA 10min 工况态分解; 损失=同工况理论出力与实测之差。量级自检为防虚高的下限闸, 不是风资源判据', 'SCADA 10-min state decomposition; loss = theoretical output for the same state minus measured. The magnitude check is a floor against inflation, not a wind-resource criterion'),
-    'rpt.emon.title':    ('逐月发电', 'Monthly generation'),
-    'rpt.emon.m': ('月', 'Month'), 'rpt.emon.act': ('上网 MWh', 'Delivered MWh'), 'rpt.emon.loss': ('损失 MWh', 'Loss MWh'),
-    'rpt.emon.pct': ('损失率', 'Loss rate'), 'rpt.emon.eflh': ('等效满发 h', 'Full-load h'), 'rpt.emon.cov': ('覆盖', 'Coverage'),
-    'rpt.emon.partial':  (' 不满月', ' partial'),
-    'rpt.emon.cal':      ('覆盖率<50% 的月不参与环比', 'Months with coverage below 50% are excluded from MoM'),
-    'rpt.blame.title':   ('损失责任分账', 'Loss by responsibility'),
-    'rpt.blame.note':    ('进设备考核的只有"故障与检修停机"; 限电属交易与调度侧。', 'Only fault and maintenance outages count against equipment; curtailment belongs to market and dispatch.'),
-    'rpt.blame.note2':   ('限电不是"低风时段收到的空令": 限电·绑定时段平均出力 1.435 MW, 正常发电时段 1.412 MW, 比值 1.02 — 风况相同而出力被压, 说明限电是真压制; 但"限电命令期·功率未被压制"那一档平均出力仅 0.724 MW (18% 额定), 确属低风时段收令。',
-                          'Curtailment is not an empty order received in low wind: mean output in bound-curtailment periods is 1.435 MW versus 1.412 MW while generating normally (ratio 1.02) — same wind, suppressed output, so the curtailment is real; the "command without suppression" tier averages only 0.724 MW (18% of rated), which is a low-wind order.'),
-    'rpt.blame.cal':     ('工况态分解 + 限电命令面绑定判别; 平均出力=实发÷台时, 用作风资源代理量', 'State decomposition with curtailment-command binding; mean output = delivered ÷ unit-hours, used as a wind-resource proxy'),
-    'rpt.ram.title':     ('可靠性 × 可维修性', 'Reliability × maintainability'),
-    'rpt.ram.note':      ('设备类 {a}h / 外部类 {b}h (电网·风况·运维操作不计设备可靠性)', 'Equipment {a}h / external {b}h (grid, wind and operations excluded from equipment reliability)'),
-    'rpt.ram.cal':       ('SCADA ≥1h 停机段, 不含调度令; 海上 MDT 含交通窗口', 'SCADA outage segments ≥1 h, excluding dispatch orders; offshore MDT includes transit windows'),
-    'rpt.parts.title':   ('部件停机', 'Downtime by component'),
-    'rpt.parts.part': ('部件', 'Component'), 'rpt.parts.h': ('停机h', 'Downtime h'), 'rpt.parts.ev': ('事件', 'Events'),
-    'rpt.parts.mtbf': ('MTBF h', 'MTBF h'), 'rpt.parts.mdt': ('MDT h', 'MDT h'), 'rpt.parts.units': ('影响台', 'Units'),
-    'rpt.parts.ext':     (' (外部)', ' (external)'),
-    'rpt.parts.cal':     ('归因=段起点±30min 首发码', 'Attribution = first alarm within ±30 min of segment start'),
-    'rpt.attrib.title':  ('归因覆盖', 'Attribution coverage'),
-    'rpt.attrib.note':   ('"无记录停机"不是没有故障, 是没归上因。', 'Unrecorded downtime is not the absence of faults; it is the absence of attribution.'),
-    'rpt.pareto.title':  ('故障帕累托', 'Fault Pareto'),     'rpt.pareto.cal': ('报警事件表', 'Alarm event table'),
-    'rpt.quad.title':    ('故障模式四象限', 'Fault-mode quadrants'),
-    'rpt.quad.note':     ('右下角报警码属重复触发, 不应按条数排定优先级。', 'Codes in the lower-right are re-triggering and should not be prioritised by count.'),
-    'rpt.quad.cal':      ('纵界=10min(一个SCADA采样格)', 'Vertical split = 10 min (one SCADA sample)'),
-    'rpt.frate.title':   ('月度故障率', 'Monthly fault rate'),
-    'rpt.frate.note':    ('柱高=日均条数, 不是月总数。', 'Bar height is the daily mean, not the monthly total.'),
-    'rpt.frate.cal':     ('不满月按天归一', 'Partial months normalised by day'),
-    'rpt.units.title':   ('重点机组与处置', 'Key units and actions'),
-    'rpt.units.head':    ('需跟踪 {n} 台 / {tot} | 瓶颈 ', 'Tracked {n} of {tot} | bottlenecks '),
-    'rpt.units.stuck':   ('{n} 台卡在「{s}」', '{n} units pending {s}'),
-    'rpt.units.state': ('状态', 'State'), 'rpt.units.ev': ('部件与证据', 'Component and evidence'),
-    'rpt.units.prog': ('进度', 'Progress'), 'rpt.units.at': ('卡在', 'Stage'),
-    'rpt.units.rising':  ('在升 {r}×', 'rising {r}×'),   'rpt.units.clear': ('全通', 'clear'),
-    'rpt.units.cal':     ('四源融合判级', 'Four-source fused rating'),
-    'rpt.chain.title':   ('决策链进度', 'Decision-chain progress'),
-    'rpt.chain.note':    ('六步: 证据 → 机制 → 判级 → 排期 → 动作 → 验收。当前 {s}。', 'Six steps: evidence → mechanism → rating → schedule → action → acceptance. Currently {s}.'),
-    'rpt.chain.cal':     ('「动作」一列不可判 — {b}', 'The "action" step cannot be judged — {b}'),
-    'rpt.loop.title':    ('端到端闭环实证', 'End-to-end closed-loop evidence'),
-    'rpt.loop.comp': ('部件', 'Component'), 'rpt.loop.date': ('换件日期', 'Replaced on'), 'rpt.loop.pre': ('换前', 'Before'),
-    'rpt.loop.post': ('换后', 'After'), 'rpt.loop.ratio': ('比值', 'Ratio'),
-    'rpt.loop.note':     ('这几台正是算法当初的检出台 ⇒ 故障前能看见、换件后能看见恢复。', 'These are the units the algorithm flagged in the first place: visible before the fault, and visibly recovered after replacement.'),
-    'rpt.loop.cal':      ('振动标量前后对比', 'Vibration scalar, before vs after'),
-    'rpt.plan.title':    ('下期计划与建议', 'Next-period plan'),
-    'rpt.plan.1':        ('补机制定性 — {n} 台停在"判了级但根因未定", 是当前最大单一瓶颈', 'Close the mechanism gap — {n} units are rated but without a root cause; the single largest bottleneck'),
-    'rpt.plan.2':        ('回收 25~26 工单 — 台账止 2024-11, 本期检修执行与闭环无法自动判定', 'Recover the 2025–26 work orders — the ledger ends 2024-11, so this period’s execution and closure cannot be judged automatically'),
-    'rpt.plan.3':        ('油液采样节奏 — 四源里油液长期 stale, 只能作回测锚', 'Oil sampling cadence — oil is chronically stale among the four sources and serves only as a back-test anchor'),
-    'rpt.plan.4':        ('限电侧 — 损失六成在交易与调度侧, 设备侧改善空间有限', 'Curtailment — six tenths of the loss sits with market and dispatch; equipment-side headroom is limited'),
-    'rpt.plan.cal':      ('建议级 — 排期与动作须经审批', 'Advisory — scheduling and actions require approval'),
-    'rpt.narr.title':    ('运行总结', 'Narrative summary'),
-    'rpt.narr.empty':    ('还没有生成 — 在上面的交互窗输入报告要求。', 'Nothing generated yet — enter a request in the panel above.'),
-    'rpt.narr.ungr':     ('⚠ 以下数值由模型自行推算, 不在原始数据摘要中, 引用前须核对: {v}', '⚠ These values were derived by the model and are not in the source summary; verify before citing: {v}'),
-    'rpt.narr.cal':      ('模型 {m} 生成 · 数字取自系统实测摘要 · 接地闸{g}', 'Generated by {m} · numbers from the measured summary · grounding gate {g}'),
-    'rpt.narr.g_ok':     ('全通过', 'passed'),
-    'rpt.narr.g_n':      ('报 {n} 处待核', 'flagged {n} to verify'),
-    'rpt.cal.title':     ('口径与数据边界', 'Basis and data limits'),
-    'rpt.cal.period':    ('统计期', 'Period'),
-    'rpt.cal.partial':   (' — {m} 数据不满月(覆盖 {c}%), 月度已按天归一', ' — {m} partial (coverage {c}%), monthly figures normalised by day'),
-    'rpt.cal.denom':     ('可用率与损失率分母不同', 'Availability and loss rate use different denominators'),
-    'rpt.cal.denom_d':   ('可用率分母剔除低风待机与调度令(问"该发时能不能发"), 损失率分母含全部时段(问"理论能发多少"); 同一低风时段在可用率里不计时间、在损失率里计入基数 — 这是两个指标的设计差异不是错, 但两者不可直接相乘或互推。低风待机对损失的贡献仅 118 MWh(0.5%)',
-                          'Availability excludes low-wind standby and dispatch orders (can it generate when it should?), loss rate includes all hours (how much could it generate?); the same low-wind hour is excluded from one and included in the other — a design difference, not an error, but the two cannot be multiplied or inferred from each other. Low-wind standby contributes only 118 MWh (0.5%) of loss'),
-    'rpt.cal.stop':      ('停机', 'Outages'),
-    'rpt.cal.stop_d':    ('可靠性口径 ≥1h 段, 电量账为逐条工况态 — 两个数都对, 不可相减', 'Reliability counts segments ≥1 h, the energy account counts every state record — both are right, and they cannot be subtracted'),
-    'rpt.cal.wo':        ('工单', 'Work orders'),
-    'rpt.cal.wo_d':      ('台账 2020~2024-11, 本报告不对本期检修执行下结论', 'Ledger 2020 to 2024-11; this report draws no conclusion on this period’s maintenance execution'),
-    'rpt.cal.rating':    ('判级', 'Ratings'),
-    'rpt.cal.rating_d':  ('由振动/温度/润滑/油液四源融合得出; 行动等级由综合报告出具', 'Fused from vibration, temperature, lubrication and oil; action levels are issued by the consolidated report'),
-    'rpt.cal.deid':      ('脱敏', 'Redaction'),
-    'rpt.cal.deid_d':    ('上报版隐去判据门槛与特征频率', 'The group version hides criterion thresholds and detailed diagnostic markers'),
-})
-
-# ── 故障统计 ──────────────────────────────────────────────────────
-_add({
-    'win.banner':        ('时间窗 {win} · 本页全部数字随上方窗切换', 'Window {win} · every number on this page follows the selector above'),
-    'win.partial':       ('窗内 {m} 数据不满月 (覆盖 {c}%), 月度图已按天归一', '{m} in this window is a partial month ({c}% coverage); monthly charts are day-normalised'),
-    'flt.eyebrow':       ('故障统计与可靠性', 'Fault statistics and reliability'),
-    'flt.alarm_face':    ('报警面', 'Alarm view'),
-    'flt.alarm_meta':    ('{n} 条 · {c} 个码 · 窗 {win}', '{n} alarms · {c} codes · window {win}'),
-    'flt.rate':          ('月度故障率', 'Monthly fault rate'),
-    'flt.rate_note':     ('柱高 = 日均条数, 不是月总数', 'Bar height is the daily mean, not the monthly total'),
-    'flt.rate_partial':  ('本窗 {m} 数据不满月 (灰虚线柱), 直接比月总数会读反。', '{m} is partial in this window (grey dashed bar); comparing monthly totals directly reads backwards.'),
-    'flt.rate_full':     ('各月覆盖完整。', 'All months fully covered.'),
-    'flt.rate_example':  ('实测: {m} 总数最低但日均 {v} 条/日 = 全窗前列。', 'Measured: {m} has the lowest total yet {v}/day, among the highest in the window.'),
-    'flt.pareto_n':      ('帕累托 · 按发生条数', 'Pareto · by count'),
-    'flt.pareto_n_note': ('条数 = 发生频次, 不反映影响程度 — 高频短时报警码 (信号抖动或重复触发) 排序靠前', 'Count = frequency, not impact — frequent short alarms (signal chatter or re-triggering) rank high'),
-    'flt.pareto_d':      ('帕累托 · 按持续时长', 'Pareto · by duration'),
-    'flt.pareto_d_note': ('时长 = 闩锁口径 (复位 − 激活), 只作排序参考不作停机时长', 'Duration = latch basis (reset − activation); a ranking aid, not downtime'),
-    'flt.quad':          ('故障模式四象限 · 条数 × 单次时长', 'Fault-mode quadrants · count × duration per event'),
-    'flt.quad_note':     ('窗内共 {n} 个码, 图上为条数前 {m} 个。', '{n} codes in the window; the plot shows the top {m} by count.'),
-    'flt.quad_partial':  ('窗含不满月 {m} (覆盖 {c}%), 条数为原始累计未按天归一。', 'The window includes partial month {m} ({c}% coverage); counts are raw totals, not day-normalised.'),
-    'flt.quad_note2':    ('点面积表示影响台数, 大点为批次共性、小点为单台; 竖分界取条数中位, 横分界取 10min (一个 SCADA 采样格)。右下角的码不要按条数排优先级 — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。',
-                          'Point area = units affected (large = batch-wide, small = single unit); the vertical split is the count median, the horizontal split 10 min (one SCADA sample). Do not prioritise lower-right codes by count — they re-trigger and are not hardware faults. The two Pareto charts above are this plot’s projections.'),
-    'flt.per_t':         ('台级分布 · 报警条数 top{n}', 'By unit · alarm count top {n}'),
-    'flt.per_t_share':   ('占全场', 'Share of fleet'),
-    'flt.per_t_note':    ('全场合计 {n} 条 · 单台占比高须先核查是否为单一报警码重复触发', 'Fleet total {n} · a high single-unit share must first be checked for one code re-triggering'),
-    'flt.seasonal':      ('故障季节性 · top 报警族 × 月', 'Seasonality · top alarm families × month'),
-    'flt.seasonal_unit': ('条/月', 'per month'),
-    'flt.seasonal_x':    ('月', 'month'),
-    'flt.seasonal_note': ('全程 19 个月 (跨 2 冬 1 夏); 未做 χ² 显著性检验前为【参考级】, 只看形态不作季节归因', '19 months (two winters, one summer); reference tier until a χ² test is run — shape only, no seasonal attribution'),
-    'flt.stop_face':     ('停机面', 'Outage view'),
-    'flt.stop_meta':     ('{h}h · {n} 起 · 口径 ≥1h 段, 不含调度令', '{h}h · {n} events · segments ≥1 h, excluding dispatch orders'),
-    'flt.ram':           ('可靠性 × 可维修性 · 部件象限', 'Reliability × maintainability · component quadrants'),
-    'flt.ram_note':      ('按停机时长排序的表看不出这层结构: 同样 500h 的停机, "频发易修"要查根因、"罕发难修"要备件, 处置相反。设备类停机 {a}h / 外部类 {b}h (外部 = 电网·风况·运维操作, 不计设备可靠性)。海上机组 MDT 普遍偏高是交通窗口决定的, 不等于维修本身慢 — 跨场比 MDT 须同为海上。',
-                          'A table sorted by downtime hides this structure: the same 500 h of downtime calls for root-cause work if frequent-and-easy, and for spares if rare-and-hard — opposite actions. Equipment {a}h / external {b}h (external = grid, wind, operations; excluded from equipment reliability). Offshore MDT runs high because of transit windows, not slow repairs — compare MDT only against other offshore sites.'),
-    'flt.attrib_note':   ('归因覆盖: "无记录停机" {h}h 是全场最大单项, 因不给 MTBF 而在上图中不出现 — 它不是"没有故障", 是"没归上因"。口径为段起点 ±30min 首发码。缺口成因待定: 此前写的"98% 的段在前 72h 内有报警 ⇒ 长停远窗"不成立 — 前 72h 有报警不等于该报警与本段停机有因果 (双模审 2026-08-28 逮)。要定成因须改判"段起止是否落在某条报警的持续区间内", 未做之前只报缺口不报原因。',
-                          'Attribution coverage: unrecorded downtime {h}h is the largest single item and does not appear above because it has no MTBF — it is not "no fault", it is "not attributed". Basis: first alarm within ±30 min of segment start. The cause of the gap is undetermined: the earlier claim "98% of segments had an alarm in the prior 72 h ⇒ long outage, far window" does not hold — an alarm in the prior 72 h does not imply causation (caught by dual-model review 2026-08-28). Determining it requires testing whether the segment falls inside an alarm’s active interval; until then only the gap is reported, not its cause.'),
-    'flt.stop_pareto':   ('停机归因帕累托', 'Outage attribution Pareto'),
-    'flt.stop_pareto_note': ('按停机时长; 事件次数见悬停', 'By downtime; event counts on hover'),
-    'flt.stop_units':    ('台级停机 top{n}', 'Outages by unit · top {n}'),
-    'flt.stop_h':        ('停机时长', 'Downtime'),
-    'flt.stop_n':        ('次数', 'Events'),
-    'flt.stop_units_note': ('停机时长高 ≠ 设备差 — 须先看归因里外部类占比 (调度令已剔除)', 'High downtime ≠ poor equipment — check the external share in the attribution first (dispatch orders already excluded)'),
-    'flt.block_err':     ('本块未算出 ({k}): {e}', 'This block could not be computed ({k}): {e}'),
-    'ch.ml.legend':      ('— {s}', '— {s}'),
-})
-
-# ── 电量算账 ──────────────────────────────────────────────────────
-_add({
-    'en.kpi_theo':     ('理论可发 (实际+损失)', 'Theoretical (delivered + loss)'),
-    'en.kpi_act':      ('实际上网 · 等效满发 {h} h', 'Delivered · full-load {h} h'),
-    'en.kpi_loss':     ('损失率 · {v} MWh', 'Loss rate · {v} MWh'),
-    'en.cap_pass':     ('通过', 'Pass'),
-    'en.cap_fail':     ('不通过', 'Fail'),
-    'en.cap_note':     ('能量守恒核 · 理论 {op} 全额定上限 {cap}', 'Energy-balance check · theoretical {op} full-rated cap {cap}'),
-    'en.balance':      ('电量平衡', 'Energy balance'),
-    'en.win':          ('窗 {win}', 'window {win}'),
-    'en.blame':        ('损失责任分账', 'Loss by responsibility'),
-    'en.blame_note':   ('最大一笔是限电不是故障。进设备考核的只有"故障与检修停机" {n} MWh; 限电属交易与调度侧, 2026 起交易策略主导, 少发未必是弃风。',
-                        'The largest item is curtailment, not faults. Only fault and maintenance outages ({n} MWh) count against equipment; curtailment belongs to market and dispatch, and from 2026 trading strategy dominates, so under-production is not necessarily lost wind.'),
-    'en.states':       ('工况态明细', 'Operating-state detail'),
-    'en.state':        ('工况态', 'State'),
-    'en.share':        ('时长占比', 'Share of hours'),
-    'en.loss_mwh':     ('损失 MWh', 'Loss MWh'),
-    'en.states_note':  ('控制参数一致性与曲线欠发筛查在固定干净窗判, 已迁入「发电性能」页 — 本页只出当期电量账。',
-                        'Control-parameter consistency and curve under-production screening are judged in a fixed clean window and live on the Generation Performance page; this page shows only the current-period energy account.'),
-})
-
-# ── 部件问题 ──────────────────────────────────────────────────────
-_add({
-    'cmp.by_system':    ('按系统查看', 'By system'),
-    'cmp.cards_note':   ('点系统卡 → 该系统全场机组与依据; 九系统 = 交付九分册一一对应; 叶片与叶根 / 塔架与基础无 SCADA 轴 (分册蓝本取 CMS / 巡检)',
-                         'Open a system card for every unit and its evidence; the nine systems map one-to-one onto the nine delivered volumes; blade-and-root and tower-and-foundation have no SCADA axis (their volumes draw on CMS and inspection)'),
-    'cmp.alarm_n':      ('报警 {a}', 'alarm {a}'),
-    'cmp.unk_n':        (' · 不可判 {u}', ' · undeterminable {u}'),
-    'cmp.fleet_n':      (' · 全场 {n} 台', ' · fleet {n}'),
-    'cmp.detail_head':  ('{sys} · 全场 {n} 台', '{sys} · fleet of {n}'),
-    'cmp.counts':       ('报警 {a} · 不可判 {u} · 良好 {g} · 优秀 {e}', 'Alarm {a} · undeterminable {u} · good {g} · excellent {e}'),
-    'cmp.alarm_units':  ('报警机组', 'Units in alarm'),
-    'cmp.unk_units':    ('不可判 (监测/数据不足, ≠ 正常)', 'Undeterminable (monitoring or data insufficient — not normal)'),
-    'cmp.good_units':   ('良好 {n} 台 (轻微偏离, 周期观察 — 点开看逐台依据)', 'Good · {n} units (slight deviation, periodic observation — expand for per-unit evidence)'),
-    'cmp.n_units':      ('({n} 台)', '({n} units)'),
-    'cmp.basis':        ('依据', 'Evidence'),
-    'cmp.detail':       ('详情', 'Detail'),
-    'cmp.problem_page': ('该问题页 →', 'Issue page →'),
-    'cmp.none':         ('本系统全场无非常态。', 'No abnormal units in this system.'),
-    'cmp.rel_eyebrow':  ('部件可靠性 · {win}', 'Component reliability · {win}'),
-    'cmp.rel_head':     ('部件停机贡献与可靠性', 'Downtime contribution and reliability by component'),
-    'cmp.rel_meta':     ('总停机 {t}h = 设备类 {a} + 外部类 {b}', 'Total downtime {t}h = equipment {a} + external {b}'),
-    'cmp.rel_note':     ('口径 ≥1h 停机段, 不含调度令 — 与"电量算账"里的停机时长{h} 不同, 差额是短于 1h 的停机段 (本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。',
-                         'Basis: outage segments ≥1 h, excluding dispatch orders — this differs from the downtime hours on Energy Accounting{h}; the gap is segments shorter than 1 h (reliability needs whole segments, the energy account counts every state record). Both are correct and must not be subtracted.'),
-    'cmp.rel_part': ('部件', 'Component'), 'cmp.rel_h': ('停机时长', 'Downtime'), 'cmp.rel_ev': ('事件', 'Events'),
-    'cmp.rel_mtbo': ('MTBO h', 'MTBO h'), 'cmp.rel_mdt': ('MDT h', 'MDT h'), 'cmp.rel_units': ('影响台', 'Units'),
-    'cmp.rel_fm':       ('典型失效模式 (按时长)', 'Typical failure mode (by duration)'),
-    'cmp.external':     ('外部', 'external'),
-    'cmp.fusion_moved': ('振动线结论与四源融合已移至独立板块 →', 'Vibration-line conclusions and four-source fusion live on their own tab →'),
-    'cmp.fusion_n':     ('({n} 台次收录)', '({n} unit-entries)'),
-    'cmp.cross_eyebrow':('多系统交叉机组', 'Units flagged across systems'),
-    'cmp.watch_head':   ('关注机组 (≥2 系统交叉, {n} 台; 点机组号 → 该台独立页)', 'Watch list (≥2 systems, {n} units; click a unit for its own page)'),
-    'cmp.n_sys':        ('系统数', 'Systems'),
-    'cmp.hit_sys':      ('命中系统', 'Systems flagged'),
-})
-
-# ── 系统维护 ──────────────────────────────────────────────────────
-_add({
-    'mt.eyebrow':    ('系统维护 · 数据输入与更新端口', 'System maintenance · data inputs and update ports'),
-    'mt.surveying':  ('盘点中…', 'Surveying…'),
-    'mt.fail':       ('盘点失败: {e}', 'Survey failed: {e}'),
-    'mt.head':       ('数据维护面 · {n} 个源', 'Data maintenance · {n} sources'),
-    'mt.note':       ('每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。要补数据就把新文件放到"约定位置"再跑"摄入命令"。',
-                      'For each source: coverage, last update, agreed location, ingest command, cadence and responsible line. To add data, put the new file at the agreed location and run the ingest command.'),
-    'mt.layer_data': ('数据层', 'Data layer'),
-    'mt.layer_mech': ('机理层', 'Domain-model layer'),      # 与经典枚举表的既有译名 'Domain model' 一致
-    'mt.layer_data_note': ('随运营滚动 — 新数据到位后跑摄入, 本体与判级自动刷新', 'Rolls with operations — run ingest when new data lands; ontology and ratings refresh automatically'),
-    'mt.layer_mech_note': ('相对稳定 — 换厂商资料或现场反馈修正时才动; 它是判据的地基, 变更须过审', 'Relatively stable — changes only with new vendor material or field corrections; it underpins the criteria, so changes go through review'),
-    'mt.src': ('源', 'Source'), 'mt.cov': ('当前覆盖', 'Coverage'), 'mt.last': ('末次更新', 'Last update'),
-    'mt.freq': ('频率', 'Cadence'), 'mt.resp': ('责任', 'Responsible line'), 'mt.how': ('补数据怎么做', 'How to add data'),
-    'mt.put': ('放到:', 'Put at:'), 'mt.run': ('跑:', 'Run:'), 'mt.rows_n': (' ({n} 条)', ' ({n} records)'),
-})
-
-# ── 发电性能 (固定干净窗; 第一性原理闸卡按用户 2026-08-28 裁定不出现在交付界面) ──
-_add({
-    'gen.eyebrow':    ('特性曲线与控制参数', 'Characteristic curves and control parameters'),
-    'gen.fixed_win':  ('随上方时间窗 · 按窗重算', 'Follows the selector · recomputed per window'),
-    'gen.win_range':  ('窗 {span}', 'window {span}'),
-    'gen.win_why':    ('七镜头与月度时序都按所选时间窗重算(含自定义起止日期)。'
-                       '提示:2025-07 ~ 2025-12 是"限电前干净判别窗"——2026 起交易策略主导,'
-                       '曲线含策略性少发,作设备判据时须注意;选到含 2026 的窗时该影响存在但不再遮掩。',
-                       'Both the seven lenses and the monthly series are recomputed for the selected window '
-                       '(including custom date ranges). Note: 2025-07 to 2025-12 is the "clean pre-curtailment" '
-                       'window — from 2026 trading strategy dominates, so curves contain strategic '
-                       'under-production; the effect is present (not hidden) when a 2026 window is selected.'),
-    'gen.loading':    ('正在计算七个镜头 (剥限电 · 逐档中位 · 同型机群残差)…', 'Computing seven lenses (curtailment stripped · per-bin medians · same-type fleet residuals)…'),
-    'gen.recomputing': ('正在按所选时间窗重算七镜头…', 'Recomputing the seven lenses for the selected window…'),
-    'gen.recomputing_short': ('正在按窗重算…', 'recomputing for this window…'),
-    'gen.ctrl':       ('控制参数一致性', 'Control-parameter consistency'),
-    'gen.ctrl_win':   ('窗 {win}', 'window {win}'),
-    'gen.cap':        ('封顶参数落位', 'Where the caps sit'),
-    'gen.cap_note':   ('台数落在几个离散位置上, 不是连成一片 — 这是配置/版本差异的形状; 磨损或工况引起的会是连续散布。两轴各自成组且分组不同台 ⇒ 参数级差异, 成因须配置台账。',
-                       'Units fall on a few discrete positions rather than a continuum — the shape of configuration or version differences; wear or operating conditions would scatter continuously. Both axes form groups with different units in each ⇒ a parameter-level difference; the cause needs the configuration ledger.'),
-    'gen.beta':       ('桨距调度同档差', 'Pitch-schedule deviation within a power band'),
-    'gen.beta_note':  ('同一功率档下的桨角与全场中位之差。{units} 偏离达中位的 {k} 倍 → {v}', 'Pitch angle minus the fleet median within the same power band. {units} deviate {k}× the median → {v}'),
-    'gen.ruler':      ('尺子巡检 · 极差 ÷ σ', 'Ruler check · range ÷ σ'),
-    'gen.ruler_note': ('判据自身的分辨力。38 台正态抽样的极差期望约 3.7σ, 明显超出即分布非单峰 — 与左图的分组互为印证; 落在期望附近的轴 (额定桨角) 则是连续分布。',
-                       "The criterion's own resolving power. For 38 normally distributed units the expected range is about 3.7σ; a clear excess means the distribution is not unimodal, corroborating the grouping on the left; an axis near the expectation (rated pitch angle) is a continuous distribution."),
-    'gen.cands':      ('曲线欠发筛查', 'Curve under-production screening'),
-    'gen.cands_meta': ('{note} · σ = {s}%', '{note} · σ = {s}%'),
-    'gen.no_cands':   ('无候选', 'no candidates'),
-    'gen.figs':       ('七个镜头 · 逐档中位与同型机群残差', 'Seven lenses · per-bin medians and same-type residuals'),
-    'ch.cap.p':       ('满发封顶', 'Power cap'),
-    'ch.cap.w':       ('转速封顶', 'Speed cap'),
-    'ch.cap.median':  ('中位 {v}', 'median {v}'),
-    'ch.cap.legend':  ('圆内数字 = 台数, 圆面积 ∝ 台数', 'number = units, area ∝ units'),
-    'ch.beta.median': ('全场中位 {v}°', 'fleet median {v}°'),
-    'ch.ruler.norm':  ('正态期望 3.7σ', 'normal expectation 3.7σ'),
-    'ch.ruler.row':   ('{r}σ  σ = {s}', '{r}σ  σ = {s}'),
-})
-
-# ── 振动融合分析 ────────────────────────────────────────────────────
-_add({
-    'fus.eyebrow':     ('振动与 SCADA 证据视图 · 展示振动结论、SCADA 指标、报警与工单线索',
-                        'Vibration and SCADA evidence view · vibration findings, SCADA indicators, alarms and work-order context'),
-    'fus.unavailable': ('融合面数据不可用: {e}', 'Fusion data unavailable: {e}'),
-    'fus.sub.matrix':  ('融合矩阵', 'Fusion matrix'),   'fus.sub.loop': ('端到端闭环', 'Closed loops'),
-    'fus.sub.units':   ('逐台情况', 'Per unit'),        'fus.sub.events': ('报警工单', 'Alarms and work orders'),
-    'fus.sub.cms':     ('振动分析', 'Vibration assessment'),
-    'fus.rules_head':  ('两条判读纪律 (随板块常显)', 'Two reading rules (always shown)'),
-    'fus.rule1':       ('① 同一失效机制的多个观测面 ≠ 多源印证 — 融合禁按源数加权 (29# 柱塞/温度/振动稳态/振动瞬态 = 四个观测面一条机制链)。',
-                        '① Several observation surfaces of one failure mechanism ≠ multi-source confirmation — fusion must not weight by source count (unit 29: plunger / temperature / steady vibration / transient vibration are four surfaces on one mechanism chain).'),
-    'fus.rule2':       ('② "没报警" ≠ "正常" — 在已知盲区上, 缺报警只说明监测面无效, 不说明设备无恙。',
-                        '② "No alarm" ≠ "normal" — on a known blind spot a missing alarm only shows the monitoring surface is ineffective, not that the equipment is fine.'),
-    'fus.matrix_head': ('全场融合矩阵 (38 台 × 6 个证据面)', 'Fleet fusion matrix (38 units × 6 evidence surfaces)'),
-    'fus.matrix_note1':('各列取数窗口不同 (见列头) — 不同窗的两格同时发红不等于"同时发生", 不可当共时印证。',
-                        'Columns use different windows (see headers) — two red cells from different windows are not simultaneous and do not corroborate each other.'),
-    'fus.matrix_note2':('每格右侧曲线 = 逐月记忆轨迹 ({n} 个月, {m0} 起; 实心点 = 现窗) — 现窗快照会丢形态, 轨迹才看得见"塌→回升→翻转"类机制签名。',
-                        'The curve in each cell is the monthly memory track ({n} months from {m0}; solid dot = current window) — a snapshot loses shape; only the track shows signatures such as collapse → recovery → reversal.'),
-    'fus.legend_state':('格子颜色 = 设备状态 (业主口径, 与 windcms 报告同一套):', 'Cell colour = equipment state (operational basis, same as the reviewed condition report):'),
-    'fus.legend_tier': ('徽章 = 证据状态 (可信度, windcms 九枚举):', 'Badge = evidence tier (confidence, the nine windcms values):'),
-    'fus.legend_note': ('两轴不可互相替代: "候选"说的是可信度不是设备好坏; "良好"说的是设备状态不是证据强度。设备状态只取自振动线的判级栏这一个来源。',
-                        "The two axes cannot substitute for each other: \"candidate\" is about confidence, not equipment health; \"good\" is about equipment, not evidence strength. Equipment state comes from one source only: the vibration line's rating column."),
-    'fus.col.vib': ('振动', 'Vibration'), 'fus.col.temp': ('温度', 'Temperature'), 'fus.col.lub': ('润滑柱塞', 'Lubrication plunger'),
-    'fus.col.oil': ('油液', 'Oil'), 'fus.col.alarm': ('报警', 'Alarms'), 'fus.col.wo': ('工单', 'Work orders'),
-    'fus.col.fixed': (' · 不随本窗', ' · fixed'), 'fus.col.display_only': (' · 只展示', ' · display only'),
-    'fus.short.nodata': ('数据不足', 'insufficient data'), 'fus.short.uneval': ('未评估', 'not assessed'),
-    'fus.short.nosample': ('未取样', 'no sample'), 'fus.short.stale': ('样龄过期', 'sample stale'),
-    'fus.short.normal': ('正常', 'normal'), 'fus.short.unlisted': ('未收录', 'not listed'),
-    'fus.tip_state':   (' — 设备状态 {s}', ' — state {s}'), 'fus.tip_tier': (' / 证据状态 {e}', ' / tier {e}'),
-    'fus.alarm_n':     ('{n} 条', '{n}'), 'fus.alarm_rank': (' · 第 {r}/{t}', ' · rank {r}/{t}'),
-    'fus.burst':       ('⚠ {p}% 是码 {c} 抖动', '⚠ {p}% is code {c} chattering'),
-    'fus.burst_tip':   ('⚠ 单码 {c} 占 {p}%, 中位 {m} 秒/次 — 条数被抖动灌满', '⚠ code {c} alone is {p}%, median {m} s per event — the count is filled by chatter'),
-    'fus.no_alarm':    ('窗内无报警', 'no alarms in window'), 'fus.fam_tip': ('{n} 条 · 全场最多 {m} 条', '{n} · fleet max {m}'),
-    'fus.wo_n':        ('{n} 单', '{n} orders'), 'fus.wo_none': ('窗内无传动链工单', 'no drivetrain work orders in window'),
-    'fus.matrix_foot': ('润滑柱塞各台设定本就不同 (全场单次循环 22~961 秒), 所以此列只展示不判级, 只有已定案的机制链才标色; 油液多数台末次取样距今超 12 个月, 此列看的是"样够不够新", 不代表当前状态。',
-                        "Plunger settings differ by unit (single-cycle 22–961 s across the fleet), so that column is display only and is coloured only for confirmed mechanism chains; most units' last oil sample is over 12 months old, so that column shows sample freshness, not current state."),
-    'fus.loop_none':   ('handoff 无端到端闭环数据', 'No closed-loop data in the current evidence package'),
-    'fus.loop_head':   ('维修后验证案例 (检出 → 换件 → 恢复)', 'Post-maintenance verification cases (detected → replaced → recovered)'),
-    'fus.loop_note':   ('有换件记录且同口径指标回落, 可作为检测链已闭合的案例; 指标仍在上升但缺少可匹配工单的机组, 需现场或台账确认后再关闭。',
-                        'A documented replacement followed by a metric reduction on the same basis is shown as a closed verification case. Units with rising metrics remain open until field confirmation, a matching work order, or ledger confirmation is available.'),
-    'fus.loop_closed': ('已闭环 · 换件后恢复 ({n} 例)', 'Verified recovery after replacement ({n})'),
-    'fus.loop_item': ('事项', 'Item'), 'fus.loop_date': ('换件日', 'Replaced'), 'fus.loop_ba': ('换前 → 换后 (同标尺)', 'Before → after (same scale)'),
-    'fus.loop_ratio': ('恢复比', 'Recovery'), 'fus.loop_verdict': ('判', 'Verdict'),
-    'fus.loop_neg':    ('负面对照: 工单后持平 {n} 例 (换件 ≠ 必然回落, 说明回落非巧合)。', 'Negative control: {n} cases flat after a work order (replacement does not always lower the metric, so the falls are not coincidence).'),
-    'fus.loop_rising': ('未闭环 · 指标上升待现场/工单确认 ({n} 台)', 'Open · rising metrics awaiting field confirmation, a matching work order, or ledger confirmation ({n} units)'),
-    'fus.loop_el': ('早期 → 近期 (同标尺)', 'Early → recent (same scale)'), 'fus.loop_x': ('倍比', 'Ratio'), 'fus.loop_state': ('状态', 'State'),
-    'fus.loop_bound':  ('数据边界: {b}', 'Data boundary: {b}'),
-    'fus.units_head':  ('逐台情况 (全场 {u} 台 / {n} 台次{f}; 深入谱图/波形 → "振动分析")', 'Per unit ({u} units / {n} entries{f}; detailed condition traces → "Vibration assessment")'),
-    'fus.units_filter':(' · 筛选: {l}', ' · filter: {l}'), 'fus.clear': ('× 清除', '× clear'),
-    'fus.lv.bad': ('定论/报警级', 'Confirmed / alarm'), 'fus.lv.warn': ('预警级', 'Warning'),
-    'fus.lv.note': ('候选/监视/观察级', 'Candidate / watch'), 'fus.lv.ok': ('销案/无新证', 'Closed / no new evidence'),
-    'fus.unlisted_head': ('未列机组 {n} 台 (振动线未收录 — 未列 ≠ 正常)', 'Unlisted units · {n} (not covered by the vibration line — unlisted ≠ normal)'),
-    'fus.unlisted_link': ('未列 {n} 台', 'unlisted {n}'),
-    'fus.temp_now': ('温度轴现状', 'Temperature now'), 'fus.lub_now': ('润滑柱塞现状', 'Plunger now'),
-    'fus.eval_head':   ('评价体系 (规范化定义 — 两轴并存, 不可互相替代)', 'Rating system (two axes, not interchangeable)'),
-    'fus.eval_state': ('设备状态 (业主口径)', 'Equipment state (operational basis)'), 'fus.eval_tier': ('证据状态 (可信度)', 'Evidence tier (confidence)'), 'fus.eval_meaning': ('含义', 'Meaning'),
-    'fus.m.bad': ('异常严重, 建议尽快停机检查', 'Severe abnormality; stop and inspect soon'), 'fus.m.bad_e': ('证据: 已定案', 'evidence: settled'),
-    'fus.m.warn': ('存在需现场核实或处置的异常', 'Abnormality needing field verification or action'), 'fus.m.warn_e': ('证据: 基本确认 / 新出现待核实', 'evidence: essentially confirmed / newly seen, to verify'),
-    'fus.m.ok': ('轻微偏离, 记录观察', 'Slight deviation; record and observe'), 'fus.m.ok_e': ('证据: 待现场核实 / 已记为基线下窗观察', 'evidence: to verify on site / recorded as baseline for the next window'),
-    'fus.m.ex': ('未见异常, 保持常规监视', 'No abnormality; routine monitoring'), 'fus.m.ex_e': ('证据: 仅记录 / 已取消', 'evidence: record only / withdrawn'),
-    'fus.m.unk': ('数据不足或监测中断 — 不等于正常', 'Insufficient data or monitoring gap — not the same as normal'), 'fus.m.unk_e': ('证据: 数据不足', 'evidence: insufficient'),
-    'fus.eval_note':   ('设备状态答"这台设备现在怎么样" (危险 > 报警 > 良好 > 优秀 > 不可判); 证据状态答"这个判断有多可信" (定论 → INSUFFICIENT)。设备状态一律取自振动线的判级栏这一个来源。本页判级为转录, 不产新判级; 标注为引用同族/谱线级/测量链/未映射的行不作部件级结论消费; claim_window = 该结论的证据窗, 窗外状态该结论不背书。',
-                        'Equipment state answers "how is this unit now" (critical > alarm > good > excellent > undeterminable); evidence tier answers "how credible is that judgement" (confirmed → INSUFFICIENT). Equipment state comes from the reviewed vibration assessment. Rows marked as same-family reference / line-level / measurement-chain / unmapped are excluded from component-level conclusions; claim_window is the evidence window, and the conclusion does not vouch for states outside it.'),
-    'fus.col.part': ('部件', 'Component'), 'fus.col.state_concl': ('设备状态 / 振动结论', 'State / vibration conclusion'),
-    'fus.col.claim': ('证据窗', 'Evidence window'), 'fus.col.claim_axis': ('26-01 → 09', '26-01 → 09'),
-    'fus.col.signal': ('振动信号', 'Vibration signal'), 'fus.col.tempaxis': ('温度轴', 'Temperature'), 'fus.col.tempaxis_note': ('±6 K', '±6 K'),
-    'fus.col.oil': ('油样', 'Oil sample'),
-    'fus.win_none': ('未标窗', 'no window'), 'fus.win_n': ('{n} 个窗段', '{n} spans'),
-    'fus.oil_fresh': ('近共时', 'recent'), 'fus.oil_stale': ('stale > 12 个月', 'stale > 12 months'), 'fus.oil_none': ('无样', 'no sample'),
-    'fus.ev_win':      ('时间窗 {win} · 顶部选择器可换', 'Window {win} · change it with the selector above'),
-    'fus.ev_head':     ('{c} — 报警 {an} 条 / {au} 台 · 工单 {wn} 条 / {wu} 台 (窗内)', '{c} — alarms {an} / {au} units · work orders {wn} / {wu} units (in window)'),
-    'fus.ev_code': ('码', 'Code'), 'fus.ev_text': ('报警内容', 'Alarm'), 'fus.ev_units': ('台数', 'Units'), 'fus.ev_dur': ('累计时长 h', 'Total h'),
-    'fus.ev_top': ('台分布 top: ', 'Top units: '), 'fus.ev_recent': ('最近 {n} 条明细', 'Latest {n} events'),
-    'fus.ev_time': ('时间', 'Time'), 'fus.ev_min': ('时长 min', 'Duration min'),
-    'fus.ev_no_alarm': ('窗内无该部件报警', 'No alarms for this component in the window'),
-    'fus.ev_wo_name': ('故障名称', 'Fault'), 'fus.ev_wo_act': ('处理', 'Action'), 'fus.ev_wo_cause': ('原因', 'Cause'),
-    'fus.ev_no_wo':    ('窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)', 'No work orders for this component in the window (ledger ends 2024-11; 2026 windows are necessarily empty — a data boundary)'),
-    'fus.cms_loading': ('载入振动评估结果…', 'Loading vibration assessment…'),
-    'fus.cms_head':    ('振动状态评估 (windcms 报告 {d} 结果转录)', 'Vibration condition assessment ({d} reviewed report)'),
-    'fus.cms_note':    ('状态管理判级, 不代表确诊数量; 判据/谱线/计算过程不在本页 — 深潜走底部独立系统。',
-                        'Condition-management ratings, not a count of confirmed faults; criteria, detailed signal markers and calculations are not on this page — use the standalone system linked below.'),
-    # ★2026-09-19: 页面上原来只有"出件日 + 证据窗末", 没有**起始日** ⇒ 用户拿 2026-03~04 的导出
-    #   只看到 4 月的记录时会问"3 月的数据呢"。窗名(w0316)只是起始日代号, 这里把实算区间摆出来并说明
-    #   为什么图上多显示末期日期 (末窗状态=取窗内最新记录, 是口径不是缺数据)。
-    #   ★英文措辞要过 lang.audit 的语言包闸: 'spectra'/'waveforms'/'characteristic frequency' 等 36 条
-    #   内部审核黑话**禁用**(2026-09-19 实逮: 首版写 spectra 直接让 /v2 出页 500)。
-    'fus.cms_window':  ('数据窗 {r} —— 本窗**实算**覆盖区间 (窗名只是起始日代号); 页面上的图谱与"末窗"状态取自窗内最新记录, 故多显示末期日期。',
-                        'Data window {r} — the measured coverage of this window (the name is only its start-date code). Charts and "latest-window" states come from the newest record inside the window, so later dates dominate the display.'),
-    'fus.cms_action':  ('需行动机组 (报警 / 不可判)', 'Units needing action (alarm / undeterminable)'),
-    'fus.cms_focus': ('状态焦点', 'Focus'), 'fus.cms_focus_note': ('(传动链: 主承前 · 主承后 · 齿轮箱 · 发电机两端)', '(drivetrain: main bearing fore / aft · gearbox · generator both ends)'),
-    'fus.cms_evidence': ('证据强度', 'Evidence strength'), 'fus.cms_act': ('行动', 'Action'),
-    'fus.cms_grades':  ('38 台部件等级一览', 'Component grades, all 38 units'),
-    'fus.cms_chain': ('部件状态链', 'Component state chain'), 'fus.cms_chain_note': ('(主承前 · 主承后 · 齿轮箱 · 发电机; 悬停看逐件等级)', '(main bearing fore / aft · gearbox · generator; hover for each grade)'),
-    'fus.cms_overall': ('综合', 'Overall'), 'fus.cms_basis': ('口径说明', 'Basis'),
-    'fus.cms_basis_note': ('本表 = windcms 状态管理判级 (含标量趋势与数据可判性); 融合矩阵 / 逐台结论 = 振动线定谳链转录 (候选以上)。两口径差异台: 仅本表列 {a} (多为标量发作 / 数据不可判, 未到定谳链); 仅定谳链列 {b} (预警级在察, 状态管理未升)。',
-                        "This table = windcms condition-management ratings (scalar trends and data judgeability); the matrix and per-unit conclusions = transcription of the vibration line's verdict chain (candidate and above). Units differing between the two: only here {a} (mostly scalar episodes or undeterminable data, not yet in the verdict chain); only in the verdict chain {b} (warning tier under watch, not escalated in condition management)."),
-    'fus.none_word': ('无', 'none'),
-    'fus.cms_link':    ('谱图 · 波形 · 判据 · 工作台等分析功能在独立 windcms 系统 →', 'Detailed condition traces, criteria and the workbench live in the standalone condition-monitoring system →'),
-    'fus.cms_open': ('打开 windcms ↗', 'Open windcms ↗'), 'fus.cms_static': ('(未启动时用静态版)', '(static copy if it is not running)'),
-    'chain.mbf': ('主轴承前', 'MB fore'), 'chain.mba': ('主轴承后', 'MB aft'), 'chain.ps': ('行星', 'Planet'),
-    'chain.ims': ('中间', 'IMS'), 'chain.hss': ('高速', 'HSS'), 'chain.gde': ('发电机DE', 'Gen DE'), 'chain.gnde': ('发电机NDE', 'Gen NDE'),
-    'chain.gb': ('齿轮箱', 'Gearbox'), 'chain.gen': ('发电机', 'Generator'),
-})
-
-# ── 判别值枚举 ────────────────────────────────────────────────────
-# 这批值在 API 侧**有意保留中文** (_KEEP_ZH): 前端拿它们做判别 (x.部件 === '无记录停机'),
-# 译了判别就失效。所以它们必须在**显示层**译 —— 数据层译反而会打穿逻辑。
-# 停机归因部件 (rel.表[].部件) 与九系统是两套分类: 它含外部类与未归因项。
-_add({
-    'attr.无记录停机':      ('无记录停机', 'Unrecorded downtime'),
-    'attr.未归类':          ('未归类', 'Unclassified'),
-    'attr.运维操作':        ('运维操作', 'Operations and maintenance actions'),
-    'attr.变桨与液压':      ('变桨与液压', 'Pitch and hydraulics'),
-    'attr.塔架/结构':       ('塔架/结构', 'Tower and structure'),
-    'attr.轮毂/叶片':       ('轮毂/叶片', 'Hub and blades'),
-    'attr.电网/供电(外部)': ('电网/供电(外部)', 'Grid and power supply (external)'),
-    'attr.风况(外部)':      ('风况(外部)', 'Wind conditions (external)'),
-})
-
-# 振动线部件形态 (fus.表[].部件) —— 带限定语的部件名, 同为判别值
-_add({
-    'vp.(振动线未收录)':              ('(振动线未收录)', '(not covered by the vibration line)'),
-    'vp.齿轮箱高速轴':                ('齿轮箱高速轴', 'Gearbox high-speed shaft'),
-    'vp.齿轮箱中间轴':                ('齿轮箱中间轴', 'Gearbox intermediate shaft'),
-    'vp.齿轮箱二级行星内齿圈':        ('齿轮箱二级行星内齿圈', 'Gearbox second-stage planetary ring gear'),
-    'vp.齿轮箱(未映射)':              ('齿轮箱(未映射)', 'Gearbox (not mapped)'),
-    'vp.齿轮箱(谱线级, 未映射到部件)': ('齿轮箱(谱线级, 未映射到部件)', 'Gearbox (signal level, not mapped to a component)'),
-    'vp.齿轮箱(测量链问题, 非部件)':   ('齿轮箱(测量链问题, 非部件)', 'Gearbox (measurement-chain issue, not a component)'),
-    'vp.发电机DE':                    ('发电机DE', 'Generator DE'),
-    'vp.发电机NDE':                   ('发电机NDE', 'Generator NDE'),
-    'vp.发电机DE+NDE(两端)':          ('发电机DE+NDE(两端)', 'Generator DE and NDE (both ends)'),
-    'vp.发电机(未映射)':              ('发电机(未映射)', 'Generator (not mapped)'),
-    'vp.发电机(引用同族, 见族主台)':  ('发电机(引用同族, 见族主台)', "Generator (same-family reference; see the family's lead unit)"),
-    'vp.主轴承(后)+集中润滑泵':       ('主轴承(后)+集中润滑泵', 'Main bearing (aft) plus centralised lubrication pump'),
-})
-
-# ── 逐台 / 单问题浮层 (原为服务端独立页 /turbine/<t> · /problem/<t>/<sys>) ──────
-# 静态站上那两条路径是 404 (nginx 只有静态文件)。经典页当年用页内浮层兜, v2 同此:
-# 数据从预烤取, 图表复用 charts.fig —— 与在线版看到的是同一批图。
-_add({
-    'pop.unit':        ('{u} · 全部问题', '{u} · all issues'),
-    'pop.problem':     ('{u} · {sys}', '{u} · {sys}'),
-    'pop.close':       ('关闭', 'Close'),
-    'pop.none':        ('本窗无记录', 'No records in this window'),
-    'pop.loading':     ('载入中…', 'Loading…'),
-    'pop.orders':      ('工单 {n} 单', '{n} work orders'),
-    'pop.orders_none': ('窗内无工单', 'No work orders in this window'),
-    'pop.o_date':      ('日期', 'Date'),
-    'pop.o_fault':     ('故障', 'Fault'),
-    'pop.o_check':     ('排查', 'Investigation'),
-    'pop.o_cause':     ('原因', 'Cause'),
-    'pop.o_act':       ('处置', 'Action'),
-    'pop.o_kind':      ('类别', 'Category'),
-    'pop.acts':        ('动作统计', 'Action counts'),
-    'pop.orders_one':  ('工单 1 单', '1 work order'),
-    'ov.evidence':     ('查看完整依据', 'View basis'),
-    'sys.other':       ('其他', 'Other'),
-    'win.through':     ('至{date}', 'Through {date}'),
-    'fig.vibbar_note': ('条为本台与全场中位之比; 色按 z (稳健离群度): 绿 <2 · 橙 2~3 · 红 ≥3',
-                        'Ratio of this unit to the fleet median; colour by z (robust outlier score): green <2, amber 2–3, red ≥3'),
-    'fig.point':        ('测点', 'Measurement point'),
-    'fig.x_median':     ('×中位', '× median'),
-    'fig.this_unit':    ('本台', 'This unit'),
-    'fig.fleet_median': ('全场中位', 'Fleet median'),
-    'fig.quartile':     ('四分位', 'Quartile band'),
-    'fig.trend':        ('趋势', 'Trend'),
-})
-
-
-# ══════════════════════════════════════════════════════════════════
-#  取用
-# ══════════════════════════════════════════════════════════════════
-
-# Display-only fallback for source evidence labels; retain the original IDs.
-from .data_tpl_en import GRADE, GRADE2
-L.update({'tier.detail.' + zh: (zh, en) for zh, en in {**GRADE, **GRADE2}.items()})
-
-LANGS = ('zh', 'en')
-_IDX = {'zh': 0, 'en': 1}
-
-
-def t(key: str, lang: str = 'zh', **kw) -> str:
-    """取一条文案。key 不存在 = **抛错**, 不静默退回中文。
-
-    静默退回是旧架构最贵的一课: 页面上多一段中文, 没有任何日志, 只有用户看得见。
-    """
-    if key not in L:
-        raise KeyError('语言包无此 key: %r' % key)
-    s = L[key][_IDX[lang]]
-    return s.format(**kw) if kw else s
-
-
-def pack(lang: str) -> dict[str, str]:
-    """整包导出, 供注入页面 (`const L = {...}`)。"""
-    i = _IDX[lang]
-    return {k: v[i] for k, v in L.items()}
-
-
-def check() -> list[str]:
-    """构建期硬闸。返回问题清单, 空 = 通过。
-
-    在**出页面之前**跑。旧架构把这些留到运行期, 于是每一条都变成一次线上残留。
-    """
-    bad = []
-    for k, v in sorted(L.items()):
-        if not isinstance(v, tuple) or len(v) != 2:
-            bad.append('%s: 值不是 (zh, en) 二元组' % k)
-            continue
-        zh, en = v
-        if not zh.strip():
-            bad.append('%s: 中文为空' % k)
-        if not en.strip():
-            bad.append('%s: 英文为空' % k)
-        # 英文条目里不许残留中日韩字符
-        cjk = [c for c in en if '一' <= c <= '鿿']
-        if cjk:
-            bad.append('%s: 英文含中文字符 %r' % (k, ''.join(cjk)[:12]))
-        low = en.lower()
-        for phrase in PUBLIC_COPY_BANNED_EN:
-            if phrase in low:
-                bad.append('%s: 英文含内部审核黑话 %r' % (k, phrase))
-        # 占位符集合必须一致 —— 英文漏掉一个 {n}, 渲染不报错, 数字凭空消失
-        pz, pe = set(_PH.findall(zh)), set(_PH.findall(en))
-        if pz != pe:
-            bad.append('%s: 占位符不一致 zh=%s en=%s' % (k, sorted(pz), sorted(pe)))
-    return bad
+# -*- coding: utf-8 -*-
+"""双语语言包 —— 一条文案一个 key, 中英并排。
+
+**为什么重做**: 旧办法是"中文页面 + 三层事后翻译" (源层 ui_en.apply / API 投影 _proj /
+显示层 MutationObserver)。那条路的症状不是一个个 bug, 是架构的必然产物:
+
+  · 6.6 万处 [translation pending] —— 查不到就留个标记, 靠显示层再藏起来
+  · placeholder / title / aria-label 翻不到 —— 它们是**属性**, 文本节点遍历扫不着
+  · 重渲染后回退中文 —— 翻译是一次性覆盖, DOM 重建就没了
+  · 正则保护三次翻车 (见 ui_en.py 顶部注释) —— 跨越把整段 HTML 藏起来, 且**静默**
+
+本模块把它反过来: **页面按语言渲染, 不存在"事后翻译"这一步**。缺英文在**构建期**
+就是硬错误 (check() 报出), 而不是到了用户眼前才变成一处中文残留。
+
+## 约定
+
+1. key 用**语义名**不用序号 —— `kpi.availability` 而非 `k017`。序号在增删条目时会漂,
+   而漂了之后是静默错位 (甲的文案落到乙的位置), 正是最难查的一类。
+2. 占位符用**命名式** `{n}` 不用位置式 `%s` —— 中英语序不同, 位置式无法重排。
+   例: ('{n} 条 · {m} 个码', '{n} alarms across {m} codes')
+3. 两语的占位符集合必须**完全一致** —— check() 硬闸。英文漏掉一个 {n} 是经典缺陷:
+   渲染出来不报错, 只是那个数字凭空消失了。
+4. 中文是**正本**。英文缺失 = 构建期 FAIL; 中文缺失 = key 不存在, 同样 FAIL。
+"""
+from __future__ import annotations
+
+import re
+
+_PH = re.compile(r'\{([a-zA-Z_][a-zA-Z0-9_]*)\}')
+PUBLIC_COPY_BANNED_EN = [
+    'handoff v2',
+    'handoff v2.2',
+    'vibration-line design v1',
+    'structure from vibration-line design',
+    'transcribed verbatim',
+    'verbatim transcription',
+    'source text transcribed',
+    'raw source transcription',
+    'fusion issues no ratings',
+    'fusion does not assign grades',
+    'issues no grades',
+    'hardest evidence',
+    'strongest evidence that the detection chain closes',
+    'no work order to match',
+    'cannot be matched to work orders',
+    'currently rising',
+    'blocked at',
+    'caveat',
+    'chinese originals',
+    'original record (chinese)',
+    'transcribed as-is',
+    'in the handoff',
+    'causality is transcribed',
+    'ratings are transcribed',
+    'transcribed from the windcms report',
+    'transcribed from the vibration line',
+    'owner basis',
+    'characteristic frequencies',
+    'characteristic-frequency',
+    'characteristic frequency',
+    'spectra',
+    'waveforms',
+    'bpfi',
+    'bpfo',
+    'ftf',
+    'bsf',
+]
+
+
+# ══════════════════════════════════════════════════════════════════
+#  语言包本体: key -> (中文, English)
+#  英文来源标注: [收割] = 取自 ui_en.PHRASES (厂商对译表/历次人工校订积累),
+#              [新] = 本次新写。收割优先 —— 那批译文经过用户逐轮点检, 比重译可靠。
+# ══════════════════════════════════════════════════════════════════
+
+L: dict[str, tuple[str, str]] = {}
+
+
+def _add(group: dict[str, tuple[str, str]]) -> None:
+    dup = set(group) & set(L)
+    if dup:
+        raise KeyError('语言包 key 重复: %s' % sorted(dup))
+    L.update(group)
+
+
+# ── 外壳 ──────────────────────────────────────────────────────────
+_add({
+    'app.title':      ('观澜 · 风电数据分析与决策支持', 'Guanlan · Wind Analytics and Decision Support'),
+    'app.brand':      ('观澜', 'Guanlan'),
+    'app.subtitle':   ('海上风电场', 'Offshore Fleet'),
+    'app.build':      ('最新完成构建', 'Latest completed build'),
+    'app.lang_zh':    ('中文', 'Chinese'),
+    'app.lang_en':    ('英文', 'English'),
+})
+
+# ── 页签 (英文全部 [收割]) ─────────────────────────────────────────
+_add({
+    'tab.overview':    ('总览', 'Overview'),
+    'tab.energy':      ('电量算账', 'Energy Accounting'),
+    'tab.component':   ('部件问题', 'Component Issues'),
+    'tab.vibration':   ('振动融合分析', 'Vibration Fusion'),
+    'tab.generation':  ('发电性能', 'Generation Performance'),
+    'tab.fault':       ('故障统计', 'Fault Statistics'),
+    'tab.decision':    ('检修决策', 'Maintenance Decisions'),
+    'tab.assistant':   ('检修助手', 'Maintenance Assistant'),
+    'tab.report':      ('汇报定制', 'Report Builder'),
+    'tab.system':      ('系统维护', 'System Maintenance'),
+    'tab.ontology':    ('本体', 'Ontology'),
+})
+
+# ── 通用动作与状态 ────────────────────────────────────────────────
+_add({
+    'ui.query':        ('查询', 'Query'),
+    'ui.loading':      ('载入中…', 'Loading…'),
+    'ui.empty':        ('无数据', 'No data'),
+    'ui.error':        ('出错了', 'Something went wrong'),
+    'ui.retry':        ('重试', 'Retry'),
+    'ui.expand':       ('展开', 'Expand'),
+    'ui.collapse':     ('收起', 'Collapse'),
+    'ui.all':          ('全部', 'All'),
+    'ui.unit':         ('机组', 'Unit'),
+    'ui.units':        ('机组', 'Units'),
+    'ui.system':       ('系统', 'System'),
+    'ui.date':         ('日期', 'Date'),
+    'ui.count':        ('条数', 'Count'),
+    'ui.not_recorded': ('台账未填', 'Not recorded in ledger'),
+})
+
+# ── 检修助手 (含此前 placeholder 漏译那条) ─────────────────────────
+_add({
+    'asst.head':        ('检修助手 — 确定性本体查询',
+                         'Maintenance Assistant — field guidance with reviewed English'),
+    'asst.head_note':   ('结果逐字取自手册与工单, 不经模型',
+                         'Grounded in manuals, fault records and work orders, with source references retained for traceability'),
+    'asst.corrective':  ('抢修', 'Corrective'),
+    'asst.corrective_flow': ('报警码 → 失效模式 → 手册分步处置 → 历史工单',
+                             'Alarm code → failure mode → manual step-by-step procedure → historical work orders'),
+    # ★这一条就是线上那处中文残留 —— placeholder 是属性, 旧显示层翻不到。
+    #   在新架构里它和别的文案没有区别: 一个 key, 两种语言, 渲染时选一个。
+    'asst.code_placeholder': ('报警码, 如 3225 / 3130 / 13902',
+                              'Alarm code, e.g. 3225 / 3130 / 13902'),
+    'asst.common':      ('常见', 'Common'),
+    'asst.preventive':  ('预防性维护', 'Preventive maintenance'),
+    'asst.preventive_flow': ('系统 → 失效模式 → 年度任务 → 作业指导步骤',
+                             'System → failure mode → annual task → work-instruction steps'),
+    'asst.faulttree':   ('故障树', 'Fault tree'),
+    'asst.faulttree_flow': ('按证据量排序的系统失效模式 + 部件级成因',
+                            'System failure modes ranked by evidence volume, with component-level cause'),
+})
+
+# ── 九系统 (交付分册口径; 与本体 component/fleet/* 同名) ────────────
+_add({
+    'sys.blade':        ('叶片与叶根', 'Blade and blade root'),
+    'sys.pitch':        ('变桨', 'Pitch'),
+    'sys.yaw':          ('偏航', 'Yaw'),
+    'sys.gearbox':      ('齿轮箱', 'Gearbox'),
+    'sys.generator':    ('发电机', 'Generator'),
+    'sys.main_bearing': ('主轴承', 'Main bearing'),
+    'sys.converter':    ('变流器', 'Converter'),
+    'sys.tower':        ('塔架与基础', 'Tower and foundation'),
+    'sys.control':      ('主控与传感网', 'Control and sensor network'),
+})
+
+# ── 设备状态 (业主口径, 与 windcms 报告同一套) ──────────────────────
+_add({
+    'st.bad':          ('危险', 'Critical'),
+    'st.warn':         ('报警', 'Alarm'),
+    'st.unk':          ('不可判', 'Undeterminable'),
+    'st.ok':           ('良好', 'Good'),
+    'st.excellent':    ('优秀', 'Excellent'),
+    'st.ok_excellent': ('良好 / 优秀', 'Good / Excellent'),
+})
+
+# ── 证据级 (SOP §0.2 六枚举) —— 盖成印 ─────────────────────────────
+_add({
+    'tier.confirmed':    ('定论', 'Confirmed'),
+    'tier.near':         ('准定论·预警', 'Near-confirmed · warning'),
+    'tier.candidate':    ('候选', 'Candidate'),
+    'tier.reference':    ('参考', 'Reference'),
+    'tier.insufficient': ('不足', 'Insufficient'),
+    'tier.withdrawn':    ('撤回', 'Withdrawn'),
+    # 印面短形 (卡上空间小; 全称进悬停)
+    'tier.confirmed.s':    ('定论', 'Conf.'),
+    'tier.near.s':         ('准定论', 'Near'),
+    'tier.candidate.s':    ('候选', 'Cand.'),
+    'tier.reference.s':    ('参考', 'Ref.'),
+    'tier.insufficient.s': ('不足', 'Insuf.'),
+    'tier.withdrawn.s':    ('撤回', 'Withd.'),
+})
+
+# ── 审级 (SOP §6.1 RV-1) ─────────────────────────────────────────
+_add({
+    'rv.dual':  ('双模型审', 'DUAL-MODEL'),
+    'rv.advci': ('AdvCI', 'ADVCI'),
+    'rv.lite':  ('lite', 'LITE'),
+    'rv.none':  ('未审', 'UNREVIEWED'),
+})
+
+# ── 总览 (设计基准 docs/design/guanlan_v2.html) ───────────────────
+_add({
+    'ov.attention':      ('需要关注', 'Needs attention'),
+    # 融合面缺件时的说明(2026-09-19 用户报「需要关注/全场状态下面没有计算输出」后补):
+    # 缺件就该说清"缺哪件、怎么补",不让两片空白被读成"没有需要关注的台"。
+    'ov.fus_gap_hint':   ('这一面(需要关注 / 全场状态)取数来自融合面;缺件时如实为空,不从交付包补齐。'
+                          '在本机放好原始件后跑一次重算即会由观澜自己的生成端补上'
+                          '(scripts/rudong_fusion_handoff.py,见 docs 的「源代码化清单」)。',
+                          'This block (needs attention / fleet status) draws on the fusion layer. '
+                          'When that product is missing it stays honestly empty rather than being filled from a delivery package; '
+                          'after placing the raw data, one recompute run rebuilds it from source (scripts/rudong_fusion_handoff.py).'),
+    'ov.attention_n':    ('{n} 台', '{n} units'),
+    'ov.ranked_by':      ('按 设备状态 × 证据量 排序', 'Ranked by equipment state × evidence weight'),
+    'ov.strip':          ('全场状态', 'Fleet status'),
+    'ov.strip_hint':     ('一格一台 · 点格定位', 'One cell per unit · click to locate'),
+    'ov.quiet':          ('其余 {n} 台未进入优先关注清单;设备状态请查看全场状态评估。',
+                          'The other {n} units are outside the priority list. Refer to fleet status for their equipment assessments.'),
+    'ov.quiet_all':      ('当前优先关注清单为空;{n} 台机组的设备状态请查看全场状态评估。', 'The priority list is empty. Refer to fleet status for the assessments of all {n} units.'),
+    'ov.caveat':         ('限定', 'Follow-up'),
+    'ov.systems':        ('九系统', 'Nine systems'),
+    'ov.systems_hint':   ('点系统看逐台依据', 'Open a system for per-unit evidence'),
+    'ov.illustrative':   ('示意数据', 'Illustrative'),
+    'ui.open':           ('查看 →', 'Open →'),
+    'ui.back_to':        ('← 返回{tab}', '← Back to {tab}'),
+    'ui.back_portal':    ('← 返回门户', '← Back to portal'),
+    'rpt.facts.title': ('可交付级结论 · 契约生成', 'Deliverable-grade conclusions · from facts contract'),
+    'rpt.facts.sub': ('{n} 条, 契约 {sha}; 数据/时间窗/结论只经契约更新', '{n} claims, contract {sha}; data / windows / verdicts change only via the contract'),
+    'rpt.facts.verdict': ('裁决', 'Verdict'), 'rpt.facts.text': ('结论', 'Conclusion'), 'rpt.facts.sys': ('系统', 'System'), 'rpt.facts.agg': ('聚合层', 'Aggregation'),
+    'ui.reselect':       ('重新选择机组 / 测点', 'Reselect unit / point'),
+})
+
+# ── KPI ───────────────────────────────────────────────────────────
+_add({
+    'kpi.avail':        ('时间可用率 (不含调度令) · {win}', 'Time-based availability (excl. dispatch) · {win}'),
+    'kpi.downtime':     ('停机时长 · {n} 起', 'Downtime · {n} events'),
+    'kpi.mtbo':         ('平均停机间隔(MTBO;非行业 MTBF)', 'Mean time between outages (MTBO; not industry MTBF)'),
+    'kpi.loss':         ('停机电量损失 · {win}', 'Downtime energy loss · {win}'),
+    'kpi.loss_dispatch':('另调度令 {n}', 'plus {n} under dispatch orders'),
+    'kpi.curtail':      ('限电·绑定 电量损失', 'Curtailment (bound) energy loss'),
+    'kpi.alarm_units':  ('九系统有报警的台数 / 全场', 'Units with a system-level alarm / fleet'),
+    'kpi.alarm_units.hint': ('口径: 九系统 (叶片与叶根/变桨/偏航/齿轮箱/发电机/主轴承/变流器/塔架与基础/主控与传感网) 中任一被判「报警」即计一台。'
+                             '与下方「全场状态」格的口径不同 —— 那里按每台 振动/温度/润滑/油液/报警/工单 六列取最严一级, 所以两处数字可以不同, 不是矛盾。',
+                            'Caliber: a unit counts if ANY of the nine systems is rated "alarm". The fleet strip below uses a different axis '
+                            '(worst of vibration/temperature/lubrication/oil/alarm/work-order per unit), so the two numbers can differ by design.'),
+})
+
+# ── 证据级补全: API 实际发出的枚举 (fus.色标) 一条不落 ──────────────
+_add({
+    'tier.candidate_new':      ('候选·新发', 'Candidate · new'),
+    'tier.candidate_baseline': ('候选·记基线', 'Candidate · baseline'),
+    'tier.normal':             ('正常', 'Normal'),
+    'tier.candidate_new.s':      ('候选·新发', 'Cand. new'),
+    'tier.candidate_baseline.s': ('候选·记基线', 'Cand. base'),
+    'tier.normal.s':             ('正常', 'Normal'),
+})
+
+# ── 链盘六步 (fus.链盘.rows[].steps[].k) ─────────────────────────
+_add({
+    'step.evidence':   ('证据', 'Evidence'),
+    'step.site_records': ('现场记录', 'site records'),
+    'step.mechanism':  ('机制', 'Mechanism'),
+    'step.rating':     ('判级', 'Rating'),
+    'step.schedule':   ('排期', 'Schedule'),
+    'step.action':     ('动作', 'Action'),
+    'step.acceptance': ('验收', 'Acceptance'),
+    'step.stuck_at':   ('卡在「{step}」', 'Pending {step}'),
+})
+
+# ── 时间窗预设 ────────────────────────────────────────────────────
+_add({
+    'win.label':   ('时间窗', 'Window'),
+    'win.presets': ('预设时间窗', 'Preset windows'),
+    'win.months':  ('按月', 'By month'),
+    # ★自定义起止日期窗(用户令 2026-09-21):含两端;月度件按月取整,日粒度件按日精确
+    'win.custom':      ('自定义', 'Custom'),
+    'win.custom_hint': ('自定义起止日期(含两端)。日粒度件(判级/停机台账/曲线)按日精确;'
+                        '月度聚合件按所跨月份取整,图上会写明。',
+                        'Custom inclusive date range. Day-resolution items (grading, stop ledger, curves) are exact; '
+                        'monthly aggregates round to the spanned months and say so on the chart.'),
+    'win.from':        ('开始日期(含)', 'Start date (inclusive)'),
+    'win.to':          ('结束日期(含)', 'End date (inclusive)'),
+    'win.apply':       ('应用', 'Apply'),
+    'win.need_both':   ('请把开始与结束日期都选上', 'Pick both start and end dates'),
+    'win.bad_order':   ('开始日期不能晚于结束日期', 'Start date must not be after the end date'),
+    'win.custom_sel':  ('自定义 {a} ~ {b}', 'Custom {a} to {b}'),
+    'cmp.grading_pending': ('正在按所选时间窗重算判级…', 'Recomputing grading for the selected window…'),
+    'cmp.grading_pending_why': ('时间窗 {span}:变桨/偏航/蓄能/温度四个判级面正按该时间窗重算(首次约 25 秒),'
+                                '下面的系统卡暂时还是上一份口径 —— 算完会自动刷新,不会静默混用。',
+                                'Window {span}: the four grading axes (pitch / yaw / accumulator / temperature) '
+                                'are being recomputed for it (about 25 s the first time). The cards below still show '
+                                'the previous grading until it lands, then refresh automatically — no silent mixing.'),
+    'win.30d':     ('近30日', 'Last 30 days'),
+    'win.90d':     ('近90日', 'Last 90 days'),
+    'win.2026':    ('2026年', '2026'),
+    'win.2026H1':  ('2026H1', '2026 H1'),
+    'win.2025H2':  ('2025H2', '2025 H2'),
+    'win.all':     ('全程', 'Full record'),
+})
+
+# ── 迁移期 ────────────────────────────────────────────────────────
+_add({
+    'ui.not_ported':   ('本页尚未迁入新版。', 'This page has not been ported to the new build yet.'),
+    'ui.open_classic': ('在经典版打开 →', 'Open in classic build →'),
+    'ui.data_hidden':  ('({n} 处数据文本尚无英文,需复核)', '({n} data strings still require English translation review)'),
+    # 手册/工单**原文**与"字段"不同: 隐藏它等于把处置步骤整个抹掉。按原文转录并标注 (2026-09-03 取舍, 可推翻)。
+    'ui.verbatim':      ('原始记录 (中文)', 'Translation review required'),
+    'ui.verbatim_note': ('{n} 处为手册/工单原文, 无英文译本, 按原文转录。',
+                         '{n} source passages require translation review and are withheld from the public English view.'),
+})
+
+# ── 检修助手 · 结果渲染 ────────────────────────────────────────────
+_add({
+    'asst.querying':     ('查询中…', 'Querying…'),
+    'asst.query_failed': ('查询失败: {e}', 'Query failed: {e}'),
+    'asst.identity':     ('系统 {sys} · 本场 {n} 条', 'System {sys} · {n} site alarms'),
+    'asst.alarm_type':   ('告警类型 {t}', 'alarm type {t}'),
+    'asst.unassigned':   ('未归属', 'unassigned'),
+    'asst.no_name':      ('(码表未收录)', '(not in the code table)'),
+    'asst.failmodes':    ('指向的失效模式', 'Failure modes indicated'),
+    'asst.fm_counts':    ('该类本场报警 {a} 条 / 工单 {w} 条', '{a} site alarms / {w} work orders in this class'),
+    'asst.steps':        ('现场处置 {n} 步', 'Field procedure · {n} steps'),
+    'asst.steps_note':   ('节点部件 = 手册原文元件位号 (确定); 判定规范 = 实机参数表按物理量匹配的候选判据, 非厂商绑定 — 现场以铭牌与图纸复核。',
+                          "Node components are the manual's own designators (definite); acceptance specs are candidate criteria matched from the live parameter table by physical quantity, not vendor-bound — verify against nameplate and drawings on site."),
+    'asst.step_n':       ('第 {n} 步', 'Step {n}'),
+    'asst.qty':          ('量: {q}', 'quantity: {q}'),
+    'asst.tags':         ('节点部件', 'Node components'),
+    'asst.nodes':        ('应检节点 · {c}', 'Nodes to check · {c}'),
+    'asst.nodes_note':   ('手册此步未给位号 → 按回路构成展开, 逐点查', 'no designator given for this step → expanded by circuit composition, check point by point'),
+    'asst.node':         ('节点', 'Node'),
+    'asst.check':        ('查什么', 'What to check'),
+    'asst.known_tags':   ('该系统手册出现过的位号 (对图纸时参考):', "Designators seen in this system's manual (for drawing cross-reference):"),
+    'asst.specs':        ('判定规范 (实机参数表)', 'Acceptance specs (live parameter table)'),
+    'asst.spec_no':      ('参数号', 'Param'),
+    'asst.spec_name':    ('名称', 'Name'),
+    'asst.spec_val':     ('整定值', 'Setting'),
+    'asst.spec_range':   ('允许范围', 'Range'),
+    'asst.drawings':     ('对应图纸', 'Related drawings'),
+    'asst.no_steps':     ('本码无手册处置条目 (手册覆盖本场 161/322 码) — 需查图纸或联系厂商支持。',
+                          'No manual procedure for this code (the manual covers 161 of 322 site codes) — consult drawings or vendor support.'),
+    'asst.wo_head':      ('现场实际怎么修的 ({sys} 历史工单 {n} 条, 台账 2020~2024)',
+                          'What was actually done on site ({sys}: {n} historical work orders, ledger 2020–2024)'),
+    'asst.wo_fault':     ('故障', 'Fault'),
+    'asst.wo_action':    ('处置', 'Action'),
+    'asst.wo_note':      ('工单时间窗止 2024-11-21; 2025–26 现场未提供 ⇒ "0 条" 是记录缺失非未修过。',
+                          'Work-order window ends 2024-11-21; 2025–26 not provided by the site, so "0" means missing records, not no repair.'),
+    'asst.tree_head':    ('{sys} 故障树 · {n} 个失效模式', '{sys} fault tree · {n} failure modes'),
+    'asst.tree_note':    ('一层 = 系统 × 失效类 (归纳自本场码表与工单) · 二层 = 手册排查方向归一 + 工单原因计数; 条长按实证量 (报警条数 + 工单 × 10), 因果来自手册与工单原文非推断, 树深两层不外推。',
+                          'Level 1 groups site systems by failure class; level 2 merges manual troubleshooting directions with work-order cause counts. Bar length reflects available evidence (alarms + work orders × 10), and the tree stops at two levels.'),
+    'asst.tree_counts':  ('本场报警 {a} 条 · 工单 {w} 条', '{a} site alarms · {w} work orders'),
+    'asst.tree_codes':   ('征兆码 {c}', 'symptom codes {c}'),
+    'asst.causes':       ('← 可能原因 (手册排查方向 + 工单实证)', '← Possible causes (manual directions + work-order evidence)'),
+    'asst.cause_counts': ('手册 {r} 次 · 工单 {w} 次 · 涉码 {c}', 'manual ×{r} · work orders ×{w} · codes {c}'),
+    'asst.no_causes':    ('(手册无该类排查条目 — 原因层待补)', '(no troubleshooting entries in the manual for this class — cause layer pending)'),
+    'asst.tree_empty':   ('该系统无失效模式记录', 'No failure modes recorded for this system'),
+    'asst.prev_head':    ('{sys} 预防链 · 失效模式 {f} 类 · 预防性任务 {n} 项', '{sys} preventive chain · {f} failure-mode classes · {n} preventive tasks'),
+    'asst.prev_fm':      ('失效模式 (按本场报警量)', 'Failure modes (by site alarm volume)'),
+    'asst.prev_tasks':   ('年度任务', 'Annual tasks'),
+    'asst.interval':     ('周期 {i}', 'interval {i}'),
+    'asst.wi_steps':     ('WI {n} 步', 'WI · {n} steps'),
+    'asst.prev_empty':   ('(该系统无匹配的年度维护条目 — 维护作业指导书按位置代号编排, 未必逐系统对齐)',
+                          '(no annual maintenance entries matched to this system — the work-instruction book is organised by location code and does not map one-to-one onto systems)'),
+})
+
+# ── 检修决策 · 本体问答 (模型参与的板块) ────────────────────────────
+_add({
+    'dec.head':        ('检修决策 · 本体问答 (推理无界, 结论受闸)',
+                        'Maintenance decisions · ontology Q&A (unbounded reasoning, gated conclusions)'),
+    'dec.classic':     ('维护标准 / 维护框架 / 数据质量三块尚未迁入新版 —',
+                        'The maintenance-standard, framework and data-quality blocks are not ported yet —'),
+    'ask.common_q':    ('{cat} · 常用问题 (点即问, 或下方自由输入)', '{cat} · common questions (click to ask, or type below)'),
+    'ask.grounding':   ('答案句句带 [对象id] 溯源 + 接地闸核验; 判级只转述本体原文带审级, 模型零判级权。',
+                        'Every answer carries an [object id] and uses the reviewed evidence layer; model responses cannot create or change equipment ratings.'),
+    'ask.placeholder': ('自由提问 ({cat})', 'Ask anything ({cat})'),
+    'ask.model':       ('模型', 'Model'),
+    'ask.no_models':   ('无可用模型', 'No models available'),
+    'ask.refs.title':  ('契约引用 ({n} 条, 契约 {sha}) — 按问题/回答匹配的可交付结论, 引文来自 qa_refs', 'Contract citations ({n}, contract {sha}) — deliverable claims matched to the question/answer; cites from qa_refs'),
+    'ask.refs.none':   ('无匹配契约条目 (契约 {sha}): 本回答未被结构化事实契约背书, 按模型输出对待', 'No matching contract claim (contract {sha}): this answer is not backed by the facts contract; treat as model output'),
+    'ask.local_down':  ('本机模型未启动 (Ollama 127.0.0.1:11434 未响应); 离线版不转云', 'Local model not running (Ollama 127.0.0.1:11434 unreachable); offline build never falls back to cloud'),
+    'ask.partial':     ('初答 · 27B 复核中', 'Quick answer · 27B refining'),
+    'ask.refined':     ('已复核 (默认档 {m} 未过校闸, 由更强模型重答)', 'Refined (default tier {m} failed the gate; re-answered by the stronger model)'),
+    'ask.xrev':        ('交叉审核', 'Cross-review'),
+    'ask.xrev_note':   ('(DeepSeek 14B 互审, +约 1 分钟)', '(cross-checked by DeepSeek 14B, + about 1 min)'),
+    'ask.xrev_title':  ('由另一模型基于同一份证据复核; 同一模型自审无法构成独立校验',
+                        'A second model reviews on the same evidence; self-review by one model is not independent'),
+    'ask.go':          ('提问', 'Ask'),
+    'ask.idle':        ('空闲 — 点常用问题或输入后提问。串行一次一问。', 'Idle — click a common question or type one. One question at a time.'),
+    'ask.empty':       ('问题为空', 'The question is empty'),
+    'ask.running':     ('推理中…', 'Reasoning…'),
+    'ask.elapsed':     ('{s} 秒', '{s} s'),
+    'ask.question':    ('问题: {q}', 'Question: {q}'),
+    'ask.verify_ok':   ('溯源核验 {n}/{n} 全真', 'Traceability check {n}/{n} verified'),
+    'ask.verify_bad':  ('溯源核验未全过 (引用 {n})', 'Traceability check incomplete ({n} references)'),
+    'ask.done_meta':   ('模型 {m} · 用时 {s} 秒 · {n} 步', 'model {m} · {s} s · {n} steps'),
+    'ask.review':      ('交叉审核 · {m}', 'Cross-review · {m}'),
+    'ask.review_note': ('由另一模型基于同一份证据复核上述答案。溯源核验仅校验数字与机组号是否越界, 本项校验结论是否有证据支撑。',
+                        'A second model reviewed the answer on the same evidence. The traceability check only tests whether numbers and unit ids are in range; this review tests whether conclusions are supported by evidence.'),
+    'ask.review_err':  ('审核未完成: {e}', 'Review incomplete: {e}'),
+    'ask.error':       ('出错: {e}', 'Error: {e}'),
+    'ask.offline':     ('离线版: 模型问答需连接系统。', 'Offline copy: the model Q&A needs a live connection to the system.'),
+    'ask.en_qa_failed':('英文质检未通过: 已拦截这次模型回答, 请重新提问。',
+                        'English QA failed: this model answer was withheld. Please ask again.'),
+})
+
+# ── 模型档位说明 (id 固定集合; 未知 id 一律按"会外发"处理 —— 宁可说重不说轻) ──
+_add({
+    'mdl.local':          ('本地', 'local'),
+    'mdl.cloud':          ('云端', 'cloud'),
+    'mdl.zero_outbound':  ('全程零外发', 'Nothing leaves the machine'),
+    'mdl.outbound_warn':  ('内容会外发到模型厂商, 场名与台号已去标识化', 'Content is sent to the model vendor; site name and unit ids are de-identified'),
+    'mdl.unknown_warn':   ('未知档位, 按内容会外发处理', 'Unknown tier — treated as sending content externally'),
+    'mdl.qwen3:8b.nm':        ('轻量 8B · 默认', 'Light 8B · default'),
+    'mdl.qwen3:8b.t':         ('约 30 到 60 秒', 'about 30–60 s'),
+    'mdl.qwen3.8:27b.nm':     ('标准 27B · 复杂题', 'Standard 27B · complex'),
+    'mdl.qwen3.8:27b.t':      ('约 2 到 4 分钟', 'about 2–4 min'),
+    'mdl.qwen3:30b-a3b.nm':   ('快 30B (a3b)', 'Fast 30B (a3b)'),
+    'mdl.qwen3:30b-a3b.t':    ('约 20 到 60 秒', 'about 20–60 s'),
+    'mdl.deepseek-r1:14b.nm': ('本机 DeepSeek 14B · 审核档', 'Local DeepSeek 14B · reviewer'),
+    'mdl.deepseek-r1:14b.t':  ('约 1 到 3 分钟', 'about 1–3 min'),
+    'mdl.deepseek-r1:32b.nm': ('本机 DeepSeek 32B', 'Local DeepSeek 32B'),
+    'mdl.deepseek-r1:32b.t':  ('约 3 到 8 分钟', 'about 3–8 min'),
+    'mdl.deepseek-chat.nm':   ('DeepSeek', 'DeepSeek'),
+    'mdl.deepseek-chat.t':    ('约 3 到 10 秒', 'about 3–10 s'),
+    'mdl.kimi-fast.nm':       ('Kimi', 'Kimi'),
+    'mdl.kimi-fast.t':        ('约 5 到 20 秒', 'about 5–20 s'),
+    'mdl.qwen-plus.nm':       ('通义千问', 'Qwen Plus'),
+    'mdl.qwen-plus.t':        ('约 5 到 20 秒', 'about 5–20 s'),
+    'mdl.deepseek-v4-pro.nm': ('云端 DS', 'DeepSeek Pro'),
+    'mdl.deepseek-v4-pro.t':  ('约 2 到 6 分钟', 'about 2–6 min'),
+})
+
+# ── 问答四类 (API 的 cat 参数取中文名, 前端按 id 映射) ───────────────
+_add({
+    'cat.equip':      ('设备类', 'Equipment'),
+    'cat.equip.hint': ('设备状态 / 机制 / 判级 — 从机组档案、对象查询与机制检索取证',
+                       'Equipment state / mechanism / rating — evidence from unit profiles, object queries and mechanism search'),
+    'cat.equip.q1':   ('29号机组现在什么状态、为什么、该怎么办?', 'What is the state of WTG29, why, and what should be done?'),
+    'cat.equip.q2':   ('全场哪些机组有报警级或定论级判级?逐台列出并注明审级', 'Which units carry alarm-level or confirmed-tier ratings? List each with its review level.'),
+    'cat.equip.q3':   ('失效机制库里有哪些机制族?各涉及哪些机组、状态如何?', 'Which mechanism families are in the failure-mechanism library, which units do they involve, and in what state?'),
+    'cat.sched':      ('检修排程决策', 'Maintenance scheduling'),
+    'cat.sched.hint': ('排期沙盘 / 天气窗 / 验收树 — ont_scenario / ont_get 取证',
+                       'Scheduling sandbox / weather windows / acceptance tree — evidence via ont_scenario / ont_get'),
+    'cat.sched.q1':   ('29号修泵什么时候做最合适?对比各情景的损失与风险', 'When is the best time to repair the pump on WTG29? Compare loss and risk across scenarios.'),
+    'cat.sched.q2':   ('未来几个月哪些月份适合安排CTV检修和吊装作业?依据历史可作业窗', 'Which of the coming months suit CTV maintenance and lifting work, based on historical workable windows?'),
+    'cat.sched.q3':   ('29号修好之后按什么判据验收?三分支分别是什么条件?', 'After the repair on WTG29, what acceptance criteria apply, and what are the three branch conditions?'),
+    'cat.loss':       ('损失分析', 'Loss analysis'),
+    'cat.loss.hint':  ('当前 3 个月损失 / 未来损失 — ont_loss / ont_scenario 取证, 金额带电价区间口径',
+                       'Current 3-month and forward-looking loss estimates — values are shown as tariff ranges'),
+    'cat.loss.q1':    ('近3个月全场损失结构如何?分状态和分机组说, 电量与金额都给', 'What is the fleet loss structure over the last 3 months? By state and by unit, energy and value.'),
+    'cat.loss.q2':    ('29号如果拖到主轴承失效, 未来损失量级是多少?与现在修对比', 'If WTG29 runs to main-bearing failure, what is the future loss magnitude versus repairing now?'),
+    'cat.loss.q3':    ('限电常态化对检修机会成本有什么影响?', 'How does persistent curtailment change the opportunity cost of maintenance?'),
+    'cat.risk':       ('安全风险预测', 'Safety-risk outlook'),
+    'cat.risk.hint':  ('风险敞口 / 劣化趋势 / 盲区 — ont_risk / ont_mechanism_search 取证, 未列 ≠ 正常',
+                       'Risk exposure / degradation trend / blind spots — evidence via ont_risk / ont_mechanism_search; unlisted ≠ normal'),
+    'cat.risk.q1':    ('当前哪些机组风险在上升需要重点盯?联动监视读数如何?', 'Which units have rising risk and need close watching? What do the linked monitoring readings show?'),
+    'cat.risk.q2':    ('监测有哪些盲区?哪些"没报警"不等于正常?', 'Where are the monitoring blind spots? Which "no alarm" cases are not the same as normal?'),
+    'cat.risk.q3':    ('29号轴承正在承受金属接触, 风险敞口怎么评估?', "Unit 29's bearing is under metal-to-metal contact; how is the risk exposure assessed?"),
+})
+
+# ── 图元 (charts.js) ──────────────────────────────────────────────
+_add({
+    'ch.quad.few':     ('时间窗内码数不足, 不作四象限', 'Too few codes in the window for a quadrant plot'),
+    'ch.quad.q1':      ('低频长时 · 硬故障', 'Rare, long · hard faults'),      'ch.quad.q1s': ('备件与检修窗', 'spares and maintenance window'),
+    'ch.quad.q2':      ('高频长时 · 重点治理', 'Frequent, long · priority'),  'ch.quad.q2s': ('优先派工', 'dispatch first'),
+    'ch.quad.q3':      ('低频短时 · 本底', 'Rare, short · background'),        'ch.quad.q3s': ('不单独处置', 'no separate action'),
+    'ch.quad.q4':      ('高频短时 · 重复触发', 'Frequent, short · re-triggering'), 'ch.quad.q4s': ('调整参数或屏蔽, 非硬件故障', 'tune or mask; not hardware'),
+    'ch.quad.xlab':    ('发生条数 (对数)', 'Occurrences (log)'),
+    'ch.quad.ylab':    ('单次中位时长 (对数)', 'Median duration per event (log)'),
+    'ch.quad.tip':     ('{n} 条 · 单次中位 {med}s · 累计 {h}h · {nt} 台', '{n} events · median {med}s · total {h}h · {nt} units'),
+    'ch.rate.tip':     ('{m}: {v} 条/日 (总 {n} 条, 数据覆盖 {cov}%)', '{m}: {v}/day ({n} total, {cov}% coverage)'),
+    'ch.rate.month':   ('{m}月', '{m}'),
+    'ch.rate.n':       ('{n}条', '{n}'),
+    'ch.rate.days':    ('仅{d}天', '{d} d only'),
+    'ch.rate.ylab':    ('条/日', 'per day'),
+    'ch.wf.theo':      ('理论可发', 'Theoretical'),
+    'ch.wf.act':       ('实际上网', 'Delivered'),
+    'ch.wf.tip':       ('{nm}: 损失 {loss} MWh, 占理论 {pct}%, 时长 {h}h', '{nm}: loss {loss} MWh, {pct}% of theoretical, {h} h'),
+    'loss.curtail_bound':   ('限电 (命令与实际功率对得上)', 'Curtailment (command matches actual power)'),
+    'loss.curtail_unbound': ('限电命令期 · 功率未被压制', 'Curtailment command · power not suppressed'),
+    'loss.dispatch':        ('调度令停机', 'Dispatch-order outage'),
+    'loss.stop':            ('故障与检修停机', 'Fault and maintenance outage'),
+    'loss.normal':          ('正常发电时的欠发', 'Under-production while generating'),
+    'loss.lowwind':         ('低风待机', 'Low-wind standby'),
+    'ch.ram.few':      ('部件数不足, 不作象限', 'Too few components for a quadrant plot'),
+    'ch.ram.q1':       ('常坏且难修', 'Frequent and hard to fix'),  'ch.ram.q1s': ('最高优先级', 'highest priority'),
+    'ch.ram.q2':       ('罕发难修', 'Rare and hard to fix'),        'ch.ram.q2s': ('备件与作业窗', 'spares and work windows'),
+    'ch.ram.q3':       ('频发易修', 'Frequent and easy to fix'),    'ch.ram.q3s': ('查根因, 换件无用', 'find root cause; replacement will not help'),
+    'ch.ram.q4':       ('少发好修', 'Rare and easy to fix'),        'ch.ram.q4s': ('维持', 'maintain'),
+    'ch.ram.xlab':     ('平均无故障间隔(MTBF,h,对数)→  越右越可靠', 'MTBF (h, log) →  more reliable to the right'),
+    'ch.ram.ylab':     ('单次停机时长(MDT,h)→  越上越难修', 'MDT (h) →  harder to repair upward'),
+    'ch.ram.equip':    ('设备类', 'Equipment'),
+    'ch.ram.ext':      ('外部类', 'External'),
+    'ch.ram.area':     ('圆面积 = 累计停机时长', 'Area = total downtime'),
+    'ch.ram.median':   ('虚线 = 本场中位分界', 'Dashed = site median split'),
+    'ch.ram.tip':      ('{p}: 平均无故障间隔(MTBF){mtbf}h · 单次停机时长(MDT){mdt}h · 停机 {h}h · {n}起 · {u}台', '{p}: MTBF {mtbf}h · MDT {mdt}h · downtime {h}h · {n} events · {u} units'),
+    'ch.ram.tip_ext':  (' · 外部类不计设备可靠性', ' · external, excluded from equipment reliability'),
+    'ch.attrib.ok':    ('已归因到部件 {h}h ({p}%)', 'Attributed to a component {h}h ({p}%)'),
+    'ch.attrib.none':  ('无记录 {h}h ({p}%)', 'Unrecorded {h}h ({p}%)'),
+    'ch.attrib.uncls': ('未归类 {h}h', 'Unclassified {h}h'),
+    'ch.pareto.none':  ('时间窗内无数据', 'No data in the window'),
+    'ch.pareto.cum80': ('累计 80%', 'cumulative 80%'),
+    'ch.pareto.cumtip':('{k} · 累计 {p}%', '{k} · cumulative {p}%'),
+    'ch.pareto.xlab':  ('报警码 (悬停看全名)', 'Alarm code (hover for full name)'),
+    'ch.pareto.key':   ('关键少数: 前 {k} 项占 80%', 'Vital few: the first {k} items make 80%'),
+    'ch.pareto.total': ('共 {n} 项, 合计 {tot}{unit}', '{n} items, total {tot}{unit}'),
+    'ch.pareto.top3':  ('前三: ', 'Top three: '),
+    'ch.blame.n1':     ('交易与调度侧', 'Market and dispatch'),   'ch.blame.d1': ('限电与调度令, 不进设备责任', 'curtailment and dispatch orders; not equipment responsibility'),
+    'ch.blame.n2':     ('设备侧', 'Equipment'),                   'ch.blame.d2': ('故障与检修停机, 唯一进设备考核', 'fault and maintenance outages; the only part assessed against equipment'),
+    'ch.blame.n3':     ('运行侧', 'Operations'),                  'ch.blame.d3': ('正常发电欠发与低风待机', 'under-production while generating, and low-wind standby'),
+    'ch.blame.title':  ('损失 {tot} MWh 按责任归属', 'Loss {tot} MWh by responsibility'),
+    'unit.h':          ('h', 'h'), 'unit.n': (' 条', ' events'), 'unit.units': ('台', 'units'),
+})
+
+# ── 汇报定制 ──────────────────────────────────────────────────────
+_add({
+    'rpt.eyebrow':      ('汇报定制 · 画布', 'Report builder · canvas'),
+    'rpt.presets':      ('预设', 'Presets'),
+    'rpt.preset.month': ('月度运行分析', 'Monthly operations'), 'rpt.preset.week': ('周报', 'Weekly'),
+    'rpt.preset.topic': ('设备专题', 'Equipment topic'),       'rpt.preset.year': ('年度总结', 'Annual summary'),
+    'rpt.preset.blank': ('空白画布', 'Blank canvas'),
+    'rpt.aud_in':       ('场站内部 (全量)', 'Site internal (full)'),
+    'rpt.aud_up':       ('上报集团 (脱敏)', 'Group report (de-identified)'),
+    'rpt.print':        ('打印 / 存 PDF', 'Print / save PDF'),
+    'rpt.export_docx':  ('导出 Word', 'Export Word'),
+    'rpt.export_xlsx':  ('导出 Excel', 'Export Excel'),
+    'rpt.up_note':      ('上报版 — 判据门槛与特征频率已脱敏', 'Group version — criterion thresholds and detailed diagnostic markers are redacted'),
+    'rpt.library':      ('组件库', 'Component library'),
+    'rpt.library_note': ('点击加入报告, 再次点击移出 · 画布里已有 {n} 块', 'Click to add, click again to remove · {n} blocks on the canvas'),
+    'rpt.hint_idle':    ('悬停查看组件说明 · 已选组件再次点击移出 · 右侧 ↑↓ 调整顺序', 'Hover for a description · click a selected block to remove · ↑↓ to reorder'),
+    'rpt.need_head':    ('输入报告要求', 'Describe the report you need'),
+    'rpt.need_hint':    ('模型依据需求选取组件, 并基于实测数据生成运行总结', 'The model picks components for the request and writes a summary from measured data'),
+    'rpt.local_note':   ('; 数据不出本地', '; data stays local'),
+    'rpt.cloud_note':   ('; 内容会外发到模型厂商', '; content is sent to the model vendor'),
+    'rpt.need_ph':      ('例如: 上报集团月报, 侧重设备可靠性与重点机组', 'e.g. monthly report for the group, focused on equipment reliability and key units'),
+    'rpt.compose':      ('生成', 'Compose'),
+    'rpt.no_models':    ('本部署未配置任何模型密钥, 编排不可用', 'No model keys configured in this deployment; composing is unavailable'),
+    'rpt.no_compose':   ('本部署未启用编排接口, 画布仍可手动拼装', 'The compose endpoint is not enabled here; the canvas can still be assembled by hand'),
+    'rpt.need_empty':   ('先输入报告要求', 'Enter the report request first'),
+    'rpt.composing':    ('模型编排中… 挑组件 + 写总结', 'Composing… picking components and writing the summary'),
+    'rpt.compose_fail': ('生成失败: {e}', 'Compose failed: {e}'),
+    'rpt.composed':     ('已按需求编排 {n} 块{narr} · {m} 用时 {s}s', 'Composed {n} blocks{narr} · {m} in {s}s'),
+    'rpt.narr_plus':    (' + 一段运行总结', ' + a narrative summary'),
+    'rpt.ungrounded_n': ('{n} 个数字待核', '{n} numbers to verify'),
+    'rpt.grounded':     ('数字全部接地', 'all numbers grounded'),
+    'rpt.empty_canvas': ('画布是空的 — 先从组件库选内容', 'The canvas is empty — pick content from the library first'),
+    'rpt.preview_empty':('文档预览窗 — 从上面的组件库点选内容, 此处显示报告预览', 'Document preview — pick components above to see the report here'),
+    'rpt.paper_title':  ('运行分析报告', 'Operations analysis report'),
+    'rpt.paper_sub':    ('统计期 {win} | {n} 台', 'Period {win} | {n} units'),
+    'rpt.up_tag':       ('上报版', 'group version'),
+    'rpt.chapters':     ('{n} 个章节', '{n} sections'),
+    'rpt.foot_src':     ('数据源 SCADA 10min · 报警事件 · 工单台账 · CMS 振动 · 油液化验', 'Sources: SCADA 10-min · alarm events · work-order ledger · CMS vibration · oil analysis'),
+    'rpt.foot_asof':    ('出件 {d}', 'issued {d}'),
+    'rpt.foot_trace':   ('结论可回溯至本体对象', 'Conclusions trace back to ontology objects'),
+    'rpt.foot_deid':    ('判据门槛与特征频率已脱敏', 'thresholds and detailed diagnostic markers redacted'),
+    'rpt.caliber_lbl':  ('口径: ', 'Basis: '),
+    'rpt.g.gen': ('发电', 'Generation'), 'rpt.g.rel': ('可靠性', 'Reliability'), 'rpt.g.flt': ('故障', 'Faults'),
+    'rpt.g.unit': ('机组', 'Units'), 'rpt.g.end': ('收尾', 'Closing'),
+    'rpt.b.kpi.nm': ('一级指标', 'Headline KPIs'),     'rpt.b.kpi.d': ('六个核心数: 上网电量 / 等效满发 / 可用率 / 损失率 / 停机间隔 / 需行动台', 'Six core numbers: delivered energy / full-load hours / availability / loss rate / MTBO / units needing action'),
+    'rpt.b.water.nm': ('能量瀑布', 'Energy waterfall'), 'rpt.b.water.d': ('理论可发 → 逐段扣损 → 实际上网, 用于定位电量流向', 'Theoretical → losses by segment → delivered; where the energy went'),
+    'rpt.b.emon.nm': ('逐月发电表', 'Monthly table'),  'rpt.b.emon.d': ('逐月上网 / 损失 / 损失率 / 等效满发, 含数据覆盖率', 'Monthly delivered / loss / loss rate / full-load hours, with data coverage'),
+    'rpt.b.blame.nm': ('损失责任分账', 'Loss by responsibility'), 'rpt.b.blame.d': ('交易调度侧 / 设备侧 / 运行侧 三段占比 — 回答"谁的责任"', 'Market-dispatch / equipment / operations shares — who is responsible'),
+    'rpt.b.ram.nm': ('可靠性象限', 'Reliability quadrant'), 'rpt.b.ram.d': ('平均无故障间隔(MTBF)× 单次停机时长(MDT)部件象限, 分出"频发易修"与"罕发难修", 处置方向相反', 'MTBF × MDT by component; frequent-easy vs rare-hard call for opposite actions'),
+    'rpt.b.parts.nm': ('部件停机表', 'Downtime by component'), 'rpt.b.parts.d': ('逐部件停机时长 / 事件数 / 平均无故障间隔(MTBF)/ 单次停机时长(MDT)/ 影响机组数', 'Per component: downtime / events / MTBF / MDT / units affected'),
+    'rpt.b.attrib.nm': ('归因覆盖', 'Attribution coverage'), 'rpt.b.attrib.d': ('已归因 vs 无记录停机的占比 — 无记录不等于没有故障', 'Attributed vs unrecorded downtime — unrecorded is not the same as no fault'),
+    'rpt.b.pareto.nm': ('故障帕累托', 'Fault Pareto'),    'rpt.b.pareto.d': ('按条数降序 + 累计 80% 线, 找关键少数', 'Descending counts with the 80% line; the vital few'),
+    'rpt.b.quad.nm': ('故障四象限', 'Fault quadrants'),  'rpt.b.quad.d': ('条数 × 单次时长, 区分重复触发与实际停机 — 按条数排序会误定优先级', 'Count × duration separates re-triggering from real outages; ranking by count alone misleads'),
+    'rpt.b.frate.nm': ('月度故障率', 'Monthly fault rate'), 'rpt.b.frate.d': ('按天归一的日均条数, 数据不满月单独标注', 'Daily-normalised counts; partial months flagged'),
+    'rpt.b.units.nm': ('重点机组', 'Key units'),         'rpt.b.units.d': ('需跟踪台的部件 / 证据源 / 六步进度 / 卡在哪一步', 'Tracked units: component / evidence sources / six-step progress / where blocked'),
+    'rpt.b.chain.nm': ('决策链进度', 'Decision-chain progress'), 'rpt.b.chain.d': ('证据→机制→判级→排期→动作→验收 六步的瓶颈分布', 'Bottlenecks across evidence → mechanism → rating → schedule → action → acceptance'),
+    'rpt.b.loop.nm': ('闭环实证', 'Closed-loop evidence'), 'rpt.b.loop.d': ('检出 → 换件 → 复测回落的历史例证, 证明这条链是通的', 'Detected → replaced → re-measured recovery; proof the chain works'),
+    'rpt.b.plan.nm': ('下期建议', 'Next-period actions'),  'rpt.b.plan.d': ('由数据推出的行动项, 不是套话', 'Actions derived from data, not boilerplate'),
+    'rpt.b.caliber.nm': ('口径与边界', 'Basis and limits'), 'rpt.b.caliber.d': ('统计期 / 残月 / 工单时间窗 / 判级性质 — 建议常驻, 没有边界的数字堆不能作汇报依据', 'Period / partial months / work-order window / nature of ratings — keep it in; numbers without limits are not a basis for reporting'),
+    'rpt.b.narr.nm': ('运行总结', 'Narrative summary'),   'rpt.b.narr.d': ('模型基于实测数据生成的运行总结 — 为图表补充文字结论, 供汇报使用', 'A model-written summary from measured data — text conclusions to accompany the charts'),
+    # 块内文案
+    'rpt.kpi.act':       ('上网电量', 'Delivered energy'),      'rpt.kpi.theo': ('理论可发 {v}', 'theoretical {v}'),
+    'rpt.kpi.mom':       ('环比 {v}%', 'MoM {v}%'),              'rpt.kpi.mom_na': ('环比不可比(末月不满)', 'MoM not comparable (partial last month)'),
+    'rpt.kpi.eflh':      ('等效满发小时', 'Full-load hours'),   'rpt.kpi.annual': ('年化约 {v} h', 'annualised ≈ {v} h'),
+    'rpt.kpi.avail':     ('时间可用率', 'Time-based availability'), 'rpt.kpi.avail_n': ('不含调度令与低风待机', 'excl. dispatch orders and low-wind standby'),
+    'rpt.kpi.loss':      ('损失率', 'Loss rate'),               'rpt.kpi.mtbo': ('平均停机间隔', 'Mean time between outages'),
+    'rpt.kpi.mtbo_n':    ('停机 {n} 起 · 单次均 {h}h', '{n} outages · mean {h}h each'),
+    'rpt.kpi.action':    ('需行动机组', 'Units needing action'), 'rpt.kpi.action_n': ('定论 {a} · 预警 {b} · 候选 {c}', 'confirmed {a} · warning {b} · candidate {c}'),
+    'rpt.water.title':   ('发电完成情况', 'Generation outcome'),
+    'rpt.water.note':    ('理论可发 {theo} = 上网 {act} + 损失 {loss} MWh ({pct}%)', 'Theoretical {theo} = delivered {act} + loss {loss} MWh ({pct}%)'),
+    'rpt.water.cap_ok':  ('量级自检: 理论低于全额定上限 {cap} MWh ✔', 'Magnitude check: theoretical is below the full-rated cap {cap} MWh ✔'),
+    'rpt.water.cap_bad': ('量级自检: 理论 ≥ 全额定上限 {cap} MWh ✗ 数字不可信', 'Magnitude check: theoretical ≥ full-rated cap {cap} MWh ✗ figures not credible'),
+    'rpt.water.cal':     ('SCADA 10min 工况态分解; 损失=同工况理论出力与实测之差。量级自检为防虚高的下限闸, 不是风资源判据', 'SCADA 10-min state decomposition; loss = theoretical output for the same state minus measured. The magnitude check is a floor against inflation, not a wind-resource criterion'),
+    'rpt.emon.title':    ('逐月发电', 'Monthly generation'),
+    'rpt.emon.m': ('月', 'Month'), 'rpt.emon.act': ('上网 MWh', 'Delivered MWh'), 'rpt.emon.loss': ('损失 MWh', 'Loss MWh'),
+    'rpt.emon.pct': ('损失率', 'Loss rate'), 'rpt.emon.eflh': ('等效满发 h', 'Full-load h'), 'rpt.emon.cov': ('覆盖', 'Coverage'),
+    'rpt.emon.partial':  (' 不满月', ' partial'),
+    'rpt.emon.cal':      ('覆盖率<50% 的月不参与环比', 'Months with coverage below 50% are excluded from MoM'),
+    'rpt.blame.title':   ('损失责任分账', 'Loss by responsibility'),
+    'rpt.blame.note':    ('进设备考核的只有"故障与检修停机"; 限电属交易与调度侧。', 'Only fault and maintenance outages count against equipment; curtailment belongs to market and dispatch.'),
+    'rpt.blame.note2':   ('限电不是"低风时段收到的空令": 限电·绑定时段平均出力 1.435 MW, 正常发电时段 1.412 MW, 比值 1.02 — 风况相同而出力被压, 说明限电是真压制; 但"限电命令期·功率未被压制"那一档平均出力仅 0.724 MW (18% 额定), 确属低风时段收令。',
+                          'Curtailment is not an empty order received in low wind: mean output in bound-curtailment periods is 1.435 MW versus 1.412 MW while generating normally (ratio 1.02) — same wind, suppressed output, so the curtailment is real; the "command without suppression" tier averages only 0.724 MW (18% of rated), which is a low-wind order.'),
+    'rpt.blame.cal':     ('工况态分解 + 限电命令面绑定判别; 平均出力=实发÷台时, 用作风资源代理量', 'State decomposition with curtailment-command binding; mean output = delivered ÷ unit-hours, used as a wind-resource proxy'),
+    'rpt.ram.title':     ('可靠性 × 可维修性', 'Reliability × maintainability'),
+    'rpt.ram.note':      ('设备类 {a}h / 外部类 {b}h (电网·风况·运维操作不计设备可靠性)', 'Equipment {a}h / external {b}h (grid, wind and operations excluded from equipment reliability)'),
+    'rpt.ram.cal':       ('SCADA ≥1h 停机段, 不含调度令; 海上 MDT 含交通窗口', 'SCADA outage segments ≥1 h, excluding dispatch orders; offshore MDT includes transit windows'),
+    'rpt.parts.title':   ('部件停机', 'Downtime by component'),
+    'rpt.parts.part': ('部件', 'Component'), 'rpt.parts.h': ('停机h', 'Downtime h'), 'rpt.parts.ev': ('事件', 'Events'),
+    'rpt.parts.mtbf': ('平均无故障间隔(MTBF,h)', 'MTBF (h)'), 'rpt.parts.mdt': ('单次停机时长(MDT,h)', 'MDT (h)'), 'rpt.parts.units': ('影响机组', 'Units'),
+    'rpt.parts.ext':     (' (外部)', ' (external)'),
+    'rpt.parts.cal':     ('归因=段起点±30min 首发码', 'Attribution = first alarm within ±30 min of segment start'),
+    'rpt.attrib.title':  ('归因覆盖', 'Attribution coverage'),
+    'rpt.attrib.note':   ('"无记录停机"不是没有故障, 是没归上因。', 'Unrecorded downtime is not the absence of faults; it is the absence of attribution.'),
+    'rpt.pareto.title':  ('故障帕累托', 'Fault Pareto'),     'rpt.pareto.cal': ('报警事件表', 'Alarm event table'),
+    'rpt.quad.title':    ('故障模式四象限', 'Fault-mode quadrants'),
+    'rpt.quad.note':     ('右下角报警码属重复触发, 不应按条数排定优先级。', 'Codes in the lower-right are re-triggering and should not be prioritised by count.'),
+    'rpt.quad.cal':      ('纵界=10min(一个SCADA采样格)', 'Vertical split = 10 min (one SCADA sample)'),
+    'rpt.frate.title':   ('月度故障率', 'Monthly fault rate'),
+    'rpt.frate.note':    ('柱高=日均条数, 不是月总数。', 'Bar height is the daily mean, not the monthly total.'),
+    'rpt.frate.cal':     ('不满月按天归一', 'Partial months normalised by day'),
+    'rpt.units.title':   ('重点机组与处置', 'Key units and actions'),
+    'rpt.units.head':    ('需跟踪 {n} 台 / {tot} | 瓶颈 ', 'Tracked {n} of {tot} | bottlenecks '),
+    'rpt.units.stuck':   ('{n} 台卡在「{s}」', '{n} units pending {s}'),
+    'rpt.units.state': ('状态', 'State'), 'rpt.units.ev': ('部件与证据', 'Component and evidence'),
+    'rpt.units.prog': ('进度', 'Progress'), 'rpt.units.at': ('卡在', 'Stage'),
+    'rpt.units.rising':  ('在升 {r}×', 'rising {r}×'),   'rpt.units.clear': ('全通', 'clear'),
+    'rpt.units.cal':     ('四源融合判级', 'Four-source fused rating'),
+    'rpt.chain.title':   ('决策链进度', 'Decision-chain progress'),
+    'rpt.chain.note':    ('六步: 证据 → 机制 → 判级 → 排期 → 动作 → 验收。当前 {s}。', 'Six steps: evidence → mechanism → rating → schedule → action → acceptance. Currently {s}.'),
+    'rpt.chain.cal':     ('「动作」一列不可判 — {b}', 'The "action" step cannot be judged — {b}'),
+    'rpt.loop.title':    ('端到端闭环实证', 'End-to-end closed-loop evidence'),
+    'rpt.loop.comp': ('部件', 'Component'), 'rpt.loop.date': ('换件日期', 'Replaced on'), 'rpt.loop.pre': ('换前', 'Before'),
+    'rpt.loop.post': ('换后', 'After'), 'rpt.loop.ratio': ('比值', 'Ratio'),
+    'rpt.loop.note':     ('这几台正是算法当初的检出台 ⇒ 故障前能看见、换件后能看见恢复。', 'These are the units the algorithm flagged in the first place: visible before the fault, and visibly recovered after replacement.'),
+    'rpt.loop.cal':      ('振动标量前后对比', 'Vibration scalar, before vs after'),
+    'rpt.plan.title':    ('下期计划与建议', 'Next-period plan'),
+    'rpt.plan.1':        ('补机制定性 — {n} 台停在"判了级但根因未定", 是当前最大单一瓶颈', 'Close the mechanism gap — {n} units are rated but without a root cause; the single largest bottleneck'),
+    'rpt.plan.2':        ('回收 25~26 工单 — 台账止 2024-11, 本期检修执行与闭环无法自动判定', 'Recover the 2025–26 work orders — the ledger ends 2024-11, so this period’s execution and closure cannot be judged automatically'),
+    'rpt.plan.3':        ('油液采样节奏 — 四源里油液长期 stale, 只能作回测锚', 'Oil sampling cadence — oil is chronically stale among the four sources and serves only as a back-test anchor'),
+    'rpt.plan.4':        ('限电侧 — 损失六成在交易与调度侧, 设备侧改善空间有限', 'Curtailment — six tenths of the loss sits with market and dispatch; equipment-side headroom is limited'),
+    'rpt.plan.cal':      ('建议级 — 排期与动作须经审批', 'Advisory — scheduling and actions require approval'),
+    'rpt.narr.title':    ('运行总结', 'Narrative summary'),
+    'rpt.narr.empty':    ('还没有生成 — 在上面的交互窗输入报告要求。', 'Nothing generated yet — enter a request in the panel above.'),
+    'rpt.narr.ungr':     ('⚠ 以下数值由模型自行推算, 不在原始数据摘要中, 引用前须核对: {v}', '⚠ These values were derived by the model and are not in the source summary; verify before citing: {v}'),
+    'rpt.narr.cal':      ('模型 {m} 生成 · 数字取自系统实测摘要 · 接地闸{g}', 'Generated by {m} · numbers from the measured summary · grounding gate {g}'),
+    'rpt.narr.g_ok':     ('全通过', 'passed'),
+    'rpt.narr.g_n':      ('报 {n} 处待核', 'flagged {n} to verify'),
+    'rpt.cal.title':     ('口径与数据边界', 'Basis and data limits'),
+    'rpt.cal.period':    ('统计期', 'Period'),
+    'rpt.cal.partial':   (' — {m} 数据不满月(覆盖 {c}%), 月度已按天归一', ' — {m} partial (coverage {c}%), monthly figures normalised by day'),
+    'rpt.cal.denom':     ('可用率与损失率分母不同', 'Availability and loss rate use different denominators'),
+    'rpt.cal.denom_d':   ('可用率分母剔除低风待机与调度令(问"该发时能不能发"), 损失率分母含全部时段(问"理论能发多少"); 同一低风时段在可用率里不计时间、在损失率里计入基数 — 这是两个指标的设计差异不是错, 但两者不可直接相乘或互推。低风待机对损失的贡献仅 118 MWh(0.5%)',
+                          'Availability excludes low-wind standby and dispatch orders (can it generate when it should?), loss rate includes all hours (how much could it generate?); the same low-wind hour is excluded from one and included in the other — a design difference, not an error, but the two cannot be multiplied or inferred from each other. Low-wind standby contributes only 118 MWh (0.5%) of loss'),
+    'rpt.cal.stop':      ('停机', 'Outages'),
+    'rpt.cal.stop_d':    ('可靠性口径 ≥1h 段, 电量账为逐条工况态 — 两个数都对, 不可相减', 'Reliability counts segments ≥1 h, the energy account counts every state record — both are right, and they cannot be subtracted'),
+    'rpt.cal.wo':        ('工单', 'Work orders'),
+    'rpt.cal.wo_d':      ('台账 2020~2024-11, 本报告不对本期检修执行下结论', 'Ledger 2020 to 2024-11; this report draws no conclusion on this period’s maintenance execution'),
+    'rpt.cal.rating':    ('判级', 'Ratings'),
+    'rpt.cal.rating_d':  ('由振动/温度/润滑/油液四源融合得出; 行动等级由综合报告出具', 'Fused from vibration, temperature, lubrication and oil; action levels are issued by the consolidated report'),
+    'rpt.cal.deid':      ('脱敏', 'Redaction'),
+    'rpt.cal.deid_d':    ('上报版隐去判据门槛与特征频率', 'The group version hides criterion thresholds and detailed diagnostic markers'),
+})
+
+# ── 故障统计 ──────────────────────────────────────────────────────
+_add({
+    'win.banner':        ('时间窗 {win} · 本页全部数字随上方时间窗切换', 'Window {win} · every number on this page follows the selector above'),
+    'win.partial':       ('时间窗内 {m} 数据不满月 (覆盖 {c}%), 月度图已按天归一', '{m} in this window is a partial month ({c}% coverage); monthly charts are day-normalised'),
+    'flt.eyebrow':       ('故障统计与可靠性', 'Fault statistics and reliability'),
+    'flt.alarm_face':    ('报警面', 'Alarm view'),
+    'flt.alarm_meta':    ('{n} 条 · {c} 个码 · 时间窗 {win}', '{n} alarms · {c} codes · window {win}'),
+    'flt.rate':          ('月度故障率', 'Monthly fault rate'),
+    'flt.rate_note':     ('柱高 = 日均条数, 不是月总数', 'Bar height is the daily mean, not the monthly total'),
+    'flt.rate_partial':  ('本时间窗 {m} 数据不满月 (灰虚线柱), 直接比月总数会读反。', '{m} is partial in this window (grey dashed bar); comparing monthly totals directly reads backwards.'),
+    'flt.rate_full':     ('各月覆盖完整。', 'All months fully covered.'),
+    'flt.rate_example':  ('实测: {m} 总数最低但日均 {v} 条/日 = 全时间窗前列。', 'Measured: {m} has the lowest total yet {v}/day, among the highest in the window.'),
+    'flt.pareto_n':      ('帕累托 · 按发生条数', 'Pareto · by count'),
+    'flt.pareto_n_note': ('条数 = 发生频次, 不反映影响程度 — 高频短时报警码 (信号抖动或重复触发) 排序靠前', 'Count = frequency, not impact — frequent short alarms (signal chatter or re-triggering) rank high'),
+    'flt.pareto_d':      ('帕累托 · 按持续时长', 'Pareto · by duration'),
+    'flt.pareto_d_note': ('时长 = 闩锁口径 (复位 − 激活), 只作排序参考不作停机时长', 'Duration = latch basis (reset − activation); a ranking aid, not downtime'),
+    'flt.quad':          ('故障模式四象限 · 条数 × 单次时长', 'Fault-mode quadrants · count × duration per event'),
+    'flt.quad_note':     ('时间窗内共 {n} 个码, 图上为条数前 {m} 个。', '{n} codes in the window; the plot shows the top {m} by count.'),
+    'flt.quad_partial':  ('窗含不满月 {m} (覆盖 {c}%), 条数为原始累计未按天归一。', 'The window includes partial month {m} ({c}% coverage); counts are raw totals, not day-normalised.'),
+    'flt.quad_note2':    ('点面积表示影响机组数, 大点为批次共性、小点为单台; 竖分界取条数中位, 横分界取 10min (一个 SCADA 采样格)。右下角的码不要按条数排优先级 — 属重复触发, 非硬件故障。上面两张帕累托对照看即此图的两条投影。',
+                          'Point area = units affected (large = batch-wide, small = single unit); the vertical split is the count median, the horizontal split 10 min (one SCADA sample). Do not prioritise lower-right codes by count — they re-trigger and are not hardware faults. The two Pareto charts above are this plot’s projections.'),
+    'flt.per_t':         ('台级分布 · 报警条数 top{n}', 'By unit · alarm count top {n}'),
+    'flt.per_t_share':   ('占全场', 'Share of fleet'),
+    'flt.per_t_note':    ('全场合计 {n} 条 · 单台占比高须先核查是否为单一报警码重复触发', 'Fleet total {n} · a high single-unit share must first be checked for one code re-triggering'),
+    'flt.seasonal':      ('故障季节性 · top 报警族 × 月', 'Seasonality · top alarm families × month'),
+    'flt.seasonal_unit': ('条/月', 'per month'),
+    'flt.seasonal_x':    ('月', 'month'),
+    'flt.seasonal_note': ('全程 19 个月 (跨 2 冬 1 夏); 未做 χ² 显著性检验前为【参考级】, 只看形态不作季节归因', '19 months (two winters, one summer); reference tier until a χ² test is run — shape only, no seasonal attribution'),
+    'flt.stop_face':     ('停机面', 'Outage view'),
+    'flt.stop_meta':     ('{h}h · {n} 起 · 口径 ≥1h 段, 不含调度令', '{h}h · {n} events · segments ≥1 h, excluding dispatch orders'),
+    'flt.ram':           ('可靠性 × 可维修性 · 部件象限', 'Reliability × maintainability · component quadrants'),
+    'flt.ram_note':      ('按停机时长排序的表看不出这层结构: 同样 500h 的停机, "频发易修"要查根因、"罕发难修"要备件, 处置相反。设备类停机 {a}h / 外部类 {b}h (外部 = 电网·风况·运维操作, 不计设备可靠性)。海上机组 MDT 普遍偏高是交通窗口决定的, 不等于维修本身慢 — 跨场比 MDT 须同为海上。',
+                          'A table sorted by downtime hides this structure: the same 500 h of downtime calls for root-cause work if frequent-and-easy, and for spares if rare-and-hard — opposite actions. Equipment {a}h / external {b}h (external = grid, wind, operations; excluded from equipment reliability). Offshore MDT runs high because of transit windows, not slow repairs — compare MDT only against other offshore sites.'),
+    'flt.attrib_note':   ('归因覆盖: "无记录停机" {h}h 是全场最大单项, 因不给 MTBF 而在上图中不出现 — 它不是"没有故障", 是"没归上因"。口径为段起点 ±30min 首发码。缺口成因待定: 此前写的"98% 的段在前 72h 内有报警 ⇒ 长停远窗"不成立 — 前 72h 有报警不等于该报警与本段停机有因果 (双模审 2026-08-28 逮)。要定成因须改判"段起止是否落在某条报警的持续区间内", 未做之前只报缺口不报原因。',
+                          'Attribution coverage: unrecorded downtime {h}h is the largest single item and does not appear above because it has no MTBF — it is not "no fault", it is "not attributed". Basis: first alarm within ±30 min of segment start. The cause of the gap is undetermined: the earlier claim "98% of segments had an alarm in the prior 72 h ⇒ long outage, far window" does not hold — an alarm in the prior 72 h does not imply causation (caught by dual-model review 2026-08-28). Determining it requires testing whether the segment falls inside an alarm’s active interval; until then only the gap is reported, not its cause.'),
+    'flt.stop_pareto':   ('停机归因帕累托', 'Outage attribution Pareto'),
+    'flt.stop_pareto_note': ('按停机时长; 事件次数见悬停', 'By downtime; event counts on hover'),
+    'flt.stop_units':    ('台级停机 top{n}', 'Outages by unit · top {n}'),
+    'flt.stop_h':        ('停机时长', 'Downtime'),
+    'flt.stop_n':        ('次数', 'Events'),
+    'flt.stop_units_note': ('停机时长高 ≠ 设备差 — 须先看归因里外部类占比 (调度令已剔除)', 'High downtime ≠ poor equipment — check the external share in the attribution first (dispatch orders already excluded)'),
+    'flt.block_err':     ('本块未算出 ({k}): {e}', 'This block could not be computed ({k}): {e}'),
+    'ch.ml.legend':      ('— {s}', '— {s}'),
+})
+
+# ── 电量算账 ──────────────────────────────────────────────────────
+_add({
+    'en.kpi_theo':     ('理论可发 (实际+损失)', 'Theoretical (delivered + loss)'),
+    'en.kpi_act':      ('实际上网 · 等效满发 {h} h', 'Delivered · full-load {h} h'),
+    'en.kpi_loss':     ('损失率 · {v} MWh', 'Loss rate · {v} MWh'),
+    'en.cap_pass':     ('通过', 'Pass'),
+    'en.cap_fail':     ('不通过', 'Fail'),
+    'en.cap_note':     ('能量守恒核 · 理论 {op} 全额定上限 {cap}', 'Energy-balance check · theoretical {op} full-rated cap {cap}'),
+    'en.balance':      ('电量平衡', 'Energy balance'),
+    'en.win':          ('窗 {win}', 'window {win}'),
+    'en.blame':        ('损失责任分账', 'Loss by responsibility'),
+    'en.blame_note':   ('最大一笔是限电不是故障。进设备考核的只有"故障与检修停机" {n} MWh; 限电属交易与调度侧, 2026 起交易策略主导, 少发未必是弃风。',
+                        'The largest item is curtailment, not faults. Only fault and maintenance outages ({n} MWh) count against equipment; curtailment belongs to market and dispatch, and from 2026 trading strategy dominates, so under-production is not necessarily lost wind.'),
+    'en.states':       ('工况态明细', 'Operating-state detail'),
+    'en.state':        ('工况态', 'State'),
+    'en.share':        ('时长占比', 'Share of hours'),
+    'en.loss_mwh':     ('损失 MWh', 'Loss MWh'),
+    'en.states_note':  ('控制参数一致性与曲线欠发筛查在固定干净窗判, 已迁入「发电性能」页 — 本页只出当期电量账。',
+                        'Control-parameter consistency and curve under-production screening are judged in a fixed clean window and live on the Generation Performance page; this page shows only the current-period energy account.'),
+})
+
+# ── 部件问题 ──────────────────────────────────────────────────────
+_add({
+    'cmp.by_system':    ('按系统查看', 'By system'),
+    'cmp.cards_note':   ('点系统卡 → 该系统全场机组与依据; 九系统 = 交付九分册一一对应; 叶片与叶根 / 塔架与基础无 SCADA 轴 (分册蓝本取 CMS / 巡检)',
+                         'Open a system card for every unit and its evidence; the nine systems map one-to-one onto the nine delivered volumes; blade-and-root and tower-and-foundation have no SCADA axis (their volumes draw on CMS and inspection)'),
+    'cmp.alarm_n':      ('报警 {a}', 'alarm {a}'),
+    'cmp.unk_n':        (' · 不可判 {u}', ' · undeterminable {u}'),
+    'cmp.fleet_n':      (' · 全场 {n} 台', ' · fleet {n}'),
+    'cmp.detail_head':  ('{sys} · 全场 {n} 台', '{sys} · fleet of {n}'),
+    'cmp.counts':       ('报警 {a} · 不可判 {u} · 良好 {g} · 优秀 {e}', 'Alarm {a} · undeterminable {u} · good {g} · excellent {e}'),
+    'cmp.alarm_units':  ('报警机组', 'Units in alarm'),
+    'cmp.unk_units':    ('不可判 (监测/数据不足, ≠ 正常)', 'Undeterminable (monitoring or data insufficient — not normal)'),
+    'cmp.good_units':   ('良好 {n} 台 (轻微偏离, 周期观察 — 点开看逐台依据)', 'Good · {n} units (slight deviation, periodic observation — expand for per-unit evidence)'),
+    'cmp.n_units':      ('({n} 台)', '({n} units)'),
+    'cmp.basis':        ('依据', 'Evidence'),
+    'cmp.detail':       ('详情', 'Detail'),
+    'cmp.problem_page': ('该问题页 →', 'Issue page →'),
+    'cmp.none':         ('本系统全场无非常态。', 'No abnormal units in this system.'),
+    'cmp.rel_eyebrow':  ('部件可靠性 · 时间窗 {win}', 'Component reliability · window {win}'),
+    'cmp.rel_head':     ('部件停机贡献与可靠性', 'Downtime contribution and reliability by component'),
+    'cmp.rel_meta':     ('总停机 {t}h = 设备类 {a} + 外部类 {b}', 'Total downtime {t}h = equipment {a} + external {b}'),
+    'cmp.rel_note':     ('口径 ≥1h 停机段, 不含调度令 — 与"电量算账"里的停机时长{h} 不同, 差额是短于 1h 的停机段 (本页按段做可靠性指标须成段, 能量账按逐条工况态算电量): 两个数都对, 不可互相相减。',
+                         'Basis: outage segments ≥1 h, excluding dispatch orders — this differs from the downtime hours on Energy Accounting{h}; the gap is segments shorter than 1 h (reliability needs whole segments, the energy account counts every state record). Both are correct and must not be subtracted.'),
+    'cmp.rel_part': ('部件', 'Component'), 'cmp.rel_h': ('停机时长', 'Downtime'), 'cmp.rel_ev': ('事件', 'Events'),
+    'cmp.rel_mtbo': ('平均停机间隔(MTBO,h)', 'MTBO (h)'), 'cmp.rel_mdt': ('单次停机时长(MDT,h)', 'MDT (h)'), 'cmp.rel_units': ('影响机组', 'Units'),
+    'cmp.rel_fm':       ('典型失效模式 (按时长)', 'Typical failure mode (by duration)'),
+    'cmp.external':     ('外部', 'external'),
+    'cmp.fusion_moved': ('振动线结论与四源融合已移至独立板块 →', 'Vibration-line conclusions and four-source fusion live on their own tab →'),
+    'cmp.fusion_n':     ('({n} 台次收录)', '({n} unit-entries)'),
+    'cmp.cross_eyebrow':('多系统交叉机组', 'Units flagged across systems'),
+    'cmp.watch_head':   ('关注机组 (≥2 系统交叉, {n} 台; 点机组号 → 该台独立页)', 'Watch list (≥2 systems, {n} units; click a unit for its own page)'),
+    'cmp.n_sys':        ('系统数', 'Systems'),
+    'cmp.hit_sys':      ('命中系统', 'Systems flagged'),
+})
+
+# ── 系统维护 ──────────────────────────────────────────────────────
+_add({
+    'mt.eyebrow':    ('系统维护 · 数据输入与更新端口', 'System maintenance · data inputs and update ports'),
+    'mt.surveying':  ('盘点中…', 'Surveying…'),
+    'mt.fail':       ('盘点失败: {e}', 'Survey failed: {e}'),
+    'mt.head':       ('数据维护面 · {n} 个源', 'Data maintenance · {n} sources'),
+    'mt.note':       ('每个源给出: 当前覆盖 / 末次更新 / 约定位置 / 摄入命令 / 更新频率 / 责任方。要补数据就把新文件放到"约定位置"再跑"摄入命令"。',
+                      'For each source: coverage, last update, agreed location, ingest command, cadence and responsible line. To add data, put the new file at the agreed location and run the ingest command.'),
+    'mt.layer_data': ('数据层', 'Data layer'),
+    'mt.layer_mech': ('机理层', 'Domain-model layer'),      # 与经典枚举表的既有译名 'Domain model' 一致
+    'mt.layer_data_note': ('随运营滚动 — 新数据到位后跑摄入, 本体与判级自动刷新', 'Rolls with operations — run ingest when new data lands; ontology and ratings refresh automatically'),
+    'mt.layer_mech_note': ('相对稳定 — 换厂商资料或现场反馈修正时才动; 它是判据的地基, 变更须过审', 'Relatively stable — changes only with new vendor material or field corrections; it underpins the criteria, so changes go through review'),
+    'mt.src': ('源', 'Source'), 'mt.cov': ('当前覆盖', 'Coverage'), 'mt.last': ('末次更新', 'Last update'),
+    'mt.freq': ('频率', 'Cadence'), 'mt.resp': ('责任', 'Responsible line'), 'mt.how': ('补数据怎么做', 'How to add data'),
+    'mt.put': ('放到:', 'Put at:'), 'mt.run': ('跑:', 'Run:'), 'mt.rows_n': (' ({n} 条)', ' ({n} records)'),
+})
+
+# ── 发电性能 (固定干净窗; 第一性原理闸卡按用户 2026-08-28 裁定不出现在交付界面) ──
+_add({
+    'gen.eyebrow':    ('特性曲线与控制参数', 'Characteristic curves and control parameters'),
+    'gen.fixed_win':  ('随上方时间窗 · 按时间窗重算', 'Follows the selector · recomputed per window'),
+    'gen.win_range':  ('时间窗 {span}', 'window {span}'),
+    'gen.win_why':    ('七镜头与月度时序都按所选时间窗重算(含自定义起止日期)。'
+                       '提示:2025-07 ~ 2025-12 是"限电前干净判别窗"——2026 起交易策略主导,'
+                       '曲线含策略性少发,作设备判据时须注意;选到含 2026 的窗时该影响存在但不再遮掩。',
+                       'Both the seven lenses and the monthly series are recomputed for the selected window '
+                       '(including custom date ranges). Note: 2025-07 to 2025-12 is the "clean pre-curtailment" '
+                       'window — from 2026 trading strategy dominates, so curves contain strategic '
+                       'under-production; the effect is present (not hidden) when a 2026 window is selected.'),
+    'gen.loading':    ('正在计算七个镜头 (剥限电 · 逐档中位 · 同型机群残差)…', 'Computing seven lenses (curtailment stripped · per-bin medians · same-type fleet residuals)…'),
+    'gen.recomputing': ('正在按所选时间窗重算七镜头…', 'Recomputing the seven lenses for the selected window…'),
+    'gen.recomputing_short': ('正在按窗重算…', 'recomputing for this window…'),
+    'gen.ctrl':       ('控制参数一致性', 'Control-parameter consistency'),
+    'gen.ctrl_win':   ('窗 {win}', 'window {win}'),
+    'gen.cap':        ('封顶参数落位', 'Where the caps sit'),
+    'gen.cap_note':   ('台数落在几个离散位置上, 不是连成一片 — 这是配置/版本差异的形状; 磨损或工况引起的会是连续散布。两轴各自成组且分组不同台 ⇒ 参数级差异, 成因须配置台账。',
+                       'Units fall on a few discrete positions rather than a continuum — the shape of configuration or version differences; wear or operating conditions would scatter continuously. Both axes form groups with different units in each ⇒ a parameter-level difference; the cause needs the configuration ledger.'),
+    'gen.beta':       ('桨距调度同档差', 'Pitch-schedule deviation within a power band'),
+    'gen.beta_note':  ('同一功率档下的桨角与全场中位之差。{units} 偏离达中位的 {k} 倍 → {v}', 'Pitch angle minus the fleet median within the same power band. {units} deviate {k}× the median → {v}'),
+    'gen.ruler':      ('尺子巡检 · 极差 ÷ σ', 'Ruler check · range ÷ σ'),
+    'gen.ruler_note': ('判据自身的分辨力。38 台正态抽样的极差期望约 3.7σ, 明显超出即分布非单峰 — 与左图的分组互为印证; 落在期望附近的轴 (额定桨角) 则是连续分布。',
+                       "The criterion's own resolving power. For 38 normally distributed units the expected range is about 3.7σ; a clear excess means the distribution is not unimodal, corroborating the grouping on the left; an axis near the expectation (rated pitch angle) is a continuous distribution."),
+    'gen.cands':      ('曲线欠发筛查', 'Curve under-production screening'),
+    'gen.cands_meta': ('{note} · σ = {s}%', '{note} · σ = {s}%'),
+    'gen.no_cands':   ('无候选', 'no candidates'),
+    'gen.figs':       ('七个镜头 · 逐档中位与同型机群残差', 'Seven lenses · per-bin medians and same-type residuals'),
+    'ch.cap.p':       ('满发封顶', 'Power cap'),
+    'ch.cap.w':       ('转速封顶', 'Speed cap'),
+    'ch.cap.median':  ('中位 {v}', 'median {v}'),
+    'ch.cap.legend':  ('圆内数字 = 台数, 圆面积 ∝ 台数', 'number = units, area ∝ units'),
+    'ch.beta.median': ('全场中位 {v}°', 'fleet median {v}°'),
+    'ch.ruler.norm':  ('正态期望 3.7σ', 'normal expectation 3.7σ'),
+    'ch.ruler.row':   ('{r}σ  σ = {s}', '{r}σ  σ = {s}'),
+})
+
+# ── 振动融合分析 ────────────────────────────────────────────────────
+_add({
+    'fus.eyebrow':     ('振动与 SCADA 证据视图 · 展示振动结论、SCADA 指标、报警与工单线索',
+                        'Vibration and SCADA evidence view · vibration findings, SCADA indicators, alarms and work-order context'),
+    'fus.unavailable': ('融合面数据不可用: {e}', 'Fusion data unavailable: {e}'),
+    'fus.sub.matrix':  ('融合矩阵', 'Fusion matrix'),   'fus.sub.loop': ('端到端闭环', 'Closed loops'),
+    'fus.sub.units':   ('逐台情况', 'Per unit'),        'fus.sub.events': ('报警工单', 'Alarms and work orders'),
+    'fus.sub.cms':     ('振动分析', 'Vibration assessment'),
+    'fus.rules_head':  ('两条判读纪律 (随板块常显)', 'Two reading rules (always shown)'),
+    'fus.rule1':       ('① 同一失效机制的多个观测面 ≠ 多源印证 — 融合禁按源数加权 (29# 柱塞/温度/振动稳态/振动瞬态 = 四个观测面一条机制链)。',
+                        '① Several observation surfaces of one failure mechanism ≠ multi-source confirmation — fusion must not weight by source count (unit 29: plunger / temperature / steady vibration / transient vibration are four surfaces on one mechanism chain).'),
+    'fus.rule2':       ('② "没报警" ≠ "正常" — 在已知盲区上, 缺报警只说明监测面无效, 不说明设备无恙。',
+                        '② "No alarm" ≠ "normal" — on a known blind spot a missing alarm only shows the monitoring surface is ineffective, not that the equipment is fine.'),
+    'fus.matrix_head': ('全场融合矩阵 (38 台 × 6 个证据面)', 'Fleet fusion matrix (38 units × 6 evidence surfaces)'),
+    'fus.matrix_note1':('各列取数窗口不同 (见列头) — 不同窗的两格同时发红不等于"同时发生", 不可当共时印证。',
+                        'Columns use different windows (see headers) — two red cells from different windows are not simultaneous and do not corroborate each other.'),
+    'fus.matrix_note2':('每格右侧曲线 = 逐月记忆轨迹 ({n} 个月, {m0} 起; 实心点 = 现窗) — 现窗快照会丢形态, 轨迹才看得见"塌→回升→翻转"类机制签名。',
+                        'The curve in each cell is the monthly memory track ({n} months from {m0}; solid dot = current window) — a snapshot loses shape; only the track shows signatures such as collapse → recovery → reversal.'),
+    'fus.legend_state':('格子颜色 = 设备状态 (业主口径, 与 windcms 报告同一套):', 'Cell colour = equipment state (operational basis, same as the reviewed condition report):'),
+    'fus.legend_tier': ('徽章 = 证据状态 (可信度, windcms 九枚举):', 'Badge = evidence tier (confidence, the nine windcms values):'),
+    'fus.legend_note': ('两轴不可互相替代: "候选"说的是可信度不是设备好坏; "良好"说的是设备状态不是证据强度。设备状态只取自振动线的判级栏这一个来源。',
+                        "The two axes cannot substitute for each other: \"candidate\" is about confidence, not equipment health; \"good\" is about equipment, not evidence strength. Equipment state comes from one source only: the vibration line's rating column."),
+    'fus.col.vib': ('振动', 'Vibration'), 'fus.col.temp': ('温度', 'Temperature'), 'fus.col.lub': ('润滑柱塞', 'Lubrication plunger'),
+    'fus.col.oil': ('油液', 'Oil'), 'fus.col.alarm': ('报警', 'Alarms'), 'fus.col.wo': ('工单', 'Work orders'),
+    'fus.col.fixed': (' · 不随本时间窗', ' · fixed'), 'fus.col.display_only': (' · 只展示', ' · display only'),
+    'fus.short.nodata': ('数据不足', 'insufficient data'), 'fus.short.uneval': ('未评估', 'not assessed'),
+    'fus.short.nosample': ('未取样', 'no sample'), 'fus.short.stale': ('样龄过期', 'sample stale'),
+    'fus.short.normal': ('正常', 'normal'), 'fus.short.unlisted': ('未收录', 'not listed'),
+    'fus.tip_state':   (' — 设备状态 {s}', ' — state {s}'), 'fus.tip_tier': (' / 证据状态 {e}', ' / tier {e}'),
+    'fus.alarm_n':     ('{n} 条', '{n}'), 'fus.alarm_rank': (' · 第 {r}/{t}', ' · rank {r}/{t}'),
+    'fus.burst':       ('⚠ {p}% 是码 {c} 抖动', '⚠ {p}% is code {c} chattering'),
+    'fus.burst_tip':   ('⚠ 单码 {c} 占 {p}%, 中位 {m} 秒/次 — 条数被抖动灌满', '⚠ code {c} alone is {p}%, median {m} s per event — the count is filled by chatter'),
+    'fus.no_alarm':    ('时间窗内无报警', 'no alarms in window'), 'fus.fam_tip': ('{n} 条 · 全场最多 {m} 条', '{n} · fleet max {m}'),
+    'fus.wo_n':        ('{n} 单', '{n} orders'), 'fus.wo_none': ('时间窗内无传动链工单', 'no drivetrain work orders in window'),
+    'fus.matrix_foot': ('润滑柱塞各台设定本就不同 (全场单次循环 22~961 秒), 所以此列只展示不判级, 只有已定案的机制链才标色; 油液多数台末次取样距今超 12 个月, 此列看的是"样够不够新", 不代表当前状态。',
+                        "Plunger settings differ by unit (single-cycle 22–961 s across the fleet), so that column is display only and is coloured only for confirmed mechanism chains; most units' last oil sample is over 12 months old, so that column shows sample freshness, not current state."),
+    'fus.loop_none':   ('暂无端到端闭环证据:现场提交的振动数据包里没有「报警→检修→复测」的成对记录',
+                        'No end-to-end closed-loop evidence yet: the submitted vibration package contains no '
+                        'alarm → repair → re-measure pairs'),
+    'fus.loop_head':   ('维修后验证案例 (检出 → 换件 → 恢复)', 'Post-maintenance verification cases (detected → replaced → recovered)'),
+    'fus.loop_note':   ('有换件记录且同口径指标回落, 可作为检测链已闭合的案例; 指标仍在上升但缺少可匹配工单的机组, 需现场或台账确认后再关闭。',
+                        'A documented replacement followed by a metric reduction on the same basis is shown as a closed verification case. Units with rising metrics remain open until field confirmation, a matching work order, or ledger confirmation is available.'),
+    'fus.loop_closed': ('已闭环 · 换件后恢复 ({n} 例)', 'Verified recovery after replacement ({n})'),
+    'fus.loop_item': ('事项', 'Item'), 'fus.loop_date': ('换件日', 'Replaced'), 'fus.loop_ba': ('换前 → 换后 (同标尺)', 'Before → after (same scale)'),
+    'fus.loop_ratio': ('恢复比', 'Recovery'), 'fus.loop_verdict': ('判', 'Verdict'),
+    'fus.loop_neg':    ('负面对照: 工单后持平 {n} 例 (换件 ≠ 必然回落, 说明回落非巧合)。', 'Negative control: {n} cases flat after a work order (replacement does not always lower the metric, so the falls are not coincidence).'),
+    'fus.loop_rising': ('未闭环 · 指标上升待现场/工单确认 ({n} 台)', 'Open · rising metrics awaiting field confirmation, a matching work order, or ledger confirmation ({n} units)'),
+    'fus.loop_el': ('早期 → 近期 (同标尺)', 'Early → recent (same scale)'), 'fus.loop_x': ('倍比', 'Ratio'), 'fus.loop_state': ('状态', 'State'),
+    'fus.loop_bound':  ('数据边界: {b}', 'Data boundary: {b}'),
+    'fus.units_head':  ('逐台情况 (全场 {u} 台 / {n} 台次{f}; 深入谱图/波形 → "振动分析")', 'Per unit ({u} units / {n} entries{f}; detailed condition traces → "Vibration assessment")'),
+    'fus.units_filter':(' · 筛选: {l}', ' · filter: {l}'), 'fus.clear': ('× 清除', '× clear'),
+    'fus.lv.bad': ('定论/报警级', 'Confirmed / alarm'), 'fus.lv.warn': ('预警级', 'Warning'),
+    'fus.lv.note': ('候选/监视/观察级', 'Candidate / watch'), 'fus.lv.ok': ('销案/无新证', 'Closed / no new evidence'),
+    'fus.unlisted_head': ('未列机组 {n} 台 (振动线未收录 — 未列 ≠ 正常)', 'Unlisted units · {n} (not covered by the vibration line — unlisted ≠ normal)'),
+    'fus.unlisted_link': ('未列 {n} 台', 'unlisted {n}'),
+    'fus.temp_now': ('温度轴现状', 'Temperature now'), 'fus.lub_now': ('润滑柱塞现状', 'Plunger now'),
+    'fus.eval_head':   ('评价体系 (规范化定义 — 两轴并存, 不可互相替代)', 'Rating system (two axes, not interchangeable)'),
+    'fus.eval_state': ('设备状态 (业主口径)', 'Equipment state (operational basis)'), 'fus.eval_tier': ('证据状态 (可信度)', 'Evidence tier (confidence)'), 'fus.eval_meaning': ('含义', 'Meaning'),
+    'fus.m.bad': ('异常严重, 建议尽快停机检查', 'Severe abnormality; stop and inspect soon'), 'fus.m.bad_e': ('证据: 已定案', 'evidence: settled'),
+    'fus.m.warn': ('存在需现场核实或处置的异常', 'Abnormality needing field verification or action'), 'fus.m.warn_e': ('证据: 基本确认 / 新出现待核实', 'evidence: essentially confirmed / newly seen, to verify'),
+    'fus.m.ok': ('轻微偏离, 记录观察', 'Slight deviation; record and observe'), 'fus.m.ok_e': ('证据: 待现场核实 / 已记为基线下窗观察', 'evidence: to verify on site / recorded as baseline for the next window'),
+    'fus.m.ex': ('未见异常, 保持常规监视', 'No abnormality; routine monitoring'), 'fus.m.ex_e': ('证据: 仅记录 / 已取消', 'evidence: record only / withdrawn'),
+    'fus.m.unk': ('数据不足或监测中断 — 不等于正常', 'Insufficient data or monitoring gap — not the same as normal'), 'fus.m.unk_e': ('证据: 数据不足', 'evidence: insufficient'),
+    'fus.eval_note':   ('设备状态答"这台设备现在怎么样" (危险 > 报警 > 良好 > 优秀 > 不可判); 证据状态答"这个判断有多可信" (定论 → INSUFFICIENT)。设备状态一律取自振动线的判级栏这一个来源。本页判级为转录, 不产新判级; 标注为引用同族/谱线级/测量链/未映射的行不作部件级结论消费; claim_window = 该结论的证据时间窗, 窗外状态该结论不背书。',
+                        'Equipment state answers "how is this unit now" (critical > alarm > good > excellent > undeterminable); evidence tier answers "how credible is that judgement" (confirmed → INSUFFICIENT). Equipment state comes from the reviewed vibration assessment. Rows marked as same-family reference / line-level / measurement-chain / unmapped are excluded from component-level conclusions; claim_window is the evidence window, and the conclusion does not vouch for states outside it.'),
+    'fus.col.part': ('部件', 'Component'), 'fus.col.state_concl': ('设备状态 / 振动结论', 'State / vibration conclusion'),
+    'fus.col.claim': ('证据时间窗', 'Evidence window'), 'fus.col.claim_axis': ('26-01 → 09', '26-01 → 09'),
+    'fus.col.signal': ('振动信号', 'Vibration signal'), 'fus.col.tempaxis': ('温度轴', 'Temperature'), 'fus.col.tempaxis_note': ('±6 K', '±6 K'),
+    'fus.col.oil': ('油样', 'Oil sample'),
+    'fus.win_none': ('未标窗', 'no window'), 'fus.win_n': ('{n} 个窗段', '{n} spans'),
+    'fus.oil_fresh': ('近共时', 'recent'), 'fus.oil_stale': ('stale > 12 个月', 'stale > 12 months'), 'fus.oil_none': ('无样', 'no sample'),
+    'fus.ev_win':      ('时间窗 {win} · 顶部选择器可换', 'Window {win} · change it with the selector above'),
+    'fus.ev_head':     ('{c} — 报警 {an} 条 / {au} 台 · 工单 {wn} 条 / {wu} 台 (时间窗内)', '{c} — alarms {an} / {au} units · work orders {wn} / {wu} units (in window)'),
+    'fus.ev_code': ('码', 'Code'), 'fus.ev_text': ('报警内容', 'Alarm'), 'fus.ev_units': ('台数', 'Units'), 'fus.ev_dur': ('累计时长 h', 'Total h'),
+    'fus.ev_top': ('台分布 top: ', 'Top units: '), 'fus.ev_recent': ('最近 {n} 条明细', 'Latest {n} events'),
+    'fus.ev_time': ('时间', 'Time'), 'fus.ev_min': ('时长 min', 'Duration min'),
+    'fus.ev_no_alarm': ('时间窗内无该部件报警', 'No alarms for this component in the window'),
+    'fus.ev_wo_name': ('故障名称', 'Fault'), 'fus.ev_wo_act': ('处理', 'Action'), 'fus.ev_wo_cause': ('原因', 'Cause'),
+    'fus.ev_no_wo':    ('时间窗内无该部件工单 (工单数据止 2024-11, 2026 类窗必空 = 数据边界)', 'No work orders for this component in the window (ledger ends 2024-11; 2026 windows are necessarily empty — a data boundary)'),
+    'fus.cms_loading': ('载入振动评估结果…', 'Loading vibration assessment…'),
+    'fus.cms_head':    ('振动状态评估 (windcms 报告 {d} 结果转录)', 'Vibration condition assessment ({d} reviewed report)'),
+    'fus.cms_note':    ('状态管理判级, 不代表确诊数量; 判据/谱线/计算过程不在本页 — 深潜走底部独立系统。',
+                        'Condition-management ratings, not a count of confirmed faults; criteria, detailed signal markers and calculations are not on this page — use the standalone system linked below.'),
+    # ★2026-09-19: 页面上原来只有"出件日 + 证据时间窗末", 没有**起始日** ⇒ 用户拿 2026-03~04 的导出
+    #   只看到 4 月的记录时会问"3 月的数据呢"。窗名(w0316)只是起始日代号, 这里把实算区间摆出来并说明
+    #   为什么图上多显示末期日期 (末窗状态=取时间窗内最新记录, 是口径不是缺数据)。
+    #   ★英文措辞要过 lang.audit 的语言包闸: 'spectra'/'waveforms'/'characteristic frequency' 等 36 条
+    #   内部审核黑话**禁用**(2026-09-19 实逮: 首版写 spectra 直接让 /v2 出页 500)。
+    'fus.cms_window':  ('数据窗 {r} —— 本时间窗**实算**覆盖区间 (窗名只是起始日代号); 页面上的图谱与"末窗"状态取自时间窗内最新记录, 故多显示末期日期。',
+                        'Data window {r} — the measured coverage of this window (the name is only its start-date code). Charts and "latest-window" states come from the newest record inside the window, so later dates dominate the display.'),
+    'fus.cms_action':  ('需行动机组 (报警 / 不可判)', 'Units needing action (alarm / undeterminable)'),
+    'fus.cms_focus': ('状态焦点', 'Focus'), 'fus.cms_focus_note': ('(传动链: 主承前 · 主承后 · 齿轮箱 · 发电机两端)', '(drivetrain: main bearing fore / aft · gearbox · generator both ends)'),
+    'fus.cms_evidence': ('证据强度', 'Evidence strength'), 'fus.cms_act': ('行动', 'Action'),
+    'fus.cms_grades':  ('38 台部件等级一览', 'Component grades, all 38 units'),
+    'fus.cms_chain': ('部件状态链', 'Component state chain'), 'fus.cms_chain_note': ('(主承前 · 主承后 · 齿轮箱 · 发电机; 悬停看逐件等级)', '(main bearing fore / aft · gearbox · generator; hover for each grade)'),
+    'fus.cms_overall': ('综合', 'Overall'), 'fus.cms_basis': ('口径说明', 'Basis'),
+    'fus.cms_basis_note': ('本表 = windcms 状态管理判级 (含标量趋势与数据可判性); 融合矩阵 / 逐台结论 = 振动线定谳链转录 (候选以上)。两口径差异台: 仅本表列 {a} (多为标量发作 / 数据不可判, 未到定谳链); 仅定谳链列 {b} (预警级在察, 状态管理未升)。',
+                        "This table = windcms condition-management ratings (scalar trends and data judgeability); the matrix and per-unit conclusions = transcription of the vibration line's verdict chain (candidate and above). Units differing between the two: only here {a} (mostly scalar episodes or undeterminable data, not yet in the verdict chain); only in the verdict chain {b} (warning tier under watch, not escalated in condition management)."),
+    'fus.none_word': ('无', 'none'),
+    'fus.cms_link':    ('谱图 · 波形 · 判据 · 工作台等分析功能在独立 windcms 系统 →', 'Detailed condition traces, criteria and the workbench live in the standalone condition-monitoring system →'),
+    'fus.cms_open': ('打开 windcms ↗', 'Open windcms ↗'), 'fus.cms_static': ('(未启动时用静态版)', '(static copy if it is not running)'),
+    'chain.mbf': ('主轴承前', 'MB fore'), 'chain.mba': ('主轴承后', 'MB aft'), 'chain.ps': ('行星', 'Planet'),
+    'chain.ims': ('中间', 'IMS'), 'chain.hss': ('高速', 'HSS'), 'chain.gde': ('发电机DE', 'Gen DE'), 'chain.gnde': ('发电机NDE', 'Gen NDE'),
+    'chain.gb': ('齿轮箱', 'Gearbox'), 'chain.gen': ('发电机', 'Generator'),
+})
+
+# ── 判别值枚举 ────────────────────────────────────────────────────
+# 这批值在 API 侧**有意保留中文** (_KEEP_ZH): 前端拿它们做判别 (x.部件 === '无记录停机'),
+# 译了判别就失效。所以它们必须在**显示层**译 —— 数据层译反而会打穿逻辑。
+# 停机归因部件 (rel.表[].部件) 与九系统是两套分类: 它含外部类与未归因项。
+_add({
+    'attr.无记录停机':      ('无记录停机', 'Unrecorded downtime'),
+    'attr.未归类':          ('未归类', 'Unclassified'),
+    'attr.运维操作':        ('运维操作', 'Operations and maintenance actions'),
+    'attr.变桨与液压':      ('变桨与液压', 'Pitch and hydraulics'),
+    'attr.塔架/结构':       ('塔架/结构', 'Tower and structure'),
+    'attr.轮毂/叶片':       ('轮毂/叶片', 'Hub and blades'),
+    'attr.电网/供电(外部)': ('电网/供电(外部)', 'Grid and power supply (external)'),
+    'attr.风况(外部)':      ('风况(外部)', 'Wind conditions (external)'),
+})
+
+# 振动线部件形态 (fus.表[].部件) —— 带限定语的部件名, 同为判别值
+_add({
+    'vp.(振动线未收录)':              ('(振动线未收录)', '(not covered by the vibration line)'),
+    'vp.齿轮箱高速轴':                ('齿轮箱高速轴', 'Gearbox high-speed shaft'),
+    'vp.齿轮箱中间轴':                ('齿轮箱中间轴', 'Gearbox intermediate shaft'),
+    'vp.齿轮箱二级行星内齿圈':        ('齿轮箱二级行星内齿圈', 'Gearbox second-stage planetary ring gear'),
+    'vp.齿轮箱(未映射)':              ('齿轮箱(未映射)', 'Gearbox (not mapped)'),
+    'vp.齿轮箱(谱线级, 未映射到部件)': ('齿轮箱(谱线级, 未映射到部件)', 'Gearbox (signal level, not mapped to a component)'),
+    'vp.齿轮箱(测量链问题, 非部件)':   ('齿轮箱(测量链问题, 非部件)', 'Gearbox (measurement-chain issue, not a component)'),
+    'vp.发电机DE':                    ('发电机DE', 'Generator DE'),
+    'vp.发电机NDE':                   ('发电机NDE', 'Generator NDE'),
+    'vp.发电机DE+NDE(两端)':          ('发电机DE+NDE(两端)', 'Generator DE and NDE (both ends)'),
+    'vp.发电机(未映射)':              ('发电机(未映射)', 'Generator (not mapped)'),
+    'vp.发电机(引用同族, 见族主台)':  ('发电机(引用同族, 见族主台)', "Generator (same-family reference; see the family's lead unit)"),
+    'vp.主轴承(后)+集中润滑泵':       ('主轴承(后)+集中润滑泵', 'Main bearing (aft) plus centralised lubrication pump'),
+})
+
+# ── 逐台 / 单问题浮层 (原为服务端独立页 /turbine/<t> · /problem/<t>/<sys>) ──────
+# 静态站上那两条路径是 404 (nginx 只有静态文件)。经典页当年用页内浮层兜, v2 同此:
+# 数据从预烤取, 图表复用 charts.fig —— 与在线版看到的是同一批图。
+_add({
+    'pop.unit':        ('{u} · 全部问题', '{u} · all issues'),
+    'pop.problem':     ('{u} · {sys}', '{u} · {sys}'),
+    'pop.close':       ('关闭', 'Close'),
+    'pop.none':        ('本时间窗无记录', 'No records in this window'),
+    'pop.loading':     ('载入中…', 'Loading…'),
+    'pop.orders':      ('工单 {n} 单', '{n} work orders'),
+    'pop.orders_none': ('时间窗内无工单', 'No work orders in this window'),
+    'pop.o_date':      ('日期', 'Date'),
+    'pop.o_fault':     ('故障', 'Fault'),
+    'pop.o_check':     ('排查', 'Investigation'),
+    'pop.o_cause':     ('原因', 'Cause'),
+    'pop.o_act':       ('处置', 'Action'),
+    'pop.o_kind':      ('类别', 'Category'),
+    'pop.acts':        ('动作统计', 'Action counts'),
+    'pop.orders_one':  ('工单 1 单', '1 work order'),
+    'ov.evidence':     ('查看完整依据', 'View basis'),
+    # ★2026-09-22 用户测试 2: 「需要关注」的行来自融合面链盘,SCADA 九系统可能没有对应报警行
+    #   (实测 WTG09 齿轮箱 bad 台)—— 此时依据必须**按本行字段自组句**,不能空着。
+    'ov.basis_fusion': ('本台依据来自融合面(九系统判级无「报警」行):部件 {part} · 链路走到第 {done}/{n} 步 · '
+                        '卡在「{stuck}」 · 证据源 {srcs}',
+                        'Basis from the fusion view (no SCADA system-level alarm row): component {part} · '
+                        'chain at step {done}/{n} · stuck at "{stuck}" · evidence sources {srcs}'),
+    'ov.all_steps_done': ('已走完(待验收)', 'all steps done (pending acceptance)'),
+    'ov.no_srcs':        ('无(该台无外部证据源)', 'none'),
+    'sys.other':       ('其他', 'Other'),
+    'win.through':     ('至{date}', 'Through {date}'),
+    'fig.vibbar_note': ('条为本台与全场中位之比; 色按 z (稳健离群度): 绿 <2 · 橙 2~3 · 红 ≥3',
+                        'Ratio of this unit to the fleet median; colour by z (robust outlier score): green <2, amber 2–3, red ≥3'),
+    'fig.point':        ('测点', 'Measurement point'),
+    'fig.x_median':     ('×中位', '× median'),
+    'fig.this_unit':    ('本台', 'This unit'),
+    'fig.fleet_median': ('全场中位', 'Fleet median'),
+    'fig.quartile':     ('四分位', 'Quartile band'),
+    'fig.trend':        ('趋势', 'Trend'),
+})
+
+
+# ══════════════════════════════════════════════════════════════════
+#  取用
+# ══════════════════════════════════════════════════════════════════
+
+# Display-only fallback for source evidence labels; retain the original IDs.
+from .data_tpl_en import GRADE, GRADE2
+L.update({'tier.detail.' + zh: (zh, en) for zh, en in {**GRADE, **GRADE2}.items()})
+
+LANGS = ('zh', 'en')
+_IDX = {'zh': 0, 'en': 1}
+
+
+def t(key: str, lang: str = 'zh', **kw) -> str:
+    """取一条文案。key 不存在 = **抛错**, 不静默退回中文。
+
+    静默退回是旧架构最贵的一课: 页面上多一段中文, 没有任何日志, 只有用户看得见。
+    """
+    if key not in L:
+        raise KeyError('语言包无此 key: %r' % key)
+    s = L[key][_IDX[lang]]
+    return s.format(**kw) if kw else s
+
+
+def pack(lang: str) -> dict[str, str]:
+    """整包导出, 供注入页面 (`const L = {...}`)。"""
+    i = _IDX[lang]
+    return {k: v[i] for k, v in L.items()}
+
+
+def check() -> list[str]:
+    """构建期硬闸。返回问题清单, 空 = 通过。
+
+    在**出页面之前**跑。旧架构把这些留到运行期, 于是每一条都变成一次线上残留。
+    """
+    bad = []
+    for k, v in sorted(L.items()):
+        if not isinstance(v, tuple) or len(v) != 2:
+            bad.append('%s: 值不是 (zh, en) 二元组' % k)
+            continue
+        zh, en = v
+        if not zh.strip():
+            bad.append('%s: 中文为空' % k)
+        if not en.strip():
+            bad.append('%s: 英文为空' % k)
+        # 英文条目里不许残留中日韩字符
+        cjk = [c for c in en if '一' <= c <= '鿿']
+        if cjk:
+            bad.append('%s: 英文含中文字符 %r' % (k, ''.join(cjk)[:12]))
+        low = en.lower()
+        for phrase in PUBLIC_COPY_BANNED_EN:
+            if phrase in low:
+                bad.append('%s: 英文含内部审核黑话 %r' % (k, phrase))
+        # 占位符集合必须一致 —— 英文漏掉一个 {n}, 渲染不报错, 数字凭空消失
+        pz, pe = set(_PH.findall(zh)), set(_PH.findall(en))
+        if pz != pe:
+            bad.append('%s: 占位符不一致 zh=%s en=%s' % (k, sorted(pz), sorted(pe)))
+    return bad

+ 2 - 2
src/windscada/perf/control.py

@@ -66,11 +66,11 @@ def registry(cfg=None, span=None):
     st = pathlib.Path(cfg['store'])
     if span:
         pr, sc = build_store(cfg, span=span, write=False)
-        out = dict(窗=f'{span[0]} ~ {span[1]} (随所选窗)')
+        out = dict(窗=f'时间窗 {span[0]} ~ {span[1]}(随所选时间窗)')
     else:
         pr = pd.read_parquet(st / 'control_profile.parquet')
         sc = pd.read_parquet(st / 'control_schedule.parquet')
-        out = dict(窗='2025-H2 (限电前干净窗, 与曲线判别窗一致)')
+        out = dict(窗='时间窗 2025-H2(限电前干净时间窗,与曲线判别时间窗一致)')
     # C1 双封顶
     for ax, col, unit, tol in (('P封顶', 'p_cap', 'kW', 25), ('ω封顶', 'w_cap', 'rpm', 8)):
         med = pr[col].median()

+ 3 - 2
src/windscada/perf/reliability.py

@@ -80,8 +80,9 @@ def overview(cfg=None, since='2026-01-01', span=None):
     dev_h = round(float(df.loc[~df['外部'].astype(bool), '停机时长h'].sum()), 0)
     ext_h = round(float(df.loc[df['外部'].astype(bool), '停机时长h'].sum()), 0)
     df = df.astype(object).where(pd.notna(df), None)      # DataFrame 会把 None 转 NaN → 出口还原 (NaN 是非法 JSON)
-    return dict(窗=f'{since[:7]} 起 {months} 个月 × {n_t} 台',
-                口径='SCADA全停机(≥1h段, 不含调度令); 归因=段起点±30min首发码; MTBF=可运行小时÷该部件事件数【参考级】; 外部类不计设备可靠性',
+    return dict(窗=f'时间窗 {since[:7]} 起 {months} 个月 × {n_t} 台',
+                口径='SCADA全停机(≥1h段, 不含调度令); 归因=段起点±30min首发码; '
+                     '平均无故障间隔(MTBF)=可运行小时÷该部件事件数【参考级】; 外部类不计设备可靠性',
                 总停机h=round(stop_h_all, 0), 设备类停机h=dev_h, 外部类停机h=ext_h,
                 表=df.to_dict('records'))
 

+ 15 - 4
src/windscada/ui/app.js

@@ -200,9 +200,20 @@
       const stuckStep = stuckKey === 'evidence' ? t('step.site_records') : t('step.' + (stuckKey || 'evidence'));
       const stuck = r.stuck ? t('step.stuck_at', { step: stuckStep }) : '';
       const nxt = dt(r.next);
+      // ★2026-09-22 实逮(用户测试 2): 「需要关注」的行来自**融合面链盘**(d.fus.链盘.rows),
+      //   而依据过去只从 SCADA 的 d.watch 取 —— 两边机组集合不一致时 why[r.t] 就是空 ⇒
+      //   「查看完整依据」展开是**空白**(实测 WTG09 齿轮箱 bad 台:watch 里没有它)。
+      //   修法: 缺依据时按**本行自己的字段**组句(部件/链路进度/卡在哪一步/证据源),永不空。
+      const evTxt = (why[r.t] || '').trim() || t('ov.basis_fusion', {
+        part: dv(r.part || '—'),
+        done: (r.done == null ? '—' : r.done),
+        n: ((r.steps || []).length || '—'),
+        stuck: r.stuck ? stuckStep : t('ov.all_steps_done'),
+        srcs: (r.srcs || []).join(' / ') || t('ov.no_srcs'),
+      });
       h += `<div class="card" id="u-${r.t}" style="--sev:var(--${{ bad: 'critical', warn: 'warning', unk: 'unknown', ok: 'good', ex: 'excellent' }[st]})">
         <div class="head"><a class="unit" href="/turbine/${r.t}">${unitNo(r.t)}</a><span class="sys">${dv(r.part || '')}</span>${stateChip(r.ostate || (r.lvl === 'bad' ? '危险' : '报警'))}</div>
-        <details class="evidence-detail"><summary>${t('ov.evidence')}</summary><div class="why">${dv(why[r.t] || '')}</div></details>
+        <details class="evidence-detail"><summary>${t('ov.evidence')}</summary><div class="why">${dv(evTxt)}</div></details>
         <div class="evid">${tierChip(r.estate)}${r.srcs && r.srcs.length ? `<span class="rv">${r.srcs.length} sources</span>` : ''}</div>
         ${stuck || nxt ? `<div class="caveat"><b>${t('ov.caveat')}</b>${esc(stuck)}${stuck && nxt ? ' — ' : ''}${esc(nxt)}</div>` : ''}</div>`;
     });
@@ -688,13 +699,13 @@
     const cnt = k => dist[k] || 0, N = Object.values(dist).reduce((a, b) => a + b, 0) || 1;
     const al = rows.filter(r => r.st === '报警'), un = rows.filter(r => r.st === '不可判');
     const tbl = (title, list) => list.length ? `<div class="panel mb"><h3>${title} ${t('cmp.n_units', { n: list.length })}</h3><div class="tblwrap"><table><tr><th style="width:74px">${t('ui.unit')}</th><th>${t('cmp.basis')}</th><th style="width:110px">${t('cmp.detail')}</th></tr>
-      ${list.map(r => `<tr><td><a href="/turbine/${r.t}"><b>${unitNo(r.t)}</b></a></td><td class="small">${dv(r.why)}</td><td><a class="small" href="/problem/${r.t}/${encodeURIComponent(sysSlug(s))}">${t('cmp.problem_page')}</a></td></tr>`).join('')}</table></div></div>` : '';
+      ${list.map(r => `<tr><td><a href="/turbine/${r.t}"><b>${unitNo(r.t)}</b></a></td><td class="small">${dv(r.why)}</td><td><a class="small" href="/problem/${r.t}/${encodeURIComponent(s)}">${t('cmp.problem_page')}</a></td></tr>`).join('')}</table></div></div>` : '';
     let h = `<section><p class="small"><a href="#tab=component" data-back="1">${t('ui.back_to', { tab: t('tab.component') })}</a></p>
       <div class="panel mb"><h3>${t('cmp.detail_head', { sys: sysName(s), n: N })}</h3>
         <div style="display:flex;height:12px;border-radius:4px;overflow:hidden;margin:8px 0 4px">${[['报警', 'critical'], ['不可判', 'unknown'], ['良好', 'good'], ['优秀', 'excellent']].map(([k, c]) => cnt(k) ? `<i style="width:${(cnt(k) / N * 100).toFixed(1)}%;background:var(--${c})"></i>` : '').join('')}</div>
         <div class="small muted">${t('cmp.counts', { a: cnt('报警'), u: cnt('不可判'), g: cnt('良好'), e: cnt('优秀') })}</div></div>`;
     h += tbl(t('cmp.alarm_units'), al) + tbl(t('cmp.unk_units'), un);
-    if (gd.length) h += `<details class="panel mb"><summary style="cursor:pointer;font-weight:600">${t('cmp.good_units', { n: gd.length })}</summary><div class="tblwrap"><table style="margin-top:10px"><tr><th style="width:74px">${t('ui.unit')}</th><th>${t('cmp.basis')}</th><th style="width:110px">${t('cmp.detail')}</th></tr>${gd.map(r => `<tr><td><a href="/turbine/${r.t}"><b>${unitNo(r.t)}</b></a></td><td class="small">${dv(r.why)}</td><td><a class="small" href="/problem/${r.t}/${encodeURIComponent(sysSlug(s))}">${t('cmp.problem_page')}</a></td></tr>`).join('')}</table></div></details>`;
+    if (gd.length) h += `<details class="panel mb"><summary style="cursor:pointer;font-weight:600">${t('cmp.good_units', { n: gd.length })}</summary><div class="tblwrap"><table style="margin-top:10px"><tr><th style="width:74px">${t('ui.unit')}</th><th>${t('cmp.basis')}</th><th style="width:110px">${t('cmp.detail')}</th></tr>${gd.map(r => `<tr><td><a href="/turbine/${r.t}"><b>${unitNo(r.t)}</b></a></td><td class="small">${dv(r.why)}</td><td><a class="small" href="/problem/${r.t}/${encodeURIComponent(s)}">${t('cmp.problem_page')}</a></td></tr>`).join('')}</table></div></details>`;
     if (!al.length && !un.length && !gd.length) h += `<div class="panel"><p class="small">${t('cmp.none')}</p></div>`;
     return h + `</section>`;
   }
@@ -721,7 +732,7 @@
           return `<tr><td><b>${partName(r.部件)}</b>${r.外部 ? ` <span class="badge">${t('cmp.external')}</span>` : ''}</td><td><i class="bar" style="width:${Math.round(r.停机时长h / maxH * 72)}px;background:${sev}"></i><span class="num small">${r.停机时长h}h · ${(r.占停机 * 100).toFixed(1)}%</span></td><td class="r">${r.事件数}</td><td class="r">${r.外部 ? '—' : (r.MTBF_h || '—')}</td><td class="r">${r.MDT_h}</td><td class="r">${r.影响台数}</td><td class="small">${dv(r.典型失效模式 || '—')}</td></tr>`; }).join('')}</table></div><p class="small muted">${dv(R.口径)}</p></div>`; }
     if (d.fus && !d.fus.err) h += `<div class="panel mb"><p class="small">${t('cmp.fusion_moved')} <a href="#tab=vibration"><b>${t('tab.vibration')}</b></a> ${t('cmp.fusion_n', { n: (d.fus.表 || []).length })}</p></div>`;
     h += `<div class="eyebrow">${t('cmp.cross_eyebrow')}</div><div class="panel mb"><h3>${t('cmp.watch_head', { n: (d.watch || []).length })}</h3><div class="tblwrap"><table><tr><th>${t('ui.unit')}</th><th class="r">${t('cmp.n_sys')}</th><th>${t('cmp.hit_sys')}</th><th>${t('cmp.basis')}</th></tr>
-      ${(d.watch || []).map(w => `<tr><td><a href="/turbine/${w.t}"><b>${unitNo(w.t)}</b></a></td><td class="r">${w.n}</td><td class="small">${(w.syss || []).map(x => `<a href="/problem/${w.t}/${encodeURIComponent(sysSlug(x))}">${sysName(x)}</a>`).join(' · ')}</td><td class="small">${dv(w.why)}</td></tr>`).join('')}</table></div></div></section>`;
+      ${(d.watch || []).map(w => `<tr><td><a href="/turbine/${w.t}"><b>${unitNo(w.t)}</b></a></td><td class="r">${w.n}</td><td class="small">${(w.syss || []).map(x => `<a href="/problem/${w.t}/${encodeURIComponent(sysFromSlug(x) || x)}">${sysName(x)}</a>`).join(' · ')}</td><td class="small">${dv(w.why)}</td></tr>`).join('')}</table></div></div></section>`;
     return h;
   }
   function bindComponent() {

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä