← 返回目录

TLA v1.0.3 实施任务清单(逐文件·逐函数)

项 内容
版本 v1.0
日期 2026-06-01
对应方案 TLA_IMPROVEMENT_PLAN_V2.md
执行方式 按 Phase 顺序,每个 Task 独立可验证,完成后打 ✅

总览

Phase 0  (P0 · 数据正确性)   ≈ 1-2 天   5 个 Task
Phase 1  (P1 · 分级报表)     ≈ 3-5 天   7 个 Task
Phase 2  (P2 · 可维护性)     ≈ 按需     3 个 Task
验证     (Verify)             贯穿全程   4 个 SQL 验收查询

Phase 0 — 数据正确性(立即)

目标:板体失控三条全部落地、0x400000 中性命名、版本 bump 触发重算


Task 0-A esc_faults.py — 码表命名修正 + P0 最小扩充

文件 trajectory-log-analyzer/tla/esc_faults.py

改动清单(当前 → 目标):

当前 修改为 原因
"ESC_FAULT_OVERVOLTAGE_PROTECTION": 0x400000 "ESC_EVENT_0x400000": 0x400000 语义未确认,中性命名
"ECU_FAULT_UNKNOWN_0x40000000": 0x40000000 "FAULT_ESC_COMMUNICATION": 0x40000000 与协议常量名对齐
"ECU_FAULT_UNKNOWN_0x80000000": 0x80000000 "FAULT_BATTERY_COMMUNICATION": 0x80000000 同上
"ECU_FAULT_UNKNOWN_0x8000000": 0x8000000 "FAULT_FLASH_COMMUNICATION": 0x8000000 同上
"BMS_FAULT_0x20": 0x20 "BMS_LOW_SOC_WARNING_20": 0x20 使用协议名
"BMS_FAULT_0x10": 0x10 "BMS_LOW_SOC_WARNING_10": 0x10 同上
"BMS_FAULT_0x1000": 0x1000 "BMS_TOTAL_VOLTAGE_LOW": 0x1000 同上
"BMS_FAULT_0x400": 0x400 "BMS_CELL_VOLTAGE_LOW": 0x400 同上

新增(P0 必须):

# PRIMARY_ESC_FAULTS 更新(两处 board_out_of_control)
PRIMARY_ESC_FAULTS = (
    "ESC_FAULT_STALL_PROTECTION",
    "ESC_EVENT_0x400000",           # 原 ESC_FAULT_OVERVOLTAGE_PROTECTION
)

新增 severity 字典(P0 最小集,供 session_analyzer 读取):

# 每条码的分级(fault / warning / notice)
ESC_SEVERITY: dict[str, str] = {
    "ESC_FAULT_STALL_PROTECTION":  "warning",
    "ESC_EVENT_0x400000":          "warning",   # TBD
    "ESC_FAULT_UNKNOWN_0x800000":  "warning",
    "ESC_FAULT_UNKNOWN_0x1000000": "warning",
    "ESC_FAULT_UNKNOWN_0x2000000": "warning",
    "ESC_FAULT_UNKNOWN_0x4":       "fault",
}

ECU_SEVERITY: dict[str, str] = {
    "FAULT_ESC_COMMUNICATION":     "fault",
    "FAULT_BATTERY_COMMUNICATION": "fault",
    "FAULT_FLASH_COMMUNICATION":   "fault",
}

BMS_SEVERITY: dict[str, str] = {
    "BMS_LOW_SOC_WARNING_20":  "notice",
    "BMS_LOW_SOC_WARNING_10":  "warning",
    "BMS_TOTAL_VOLTAGE_LOW":   "notice",
    "BMS_CELL_VOLTAGE_LOW":    "notice",
}

# 参与 board_out_of_control 合成的 ESC 码
ESC_BOARD_OUT_OF_CONTROL: frozenset[str] = frozenset({
    "ESC_FAULT_STALL_PROTECTION",
    "ESC_EVENT_0x400000",
})

