"""MCDT 结果指标:验收节点与关键事实节点的评分卡输入模型。 起点事实(照实写清,不得淡化): - 此前 v6 批头 b7ac9d7 全仓 grep MCDT / 关键事实节点 / 验收节点为 0 命中:此前代码里根本没有 MCDT。 - 本模块只是评分卡输入模型,不是产品功能。不得声称 MCDT 已是 产品功能、已接入生产评分、已跑通真实审计循环。 - 本模块不改任何生产行为:不碰 #238 auto_apply(保持 False)、 不碰 #247“纠正为 pass 本身不解封”、不改 v1.1 契约、不接线到 build_scorecard / 服务器端点 / 门禁。本模块的输出只是“每维的 得分输入”,调用方是否采用、如何采用,须另行拍板。 设计(v7 机制第 8 项,只做这一项): - acceptance_node(验收节点):一项可验收的结果主张,至少含 id、title、acceptance_criteria(列表)、status、evidence_refs。 另含 dimension:该节点映射到哪一维(7 维 id 之一);无 dimension 或 dimension 非法时该节点不映射到任何维(记入未映射清单), 绝不靠猜测、按 id 前缀、按其它维借分。 - key_fact_node(关键事实节点):一项支撑/证伪验收的关键事实, 至少含 id、fact、status、source。另含 dimension:与验收节点同 规则;无 dimension 的关键事实不影响任何维。 - status 词汇(两类节点共用):pending / pass / fail / unknown。 pending = 尚未判定;unknown = 未知/未验证;fail = 已证伪或未达标; pass = 已验收通过。其它任何值一律按 unknown 处理(fail-closed, 不把拼写错误读成通过)。 映射到哪几维(写清,不留含糊): - delivery_quality(交付质量):验收节点证明交付结果达到验收标准。 - gate_audit(门禁与审计有效性):验收节点证明门禁真实拦截、 审计结论可复核。 - arch_contract(架构与契约一致性):验收节点证明契约冻结、 架构 / 目录 / 导入一致。 - 其余四维 test_coverage、security_ledger、cost_quota、ops_scale: 本项不预设节点。无对应验收节点时一律记 unknown,并在 missing_items 里说明;不得用过程指标(测试数、覆盖率)填补, 不得从其它维借分。注意 test_coverage 尤其如此:覆盖率数字 本身是过程指标,即使调用方手头有覆盖率,也不能因此把 test_coverage 从 unknown 改成 pass/fail——只有映射到该维的 验收节点状态变动才能改分。 映射规则(nodes_to_dimension_inputs): (a) 某维无对应验收节点时,该维输入为 unknown(不得用过程指标填补)。 (b) 过程指标(test_count、coverage)只作为证据字段透传,绝不直接 进入得分输入:它们只出现在返回值的顶层 process_evidence 里, 绝不出现在任何一维的 score_input / status / basis 里。只改 test_count / coverage 而节点状态不变时,每维的得分输入不变; 只改节点状态(pending -> pass 或 pass -> fail)时,映射输出变化。 节点 dict 上附带的 test_count / coverage 等额外键同样被忽略, 只读 id / title / acceptance_criteria / status / evidence_refs / dimension(验收节点)与 id / fact / status / source / dimension (关键事实节点)。 (c) 节点状态为 fail 时对应维不得为 pass;节点状态 unknown 时对应维 为 unknown。具体聚合(fail-closed,优先级 fail > unknown > pending > pass 的反向表述见代码): - 任一映射验收节点 fail -> 该维 fail; - 任一映射关键事实 fail -> 该维 fail(已证伪的关键事实不得 让该维通过,即使验收节点全 pass); - 否则任一映射验收节点 unknown 或 pending -> 该维 unknown; - 否则任一映射关键事实 unknown 或 pending -> 该维 unknown (支撑事实未验证,不得宣称通过); - 否则(映射验收节点全 pass,且映射关键事实全 pass 或无映射 关键事实)-> 该维 pass。 关键事实单独存在时不能让维通过:无验收节点时该维仍 unknown。 阈值口径(沿用 audit_scorecard.py): - thresholds 参数只用于报告 thresholds_source,绝不改变任何一维 的状态:传阈值不能把 unknown 变成 pass。 - 默认 thresholds_source = "test_default_unapproved"(测试默认值、 未拍板);只有调用方显式传入且全部条目 source 均为 "user_approved" 时才记 "user_approved"。本模块绝不把测试默认 值写成生产已拍板;B / C 维与阈值本身不在本项决定范围。 """ from __future__ import annotations from dataclasses import dataclass, field # 维顺序与 id 与 cloud/audit_scorecard.py 的 DIMENSIONS 保持一致; # 若那边增维、减维、改名,本模块须同步改(测试钉住两者相等)。 DIMENSION_IDS = [ "delivery_quality", "gate_audit", "arch_contract", "test_coverage", "security_ledger", "cost_quota", "ops_scale", ] # 本项有映射语义说明的三维(其余四维无预设节点,见模块 docstring)。 MAPPED_DIMENSIONS_DOC = { "delivery_quality": "验收节点证明交付结果达到验收标准。", "gate_audit": "验收节点证明门禁真实拦截、审计结论可复核。", "arch_contract": "验收节点证明契约冻结、架构 / 目录 / 导入一致。", } VALID_STATUSES = ("pending", "pass", "fail", "unknown") MODEL_NOTE = ( "MCDT input model only, not a product feature: " "v6 head b7ac9d7 had 0 hits for MCDT / acceptance nodes / " "key fact nodes; this module only maps node statuses to " "per-dimension score inputs and does not wire into scoring, " "gates, or production." ) @dataclass class AcceptanceNode: """验收节点。status 非法值在映射时按 unknown 处理。""" id: str title: str acceptance_criteria: list = field(default_factory=list) status: str = "unknown" evidence_refs: list = field(default_factory=list) dimension: str | None = None @dataclass class KeyFactNode: """关键事实节点。status 非法值在映射时按 unknown 处理。""" id: str fact: str status: str = "unknown" source: str = "" dimension: str | None = None def _as_dict(node): """接受 dataclass 实例或 dict,一律转成 dict 只读视图。""" if isinstance(node, dict): return node if hasattr(node, "__dataclass_fields__"): return { f: getattr(node, f) for f in node.__dataclass_fields__ # noqa: B905 } raise TypeError("node must be a dict or dataclass, got %r" % type(node)) def _norm_status(value): """非法状态 -> unknown(fail-closed,不把拼写错误读成通过)。""" if value in VALID_STATUSES: return value return "unknown" def _thresholds_source(thresholds): """与 audit_scorecard.py 同口径:只有全 user_approved 才算已拍板。""" if not thresholds: return "test_default_unapproved" sources = set() for entry in thresholds.values(): if isinstance(entry, dict): sources.add(entry.get("source", "test_default_unapproved")) else: sources.add("test_default_unapproved") if sources == {"user_approved"}: return "user_approved" return "test_default_unapproved" def nodes_to_dimension_inputs(acceptance_nodes, key_fact_nodes, thresholds=None, process_metrics=None): """把节点状态映射为每维的得分输入。 acceptance_nodes / key_fact_nodes:AcceptanceNode / KeyFactNode 实例或同字段 dict 的列表。process_metrics:过程指标 dict (如 {"test_count": ..., "coverage": ...}),只透传到顶层 process_evidence,绝不进入任何一维的得分输入。 返回:{"dimensions": {维 id: 维输入}, "dimension_order": [...], "process_evidence": {...}, "unmapped_acceptance_node_ids": [...], "unmapped_key_fact_node_ids": [...], "thresholds_source": ..., "model_note": ...}。 每维输入含 dimension / status / score_input / acceptance_node_ids / key_fact_node_ids / missing_items / basis;score_input 里只有 节点状态计数,绝无 test_count / coverage 键。 """ acceptance_nodes = list(acceptance_nodes or []) key_fact_nodes = list(key_fact_nodes or []) per_dim_acc = {dim: [] for dim in DIMENSION_IDS} per_dim_facts = {dim: [] for dim in DIMENSION_IDS} unmapped_acc, unmapped_facts = [], [] for raw in acceptance_nodes: node = _as_dict(raw) dim = node.get("dimension") if dim in DIMENSION_IDS: per_dim_acc[dim].append(node) else: unmapped_acc.append(node.get("id")) for raw in key_fact_nodes: node = _as_dict(raw) dim = node.get("dimension") if dim in DIMENSION_IDS: per_dim_facts[dim].append(node) else: unmapped_facts.append(node.get("id")) dimensions = {} for dim in DIMENSION_IDS: acc = per_dim_acc[dim] facts = per_dim_facts[dim] acc_statuses = [_norm_status(n.get("status")) for n in acc] fact_statuses = [_norm_status(n.get("status")) for n in facts] missing, basis = [], [] if not acc: missing.append("no_acceptance_node_mapped") if dim not in MAPPED_DIMENSIONS_DOC: missing.append( "no_preset_node_for_dimension:" " process metrics must not fill this dimension") basis.append("no mapped acceptance node: dimension unknown") status = "unknown" elif "fail" in acc_statuses: status = "fail" basis.append("mapped acceptance node fail: dimension not pass") elif "fail" in fact_statuses: status = "fail" basis.append("mapped key fact fail: dimension not pass") elif "unknown" in acc_statuses or "pending" in acc_statuses: status = "unknown" basis.append( "mapped acceptance node not yet pass" " (unknown/pending): dimension unknown") elif "unknown" in fact_statuses or "pending" in fact_statuses: status = "unknown" basis.append( "mapped key fact not yet verified" " (unknown/pending): dimension unknown") else: status = "pass" basis.append( "all mapped acceptance nodes pass and mapped key facts" " pass or absent") score_input = { "node_status": status, "acceptance_pass_count": acc_statuses.count("pass"), "acceptance_fail_count": acc_statuses.count("fail"), "acceptance_pending_count": acc_statuses.count("pending"), "acceptance_unknown_count": acc_statuses.count("unknown"), "key_fact_pass_count": fact_statuses.count("pass"), "key_fact_fail_count": fact_statuses.count("fail"), "key_fact_pending_count": fact_statuses.count("pending"), "key_fact_unknown_count": fact_statuses.count("unknown"), } dimensions[dim] = { "dimension": dim, "status": status, "score_input": score_input, "acceptance_node_ids": [n.get("id") for n in acc], "key_fact_node_ids": [n.get("id") for n in facts], "missing_items": missing, "basis": basis, } return { "dimensions": dimensions, "dimension_order": list(DIMENSION_IDS), # 过程指标只在这里透传:顶层证据字段,不是得分输入。 "process_evidence": dict(process_metrics) if process_metrics else {}, "unmapped_acceptance_node_ids": unmapped_acc, "unmapped_key_fact_node_ids": unmapped_facts, "thresholds_source": _thresholds_source(thresholds), "model_note": MODEL_NOTE, }