feat(v2): T06-b 方案装配与依赖预检(可定位的 E-19 文案)
T06 第二块。核对后确认 component_registry.freeze() 已实现大部分 AC-28: 按 release 指针解析依赖、原生方法文本装配时固化、循环依赖与输出冲突检查、 数据域一致性("演示与正式组件不能混用")、content_hash。因此本批**不重复实现冻结**, 只补真正的缺口。 新增 contracts:JudgmentBinding、ReviewPlan(含维度、关键核查项、专项绑定)。 新增 review_plan.check_dependencies() / assemble(): - **缺口**:未发布/已撤回的依赖原先抛裸 KeyError,被 translate() 映射成 404 "对象不存在",用户无法定位是哪一项。现改为逐个检查并**指明依赖名与原因** ("尚未发布或已撤回"/"版本状态为 X"/"发布指针指向的版本不存在") - 只用 release 指针判定可用性——草稿与已撤回天然没有指针,这正是 BR-27 "草稿或已撤回组件不得进入新运行"的机械表达 - 装配时接入 T06-a 的维度解析与呈现判定,并做不丢失断言 测试:test_review_plan.py 增至 25 项(新增 7 项装配)。其中版本换版后 content_hash 必须变化,用于证明版本确实被冻结进结果而不是每次重解析。 全量 340 项 OK(333 → 340);verify.py 10/10 PASS。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ BR-28 / E-20 / AC-29 / AC-44。
|
||||
from __future__ import annotations
|
||||
|
||||
from contracts.prescreen import DimensionSpec
|
||||
from services import assistant_store as store
|
||||
from services import component_registry as registry
|
||||
|
||||
RADAR_MIN, RADAR_MAX = 3, 8
|
||||
|
||||
@@ -111,3 +113,71 @@ def assert_nothing_dropped(result: dict, dimensions) -> None:
|
||||
raise ValueError(f"呈现判定丢失了维度:{sorted(missing)}")
|
||||
if shown - expected:
|
||||
raise ValueError(f"呈现判定出现了未知维度:{sorted(shown - expected)}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 方案装配(T06-b)
|
||||
# 依赖不可用时的统一文案(对应 E-19 PLAN_DEPENDENCY_INVALID)。
|
||||
DEPENDENCY_INVALID = "评审方案存在不可用组件,请更新后重新发起;不暴露无权组件"
|
||||
REF_GROUPS = ("skills", "rules", "template", "cards")
|
||||
|
||||
|
||||
def _unusable(db, component_id: str) -> str | None:
|
||||
"""返回不可用原因;可用则 None。
|
||||
|
||||
只用**已发布指针**(release)判定:草稿、已撤回、从未发布的组件都没有 release 行,
|
||||
因此天然被排除——这正是"草稿或已撤回组件不得进入新运行"(BR-27)的机械表达。
|
||||
"""
|
||||
try:
|
||||
pointer = store.get(db, "release", component_id)
|
||||
except KeyError:
|
||||
return "尚未发布或已撤回"
|
||||
try:
|
||||
version = store.get(db, "component_version", pointer["version_id"])
|
||||
except KeyError:
|
||||
return "发布指针指向的版本不存在"
|
||||
if version.get("status") != "published":
|
||||
return f'版本状态为 {version.get("status")}'
|
||||
return None
|
||||
|
||||
|
||||
def check_dependencies(db, plan: dict) -> None:
|
||||
"""装配前预检:逐个说明**哪个**依赖不可用,而不是抛裸 KeyError。
|
||||
|
||||
裸 KeyError 会被 translate() 映射成 404,用户看到"对象不存在",无法定位是哪一项。
|
||||
"""
|
||||
problems = []
|
||||
for group in REF_GROUPS:
|
||||
keys = plan.get(group) if group != "template" else [plan.get("template")]
|
||||
for key in [item for item in (keys or []) if item]:
|
||||
reason = _unusable(db, key)
|
||||
if reason:
|
||||
problems.append(f"{group}:{key}({reason})")
|
||||
if problems:
|
||||
raise ValueError(f"{DEPENDENCY_INVALID};不可用依赖:{'、'.join(sorted(problems))}")
|
||||
|
||||
|
||||
def assemble(db, plan_id: str, legacy_catalog=None) -> dict:
|
||||
"""装配并冻结一个评审方案。
|
||||
|
||||
不重复实现冻结:解析、原生方法文本冻结、循环依赖与输出冲突检查、数据域一致性、
|
||||
content_hash 全部委托 `component_registry.freeze()`。本函数只加两件事:
|
||||
1. 依赖预检,给出可定位的 E-19 文案(而非 404 的"对象不存在")
|
||||
2. 维度解析与呈现判定(T06-a),并保证维度不丢失
|
||||
"""
|
||||
reason = _unusable(db, plan_id)
|
||||
if reason:
|
||||
raise ValueError(f"评审方案不可用({reason});{DEPENDENCY_INVALID}")
|
||||
|
||||
published_plan = registry.resolve(db, plan_id)
|
||||
if published_plan.get("kind") != "plan":
|
||||
raise ValueError("所选组件不是业务评审方案")
|
||||
check_dependencies(db, published_plan)
|
||||
|
||||
frozen = registry.freeze(db, plan_id, legacy_catalog)
|
||||
dimensions = resolve_dimensions(published_plan.get("dimensions", []))
|
||||
result = presentation(dimensions)
|
||||
assert_nothing_dropped(result, dimensions)
|
||||
|
||||
frozen["dimensions"] = dimensions
|
||||
frozen["presentation"] = result
|
||||
return frozen
|
||||
|
||||
Reference in New Issue
Block a user