验证:

python -c "from tla.esc_faults import ESC_FAULTS, ESC_BOARD_OUT_OF_CONTROL; print(ESC_FAULTS); print(ESC_BOARD_OUT_OF_CONTROL)"

Task 0-B session_analyzer.py — 新增三个字段 + 修正 has_fault

文件 trajectory-log-analyzer/tla/session_analyzer.py

第 1 步: 改版本号(第 17 行)

# 改前
ANALYZER_VERSION = "1.0.2"
# 改后
ANALYZER_VERSION = "1.0.3"

第 2 步: analyze_trajectory_row() 中新增两个辅助函数(文件顶部)

def is_invalid_motor_speed(motor_speed) -> bool:
    """|motorSpeed| >= 20000 (0.1 RPM单位,固件阈值),排除 None。"""
    if motor_speed is None:
        return False
    v = int(motor_speed)
    return v <= -20000 or v >= 20000

第 3 步: analyze_trajectory_row() 函数体内,在 corrupt_cnt 计算之后新增:

# ── P0 新增:invalid_speed_points ─────────────────────────────────────
invalid_speed_cnt = sum(
    1 for p in tl
    if not is_corrupt_motor_speed(p.get("motorSpeed"))   # 排除脏包,避免重叠计数
    and is_invalid_motor_speed(p.get("motorSpeed"))
)

# ── P0 新增:board_out_of_control ─────────────────────────────────────
stall_cnt = rep.esc_fault_points.get("ESC_FAULT_STALL_PROTECTION", 0)
esc_0x400000_cnt = rep.esc_fault_points.get("ESC_EVENT_0x400000", 0)

has_board_out_of_control = bool(
    stall_cnt > 0
    or esc_0x400000_cnt > 0
    or invalid_speed_cnt > 0
)

第 4 步: has_fault 重定义(原第 66-71 行位置),改为:

# v1.0.3: has_fault 收口 = 协议故障 OR 板体失控
# 旧: severity in (high,critical) OR 任意码命中 → 过宽(BMS 低电触发)
has_fault = (
    rep.severity in ("high", "critical")      # 保留评分路径(stall主导会话)
    or bool(rep.esc_fault_points)             # ESC 任意码(含 warning 级堵转)
    or bool(rep.ecu_fault_points)             # ECU 通信故障
    or has_board_out_of_control               # 新增:失控合成
)
# 注意: BMS 单独触发不再构成 has_fault(只计入 has_warning, 见 P1)

第 5 步: return dict 中新增三个字段(追加到现有字段之后):

# P0 新增字段
"invalid_speed_points":      invalid_speed_cnt,
"has_board_out_of_control":  1 if has_board_out_of_control else 0,
# 向后兼容:overvoltage_points 保留同值双写
"esc_event_0x400000_points": esc_0x400000_cnt,
"overvoltage_points":        esc_0x400000_cnt,   # 与原字段同名,值相同

注意: overvoltage_ratio 也需同步更新(原来引用 ESC_FAULT_OVERVOLTAGE_PROTECTION,现在改为 ESC_EVENT_0x400000):

# 改前
"overvoltage_ratio": rep.esc_fault_ratio.get("ESC_FAULT_OVERVOLTAGE_PROTECTION", 0),
# 改后
"overvoltage_ratio": rep.esc_fault_ratio.get("ESC_EVENT_0x400000", 0),

第 6 步: _count_stall_thr_mismatch() 不需要改(仍用 0x20000 bitmask,与名字无关)。

验证:

cd trajectory-log-analyzer
python -c "
from tla.session_analyzer import ANALYZER_VERSION, is_invalid_motor_speed
print('version:', ANALYZER_VERSION)
assert is_invalid_motor_speed(20000) == True
assert is_invalid_motor_speed(-20001) == True
assert is_invalid_motor_speed(19999) == False
assert is_invalid_motor_speed(None) == False
print('is_invalid_motor_speed: OK')
"

