diff --git a/governance/policy/metrics.yaml b/governance/policy/metrics.yaml new file mode 100644 index 0000000..accf407 --- /dev/null +++ b/governance/policy/metrics.yaml @@ -0,0 +1,90 @@ +version: 1 +# ============================================================================ +# metrics.yaml —— 度量 dashboard 完整版阈值与声明位(W5-C4 .github#227,ADR-0073) +# 真源: 本文件(阈值/护栏/数据源声明唯一来源,宪法 §4A"阈值唯一来源"原则同源); +# 执行: governance/metrics.py(纯计算库,零网络)+ governance/dashboard-update.py +# (API 采集+呈现,butler-ledger 每 15min 驱动)+ governance/board-sync.py(板字段)。 +# 宪法: §8(度量全景——北极星是指标对非单一指标,防 Goodhart)/§12(状态可视化三投影) +# /§7(反退化——注意力成本如实入账不洗白)。 +# 原则: 数据源未落的指标一律声明 pending,dashboard 显示 pending 不显示 0 +# (ADR-0073 决策 7:缺数据不得被渲染成好数据)。 +# ============================================================================ + +north_star: + # 北极星对同屏互锁(AC-1 / ADR-0073 决策 1):零接触合并数 × 质量护栏同屏; + # 任一护栏 red → 合并数【显示归零】+原因标注(呈现层归零,原始计数保留在 JSON + # raw 字段——非数据删除);护栏回绿自动恢复显示。pending 护栏不触发归零 + # (缺证据≠劣化,按决策 7 独立显示盲区),但归零判定只认 red。 + window_days: 7 + guardrails: + # 逃逸率>0 持续:当前窗与上一窗(各 7 天,事件时戳直算——无跨轮状态残留)均>0 + # 分子=非演习 [auto-revert] PR + post-merge 冒烟 P0 issue(sli-report 同口径) + escape_rate_sustained: + red_when: "current_window_events>0 and previous_window_events>0" + # 回滚率:非演习 [auto-revert] PR 数 / 窗口内 merged PR 数(零分母=pending 不红) + revert_rate: + red_when_gt: 0.05 + # holdout 与主套件通过率差(ADR-0073 决策 1 三护栏之一;读取位未建——诚实 pending) + holdout_gap: + red_when_gt: 0.05 + data_source: pending + # 种子缺陷演习红率(应≈100%,ADR-0069);零可判定演习=pending 不红(不造 100%) + drill_red_rate: + red_when_lt: 1.0 + data_source: governance/drill/history.jsonl + # 仲裁误放行(arbiter 台账,ADR-0054 §7;窗口内 false-allow 行数>0 即 red) + false_allow: + red_when_gt: 0 + data_source: Cloudbird-Software/arbiter:tests/false_decision_ledger.jsonl + # 未经仲裁的状态变更泄漏(检测面未建——诚实 pending,不参与互锁触发) + state_change_leak: + red_when_gt: 0 + data_source: pending + +attention: + # 注意力会计(AC-2 / 宪法 §7):判断工作如实计量,防"签署退化成隐形 review" + # 签署耗时统计窗(type:intent issue 的 state:ir-draft→state:ir-signed timeline 差) + sign_window_days: 90 + # 签署耗时低于此秒数=可疑快速签署(退化信号计数——判断不可压缩,太快=没判断) + suspicious_fast_sign_seconds: 60 + # needs-human p90 停留超此小时数=整机停摆(宪法 §7:单独显示,不洗白成队列健康) + needs_human_p90_stop_hours: 24 + # 超时默认触发数 / 每合并 owner 分钟 / 周审计超时率:决策卡与审计包数据源未落 + # ——dashboard 显示 pending(不造数) + +security: + # 安全正确性(AC-2 / 宪法 §8):误放行/误拒窗口(台账按 date 字段过滤) + false_decision_window_days: 30 + # 误拒数:同台账 kind=false-deny(infra 不算误拒,ADR-0054 §7 约定) + # 陷阱拦截率(ADR-0071 W5-C2 硬谓词信任门)与泄漏数:数据源未落 → pending + +cost: + # 成本(AC-2 / 宪法 §8):单 IR 美元=(Actions 分钟+LLM token 归账)声明价折算/当月 IR 数 + # 全仓公开计费净额 $0(ADR-0020)——此为"失控速率"虚拟成本口径,非账单 + actions_price_per_minute_usd: 0.008 + llm_price_per_1k_tokens_usd: 0.002 + # 重数据源(CI-Workflows metering 归账,ADR-0062)快照复用 TTL:15min 刷新节奏下 + # 每 15min 拉 tarball 属浪费——快佐证龄>TTL 才重拉(快照与龄随 JSON 区回传) + snapshot_ttl_minutes: 60 + +user_results: + # 用户结果指标(AC-4 / 宪法 §8:防产品漂向"oracle 友好型") + # 各产品仓在 :metrics/user-result.yaml 声明一个用户侧结果指标: + # {metric_key: str, value: number, unit: str, updated_at: ISO}——文件缺失=pending + read_path: metrics/user-result.yaml + products: + - repo: Shorts_Director + - repo: Script_Writer + - repo: Use-up-Plan + - repo: AI_Web_School + - repo: mutual + # 季度难测产品/功能配额(配额制记录位——每季度刻意做一个难测的): + # owner 每季度在 entries 回填 {quarter: "2026-Q4", product: str, goal: str, status: planned|doing|done}; + # entries 空=本季未立(诚实显示,不预填占位值) + quarterly_hard_quota: + entries: [] + +board: + # factory-floor 板字段完善(AC-3 / 宪法 §12):谓词状态数据源=W5-C2 硬谓词信任门 + # (ADR-0071,进行中)——未落前板字段存在但值恒为 pending 标注(字段占位≠造数) + predicate_status_pending: "pending(W5-C2)" diff --git a/governance/tests/test-metrics-policy.sh b/governance/tests/test-metrics-policy.sh new file mode 100644 index 0000000..4eb8874 --- /dev/null +++ b/governance/tests/test-metrics-policy.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# test-metrics-policy.sh —— metrics.yaml policy schema 自测(W5-C4 .github#227,ADR-0073) +# +# 阈值唯一真源=policy 文件(宪法 §4A 同源原则):本测试锁定 schema 完整性, +# 缺护栏/缺阈值/畸形配额记录即红——防"互锁静默缺一路护栏"(北极星对漏一路 +# 等于 Goodhart 通道敞开)。数值语义(归零触发/聚合口径)由 test-metrics-northstar.sh +# 与 test-metrics-groups.sh 锁定。用法:bash governance/tests/test-metrics-policy.sh +# (零网络;需 python3+PyYAML——CI gate 预装,本地按 lib.sh 同款探测垫片) +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +GOV="$(cd "$HERE/.." && pwd)" + +PASS=0; FAIL=0 +pass() { PASS=$((PASS+1)); echo "PASS $1"; } +fail() { FAIL=$((FAIL+1)); echo "FAIL $1"; } + +# --- python 解释器探测(CI 恒有 python3;本地 Git Bash python3 可能是商店 stub) --- +PY="" +for c in "${PYTHON:-}" python3 python py -3; do + [[ -n "$c" ]] || continue + "$c" -c 'import sys, yaml; print("ok")' >/dev/null 2>&1 || continue + PY="$c"; break +done +[[ -n "$PY" ]] || { echo "::error::无可用 python(含 pyyaml)"; exit 2; } + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT + +"$PY" - "$GOV/policy/metrics.yaml" "$GOV/REPOS.yaml" >"$TMP/out.txt" 2>"$TMP/err.txt" <<'PYEOF' +import sys, yaml + +errs = [] +def need(cond, msg): + if not cond: + errs.append(msg) + +try: + m = yaml.safe_load(open(sys.argv[1], encoding="utf-8")) + repos = yaml.safe_load(open(sys.argv[2], encoding="utf-8")) +except Exception as e: + print(f"FATAL 解析失败: {e}", file=sys.stderr); sys.exit(2) + +need(isinstance(m, dict), "顶层非对象") +need(m.get("version") == 1, "version 须为 1") + +# --- 北极星护栏全集(缺一路=互锁盲区——ADR-0073 决策 1+卡面护栏面) --- +ns = m.get("north_star") or {} +GUARDS = ("escape_rate_sustained", "revert_rate", "holdout_gap", + "drill_red_rate", "false_allow", "state_change_leak") +g = ns.get("guardrails") or {} +for k in GUARDS: + need(k in g, f"north_star.guardrails 缺 {k}") +need(set(g) == set(GUARDS), f"guardrails 键集须恰为 {GUARDS}(多/少都算漂移),现={sorted(g)}") +need(isinstance(ns.get("window_days"), int) and ns["window_days"] > 0, + "north_star.window_days 须为正整数") +# 数据源 pending 必须显式声明(缺证据≠劣化,但盲区必须可见——ADR-0073 决策 7) +for k in ("holdout_gap", "state_change_leak"): + need((g.get(k) or {}).get("data_source") == "pending", + f"guardrails.{k}.data_source 须为 pending(读取位未建——诚实声明)") +need((g.get("drill_red_rate") or {}).get("data_source") == "governance/drill/history.jsonl", + "drill_red_rate 数据源须指向 drill 台账") +need("false_decision_ledger.jsonl" in str((g.get("false_allow") or {}).get("data_source")), + "false_allow 数据源须指向 arbiter 误放行台账") +# 数值阈值方向性(回滚率上限/演习红率下限) +rr = g.get("revert_rate") or {} +need(isinstance(rr.get("red_when_gt"), (int, float)) and 0 < rr["red_when_gt"] < 1, + "revert_rate.red_when_gt 须为 (0,1) 数值") +dr = g.get("drill_red_rate") or {} +need(isinstance(dr.get("red_when_lt"), (int, float)) and 0.5 <= dr["red_when_lt"] <= 1.0, + "drill_red_rate.red_when_lt 须为 [0.5,1.0](红率目标≈100%)") +fa = g.get("false_allow") or {} +need(fa.get("red_when_gt") == 0, "false_allow.red_when_gt 须为 0(一票即破线)") + +# --- 注意力会计阈值 --- +at = m.get("attention") or {} +need(isinstance(at.get("suspicious_fast_sign_seconds"), (int, float)) + and 0 < at["suspicious_fast_sign_seconds"] <= 600, + "suspicious_fast_sign_seconds 须为 (0,600] 秒(>10min 不再算「快速」签署)") +need(isinstance(at.get("needs_human_p90_stop_hours"), (int, float)) + and at["needs_human_p90_stop_hours"] == 24, + "needs_human_p90_stop_hours 须为 24(宪法 §7:超 1 天=整机停摆)") +need(isinstance(at.get("sign_window_days"), int) and at["sign_window_days"] > 0, + "sign_window_days 须为正整数") + +# --- 成本声明价 + 快照 TTL --- +co = m.get("cost") or {} +for k in ("actions_price_per_minute_usd", "llm_price_per_1k_tokens_usd"): + need(isinstance(co.get(k), (int, float)) and co[k] > 0, f"cost.{k} 须为正数") +need(isinstance(co.get("snapshot_ttl_minutes"), int) and co["snapshot_ttl_minutes"] >= 15, + "snapshot_ttl_minutes 须 ≥15(快于刷新节奏=每轮重拉,TTL 失效)") + +# --- 用户结果指标:产品清单非空、唯一、且都在组织地图内;配额记录位形状 --- +ur = m.get("user_results") or {} +prods = [p.get("repo") for p in (ur.get("products") or [])] +need(len(prods) >= 1, "user_results.products 须非空(宪法 §8:每产品至少一个指标位)") +need(len(prods) == len(set(prods)), f"products 有重复仓: {prods}") +declared = {r["name"] for r in (repos.get("repos") or [])} +for p in prods: + need(p in declared, f"产品仓 {p} 不在 REPOS.yaml 组织地图(GM-4 申报面)") +need(bool(ur.get("read_path")), "user_results.read_path 缺失(产品仓读取位约定)") +quota = (ur.get("quarterly_hard_quota") or {}).get("entries") or [] +need(isinstance(quota, list), "quarterly_hard_quota.entries 须为列表(记录位)") +for e in quota: + need(isinstance(e, dict) and e.get("quarter") and e.get("product"), + f"配额 entry 形状非法(须 quarter+product): {e}") + need(e.get("status") in (None, "planned", "doing", "done"), + f"配额 entry.status 非法: {e}") + +# --- 板字段 pending 标注(AC-3:谓词数据源未落=值 pending,不造数) --- +need(str((m.get("board") or {}).get("predicate_status_pending", "")).startswith("pending"), + "board.predicate_status_pending 须以 pending 开头") + +for e in errs: + print("ERR", e) +sys.exit(1 if errs else 0) +PYEOF +if [[ $? -eq 0 ]]; then + pass "metrics.yaml schema 完整(护栏全集/阈值方向/pending 声明/产品面/配额位)" +else + fail "metrics.yaml schema 校验失败:"; sed 's/^/ /' "$TMP/err.txt" "$TMP/out.txt" 2>/dev/null +fi + +echo "== test-metrics-policy: pass=$PASS fail=$FAIL ==" +[[ $FAIL -eq 0 ]]