1173 lines
55 KiB
Python
1173 lines
55 KiB
Python
"""7 维项目级审计评分卡(纯函数、标准库 only)。
|
||
|
||
定位:这是一个评分卡,不是门禁、不是审计、不是自动循环。
|
||
它不重写 metrics、arch_conformance、ledger、audit_loop 的逻辑,
|
||
只接受调用方显式传入的结构化证据,算出 7 维各自的分数。
|
||
|
||
计分规则(钉死,不可改为平均分达标):
|
||
- 每维分数 0-10,保留一位小数;>=9 通过,<9 不通过。
|
||
- 总体结论:7 维全部 >=9 才算达标(overall="pass")。
|
||
任一维 <9,总体就不达标(overall="not_pass")。
|
||
- 平均分只作参考信息(average_score),绝不能作为达标依据。
|
||
代码里把“平均分达标”写成明确的不通过路径并加测试钉住:
|
||
例如 6 维 10 分、1 维 4 分,平均 9.1 分,总体仍是不达标。
|
||
- 未知处理:任何一维的数据源缺失、未知、未验证,
|
||
该维分数记为 None(未知),不记 0、不记 10、不估算,
|
||
总体直接不达标,并列出缺失项(missing_items)。
|
||
“没数据”既不算通过,也不算 0 分去拉低平均分后又用平均分糊弄。
|
||
- 未实测处理:调用方传入“夹具模拟”“视口替代”“未编译端代码评审”
|
||
这类证据时,该项记为“未实测”(untested_items),而不是“通过”。
|
||
若某维的关键证据全部是未实测,该维记为 None。
|
||
- 阈值:DEFAULT_THRESHOLDS 全部标注 source="test_default_unapproved"
|
||
(测试默认值、未拍板)。生产阈值须用户拍板后,由调用方显式
|
||
传入 thresholds(含 source="user_approved")才能替换默认值。
|
||
本模块绝不把测试初值写成生产值。
|
||
- 与已定语义的关系(本模块不改它们):
|
||
#238 evaluate_feedback 的 auto_apply 保持 False;
|
||
#247 纠正为 pass 本身不解封;
|
||
#244 CONDITIONAL 先映射为 fail 再记(本模块只接受已映射的结论,
|
||
若调用方传入 verdict="conditional",记为未实测/无效证据,不算通过)。
|
||
- 基线漂移门禁(v7 机制第 5 项,本模块新增):
|
||
build_scorecard(..., baseline_head=None) 的 baseline_head 是机器字段,
|
||
只能由调用方显式传入当前 main 头;本模块绝不在生产代码里探测
|
||
版本控制状态、绝不读版本控制目录、绝不编造一个头。
|
||
不传入(或传入空串)时 baseline_head 为 None,并在 missing_items
|
||
里记 baseline_head_missing,不把缺失读成“与当前一致”。
|
||
check_baseline_stale(scorecard, current_main_head) 是唯一的漂移判定:
|
||
current_main_head != baseline_head 时 stale=True,且该旧分整体结论
|
||
不得再作 pass 依据(返回里以 usable_as_pass_basis=False 明示,
|
||
调用方凭此字段判断)。stale 是附加字段:本项绝不改写
|
||
build_scorecard 的 overall / overall_pass 旧语义,旧值保留以便追溯,
|
||
测试断言 stale 字段而非改写旧 overall。
|
||
冻结版的服务端存储见 cloud/scorecard_baseline_store.py(只追加、
|
||
不提供改删旧冻结行的方法);阈值仍一律标 test_default_unapproved,
|
||
除非调用方显式传入 user_approved。
|
||
|
||
7 维定义(不要擅自增维、减维、改名换义):
|
||
1. delivery_quality 交付质量
|
||
2. gate_audit 门禁与审计有效性
|
||
3. arch_contract 架构与契约一致性
|
||
4. test_coverage 测试与验证覆盖
|
||
5. security_ledger 安全与账本可信
|
||
6. cost_quota 成本与配额可观测
|
||
7. ops_scale 生产运维与规模
|
||
|
||
10 维到 7 维的合并映射(写进文档,也写在这里):
|
||
- 流程完整性并入门禁与审计有效性(G0-G8 最终靠门禁和重审证明);
|
||
- 自动化程度并入门禁与审计有效性 + 生产运维与规模
|
||
(自动化的证据就是自动循环是否真实跑通、真实 App 是否安装);
|
||
- 文档与技能并入架构与契约一致性
|
||
(技能文本、附录溯源、移植规则最终都是契约一致性的一部分)。
|
||
|
||
输入格式:build_scorecard(evidence, thresholds=None, rubric_version=None,
|
||
baseline_head=None, evidence_rows=None, user_side_items=None,
|
||
product_evidence=None)
|
||
rubric_version 只是卡片上的版本锚点(版本化持久存储见
|
||
cloud/scorecard_rubric.py:标尺改动写新版本行、旧行只读,
|
||
本函数不查库、不改旧分)。
|
||
输入格式:build_scorecard(evidence, thresholds=None, user_side_items=None) evidence: dict,键为上面 7 个维 id,值为该维的证据 dict。
|
||
每个维的证据字段见各 _score_* 函数的 docstring。
|
||
调用方必须带来源、时间、检查名;本模块不自动从测试文件、
|
||
摘要文档、PR 描述里拼凑分数。
|
||
|
||
三张表拆分(v7 机制第 2 项,见 _SCORERS 后的注释):
|
||
build_code_scorecard:代码证据 4 维,不含生产项;
|
||
build_production_scorecard:生产证据 3 维,缺项不拉低代码表,
|
||
可选 user_side_items 占位证据(pending 即挡住生产表 overall);
|
||
build_product_scorecard:全部 7 维 + 真实产品轮门禁,无真实
|
||
产品轮证据时恒 not_pass;这只是评分卡输入结构拆分,
|
||
不代表任何产品功能已落地。build_scorecard 的返回 dict
|
||
同时带 code/production/product 三个子对象,GET /audit-scorecard
|
||
端点返回的示例卡因此也带这三个子对象(端点已接线)。v7 机制第 7 项 user_side_items(本模块内实现,不依赖第 2 项的三张表函数):
|
||
- evidence 可带可选键 "user_side_items",或由 build_scorecard 的
|
||
user_side_items 关键字参数显式传入(关键字参数优先于 evidence 键)。
|
||
每项至少含 id、status(pending/done/blocked)、blocked_by
|
||
(pending/blocked 时必填,例如 user/host_env/contract_decision)。
|
||
- 非法项绝不静默丢弃、绝不编造状态:记入
|
||
user_side_items["invalid_items"](含下标与原因),不进 items。
|
||
- 输出增加 user_side_items 字段:原样透传有效项(多余字段保留),
|
||
并附汇总 pending_ids、done_ids、blocked_ids、blocked_by_summary。
|
||
- 三表规则见 apply_user_side_three_table_rule():代码表结论
|
||
(code_overall)绝不因用户侧 pending 被拉低;用户侧有未完成项
|
||
(pending/blocked)或非法项时,生产表结论强制为 not_pass,
|
||
原因字段明确指向用户侧(如 "user_side_pending: B1,B2,...");
|
||
user_side_items 为空/缺失时,输出 user_side_unknown=True,
|
||
生产表原因写 user_side_unknown,绝不把“无用户侧项”写成
|
||
“用户侧已完成”。
|
||
- 本项不做 B/C 拍板、不改阈值:阈值仍为 test_default_unapproved;
|
||
B/C 清单只是 id 与标题常量(见 USER_SIDE_ITEM_IDS),未知 id
|
||
不因此判非法(清单可能扩展),非法只按缺字段/未知状态/缺
|
||
blocked_by/重复 id 判定。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import json
|
||
|
||
PASS_SCORE = 9.0
|
||
|
||
DIMENSIONS = [
|
||
("delivery_quality", "交付质量"),
|
||
("gate_audit", "门禁与审计有效性"),
|
||
("arch_contract", "架构与契约一致性"),
|
||
("test_coverage", "测试与验证覆盖"),
|
||
("security_ledger", "安全与账本可信"),
|
||
("cost_quota", "成本与配额可观测"),
|
||
("ops_scale", "生产运维与规模"),
|
||
]
|
||
|
||
DIMENSION_IDS = [d[0] for d in DIMENSIONS]
|
||
|
||
# 阈值表:每一项都带 source,默认值一律是测试初值、未拍板。
|
||
# “9 分线”是达到 9 分的下限,“10 分线”是达到 10 分的下限,
|
||
# “floor”是低于该值后分数进入 0-5 区间的参考点(不是达标线)。
|
||
# 这些数值本身不是生产标准;生产阈值须用户拍板后显式传入。
|
||
DEFAULT_THRESHOLDS = {
|
||
"defect_rate_nine": {"value": 0.05, "source": "test_default_unapproved"},
|
||
"defect_rate_ten": {"value": 0.02, "source": "test_default_unapproved"},
|
||
"defect_rate_floor": {"value": 0.20, "source": "test_default_unapproved"},
|
||
"first_pass_nine": {"value": 0.90, "source": "test_default_unapproved"},
|
||
"first_pass_ten": {"value": 0.95, "source": "test_default_unapproved"},
|
||
"first_pass_floor": {"value": 0.50, "source": "test_default_unapproved"},
|
||
"rework_rate_nine": {"value": 0.10, "source": "test_default_unapproved"},
|
||
"rework_rate_ten": {"value": 0.05, "source": "test_default_unapproved"},
|
||
"rework_rate_floor": {"value": 0.50, "source": "test_default_unapproved"},
|
||
"overturn_rate_floor": {"value": 0.30, "source": "test_default_unapproved"},
|
||
"coverage_nine": {"value": 0.85, "source": "test_default_unapproved"},
|
||
"coverage_ten": {"value": 0.90, "source": "test_default_unapproved"},
|
||
"coverage_floor": {"value": 0.75, "source": "test_default_unapproved"},
|
||
"sampling_normal": {"value": 0.10, "source": "test_default_unapproved"},
|
||
"sampling_critical": {"value": 0.50, "source": "test_default_unapproved"},
|
||
"parallel_scale_target": {"value": 40, "source": "test_default_unapproved"},
|
||
}
|
||
|
||
UNTESTED_KINDS = {"fixture", "viewport_substitute", "code_review_only",
|
||
"fixture_simulated", "dry_run", "estimated"}
|
||
|
||
# v7 审计 B/C 用户侧清单(id 与标题;仅作常量与测试证据,不作拍板)。
|
||
USER_SIDE_ITEM_IDS = [
|
||
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
||
"C1", "C2", "C3", "C4",
|
||
]
|
||
|
||
USER_SIDE_ITEM_TITLES = {
|
||
"B1": "分支保护",
|
||
"B2": "合并部署窗口",
|
||
"B3": "阈值拍板",
|
||
"B4": "金任务人工审定",
|
||
"B5": "真实 PG 环境",
|
||
"B6": "真实 App 安装",
|
||
"B7": "第二台主机与账单",
|
||
"B8": "v1.1 确认",
|
||
"B9": "回滚演练窗口",
|
||
"C1": "证据绑定强制化与签名",
|
||
"C2": "live 验证接线",
|
||
"C3": "锚点策略",
|
||
"C4": "actual-only 语义",
|
||
}
|
||
|
||
VALID_USER_SIDE_STATUSES = {"pending", "done", "blocked"}
|
||
|
||
|
||
def normalize_user_side_items(raw):
|
||
"""归一化用户侧项:有效项原样透传,非法项记入 invalid_items。
|
||
|
||
返回 dict,键:items、pending_ids、done_ids、blocked_ids、
|
||
unfinished_ids、blocked_by_summary、invalid_items、count、
|
||
all_pending、all_done、user_side_unknown。
|
||
user_side_unknown 仅在 raw 为 None 或空列表时为 True;
|
||
非列表、非法项等情形不算 unknown,而是 invalid(生产表
|
||
同样不能因此通过,原因指向 invalid,见三表规则)。
|
||
本函数不修改调用方传入的原始项(逐项浅拷贝)。
|
||
"""
|
||
summary = {
|
||
"items": [],
|
||
"pending_ids": [],
|
||
"done_ids": [],
|
||
"blocked_ids": [],
|
||
"unfinished_ids": [],
|
||
"blocked_by_summary": {},
|
||
"invalid_items": [],
|
||
"count": 0,
|
||
"all_pending": False,
|
||
"all_done": False,
|
||
"user_side_unknown": False,
|
||
}
|
||
if raw is None:
|
||
summary["user_side_unknown"] = True
|
||
return summary
|
||
if isinstance(raw, (str, bytes)) or not isinstance(raw, (list, tuple)):
|
||
summary["invalid_items"].append({
|
||
"index": None,
|
||
"reason": "user_side_items_not_a_list",
|
||
"raw_type": type(raw).__name__,
|
||
})
|
||
return summary
|
||
if len(raw) == 0:
|
||
summary["user_side_unknown"] = True
|
||
return summary
|
||
seen_ids = set()
|
||
for index, item in enumerate(raw):
|
||
if not isinstance(item, dict):
|
||
summary["invalid_items"].append({
|
||
"index": index,
|
||
"reason": "item_not_a_dict",
|
||
"raw_type": type(item).__name__,
|
||
})
|
||
continue
|
||
item_id = item.get("id")
|
||
if not isinstance(item_id, str) or not item_id:
|
||
summary["invalid_items"].append({
|
||
"index": index,
|
||
"reason": "id_missing",
|
||
})
|
||
continue
|
||
if item_id in seen_ids:
|
||
summary["invalid_items"].append({
|
||
"index": index,
|
||
"id": item_id,
|
||
"reason": "duplicate_id",
|
||
})
|
||
continue
|
||
status = item.get("status")
|
||
if status not in VALID_USER_SIDE_STATUSES:
|
||
summary["invalid_items"].append({
|
||
"index": index,
|
||
"id": item_id,
|
||
"reason": "status_missing_or_unknown",
|
||
"status": status,
|
||
})
|
||
continue
|
||
blocked_by = item.get("blocked_by")
|
||
if status in ("pending", "blocked"):
|
||
if not isinstance(blocked_by, str) or not blocked_by:
|
||
summary["invalid_items"].append({
|
||
"index": index,
|
||
"id": item_id,
|
||
"reason": "blocked_by_missing",
|
||
})
|
||
continue
|
||
seen_ids.add(item_id)
|
||
copied = dict(item)
|
||
summary["items"].append(copied)
|
||
if status == "pending":
|
||
summary["pending_ids"].append(item_id)
|
||
summary["unfinished_ids"].append(item_id)
|
||
elif status == "blocked":
|
||
summary["blocked_ids"].append(item_id)
|
||
summary["unfinished_ids"].append(item_id)
|
||
else:
|
||
summary["done_ids"].append(item_id)
|
||
if isinstance(blocked_by, str) and blocked_by:
|
||
summary["blocked_by_summary"].setdefault(blocked_by, []).append(item_id)
|
||
summary["count"] = len(summary["items"])
|
||
if summary["items"]:
|
||
summary["all_pending"] = all(
|
||
it["status"] == "pending" for it in summary["items"])
|
||
summary["all_done"] = all(
|
||
it["status"] == "done" for it in summary["items"])
|
||
return summary
|
||
|
||
|
||
def _extract_overall(value):
|
||
"""从字符串或评分卡 dict 里取出 overall(字符串原样返回)。"""
|
||
if isinstance(value, dict):
|
||
for key in ("overall", "code_overall", "production_overall"):
|
||
if isinstance(value.get(key), str):
|
||
return value[key]
|
||
return None
|
||
if isinstance(value, str):
|
||
return value
|
||
return None
|
||
|
||
|
||
def apply_user_side_three_table_rule(code_overall, production_overall,
|
||
user_side_items):
|
||
"""三表规则(v7 机制第 7 项;位置:本模块)。
|
||
|
||
输入:代码表结论、生产表结论(各为 "pass"/"not_pass" 字符串,
|
||
或含 overall 键的评分卡 dict)、user_side_items 原始列表。
|
||
输出:代码表结论原样不动;生产表结论在用户侧未完成/非法/
|
||
未知时强制为 not_pass,原因明确指向用户侧;用户侧全部 done
|
||
且无非法项时,生产表结论跟随其自身输入结论,不被本规则改写。
|
||
本函数是纯函数,不读库、不改阈值、不做 B/C 拍板。
|
||
"""
|
||
normalized = normalize_user_side_items(user_side_items)
|
||
code = _extract_overall(code_overall)
|
||
production = _extract_overall(production_overall)
|
||
forced = False
|
||
reason = None
|
||
if normalized["user_side_unknown"]:
|
||
production = "not_pass"
|
||
reason = ("user_side_unknown: no user_side_items supplied; "
|
||
"not equivalent to user_side_done")
|
||
forced = True
|
||
elif normalized["invalid_items"]:
|
||
production = "not_pass"
|
||
reasons = "; ".join(
|
||
"%s(%s)" % (inv.get("reason", "invalid"),
|
||
inv.get("id", inv.get("index")))
|
||
for inv in normalized["invalid_items"])
|
||
reason = "user_side_invalid: %s" % reasons
|
||
if normalized["unfinished_ids"]:
|
||
reason += "; user_side_pending: %s" % ",".join(
|
||
normalized["unfinished_ids"])
|
||
forced = True
|
||
elif normalized["unfinished_ids"]:
|
||
production = "not_pass"
|
||
reason = "user_side_pending: %s" % ",".join(
|
||
normalized["unfinished_ids"])
|
||
forced = True
|
||
return {
|
||
"code_overall": code,
|
||
"production_overall": production,
|
||
"production_reason": reason,
|
||
"user_side_forced_production_not_pass": forced,
|
||
"code_unchanged_by_user_side": True,
|
||
"user_side_unknown": normalized["user_side_unknown"],
|
||
"user_side_items": normalized,
|
||
}
|
||
|
||
|
||
def thresholds_source(thresholds):
|
||
"""阈值来源:只有调用方显式传入 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"
|
||
|
||
|
||
# 旧私有名的兼容别名(v2cov 评分卡测试仍从这里导入它)。
|
||
_thresholds_source = thresholds_source
|
||
|
||
|
||
def _effective_thresholds(thresholds):
|
||
"""实际用于计分的阈值表:默认值 + 调用方显式覆盖,逐项归一化。
|
||
|
||
未被覆盖的键仍取 DEFAULT_THRESHOLDS(与 _t 的逐键回落一致),
|
||
因此卡片上报告的 thresholds 就是真正算分的表,不再是“恒为
|
||
默认值”的旧写法。数值本身仍一律标来源,未拍板的不冒充生产值。
|
||
"""
|
||
effective = {k: dict(v) for k, v in DEFAULT_THRESHOLDS.items()}
|
||
for key, entry in (thresholds or {}).items():
|
||
if isinstance(entry, dict):
|
||
merged = {"value": entry["value"]}
|
||
merged["source"] = entry.get("source", "test_default_unapproved")
|
||
effective[key] = merged
|
||
else:
|
||
effective[key] = {"value": entry,
|
||
"source": "test_default_unapproved"}
|
||
return effective
|
||
|
||
|
||
def _t(thresholds, key):
|
||
if thresholds and key in thresholds:
|
||
entry = thresholds[key]
|
||
if isinstance(entry, dict):
|
||
return entry["value"]
|
||
return entry
|
||
return DEFAULT_THRESHOLDS[key]["value"]
|
||
|
||
|
||
def _round1(value):
|
||
return round(float(value) + 1e-9, 1)
|
||
|
||
|
||
def _score_higher(value, nine, ten, floor):
|
||
"""越高越好:floor->5 分,nine->9 分,ten->10 分,分段线性,低于 floor 按比例。"""
|
||
if value is None:
|
||
return None
|
||
v = float(value)
|
||
if v >= ten:
|
||
return 10.0
|
||
if v >= nine:
|
||
return 9.0 + (v - nine) / (ten - nine) if ten > nine else 10.0
|
||
if v >= floor:
|
||
return 5.0 + 4.0 * (v - floor) / (nine - floor) if nine > floor else 5.0
|
||
if floor > 0:
|
||
return max(0.0, 5.0 * v / floor)
|
||
return 0.0
|
||
|
||
|
||
def _score_lower(value, ten, nine, floor):
|
||
"""越低越好:ten->10 分,nine->9 分,floor->5 分,低于 ten 满分,高于 floor 递减。"""
|
||
if value is None:
|
||
return None
|
||
v = float(value)
|
||
if v <= ten:
|
||
return 10.0
|
||
if v <= nine:
|
||
return 9.0 + (nine - v) / (nine - ten) if nine > ten else 10.0
|
||
if v <= floor:
|
||
return 5.0 + 4.0 * (floor - v) / (floor - nine) if floor > nine else 5.0
|
||
return max(0.0, 5.0 * floor / v) if v > 0 else 0.0
|
||
|
||
|
||
def _result(dim_id, score, basis, missing, untested):
|
||
if score is None:
|
||
status = "unknown"
|
||
elif score >= PASS_SCORE:
|
||
status = "pass"
|
||
else:
|
||
status = "fail"
|
||
return {
|
||
"id": dim_id,
|
||
"score": _round1(score) if score is not None else None,
|
||
"status": status,
|
||
"basis": basis,
|
||
"missing_items": missing,
|
||
"untested_items": untested,
|
||
}
|
||
|
||
|
||
def _score_delivery(ev, th):
|
||
"""证据:first_pass_rate, defect_rate, rework_rate,
|
||
first_pass_known, defect_known, rework_known, attempts_present,
|
||
evidence_kind("real" 以外若为 estimated/fixture 则记未实测)。"""
|
||
missing, untested, basis = [], [], []
|
||
for key in ("first_pass_rate", "defect_rate", "rework_rate"):
|
||
if ev.get(key) is None:
|
||
missing.append(key)
|
||
for key in ("first_pass_known", "defect_known", "rework_known"):
|
||
if not ev.get(key):
|
||
missing.append(key + "_not_known")
|
||
if not ev.get("attempts_present"):
|
||
missing.append("attempts_present_false_or_missing")
|
||
kind = ev.get("evidence_kind")
|
||
if kind in UNTESTED_KINDS:
|
||
untested.append("metrics_evidence_kind=%s" % kind)
|
||
if missing or untested:
|
||
return _result("delivery_quality", None, basis, missing, untested)
|
||
subs = [
|
||
("first_pass_rate", _score_higher(ev["first_pass_rate"],
|
||
_t(th, "first_pass_nine"),
|
||
_t(th, "first_pass_ten"),
|
||
_t(th, "first_pass_floor"))),
|
||
("defect_rate", _score_lower(ev["defect_rate"],
|
||
_t(th, "defect_rate_ten"),
|
||
_t(th, "defect_rate_nine"),
|
||
_t(th, "defect_rate_floor"))),
|
||
("rework_rate", _score_lower(ev["rework_rate"],
|
||
_t(th, "rework_rate_ten"),
|
||
_t(th, "rework_rate_nine"),
|
||
_t(th, "rework_rate_floor"))),
|
||
]
|
||
for name, sub in subs:
|
||
basis.append("%s=%s -> %.1f" % (name, ev[name], sub))
|
||
return _result("delivery_quality", min(s for _, s in subs), basis, missing, untested)
|
||
|
||
|
||
def _score_gate(ev, th):
|
||
"""证据:hard_gate_enforced(bool/None)、gate_items_passed、
|
||
gate_items_total、merge_blocked_verified、loop_end_to_end_verified、
|
||
sampling_misjudgment_rate(None 表示未抽查/未知)、
|
||
audit_verdict("conditional" 记无效证据,不算通过)。"""
|
||
missing, untested, basis = [], [], []
|
||
if ev.get("hard_gate_enforced") is None:
|
||
missing.append("hard_gate_enforced_unknown")
|
||
if ev.get("gate_items_passed") is None or ev.get("gate_items_total") is None:
|
||
missing.append("gate_items_passed_or_total_missing")
|
||
if ev.get("audit_verdict") == "conditional":
|
||
untested.append("audit_verdict=conditional_not_a_pass")
|
||
if ev.get("evidence_kind") in UNTESTED_KINDS:
|
||
untested.append("gate_evidence_kind=%s" % ev["evidence_kind"])
|
||
if missing or untested:
|
||
return _result("gate_audit", None, basis, missing, untested)
|
||
if not ev.get("hard_gate_enforced"):
|
||
basis.append("hard_gate_enforced=False(record 模式):已知未强制,分数封顶 4.0")
|
||
return _result("gate_audit", 4.0, basis, missing, untested)
|
||
total = ev["gate_items_total"]
|
||
passed = ev["gate_items_passed"]
|
||
rate = passed / total if total else 0.0
|
||
sub_gate = _score_higher(rate, 0.99, 1.0, 0.80)
|
||
basis.append("gate_items %s/%s=%.3f -> %.1f" % (passed, total, rate, sub_gate))
|
||
subs = [sub_gate]
|
||
if not ev.get("merge_blocked_verified"):
|
||
basis.append("merge_blocked_verified=False:合并阻塞未真实验证,分数封顶 4.0")
|
||
return _result("gate_audit", 4.0, basis, missing, untested)
|
||
if not ev.get("loop_end_to_end_verified"):
|
||
basis.append("loop_end_to_end_verified=False:自动循环端到端未真实验证,分数封顶 4.0")
|
||
return _result("gate_audit", 4.0, basis, missing, untested)
|
||
mis = ev.get("sampling_misjudgment_rate")
|
||
if mis is None:
|
||
missing.append("sampling_misjudgment_rate_unknown")
|
||
return _result("gate_audit", None, basis, missing, untested)
|
||
sub_mis = _score_lower(mis, 0.0, 0.02, _t(th, "overturn_rate_floor"))
|
||
basis.append("sampling_misjudgment_rate=%s -> %.1f" % (mis, sub_mis))
|
||
subs.append(sub_mis)
|
||
return _result("gate_audit", min(subs), basis, missing, untested)
|
||
|
||
|
||
def _score_arch(ev, th):
|
||
"""证据:contract_frozen、arch_violations、directory_violations、
|
||
import_violations、stack_changed_without_approval(bool/None)。"""
|
||
missing, untested, basis = [], [], []
|
||
for key in ("arch_violations", "directory_violations", "import_violations"):
|
||
if ev.get(key) is None:
|
||
missing.append(key + "_unknown")
|
||
if ev.get("contract_frozen") is None:
|
||
missing.append("contract_frozen_unknown")
|
||
if missing:
|
||
return _result("arch_contract", None, basis, missing, untested)
|
||
total_violations = (ev["arch_violations"] + ev["directory_violations"]
|
||
+ ev["import_violations"])
|
||
basis.append("violations total=%s (arch=%s dir=%s import=%s)"
|
||
% (total_violations, ev["arch_violations"],
|
||
ev["directory_violations"], ev["import_violations"]))
|
||
if not ev.get("contract_frozen"):
|
||
basis.append("contract_frozen=False:已知未冻结,分数封顶 4.0")
|
||
return _result("arch_contract", 4.0, basis, missing, untested)
|
||
if ev.get("stack_changed_without_approval"):
|
||
basis.append("stack_changed_without_approval=True:分数封顶 4.0")
|
||
return _result("arch_contract", 4.0, basis, missing, untested)
|
||
if total_violations > 0:
|
||
basis.append("violations>0:分数封顶 5.0")
|
||
return _result("arch_contract", 5.0, basis, missing, untested)
|
||
return _result("arch_contract", 10.0, basis, missing, untested)
|
||
|
||
|
||
def _score_test(ev, th):
|
||
"""证据:prd_coverage_rate、agent_six_complete、cross_matrix_coverage_rate、
|
||
coverage_pct、lint_items、mypy_items、bandit_high、
|
||
cross_matrix_kind("real" 以外记未实测)、real_pg(bool)、
|
||
real_service(bool)。"""
|
||
missing, untested, basis = [], [], []
|
||
for key in ("prd_coverage_rate", "cross_matrix_coverage_rate", "coverage_pct"):
|
||
if ev.get(key) is None:
|
||
missing.append(key + "_missing")
|
||
kind = ev.get("cross_matrix_kind")
|
||
if kind in UNTESTED_KINDS or kind in ("viewport_substitute", "code_review_only"):
|
||
untested.append("cross_matrix_kind=%s" % kind)
|
||
if kind is not None and kind != "real":
|
||
untested.append("cross_matrix_not_real")
|
||
if not ev.get("real_pg"):
|
||
untested.append("real_pg_not_verified")
|
||
if not ev.get("real_service"):
|
||
untested.append("real_service_not_verified")
|
||
if missing or untested:
|
||
return _result("test_coverage", None, basis, missing, untested)
|
||
if not ev.get("agent_six_complete"):
|
||
basis.append("agent_six_complete=False:agent 专属六项缺项,分数封顶 4.0")
|
||
return _result("test_coverage", 4.0, basis, missing, untested)
|
||
subs = [
|
||
("prd_coverage_rate", _score_higher(ev["prd_coverage_rate"], 0.99, 1.0, 0.80)),
|
||
("cross_matrix_coverage_rate", _score_higher(ev["cross_matrix_coverage_rate"], 0.99, 1.0, 0.80)),
|
||
("coverage_pct", _score_higher(ev["coverage_pct"],
|
||
_t(th, "coverage_nine"),
|
||
_t(th, "coverage_ten"),
|
||
_t(th, "coverage_floor"))),
|
||
]
|
||
for name, sub in subs:
|
||
basis.append("%s=%s -> %.1f" % (name, ev[name], sub))
|
||
# 静态检查:数值缺失记缺失,不估算;有值时,>0 即封顶(9 分线要求接近清零)。
|
||
for key in ("lint_items", "mypy_items", "bandit_high"):
|
||
if ev.get(key) is None:
|
||
missing.append(key + "_missing")
|
||
if missing:
|
||
return _result("test_coverage", None, basis, missing, untested)
|
||
static_total = ev["lint_items"] + ev["mypy_items"] + ev["bandit_high"]
|
||
basis.append("static total lint=%s mypy=%s bandit_high=%s"
|
||
% (ev["lint_items"], ev["mypy_items"], ev["bandit_high"]))
|
||
if static_total > 0:
|
||
basis.append("static_total>0:分数封顶 8.0(静态检查未接近清零)")
|
||
return _result("test_coverage", min(8.0, min(s for _, s in subs)), basis, missing, untested)
|
||
return _result("test_coverage", min(s for _, s in subs), basis, missing, untested)
|
||
|
||
|
||
def _score_security(ev, th):
|
||
"""证据:ledger_verify_ok(bool/None)、ledger_coverage_rate、
|
||
shell_whitelist_pass_rate、branch_protection
|
||
("protected"/"unprotected"/"unknown"/None)、anchor_check_recent。"""
|
||
missing, untested, basis = [], [], []
|
||
if ev.get("ledger_verify_ok") is None:
|
||
missing.append("ledger_verify_ok_unknown")
|
||
if ev.get("ledger_coverage_rate") is None:
|
||
missing.append("ledger_coverage_rate_missing")
|
||
bp = ev.get("branch_protection")
|
||
if bp is None or bp == "unknown":
|
||
missing.append("branch_protection_unknown")
|
||
if missing:
|
||
return _result("security_ledger", None, basis, missing, untested)
|
||
if bp == "unprotected":
|
||
basis.append("branch_protection=unprotected:已知未保护,分数封顶 4.0")
|
||
return _result("security_ledger", 4.0, basis, missing, untested)
|
||
if not ev.get("ledger_verify_ok"):
|
||
basis.append("ledger_verify_ok=False:账本校验未通过,分数封顶 4.0")
|
||
return _result("security_ledger", 4.0, basis, missing, untested)
|
||
subs = [_score_higher(ev["ledger_coverage_rate"], 0.99, 1.0, 0.80)]
|
||
basis.append("ledger_coverage_rate=%s -> %.1f" % (ev["ledger_coverage_rate"], subs[0]))
|
||
rate = ev.get("shell_whitelist_pass_rate")
|
||
if rate is not None:
|
||
sub = _score_higher(rate, 0.99, 1.0, 0.80)
|
||
subs.append(sub)
|
||
basis.append("shell_whitelist_pass_rate=%s -> %.1f" % (rate, sub))
|
||
if not ev.get("anchor_check_recent"):
|
||
basis.append("anchor_check_recent=False:锚点定期校验未落地,分数封顶 8.0")
|
||
return _result("security_ledger", min(8.0, min(subs)), basis, missing, untested)
|
||
return _result("security_ledger", min(subs), basis, missing, untested)
|
||
|
||
|
||
def _score_cost(ev, th):
|
||
"""证据:cost_records_complete(bool)、unpriced_as_zero(bool,
|
||
True 即违规)、per_creator_present、quota_cutoff_verified、
|
||
thresholds_approved(bool,仅影响阈值来源标注,不封顶分数)。"""
|
||
missing, untested, basis = [], [], []
|
||
if not ev.get("cost_records_complete"):
|
||
missing.append("cost_records_complete_false_or_missing"
|
||
"(turns/llm_requests/usd_micros 缺失)")
|
||
if ev.get("unpriced_as_zero") is None:
|
||
missing.append("unpriced_as_zero_unknown")
|
||
if missing:
|
||
return _result("cost_quota", None, basis, missing, untested)
|
||
if ev.get("unpriced_as_zero"):
|
||
basis.append("unpriced_as_zero=True:无价记成 0/免费,分数封顶 0.0")
|
||
return _result("cost_quota", 0.0, basis, missing, untested)
|
||
if not ev.get("quota_cutoff_verified"):
|
||
basis.append("quota_cutoff_verified=False:配额硬切断未真实验证,分数封顶 4.0")
|
||
return _result("cost_quota", 4.0, basis, missing, untested)
|
||
if not ev.get("per_creator_present"):
|
||
basis.append("per_creator_present=False:按创建者成本未落地,分数封顶 8.0")
|
||
return _result("cost_quota", 8.0, basis, missing, untested)
|
||
basis.append("cost_records_complete=True, quota_cutoff_verified=True, "
|
||
"per_creator_present=True, unpriced_as_zero=False")
|
||
return _result("cost_quota", 10.0, basis, missing, untested)
|
||
|
||
|
||
def _score_ops(ev, th):
|
||
"""证据:ci_green_rate(None 表示 CI 未绿/未知)、deploy_success_rate、
|
||
rollback_drill_done、ha_status
|
||
("second_host"/"single_host_accepted"/"single_host_unaccepted"/None)、
|
||
parallel_scale_proven(int/None)、parallel_scale_accepted_alternative(bool)。"""
|
||
missing, untested, basis = [], [], []
|
||
if ev.get("ci_green_rate") is None:
|
||
missing.append("ci_green_rate_unknown(CI 未绿或未统计)")
|
||
if ev.get("rollback_drill_done") is None:
|
||
missing.append("rollback_drill_done_unknown")
|
||
ha = ev.get("ha_status")
|
||
if ha is None:
|
||
missing.append("ha_status_unknown")
|
||
if missing:
|
||
return _result("ops_scale", None, basis, missing, untested)
|
||
if not ev.get("rollback_drill_done"):
|
||
basis.append("rollback_drill_done=False:回滚演练未真实跑过,分数封顶 4.0")
|
||
return _result("ops_scale", 4.0, basis, missing, untested)
|
||
subs = [_score_higher(ev["ci_green_rate"], 0.99, 1.0, 0.80)]
|
||
basis.append("ci_green_rate=%s -> %.1f" % (ev["ci_green_rate"], subs[0]))
|
||
if ev.get("deploy_success_rate") is not None:
|
||
sub = _score_higher(ev["deploy_success_rate"], 0.99, 1.0, 0.80)
|
||
subs.append(sub)
|
||
basis.append("deploy_success_rate=%s -> %.1f" % (ev["deploy_success_rate"], sub))
|
||
if ha == "single_host_unaccepted":
|
||
basis.append("ha_status=single_host_unaccepted:分数封顶 5.0")
|
||
return _result("ops_scale", 5.0, basis, missing, untested)
|
||
proven = ev.get("parallel_scale_proven")
|
||
target = _t(th, "parallel_scale_target")
|
||
if proven is None and not ev.get("parallel_scale_accepted_alternative"):
|
||
missing.append("parallel_scale_proven_unknown")
|
||
return _result("ops_scale", None, basis, missing, untested)
|
||
if proven is not None:
|
||
sub = _score_higher(min(proven, target) / target if target else 0,
|
||
0.99, 1.0, 0.5)
|
||
# 规模是离散证据:40 人满分,18 人约 4.5 分,上面线性公式在
|
||
# 0.5-1.0 区间给出 5-10 分,与“18 人约 4.5”不一致,故直接用比例*10。
|
||
sub = _round1(10.0 * min(proven, target) / target) if target else 0.0
|
||
subs.append(sub)
|
||
basis.append("parallel_scale_proven=%s target=%s -> %.1f"
|
||
% (proven, target, sub))
|
||
return _result("ops_scale", min(subs), basis, missing, untested)
|
||
|
||
|
||
_SCORERS = {
|
||
"delivery_quality": _score_delivery,
|
||
"gate_audit": _score_gate,
|
||
"arch_contract": _score_arch,
|
||
"test_coverage": _score_test,
|
||
"security_ledger": _score_security,
|
||
"cost_quota": _score_cost,
|
||
"ops_scale": _score_ops,
|
||
}
|
||
|
||
|
||
# ---- v7 机制第 4 项:分数证据绑定与签名(只做这一项) ----
|
||
# 诚实边界(先写清,再写代码):
|
||
# - 本节是纯函数:只算哈希与验签结果,不写库、不写账本、
|
||
# 不移动 cloud/ledger.py 的链头、不改变总体达标规则。
|
||
# evidence_ledger_head 是调用方传入的证据行即时算出的链头,
|
||
# 不是已持久化/已外部锚定的头;持久化与外部锚定是调用方
|
||
# 与运营方的事,本模块绝不谎称已上链入库。
|
||
# - 防重盖戳靠签名,不靠本节的哈希:哈希只能证明“这份证据体
|
||
# 就是算分时那份”(改动即哈希变),不能证明“这份证据体是
|
||
# 新取的”。没有有效签名的证据,调用方仍可重盖戳再交——
|
||
# 与 cloud/evidence_binding.py 的说明同款局限,本节不冒充解决。
|
||
# - 生产签名密钥供给仍是待定的运营方事项:本模块不含任何
|
||
# 生产密钥、不读生产 env、不新增密钥落盘路径、不默认信任。
|
||
# 机制只是代码就绪,不是生产已激活;真实 PG 上的链验证未做,
|
||
# 本项测试只用纯函数内存计算(连 SQLite 账本集成都没接)。
|
||
# - 签名验证是 fail-closed:无验证器、签名缺失、验证器拒绝、
|
||
# 验证器抛异常,一律 verified=False,绝不默认 verified=True。
|
||
# 验签结果不改变 build_scorecard 的 overall:总体达标仍只看
|
||
# 7 维分数,签名是调用方在传证据行之前自行核验的附加层,
|
||
# 把验签接入门禁/达标是另一次契约决定,不在本项悄悄做。
|
||
EVIDENCE_CHAIN_GENESIS = hashlib.sha256(
|
||
b"scorecard-evidence-genesis-v1").hexdigest()
|
||
|
||
|
||
def canonical_evidence_bytes(evidence_row):
|
||
"""证据体全文的规范字节:dict 按 sort_keys 紧凑 JSON 编码。
|
||
|
||
全文即整个 dict,不只取其中某个字段;字段增减、值改动、
|
||
键顺序之外的任何内容变化都会改变字节,从而改变哈希。
|
||
非 JSON 可序列化内容抛 ValueError,不静默丢字段。
|
||
"""
|
||
if isinstance(evidence_row, bytes):
|
||
return evidence_row
|
||
if isinstance(evidence_row, str):
|
||
return evidence_row.encode("utf-8")
|
||
try:
|
||
return json.dumps(evidence_row, sort_keys=True,
|
||
separators=(",", ":"),
|
||
ensure_ascii=False).encode("utf-8")
|
||
except (TypeError, ValueError) as exc:
|
||
raise ValueError(
|
||
"evidence row must be JSON-serializable: %s" % (exc,))
|
||
|
||
|
||
def hash_evidence_body(evidence_row):
|
||
"""证据体全文哈希:规范字节的 SHA-256 hex。"""
|
||
return hashlib.sha256(
|
||
canonical_evidence_bytes(evidence_row)).hexdigest()
|
||
|
||
|
||
def compute_evidence_ledger(evidence_rows):
|
||
"""对调用方传入的证据行算链头与行 id 列表(纯函数)。
|
||
|
||
链式构造(仿 cloud/ledger.py 的头推进,不写库):
|
||
head_0 = EVIDENCE_CHAIN_GENESIS
|
||
head_{i+1} = sha256((head_i + body_hash_i).encode())
|
||
因此任一证据体改动、行顺序变化都会改变最终头哈希;
|
||
只改一行 id 而不动其余内容时,该行体哈希随之变,头也变。
|
||
返回 {"head": str|None, "row_ids": [...], "body_hashes": [...]}。
|
||
- evidence_rows 为 None 或空:head=None、row_ids=[]、
|
||
body_hashes=[],绝不编造一个头或 id。
|
||
- 行 id 只取行 dict 里现成的 "id"(且不为 None);没有 id
|
||
的行仍参与头计算,但不为其发明 id(row_ids 长度可小于
|
||
行数,调用方要可审计映射须每行自带 id)。
|
||
- 非 dict 行抛 ValueError:不静默跳过、不猜 id。
|
||
"""
|
||
if evidence_rows is None:
|
||
return {"head": None, "row_ids": [], "body_hashes": []}
|
||
rows = list(evidence_rows)
|
||
if not rows:
|
||
return {"head": None, "row_ids": [], "body_hashes": []}
|
||
row_ids, body_hashes = [], []
|
||
head = EVIDENCE_CHAIN_GENESIS
|
||
for row in rows:
|
||
if not isinstance(row, dict):
|
||
raise ValueError("evidence row must be a dict")
|
||
body_hash = hash_evidence_body(row)
|
||
body_hashes.append(body_hash)
|
||
head = hashlib.sha256(
|
||
(head + body_hash).encode("utf-8")).hexdigest()
|
||
if row.get("id") is not None:
|
||
row_ids.append(row["id"])
|
||
return {"head": head, "row_ids": row_ids, "body_hashes": body_hashes}
|
||
|
||
|
||
def verify_signature(evidence_bytes, signature, verifier=None):
|
||
"""签名验证器:可注入接口,fail-closed。
|
||
|
||
参数:
|
||
- evidence_bytes: bytes(或 str,按 UTF-8 编码);其它类型
|
||
抛 TypeError(这是调用方编程错误,不是“验签失败”)。
|
||
- signature: 调用方出示的签名(bytes/str 或其它验证器能
|
||
接受的形式);为 None 或空 bytes/str 即缺失。
|
||
- verifier: 调用方注入的可调用对象,签名
|
||
verifier(evidence_bytes: bytes, signature) -> bool。
|
||
生产验证器由运营方在密钥供给到位后注入,本模块不提供、
|
||
不默认、不从 env/文件读任何验证器或密钥。
|
||
返回 {"verified": bool, "reason": str},reason 取值:
|
||
"verified"(验证器返回真)、"verifier_rejected"(返回假)、
|
||
"no_verifier"(未注入验证器)、"missing_signature"
|
||
(签名缺失)、"verifier_error"(验证器抛异常,fail-closed)。
|
||
默认无验证器时 verified=False,绝不默认 True。
|
||
"""
|
||
if not isinstance(evidence_bytes, (bytes, str)):
|
||
raise TypeError("evidence_bytes must be bytes or str")
|
||
if verifier is None:
|
||
return {"verified": False, "reason": "no_verifier"}
|
||
if not callable(verifier):
|
||
raise TypeError("verifier must be callable or None")
|
||
if signature is None:
|
||
return {"verified": False, "reason": "missing_signature"}
|
||
if isinstance(signature, (bytes, str)) and len(signature) == 0:
|
||
return {"verified": False, "reason": "missing_signature"}
|
||
payload = (evidence_bytes if isinstance(evidence_bytes, bytes)
|
||
else evidence_bytes.encode("utf-8"))
|
||
try:
|
||
result = verifier(payload, signature)
|
||
except Exception:
|
||
return {"verified": False, "reason": "verifier_error"}
|
||
if result:
|
||
return {"verified": True, "reason": "verified"}
|
||
return {"verified": False, "reason": "verifier_rejected"}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 三张表拆分(v7 机制第 2 项;仅评分卡输入结构拆分,不代表产品功能落地)
|
||
#
|
||
# 维分配(写死,不按调用方喜好重分;理由见各表注释):
|
||
# - 代码表 CODE_DIMENSION_IDS:delivery_quality、gate_audit、
|
||
# arch_contract、test_coverage。证据全部来自代码侧:交付指标、
|
||
# 门禁/审计、架构契约、测试覆盖——本地/代码审计即可验证,
|
||
# 不需要生产部署、真实 App、第二主机或用户侧拍板。
|
||
# - 生产表 PRODUCTION_DIMENSION_IDS:security_ledger、cost_quota、
|
||
# ops_scale。证据只在生产侧存在:分支保护与账本可信、
|
||
# 成本/配额真实记录与硬切断、CI/部署/回滚/HA/规模。
|
||
# 生产证据缺失时该表维记 None、总体 not_pass,绝不拉低代码表。
|
||
# - 产品表 PRODUCT_DIMENSION_IDS:沿用全部 7 维,但加一道真实
|
||
# 产品轮门禁(见 build_product_scorecard)。产品不是第 8 维,
|
||
# 也不是新指标:产品表 = “在真实产品轮里复测同一 7 维”。
|
||
# 没有真实产品轮证据时,产品表全部维记 None、总体 not_pass,
|
||
# 绝不把代码表/生产表的分数冒充成产品分数,也绝不声称
|
||
# 产品功能已落地。
|
||
#
|
||
# 独立性规则(与 build_scorecard 的旧规则一致,各表各自独立):
|
||
# - 每张表各自 overall 独立:本表全部维 >=9 才 pass,任一维
|
||
# <9 或 None 即 not_pass;average_score 只作参考,绝不作达标依据。
|
||
# - 代码表不含生产项:生产证据缺失、B/C 用户侧项 pending,
|
||
# 都不影响代码表的 overall(生产表才被挡住)。
|
||
# - 生产表与第 7 项 user_side_items 的配合是可选参数,不依赖
|
||
# 第 7 项的代码:调用方不传 user_side_items 时只按生产证据计分;
|
||
# 传了且其中有 pending 项时,生产表 overall 被强制 not_pass
|
||
# (维分数照常展示,不篡改),并在 missing_items 里逐项列出。
|
||
# - 阈值:三张表共用 DEFAULT_THRESHOLDS,默认值一律
|
||
# source="test_default_unapproved",未拍板;生产阈值须用户
|
||
# 拍板后由调用方显式传入 user_approved 阈值。
|
||
# ---------------------------------------------------------------------------
|
||
|
||
CODE_DIMENSION_IDS = [
|
||
"delivery_quality",
|
||
"gate_audit",
|
||
"arch_contract",
|
||
"test_coverage",
|
||
]
|
||
|
||
PRODUCTION_DIMENSION_IDS = [
|
||
"security_ledger",
|
||
"cost_quota",
|
||
"ops_scale",
|
||
]
|
||
|
||
PRODUCT_DIMENSION_IDS = list(DIMENSION_IDS)
|
||
|
||
USER_SIDE_DONE_STATUSES = {
|
||
"done", "approved", "complete", "resolved", "user_approved",
|
||
}
|
||
|
||
PRODUCT_ROUND_REQUIRED_FIELDS = (
|
||
"real_product_round",
|
||
"real_app_installed",
|
||
"real_user_flow_verified",
|
||
"product_round_as_of",
|
||
)
|
||
|
||
|
||
def _build_table(dim_ids, evidence, th):
|
||
"""按给定维子集算一张表(纯函数;overall 与平均分规则同旧卡)。"""
|
||
evidence = evidence or {}
|
||
dimensions = []
|
||
missing_all, untested_all = [], []
|
||
for dim_id in dim_ids:
|
||
result = _SCORERS[dim_id](evidence.get(dim_id) or {}, th)
|
||
dimensions.append(result)
|
||
missing_all.extend("%s: %s" % (dim_id, m) for m in result["missing_items"])
|
||
untested_all.extend("%s: %s" % (dim_id, u) for u in result["untested_items"])
|
||
scores = [d["score"] for d in dimensions if d["score"] is not None]
|
||
average = _round1(sum(scores) / len(scores)) if scores else None
|
||
# 钉死:平均分绝不能作为达标依据(各表各自独立判断)。
|
||
overall_pass = all(d["score"] is not None and d["score"] >= PASS_SCORE
|
||
for d in dimensions)
|
||
overall = "pass" if overall_pass else "not_pass"
|
||
return {
|
||
"dimensions": dimensions,
|
||
"average_score": average,
|
||
"average_is_not_pass_criterion": True,
|
||
"overall": overall,
|
||
"overall_pass": overall_pass,
|
||
"pass_score": PASS_SCORE,
|
||
"missing_items": missing_all,
|
||
"untested_items": untested_all,
|
||
"thresholds_source": thresholds_source(th),
|
||
"thresholds": _effective_thresholds(th), }
|
||
|
||
|
||
def check_baseline_stale(scorecard, current_main_head):
|
||
"""判定一张旧评分卡是否因 main 推进而失效(基线漂移门禁)。
|
||
|
||
参数全部由调用方传入:scorecard 是 build_scorecard 的输出 dict,
|
||
current_main_head 是调用方当前确认的 main 头。本函数不在生产
|
||
代码里探测版本控制状态,也不读版本控制目录。
|
||
|
||
返回 dict(调用方凭 stale / usable_as_pass_basis 判断,绝不能
|
||
只看旧卡的 overall / overall_pass):
|
||
- stale=True 时:旧分不得再作 pass 依据
|
||
(usable_as_pass_basis=False、overall_usable_as_pass_basis=False、
|
||
overall_must_not_be_used_as_pass_basis=True),即使旧卡的
|
||
overall 本身仍是 "pass"——旧值保留只为追溯,不代表现分有效。
|
||
- stale 的三种情形:baseline_head 缺失(无法证明新鲜,失败关闭,
|
||
不编造为一致);current_main_head 缺失(无法核对,失败关闭);
|
||
两者均有值但不相等(main 已推进,旧分过期)。
|
||
- 仅当两者均有值且相等时 stale=False,旧分仍可按原 overall 读。
|
||
"""
|
||
scorecard = scorecard or {}
|
||
baseline = scorecard.get("baseline_head")
|
||
if baseline is not None and baseline == "":
|
||
baseline = None
|
||
if current_main_head is not None and current_main_head == "":
|
||
current_main_head = None
|
||
if baseline is None:
|
||
reason = ("baseline_head_missing: scorecard carries no frozen "
|
||
"baseline_head, freshness cannot be proven")
|
||
stale = True
|
||
elif current_main_head is None:
|
||
reason = ("current_main_head_missing: caller supplied no current "
|
||
"main head, drift cannot be checked")
|
||
stale = True
|
||
elif current_main_head != baseline:
|
||
reason = ("baseline_head_drifted: baseline_head=%s "
|
||
"current_main_head=%s" % (baseline, current_main_head))
|
||
stale = True
|
||
else:
|
||
reason = "baseline_head_matches_current_main_head"
|
||
stale = False
|
||
return {
|
||
"stale": stale,
|
||
"baseline_head": baseline,
|
||
"current_main_head": current_main_head,
|
||
"reason": reason,
|
||
"usable_as_pass_basis": not stale,
|
||
"overall_usable_as_pass_basis": not stale,
|
||
"overall_must_not_be_used_as_pass_basis": stale,
|
||
}
|
||
|
||
|
||
def build_code_scorecard(evidence, thresholds=None):
|
||
"""代码表:仅 delivery_quality、gate_audit、arch_contract、test_coverage。
|
||
|
||
不含任何生产项;生产证据缺失、用户侧 B/C 项 pending 都不影响
|
||
本表 overall。本表全部 4 维 >=9 才 pass。
|
||
"""
|
||
return _build_table(CODE_DIMENSION_IDS, evidence, thresholds or {})
|
||
|
||
|
||
def _normalize_user_side_items(user_side_items):
|
||
"""把 user_side_items 占位证据归一化为 [{"id":..., "status":...}]。
|
||
|
||
接受:None(= 未提供,不臆断 pending)、dict{id: status}、
|
||
list[dict{id, status}]、list[str](字符串只有 id、无状态,
|
||
按 pending 计——状态未知绝不算 done)。
|
||
"""
|
||
if user_side_items is None:
|
||
return []
|
||
if isinstance(user_side_items, dict):
|
||
return [{"id": k, "status": v} for k, v in user_side_items.items()]
|
||
normalized = []
|
||
for item in user_side_items:
|
||
if isinstance(item, dict):
|
||
normalized.append({"id": item.get("id"),
|
||
"status": item.get("status")})
|
||
else:
|
||
normalized.append({"id": item, "status": "pending"})
|
||
return normalized
|
||
|
||
|
||
def build_production_scorecard(evidence, thresholds=None, user_side_items=None):
|
||
"""生产表:仅 security_ledger、cost_quota、ops_scale。
|
||
|
||
user_side_items 为可选占位证据(不依赖第 7 项的代码):
|
||
不传时只按生产证据计分;传了且有 pending 项时,维分数照常
|
||
展示,但 overall 被强制 not_pass,并在 missing_items 里逐项
|
||
列出 pending 项。生产证据缺失本身已使本表 not_pass。
|
||
"""
|
||
table = _build_table(PRODUCTION_DIMENSION_IDS, evidence, thresholds or {})
|
||
items = _normalize_user_side_items(user_side_items)
|
||
pending = [it for it in items
|
||
if it.get("status") not in USER_SIDE_DONE_STATUSES]
|
||
table["user_side_items"] = items
|
||
table["user_side_pending"] = pending
|
||
table["user_side_blocked"] = bool(pending)
|
||
if pending:
|
||
table["missing_items"] = list(table["missing_items"]) + [
|
||
"user_side_items_pending: %s" % it.get("id") for it in pending]
|
||
table["overall"] = "not_pass"
|
||
table["overall_pass"] = False
|
||
return table
|
||
|
||
|
||
def build_product_scorecard(evidence, thresholds=None, product_evidence=None):
|
||
"""产品表:全部 7 维,但须先过真实产品轮门禁。
|
||
|
||
product_evidence 须同时满足:real_product_round、real_app_installed、
|
||
real_user_flow_verified 均为真,且 product_round_as_of 非空。
|
||
门禁不过时,全部维记 None(unknown)、总体 not_pass,绝不把
|
||
代码表/生产表分数冒充成产品分数;本函数不声称任何产品功能已落地。
|
||
门禁通过时,7 维按与 build_scorecard 相同的 scorer 计分。
|
||
"""
|
||
product_evidence = product_evidence or {}
|
||
missing_fields = [f for f in PRODUCT_ROUND_REQUIRED_FIELDS
|
||
if not product_evidence.get(f)]
|
||
if missing_fields:
|
||
dimensions = []
|
||
missing_all = []
|
||
for dim_id in PRODUCT_DIMENSION_IDS:
|
||
missing = ["product_round_evidence_missing: %s" % f
|
||
for f in missing_fields]
|
||
dimensions.append(_result(dim_id, None, [], missing, []))
|
||
missing_all.extend("%s: %s" % (dim_id, m) for m in missing)
|
||
return {
|
||
"dimensions": dimensions,
|
||
"average_score": None,
|
||
"average_is_not_pass_criterion": True,
|
||
"overall": "not_pass",
|
||
"overall_pass": False,
|
||
"pass_score": PASS_SCORE,
|
||
"missing_items": missing_all,
|
||
"untested_items": [],
|
||
"thresholds_source": _thresholds_source(thresholds or {}),
|
||
"product_gate_passed": False,
|
||
"product_round_missing_fields": missing_fields,
|
||
}
|
||
table = _build_table(PRODUCT_DIMENSION_IDS, evidence, thresholds or {})
|
||
table["product_gate_passed"] = True
|
||
table["product_round"] = {
|
||
"real_product_round": True,
|
||
"real_app_installed": True,
|
||
"real_user_flow_verified": True,
|
||
"product_round_as_of": product_evidence.get("product_round_as_of"),
|
||
}
|
||
return table
|
||
|
||
|
||
def build_scorecard(evidence, thresholds=None, rubric_version=None,
|
||
baseline_head=None, evidence_rows=None,
|
||
user_side_items=None, product_evidence=None):
|
||
"""算出 7 维评分卡。evidence 缺键按空证据处理(该维记 None)。
|
||
|
||
兼容:旧返回字段(dimensions/average_score/overall 等)语义不变,
|
||
仅新增 code/production/product 三个子对象(见三张表拆分注释):
|
||
code=build_code_scorecard,production=build_production_scorecard
|
||
(user_side_items 可选占位),product=build_product_scorecard
|
||
(无真实产品轮证据时恒为 not_pass)。新增键不改变旧字段的值。
|
||
另新增 rubric_version(见 cloud/scorecard_rubric.py)、
|
||
baseline_head(机器字段,见 check_baseline_stale)、
|
||
evidence_ledger_head 与 evidence_row_ids(见
|
||
compute_evidence_ledger)三个机制字段;无证据行时头为 None。
|
||
"""
|
||
evidence = evidence or {}
|
||
th = thresholds or {}
|
||
if baseline_head is not None and baseline_head == "":
|
||
baseline_head = None
|
||
ledger = compute_evidence_ledger(evidence_rows)
|
||
report = _build_table(DIMENSION_IDS, evidence, th)
|
||
if baseline_head is None:
|
||
report["missing_items"] = list(report["missing_items"]) + [
|
||
"baseline_head_missing"]
|
||
report["baseline_head"] = baseline_head
|
||
report["rubric_version"] = rubric_version
|
||
report["evidence_ledger_head"] = ledger["head"]
|
||
report["evidence_row_ids"] = ledger["row_ids"]
|
||
report["thresholds"] = _effective_thresholds(th)
|
||
report["code"] = build_code_scorecard(evidence, thresholds)
|
||
raw_user_side = (user_side_items if user_side_items is not None
|
||
else evidence.get("user_side_items"))
|
||
report["production"] = build_production_scorecard(
|
||
evidence, thresholds, raw_user_side)
|
||
report["product"] = build_product_scorecard(
|
||
evidence, thresholds, product_evidence)
|
||
# v7 机制第 7 项三表规则(合一接线):代码表结论绝不因用户侧
|
||
# pending 被拉低;用户侧未完成/非法/未知时顶层
|
||
# production_overall 强制 not_pass 且原因指向用户侧。注意:
|
||
# 顶层 production_overall 是规则结论,report["production"]
|
||
# 子对象仍是第 2 项的证据表(维分数照常展示),两者分工不同,
|
||
# 顶层字段不回写子对象 overall,以免第 2 项的证据表语义被改。
|
||
rule = apply_user_side_three_table_rule(
|
||
report["code"]["overall"], report["production"]["overall"],
|
||
raw_user_side)
|
||
report["user_side_items"] = rule["user_side_items"]
|
||
report["code_overall"] = rule["code_overall"]
|
||
report["production_overall"] = rule["production_overall"]
|
||
report["production_reason"] = rule["production_reason"]
|
||
report["user_side_unknown"] = rule["user_side_unknown"]
|
||
report["code_unchanged_by_user_side"] = True
|
||
report["user_side_forced_production_not_pass"] = (
|
||
rule["user_side_forced_production_not_pass"])
|
||
return report
|
||
|
||
|
||
|
||
|
||
def example_scorecard_as_of_2026_10_31():
|
||
"""2026-10-31 快照示例卡(历史示例,不是当前系统得分)。
|
||
|
||
证据是本函数 2026-10-31 落笔时记录的快照值(生产 v0.2、
|
||
main aebc623、覆盖率 0.7787 等),此后未重新实测;调用方
|
||
绝不能把它读成当前系统得分。要当前分数,须显式把新证据
|
||
传给 build_scorecard()。凡是只到 PR、未合并部署、未真实
|
||
验证的,在这里都记为未知或已知未强制,不估算、不写成
|
||
判断分。阈值仍为测试默认值
|
||
(source="test_default_unapproved"),未拍板。
|
||
总体结论必然是不达标。
|
||
本示例无冻结基线:build_scorecard 未传 baseline_head,故
|
||
baseline_head=None 且 missing_items 含 baseline_head_missing,
|
||
绝不编造一个头把它写成现分。
|
||
"""
|
||
evidence = {
|
||
"delivery_quality": {
|
||
"first_pass_rate": None, "defect_rate": None, "rework_rate": None,
|
||
"first_pass_known": False, "defect_known": False,
|
||
"rework_known": False, "attempts_present": False,
|
||
},
|
||
"gate_audit": {
|
||
"hard_gate_enforced": False, "gate_items_passed": 0,
|
||
"gate_items_total": 5, "merge_blocked_verified": False,
|
||
"loop_end_to_end_verified": False,
|
||
"sampling_misjudgment_rate": None,
|
||
},
|
||
"arch_contract": {
|
||
"contract_frozen": None, "arch_violations": None,
|
||
"directory_violations": None, "import_violations": None,
|
||
},
|
||
"test_coverage": {
|
||
"prd_coverage_rate": None, "cross_matrix_coverage_rate": None,
|
||
"coverage_pct": 0.7787, "cross_matrix_kind": "viewport_substitute",
|
||
"real_pg": False, "real_service": False,
|
||
"agent_six_complete": True,
|
||
"lint_items": 109, "mypy_items": 0, "bandit_high": 4,
|
||
},
|
||
"security_ledger": {
|
||
"ledger_verify_ok": None, "ledger_coverage_rate": None,
|
||
"branch_protection": "unknown",
|
||
},
|
||
"cost_quota": {
|
||
"cost_records_complete": False, "unpriced_as_zero": False,
|
||
"per_creator_present": False, "quota_cutoff_verified": False,
|
||
},
|
||
"ops_scale": {
|
||
"ci_green_rate": None, "rollback_drill_done": False,
|
||
"ha_status": "single_host_unaccepted",
|
||
"parallel_scale_proven": 18,
|
||
},
|
||
}
|
||
report = build_scorecard(evidence)
|
||
report["evidence_as_of"] = "2026-10-31"
|
||
report["evidence_kind"] = "historical_example_snapshot"
|
||
report["not_current_system_score"] = True
|
||
return report
|