Task 0-C fault_engine.py — 字段名同步

文件 trajectory-log-analyzer/tla/fault_engine.py

fault_engine.py 内部通过 rep.esc_fault_points.get("ESC_FAULT_OVERVOLTAGE_PROTECTION", 0) 取值(第 171 行),需同步改名:

# 改前(第 171 行)
ov_cnt = rep.esc_fault_points.get("ESC_FAULT_OVERVOLTAGE_PROTECTION", 0)

# 改后
ov_cnt = rep.esc_fault_points.get("ESC_EVENT_0x400000", 0)

同步更新 symptoms 输出(第 174 行):

# 改前
rep.symptoms.append(f"overvoltage:{ov_cnt}/{n}")
# 改后(保持可读)
rep.symptoms.append(f"esc_0x400000:{ov_cnt}/{n}")

第 176 行 symptom 名同步:

# 改前
rep.symptoms.append(f"overvoltage_under_load:{ov_under_load}")
# 改后
rep.symptoms.append(f"esc_0x400000_under_load:{ov_under_load}")

验证:

python -c "
from tla.esc_faults import ESC_FAULTS
assert 'ESC_EVENT_0x400000' in ESC_FAULTS
assert 'ESC_FAULT_OVERVOLTAGE_PROTECTION' not in ESC_FAULTS
print('fault_engine name sync: OK')
"

Task 0-D 迁移 SQL — P0 新增列

新建文件 trajectory-log-analyzer/sql/migrations/20260601_tla_v1_0_3_p0.sql

-- TLA v1.0.3 P0 迁移:新增板体失控与中性命名字段
-- 执行前请备份:mysqldump surf-dev surf_motion_trajectory_analysis > backup_before_1_0_3.sql
-- 执行方式:mysql -h HOST -u USER -p surf-dev < sql/migrations/20260601_tla_v1_0_3_p0.sql

SET NAMES utf8mb4;

-- 1. 主表新增 4 列(DEFAULT 安全,不阻塞读)
ALTER TABLE surf_motion_trajectory_analysis

  ADD COLUMN `invalid_speed_points` INT NOT NULL DEFAULT 0
    COMMENT '|motorSpeed|>=20000 的轨迹点数(固件异常转速阈值,0.1RPM单位)'
    AFTER `stall_thr_motor_mismatch`,

  ADD COLUMN `has_board_out_of_control` TINYINT(1) NOT NULL DEFAULT 0
    COMMENT '板体失控:堵转(0x20000) OR ESC_0x400000 OR 超范围转速'
    AFTER `invalid_speed_points`,

  ADD COLUMN `esc_event_0x400000_points` INT NOT NULL DEFAULT 0
    COMMENT 'escEventCode & 0x400000 点数(原 overvoltage_points 同义中性名)'
    AFTER `overvoltage_ratio`;

-- 2. 新增索引(可选,视查询频率决定)
ALTER TABLE surf_motion_trajectory_analysis
  ADD KEY `idx_board_out_of_control` (`has_board_out_of_control`);

-- 3. 验证列已加入
SELECT COLUMN_NAME, COLUMN_DEFAULT, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_NAME = 'surf_motion_trajectory_analysis'
  AND COLUMN_NAME IN ('invalid_speed_points','has_board_out_of_control','esc_event_0x400000_points')
ORDER BY ORDINAL_POSITION;

执行顺序: 1. 先执行 SQL(新列 DEFAULT 安全,线上无需停服) 2. 再部署新代码(此时新字段才有非零值写入)


Task 0-E 本地端到端验证

用本地测试数据跑一遍,确认新字段输出正确:

cd trajectory-log-analyzer

# 用 .test-data 已有的缓存文件验证
python -c "
import json, pathlib
from tla.session_analyzer import analyze_trajectory_row

