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。
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""维度字典: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}
|