Files
meida_backend/tests/unit/test_report_dimensions.py
T
xsl e7076c9c01 feat(report): deterministic vision provider and 32-dimension diagnosis assembly
dimensions.py 逐行编码需求文档 §4(32 项四要素、缺失显式 skipped)+
VisionProvider 接口位与确定性实现 + 规则引擎联动 rule_hits(T-P3-01/06)。

实现:
- dimensions.py:DimensionSpec 恰 32 项(number 1~32 连续,33/34 永不复用),
  basic_coverage 口径机器可读:full 10 / label 3 / partial 8 / none 11,
  初级覆盖 21/32(BASIC_FULL_KEYS + BASIC_COVERED_KEYS)。
- vision.py:VisionProvider Protocol(派生特征形状写进 docstring,真实
  RTMPose/BiRefNet 实现后续按此替换)+ DeterministicVisionProvider(问卷
  BMI/自评驱动体型与肤色,sha256(object_key+canonical问卷) 驱动其余枚举,
  同输入同输出)+ build_vision_provider(未知名 CONFIG_ERROR)。
- vision_stage.py:derived_features 已存在 → skipped/reused(T-P3-09);
  否则分析 → 写中间态并 flush;photo_meta.quality_flags 透传进产物(T-P3-01)。
- diagnosis_stage.py:32 维恒全键,缺输入显式 skipped+原因(T-P3-06 红线);
  每维四要素 value/confidence/source/evidence/rule_hits + status;构造
  RuleContext → RuleService 载入 published 规则 → evaluate,body_shape/color
  类命中回落 #5/#19/#21,全量命中聚合进 #28/#30 的 rule_hits 与 evidence;
  色板 #19/#20/#21 从 kb_color 按季型读取,库空 → skipped「色板未录入」。

测试:单测 29 项(32 项完整性/编号连续/确定性/工厂拒绝未知名;四要素齐全
性——故意缺问卷+缺视觉断言 32 键全在且全部 skipped 带原因;规则命中进
rule_hits 用内置最小规则集不连库);集成 1 项(种 2 条 published 规则 →
VisionStage → DiagnosisStage:中间态落库与复用 skipped/reused、32 维全键、
rule_hits 出现、kb_color 空时色板 skipped、coverage basic_full=10/32)。
全绿:ruff check/format、mypy packages/report、lint-imports;单测 536 通过
(3 处既有 skip)、集成 46 通过。

偏差说明:
1. 任务文档写 RulesService,实际类名为 RuleService(packages/rules/service.py),按实际代码调用。
2. kb_color 联表键采用四季代码 spring/summer/autumn/winter(仓库无既定口径),已在 vision.py 注释声明。
3. 任务文档只给 basic_coverage 四枚举,需求表格中的「粗判/8色/3色」归入 partial、「Top1」归入 label。
2026-08-14 22:14:54 +08:00

88 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""维度字典:32 项完整性、编号连续、33/34 废弃不复用、初级口径机器可读(T-P3-06)。"""
from __future__ import annotations
from packages.report.dimensions import (
BASIC_COVERAGE_VALUES,
BASIC_COVERED_KEYS,
BASIC_FULL_KEYS,
DIMENSION_BY_KEY,
DIMENSION_BY_NUMBER,
DIMENSION_SOURCES,
DIMENSIONS,
)
#: 需求文档 §4 逐行统计出的初级 full 覆盖集(10 项)——本文件用硬编码预期
#: 对照 BASIC_FULL_KEYS,任何一边漂移都会立刻暴露。
_EXPECTED_BASIC_FULL = {
"gender",
"height_weight",
"birthday",
"body_shape",
"face_shape",
"skin_tone",
"season_code",
"psyche_style",
"fusion_style_name",
"core_goals",
}
#: 初级有产出(full + label + partial)= 21 项,即验收「初级 21/32」口径。
_EXPECTED_BASIC_COVERED_COUNT = 21
def test_exactly_32_dimensions_with_consecutive_numbers() -> None:
assert len(DIMENSIONS) == 32
assert [spec.number for spec in DIMENSIONS] == list(range(1, 33))
assert len(DIMENSION_BY_NUMBER) == 32
assert len(DIMENSION_BY_KEY) == 32
def test_keys_unique_and_stable() -> None:
keys = [spec.key for spec in DIMENSIONS]
assert len(set(keys)) == 32
# 字典键与 kb/记忆系统词汇对齐的锚点抽查。
for anchor in ("body_shape", "face_shape", "season_code"):
assert anchor in DIMENSION_BY_KEY
def test_retired_numbers_33_34_never_reused() -> None:
# TD-31(2026-08-13)移除五行分布/幸运色,编号永久废弃。
assert 33 not in DIMENSION_BY_NUMBER
assert 34 not in DIMENSION_BY_NUMBER
all_names = "".join(spec.name_cn for spec in DIMENSIONS)
assert "五行" not in all_names
assert "幸运色" not in all_names
def test_enums_legal() -> None:
for spec in DIMENSIONS:
assert spec.basic_coverage in BASIC_COVERAGE_VALUES
assert spec.source in DIMENSION_SOURCES
assert spec.name_cn, spec.key
def test_basic_full_keys_match_row_by_row_count() -> None:
# 机器可读口径 vs 逐行统计:两边必须同时成立。
assert BASIC_FULL_KEYS == _EXPECTED_BASIC_FULL
assert (
frozenset(spec.key for spec in DIMENSIONS if spec.basic_coverage == "full")
== BASIC_FULL_KEYS
)
assert len(BASIC_FULL_KEYS) == 10
def test_basic_covered_keys_is_21_of_32() -> None:
derived = frozenset(spec.key for spec in DIMENSIONS if spec.basic_coverage != "none")
assert derived == BASIC_COVERED_KEYS
assert len(BASIC_COVERED_KEYS) == _EXPECTED_BASIC_COVERED_COUNT
assert BASIC_FULL_KEYS <= BASIC_COVERED_KEYS
def test_coverage_split_matches_requirement_stats() -> None:
# 需求文档 §4 统计表:初级 full 10 / label+partial 11 / none 11,合计 32。
counts = {"full": 0, "label": 0, "partial": 0, "none": 0}
for spec in DIMENSIONS:
counts[spec.basic_coverage] += 1
assert counts == {"full": 10, "label": 3, "partial": 8, "none": 11}