# 加载一个本地缓存文件
cache = pathlib.Path('.test-data/cache')
for f in cache.glob('*.txt'):
    raw = f.read_bytes()
    data = json.loads(raw)
    row = {'id': 99999, 'object_key': f.name, 'run_data': '2026-01-01', 'run_time': None}
    rec = analyze_trajectory_row(row, data, raw_for_hash=raw)
    print(f'file={f.name[:40]}')
    print(f'  invalid_speed_points={rec[\"invalid_speed_points\"]}')
    print(f'  has_board_out_of_control={rec[\"has_board_out_of_control\"]}')
    print(f'  esc_event_0x400000_points={rec[\"esc_event_0x400000_points\"]}')
    print(f'  overvoltage_points={rec[\"overvoltage_points\"]}')
    print(f'  has_fault={rec[\"has_fault\"]}')
    print(f'  analyzer_version={rec[\"analyzer_version\"]}')
    break
"

期望: - analyzer_version = "1.0.3" - esc_event_0x400000_points = overvoltage_points(值完全相同) - 有堵转/超范围转速的会话 has_board_out_of_control = 1 - 仅 BMS 低电的会话 has_board_out_of_control = 0


Phase 1 — 三级分级报表(1-2 周后)

目标:有完整 ESC/ECU/BMS 三级分级;has_protocol_fault / has_warning / fault_severity_display 落地;BMS 细分;日聚合更新


Task 1-A esc_faults.py — 全量码表扩充

文件 trajectory-log-analyzer/tla/esc_faults.py

将 ESC_FAULTS 从 6 个码扩至 24 个(完整清单见 TLA_IMPROVEMENT_PLAN_V2.md §6.1),
同时完善 ECU_FAULTS(6 码全覆盖)和 BMS_FAULTS(22 码全覆盖)。

验收标准:

from tla.esc_faults import ESC_FAULTS, ECU_FAULTS, BMS_FAULTS
assert len(ESC_FAULTS) >= 24, f"ESC 码数不足: {len(ESC_FAULTS)}"
assert len(ECU_FAULTS) == 6,  f"ECU 码数不足: {len(ECU_FAULTS)}"
assert len(BMS_FAULTS) >= 22, f"BMS 码数不足: {len(BMS_FAULTS)}"
# 所有码有 severity 映射
from tla.esc_faults import ESC_SEVERITY, ECU_SEVERITY, BMS_SEVERITY
assert set(ESC_FAULTS) == set(ESC_SEVERITY)
assert set(ECU_FAULTS) == set(ECU_SEVERITY)
assert set(BMS_FAULTS) == set(BMS_SEVERITY)
print("码表完整性: OK")

Task 1-B session_analyzer.py — 三级分级字段

文件 trajectory-log-analyzer/tla/session_analyzer.py

在 analyze_trajectory_row() 中(invalid_speed_cnt 计算之后)新增分级统计循环:

from tla.esc_faults import ESC_SEVERITY, ECU_SEVERITY, BMS_SEVERITY

# ── P1: 分级统计 ───────────────────────────────────────────────────────
hit_esc_severity: set[str] = set()   # 本会话命中的 ESC severity 集合
hit_ecu_severity: set[str] = set()
bms_notice_cnt = bms_warning_cnt = bms_fault_cnt = 0

for p in tl:
    for name in match_esc_faults(int(p.get("escEventCode") or 0)):
        hit_esc_severity.add(ESC_SEVERITY.get(name, "unknown"))
    for name in match_ecu_faults(int(p.get("ecuEventCode") or 0)):
        hit_ecu_severity.add(ECU_SEVERITY.get(name, "unknown"))
    for name in match_bms_faults(int(p.get("bmsEventCode") or 0)):
        sev = BMS_SEVERITY.get(name, "unknown")
        if sev == "notice":    bms_notice_cnt  += 1
        elif sev == "warning": bms_warning_cnt += 1
        elif sev == "fault":   bms_fault_cnt   += 1

has_protocol_fault = (
    "fault" in hit_esc_severity
    or "fault" in hit_ecu_severity
    or bms_fault_cnt > 0
)
has_warning = (
    not has_protocol_fault
    and (
        bool(hit_esc_severity - {"fault"})
        or bool(hit_ecu_severity - {"fault"})
        or bms_warning_cnt > 0
        or bms_notice_cnt > 0
    )
)

# 展示用最高级
if has_protocol_fault or has_board_out_of_control:
    fault_severity_display = "fault"
elif has_warning or has_board_out_of_control:
    fault_severity_display = "warning"
elif bms_notice_cnt > 0:
    fault_severity_display = "notice"
else:
    fault_severity_display = "none"

# has_fault 最终定义(覆盖 P0 的临时版本)
has_fault = has_protocol_fault or has_board_out_of_control

return dict 追加:

"has_protocol_fault":      1 if has_protocol_fault else 0,
"has_warning":             1 if has_warning else 0,
"fault_severity_display":  fault_severity_display,
"bms_notice_points":       bms_notice_cnt,
"bms_warning_points":      bms_warning_cnt,
"bms_critical_points":     bms_fault_cnt,
"bms_fault_points":        bms_fault_cnt,    # v1.0.3 重定义:仅故障级

Task 1-C _build_fault_details() — 明细表新增分级列

文件 trajectory-log-analyzer/tla/session_analyzer.py 中的 _build_fault_details()

导入 severity 字典,构建 severity_map 和 boc_codes:

from tla.esc_faults import (
    ESC_SEVERITY, ECU_SEVERITY, BMS_SEVERITY, ESC_BOARD_OUT_OF_CONTROL
)

_SEVERITY_MAP = {**ESC_SEVERITY, **ECU_SEVERITY, **BMS_SEVERITY}
_BOC_CODES = ESC_BOARD_OUT_OF_CONTROL  # 参与失控合成的码

def _build_fault_details(trajectory_id: int, tl: list[dict]) -> list[dict]:
    ...
    rows.append({
        "trajectory_id":               trajectory_id,
        "fault_domain":                domain,
        "fault_code":                  name,
        "severity_level":              _SEVERITY_MAP.get(name, "unknown"),  # 新增
        "board_out_of_control_related": 1 if name in _BOC_CODES else 0,    # 新增
        "fault_mask":                  mask,
        "point_count":                 cnt,
        "point_ratio":                 round(cnt / n, 4),
        "first_seen_ms":               first_ms,
    })

Task 1-D 迁移 SQL — P1 新增列

新建文件 trajectory-log-analyzer/sql/migrations/20260601_tla_v1_0_3_p1.sql

-- TLA v1.0.3 P1 迁移:三级分级字段 + BMS 细分 + 故障明细扩列

ALTER TABLE surf_motion_trajectory_analysis

  ADD COLUMN `has_protocol_fault`    TINYINT(1) NOT NULL DEFAULT 0
    COMMENT '命中协议级故障码(对应 App 红色弹窗 backIdx=2)'
    AFTER `has_board_out_of_control`,

  ADD COLUMN `has_warning`           TINYINT(1) NOT NULL DEFAULT 0
    COMMENT '命中告警/提醒级(App 黄/蓝弹窗)且无协议故障'
    AFTER `has_protocol_fault`,

  ADD COLUMN `fault_severity_display` VARCHAR(16) NOT NULL DEFAULT 'none'
    COMMENT '展示用最高级别: fault|warning|notice|none'
    AFTER `has_warning`,

  ADD COLUMN `bms_notice_points`     INT NOT NULL DEFAULT 0
    COMMENT 'BMS 提醒级事件点数(低电20%/单体偏低等)'
    AFTER `bms_fault_points`,

  ADD COLUMN `bms_warning_points`    INT NOT NULL DEFAULT 0
    COMMENT 'BMS 告警级事件点数(低电10%/高温限速等)'
    AFTER `bms_notice_points`,

  ADD COLUMN `bms_critical_points`   INT NOT NULL DEFAULT 0
    COMMENT 'BMS 故障级事件点数(压差过大/单体失效/过流等)'
    AFTER `bms_warning_points`;

-- 索引
ALTER TABLE surf_motion_trajectory_analysis
  ADD KEY `idx_protocol_fault`  (`has_protocol_fault`),
  ADD KEY `idx_fault_severity`  (`fault_severity_display`);

-- 故障明细表新增分级列
ALTER TABLE surf_motion_trajectory_fault_detail
  ADD COLUMN `severity_level` VARCHAR(8) NOT NULL DEFAULT 'unknown'
    COMMENT 'fault|warning|notice|unknown'
    AFTER `fault_code`,
  ADD COLUMN `board_out_of_control_related` TINYINT(1) NOT NULL DEFAULT 0
    COMMENT '是否参与板体失控合成'
    AFTER `severity_level`;

-- 日聚合表新增失控/分级统计列
ALTER TABLE surf_log_version_daily_stat
  ADD COLUMN `session_board_out_of_control` INT NOT NULL DEFAULT 0
    COMMENT '板体失控会话数' AFTER `session_overvoltage`,
  ADD COLUMN `session_warning`              INT NOT NULL DEFAULT 0
    COMMENT '纯告警(非协议故障)会话数' AFTER `session_board_out_of_control`,
  ADD COLUMN `session_protocol_fault`       INT NOT NULL DEFAULT 0
    COMMENT '协议故障会话数' AFTER `session_warning`,
  ADD COLUMN `invalid_speed_points_sum`     BIGINT NOT NULL DEFAULT 0
    COMMENT '超范围转速点数合计' AFTER `session_protocol_fault`;

Task 1-E repository.py — refresh_daily_stats 更新

文件 trajectory-log-analyzer/tla/db/repository.py

在 sql_version 中补充新聚合列(INSERT INTO ... SELECT ... 和 ON DUPLICATE KEY UPDATE 两处都要加):

sql_version = f"""
    INSERT INTO surf_log_version_daily_stat (
        stat_date, esc_version, ecu_version, bms_version, rc_version,
        session_total, session_fault, session_critical, session_stall,
        session_overvoltage, session_ecu_comm, session_corrupt_speed,
        session_board_out_of_control,    -- 新增
        session_warning,                 -- 新增
        session_protocol_fault,          -- 新增
        invalid_speed_points_sum,        -- 新增
        stall_points_sum
    )
    SELECT
        session_date,
        IFNULL(esc_version,''), IFNULL(ecu_version,''), IFNULL(bms_version,''), IFNULL(rc_version,''),
        COUNT(*),
        SUM(has_fault),
        SUM(severity = 'critical'),
        SUM(stall_points > 0),
        SUM(overvoltage_points > 0),
        SUM(ecu_comm_points > 0),
        SUM(corrupt_speed_points > 0),
        SUM(has_board_out_of_control),   -- 新增
        SUM(has_warning),                -- 新增
        SUM(has_protocol_fault),         -- 新增
        SUM(invalid_speed_points),       -- 新增
        SUM(stall_points)
    FROM surf_motion_trajectory_analysis
    WHERE session_date IS NOT NULL {date_filter}
    GROUP BY 1,2,3,4,5
    ON DUPLICATE KEY UPDATE
        session_total=VALUES(session_total),
        session_fault=VALUES(session_fault),
        session_critical=VALUES(session_critical),
        session_stall=VALUES(session_stall),
        session_overvoltage=VALUES(session_overvoltage),
        session_ecu_comm=VALUES(session_ecu_comm),
        session_corrupt_speed=VALUES(session_corrupt_speed),
        session_board_out_of_control=VALUES(session_board_out_of_control),  -- 新增
        session_warning=VALUES(session_warning),                            -- 新增
        session_protocol_fault=VALUES(session_protocol_fault),              -- 新增
        invalid_speed_points_sum=VALUES(invalid_speed_points_sum),          -- 新增
        stall_points_sum=VALUES(stall_points_sum)
"""

Task 1-F analysis/session_analyzer.py — 与 tla 版本对齐

文件 analysis/session_analyzer.py

当前版本为 1.0.0,需将 P0/P1 的新字段同步过来。
最简单的方式:将 analysis/session_analyzer.py 改为直接导入 tla 版本:

# analysis/session_analyzer.py 最简同步方案
# 将文件内容替换为:
from tla.session_analyzer import *  # noqa: F401,F403
from tla.session_analyzer import ANALYZER_VERSION, analyze_trajectory_row

或者保持独立文件但人工同步关键函数(若 analysis 目录不在 Python 路径上,则手动复制)。


Task 1-G 全量重算部署

# 1. 确保 P0 SQL 已执行
mysql -h HOST -u USER -p surf-dev < sql/migrations/20260601_tla_v1_0_3_p0.sql
# 2. 确保 P1 SQL 已执行
mysql -h HOST -u USER -p surf-dev < sql/migrations/20260601_tla_v1_0_3_p1.sql
# 3. 构建并部署新镜像
docker compose build
docker compose up -d  # 或 docker compose run --rm analyzer(单次跑)
# 4. 全量重算(bms_fault_points 重定义是 breaking change,所有行都要重算)
FORCE_REANALYZE=1 BATCH_SIZE=200 docker compose run --rm analyzer

Phase 2 — 可维护性(按需)

Task 2-A fault_catalog.yaml 外置码表

新建 trajectory-log-analyzer/tla/fault_catalog.yaml,内容见 TLA_IMPROVEMENT_PLAN_V2.md §5。

Task 2-B esc_faults.py 改为从 YAML 加载

import yaml, pathlib

def _load_catalog() -> dict:
    p = pathlib.Path(__file__).parent / "fault_catalog.yaml"
    return yaml.safe_load(p.read_text(encoding="utf-8"))

_CATALOG = _load_catalog()

def _build_dicts(domain: str):
    faults, severity, boc = {}, {}, set()
    for entry in _CATALOG.get(domain, []):
        code = entry["code"]
        faults[code]   = entry["mask"]
        severity[code] = entry.get("severity", "unknown")
        if entry.get("board_out_of_control"):
            boc.add(code)
    return faults, severity, frozenset(boc)

ESC_FAULTS, ESC_SEVERITY, ESC_BOARD_OUT_OF_CONTROL = _build_dicts("esc")
ECU_FAULTS, ECU_SEVERITY, _                         = _build_dicts("ecu")
BMS_FAULTS, BMS_SEVERITY, _                         = _build_dicts("bms")

Task 2-C 单元测试补全

新建 trajectory-log-analyzer/tests/test_fault_grading.py:

"""验证每条协议码都有正确的分级映射。"""
from tla.esc_faults import ESC_FAULTS, ESC_SEVERITY, ECU_FAULTS, ECU_SEVERITY, BMS_FAULTS, BMS_SEVERITY

def test_all_esc_codes_have_severity():
    for code in ESC_FAULTS:
        assert code in ESC_SEVERITY, f"ESC 码 {code} 缺少 severity"

def test_all_ecu_codes_have_severity():
    for code in ECU_FAULTS:
        assert code in ECU_SEVERITY, f"ECU 码 {code} 缺少 severity"

def test_all_bms_codes_have_severity():
    for code in BMS_FAULTS:
        assert code in BMS_SEVERITY, f"BMS 码 {code} 缺少 severity"

def test_stall_is_board_out_of_control():
    from tla.esc_faults import ESC_BOARD_OUT_OF_CONTROL
    assert "ESC_FAULT_STALL_PROTECTION" in ESC_BOARD_OUT_OF_CONTROL

def test_0x400000_is_board_out_of_control():
    from tla.esc_faults import ESC_BOARD_OUT_OF_CONTROL
    assert "ESC_EVENT_0x400000" in ESC_BOARD_OUT_OF_CONTROL

def test_invalid_speed():
    from tla.session_analyzer import is_invalid_motor_speed
    assert is_invalid_motor_speed(20000) is True
    assert is_invalid_motor_speed(-20000) is True
    assert is_invalid_motor_speed(19999) is False
    assert is_invalid_motor_speed(None) is False

验收 SQL(每阶段部署后执行)

V-1 P0 验收:新字段存在且有非零值

-- 确认新列存在
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_NAME = 'surf_motion_trajectory_analysis'
  AND COLUMN_NAME IN ('invalid_speed_points','has_board_out_of_control','esc_event_0x400000_points');
-- 期望 = 3

-- 确认重算后有失控会话
SELECT COUNT(*) AS boc_sessions
FROM surf_motion_trajectory_analysis
WHERE has_board_out_of_control = 1 AND analyzer_version = '1.0.3';

-- 转速超范围会话数
SELECT COUNT(*) AS invalid_spd_sessions
FROM surf_motion_trajectory_analysis
WHERE invalid_speed_points > 0 AND analyzer_version = '1.0.3';

V-2 P0 验收:原 overvoltage_points 与新字段一致

-- 必须为 0(双写完全一致)
SELECT COUNT(*) AS mismatch
FROM surf_motion_trajectory_analysis
WHERE overvoltage_points != esc_event_0x400000_points
  AND analyzer_version = '1.0.3';

V-3 P1 验收:BMS 低电不再单独触发 has_fault

-- 这类会话 v1.0.3 下 has_fault 应 = 0,has_warning = 1
SELECT
  has_fault,
  has_warning,
  has_protocol_fault,
  has_board_out_of_control,
  fault_severity_display,
  bms_warning_points,
  bms_fault_points
FROM surf_motion_trajectory_analysis
WHERE stall_points = 0
  AND esc_event_0x400000_points = 0
  AND invalid_speed_points = 0
  AND ecu_comm_points = 0
  AND ecu_other_points = 0
  AND bms_fault_points = 0        -- 无 BMS 故障级
  AND bms_warning_points > 0      -- 只有 BMS 告警
  AND analyzer_version = '1.0.3'
LIMIT 10;
-- 期望:has_fault=0, has_warning=1, fault_severity_display='warning'

V-4 P1 验收:故障分级分布合理

-- 看分布,确认 notice/warning/fault 均有数据且合理
SELECT
    fault_severity_display,
    COUNT(*) AS cnt,
    ROUND(COUNT(*)*100.0 / SUM(COUNT(*)) OVER(), 1) AS pct
FROM surf_motion_trajectory_analysis
WHERE analyzer_version = '1.0.3'
GROUP BY fault_severity_display
ORDER BY FIELD(fault_severity_display,'fault','warning','notice','none');

文件改动汇总

文件 Phase 改动类型 预计行数
tla/esc_faults.py P0+P1 改名 + 扩充 + 新增 severity 字典 +100 行
tla/fault_engine.py P0 字段名同步 3 处 ~5 行
tla/session_analyzer.py P0+P1 新函数 + 新字段 + has_fault 重定义 +60 行
tla/db/repository.py P1 refresh_daily_stats SQL 扩列 +15 行
sql/migrations/20260601_tla_v1_0_3_p0.sql P0 新建 30 行
sql/migrations/20260601_tla_v1_0_3_p1.sql P1 新建 50 行
analysis/session_analyzer.py P1 同步或导入 <10 行
tla/fault_catalog.yaml P2 新建 ~250 行
tests/test_fault_grading.py P2 新建 ~40 行

变更记录

版本 日期 说明
v1.0 2026-06-01 首版:逐文件逐函数 Checklist,对应 TLA_IMPROVEMENT_PLAN_V2.md