From 3919cf0c4070411776286e156c111a513f48ea5e Mon Sep 17 00:00:00 2001 From: "cloudbrid-agent[bot]" Date: Sun, 30 Aug 2026 03:56:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20IR-0006=20=E9=AA=8C=E6=94=B6=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=E4=BA=94=E8=BF=9E=E4=BF=AE=EF=BC=88#470/#471/#472/#46?= =?UTF-8?q?8/#469/#473=EF=BC=89=E2=80=94=E2=80=94=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E6=89=A7=E6=B3=95=E9=94=AE=E6=98=A0=E5=B0=84+metering=20?= =?UTF-8?q?=E6=BA=90+gate=20=E9=80=90=E6=B5=8B=E5=88=A4=E5=AE=9A+retention?= =?UTF-8?q?=20=E5=A3=B0=E6=98=8E+checkpoint=20=E8=B0=83=E5=BA=A6=EF=BC=88A?= =?UTF-8?q?DR-0103=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/butler-reconcile.yml | 4 + .github/workflows/evidence-checkpoint.yml | 102 +++++++++++++++++++++ .github/workflows/gate.yml | 59 +++++++++++- docs/NAVIGATION.md | 2 +- governance/cost-check.sh | 13 ++- governance/evidence-query.sh | 17 +++- governance/policy/retention.yaml | 33 +++++++ governance/tests/test-cost-wave-channel.sh | 39 ++++++++ governance/tests/test-evidence-shadow.sh | 7 +- governance/tests/test-feedback-edge.sh | 9 +- governance/tests/test-wave-schema.sh | 40 ++++++++ governance/wave_schema.py | 26 ++++-- specs/IR-0006/acceptance.md | 16 +++- specs/IR-0006/spec.md | 1 + 14 files changed, 343 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/evidence-checkpoint.yml create mode 100644 governance/policy/retention.yaml diff --git a/.github/workflows/butler-reconcile.yml b/.github/workflows/butler-reconcile.yml index d30ff5e..8ffb21d 100644 --- a/.github/workflows/butler-reconcile.yml +++ b/.github/workflows/butler-reconcile.yml @@ -68,6 +68,10 @@ jobs: git -C ledger checkout -b butler-ledger fi BASE="ledger/$SHADOW" + # #471 关联根因:但书账本首建时 butler-ledger 分支不存在→克隆默认分支, + # 而 main 上 governance/butler/ 被 gitignore(影子真源在 *-ledger 分支)—— + # 目录缺位使 : > "$BASE" 崩、butler 源永远 0 条。先建目录再触基链。 + mkdir -p "$(dirname "$BASE")" [[ -f "$BASE" ]] || : > "$BASE" python3 governance/evidence_shadow.py relink --base "$BASE" --local "$SHADOW" --out merged.jsonl python3 governance/evidence_shadow.py verify --file merged.jsonl diff --git a/.github/workflows/evidence-checkpoint.yml b/.github/workflows/evidence-checkpoint.yml new file mode 100644 index 0000000..409f2b2 --- /dev/null +++ b/.github/workflows/evidence-checkpoint.yml @@ -0,0 +1,102 @@ +name: evidence-checkpoint +# IR-0006 BEH-02 月度 checkpoint 调度(#469 2B 补件,ADR-0103 / BUDGET-02) +# +# 为什么:checkpoint 生成能力已存在(archive 仓 scripts/write_evidence.py +# --checkpoint,attest-drill/eval-wave 演练时调用过)但无月度 cron 承载—— +# BEH-02「每月末做 checkpoint」此前靠人工/演练顺带,一旦忘记,账本月度 +# hash 锚点断档(篡改检测窗口拉长)。本工作流补上调度面+新鲜度守卫: +# - 月度(每月 1 号 07:33 UTC):生成当月 checkpoint → verify 链完整 → +# 有变化则以 PR 形态提交 archive(PR 面 archive gate 再跑一轮 +# verify_evidence——双保险);无变化(幂等)=绿跳过。 +# - 周度(每周三 07:11 UTC):新鲜度守卫——最新 checkpoint 超 40 天=红 +# (fail-closed:API 失败亦红,防月度跑飞无人知)。 +# 节奏声明真源=governance/policy/retention.yaml(BUDGET-02 声明位,#468)。 +on: + schedule: + - cron: "33 7 1 * *" # 月度 checkpoint 生成(BEH-02) + - cron: "11 7 * * 3" # 周三新鲜度守卫(40 天窗口) + workflow_dispatch: + inputs: + mode: + { description: "run 模式(generate=生成+PR / freshness=新鲜度守卫)", type: string, required: false, default: "generate" } + +permissions: + contents: read + +concurrency: + group: evidence-checkpoint + cancel-in-progress: false + +jobs: + generate: + if: github.event_name == 'workflow_dispatch' || github.event.schedule == '33 7 1 * *' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: 拉取 archive 仓(判定层账本+脚本) + env: + GOVERNANCE_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + run: | + set -euo pipefail + git clone --depth 1 \ + "https://x-access-token:${GOVERNANCE_TOKEN}@github.com/Cloudbird-Software/archive.git" archive + - name: 生成当月 checkpoint(write_evidence --checkpoint,幂等只前进) + run: | + set -euo pipefail + cd archive + python3 scripts/write_evidence.py --checkpoint \ + --ledger evidence/ledger.jsonl --checkpoints evidence/checkpoints + - name: 独立复算(链完整+checkpoint 对账,fail-closed) + run: | + set -euo pipefail + cd archive + python3 scripts/verify_evidence.py --ledger evidence/ledger.jsonl + - name: 有变化 → 分支+PR(archive gate 面再验一轮) + env: + GOVERNANCE_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + run: | + set -euo pipefail + cd archive + if git diff --quiet; then + echo "OK 幂等——当月 checkpoint 已在(无新链头),跳过提交" + exit 0 + fi + git config user.name "cloudbrid-agent[bot]" + git config user.email "cloudbrid-agent[bot]@users.noreply.github.com" + BR="evidence-checkpoint-$(date -u +%Y-%m)" + git checkout -b "$BR" + git add evidence/checkpoints/ + git commit -m "evidence: 月度 checkpoint ${BR}(BEH-02,evidence-checkpoint.yml 调度)" + git push "https://x-access-token:${GOVERNANCE_TOKEN}@github.com/Cloudbird-Software/archive.git" HEAD:refs/heads/"$BR" + export GH_TOKEN="$GOVERNANCE_TOKEN" + gh pr create --repo Cloudbird-Software/archive --base main --head "$BR" \ + --title "evidence: 月度 checkpoint ${BR}(BEH-02 自动调度)" \ + --body "月度 checkpoint 自动生成(evidence-checkpoint.yml,#469 2B)。verify_evidence 独立复算通过后提交;PR 面 archive gate 将再跑一轮链完整性+checkpoint 对账(fail-closed 双保险)。" + + freshness: + if: github.event_name == 'schedule' && github.event.schedule == '11 7 * * 3' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: 最新 checkpoint 新鲜度(>40 天=红,API 失败=红) + env: + GH_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + run: | + set -euo pipefail + # fail-closed:拉取失败=守卫面失明,红 + LATEST=$(gh api repos/Cloudbird-Software/archive/contents/evidence/checkpoints \ + --jq '[.[].name | select(test("^\\d{4}-\\d{2}\\.json$"))] | max') \ + || { echo "::error::checkpoints 目录拉取失败(fail-closed)"; exit 1; } + [[ -n "$LATEST" ]] || { echo "::error::无任何 checkpoint(fail-closed)"; exit 1; } + MONTH="${LATEST%.json}" + python3 - "$MONTH" <<'EOF' + import datetime, sys + month = sys.argv[1] + ckpt = datetime.datetime.strptime(month, "%Y-%m").replace(tzinfo=datetime.timezone.utc) + # 月内任一天即可信:按月初计龄(保守——若按月末会更宽松) + age = (datetime.datetime.now(datetime.timezone.utc) - ckpt).days + print(f"最新 checkpoint {month},年龄 {age} 天(窗口 40 天)") + if age > 40: + print(f"::error::checkpoint 过期({age} 天 > 40 天窗口)——月度生成跑飞,人工介入") + sys.exit(1) + EOF diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 70ee9e4..f9acbb4 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -183,13 +183,70 @@ jobs: # IR-0002 #143 收口:§12 (a)/(b) 分类器 fixture 自测入 CI——(b) 检出 # 灵敏度(INV-4)自此有机器证据,改分类器不连带过测即红。目录内新增 # test-*.sh 自动纳入;目录为空=测试面丢失,fail-closed。 + # #471 修复:逐测试判定——任何单测红都使本步红(原 for 循环退出码= + # 最后一个测试,中途红被吞掉=CI 假绿) shopt -s nullglob tests=(governance/tests/test-*.sh) shopt -u nullglob if [[ ${#tests[@]} -eq 0 ]]; then echo "::error::governance/tests 无 test-*.sh——测试面丢失(fail-closed)"; exit 1 fi - for t in "${tests[@]}"; do echo "-- $t"; bash "$t"; done + reds=() + for t in "${tests[@]}"; do + echo "-- $t" + if ! bash "$t"; then reds+=("$t"); fi + done + if [[ ${#reds[@]} -gt 0 ]]; then + printf '::error::治理自测红(%d 个):%s\n' "${#reds[@]}" "${reds[*]}" + exit 1 + fi + - name: acceptance 收口守卫(#473:验收报告合并时 IR 项下不得有 open bug/P0 issue) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" + REPO_NAME: ${{ github.repository }} + run: | + set -euo pipefail + # 触发面:PR 触碰 specs/*/acceptance.md(fail-closed:清单拉取失败=红) + PAGE=1; FILES="" + while :; do + RESP=$(gh api "$PR_API/files?per_page=100&page=$PAGE") \ + || { echo "::error::PR 文件清单拉取失败(fail-closed)"; exit 1; } + FILES="$FILES"$'\n'"$(echo "$RESP" | jq -r '.[].filename')" + [[ $(echo "$RESP" | jq 'length') -lt 100 ]] && break + PAGE=$((PAGE+1)) + done + ACCEPTS=$(grep -E '^specs/[^/]+/acceptance\.md$' <<<"$FILES" || true) + if [[ -z "$ACCEPTS" ]]; then + echo "OK 本 PR 不触碰 acceptance.md——守卫不适用"; exit 0 + fi + # 豁免面:本 PR body 声明将关闭的 issue(Fixes/Closes #n)——修复合流=缺口收口 + PRBODY=$(gh api "$PR_API" --jq .body 2>/dev/null || echo "") + FIXED=$(grep -oE '(Fixes|Closes|Resolves) #[0-9]+' <<<"$PRBODY" \ + | grep -oE '[0-9]+' | sort -un | tr '\n' ' ' || true) + echo "acceptance 变更:$ACCEPTS;本 PR 关闭豁免:${FIXED:-无}" + # 逐 acceptance:解析其 IR 引用(- IR: ...#NNN 行)→ 查 IR 项下 open bug/P0 + for f in $ACCEPTS; do + IR_NUM=$(grep -oE '#[0-9]+' <<<"$(grep -m1 -E '^- IR: .+#' "$f" || true)" | head -1 | tr -d '#') + if [[ -z "$IR_NUM" ]]; then + echo "::error::$f 无法解析 IR 引用(- IR: 行缺 #NNN——fail-closed)"; exit 1 + fi + echo "-- $f → IR #$IR_NUM" + for LBL in bug P0; do + BADS=$(gh api "search/issues?q=repo:${REPO_NAME}+is:issue+is:open+label:${LBL}+%23${IR_NUM}+in:body" \ + --jq '.items[].number' 2>/dev/null) || { echo "::error::issue 检索失败(label:$LBL,fail-closed)"; exit 1; } + for n in $BADS; do + if grep -qw "$n" <<<"$(echo ${FIXED:-} | tr ' ' '\n')" 2>/dev/null; then + echo " 豁免 #$n(本 PR 关闭)" + else + echo "::error::IR #$IR_NUM 项下 open ${LBL} issue #$n 未登记收口——验收报告不得先于缺陷合并(#473)" + exit 1 + fi + done + done + done + echo "OK acceptance 收口守卫通过" - name: REPOS.yaml 引用自检(无重名仓) run: | python3 - <<'EOF' diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md index 7a3e6e0..ae06396 100644 --- a/docs/NAVIGATION.md +++ b/docs/NAVIGATION.md @@ -31,7 +31,7 @@ | 产品仓(AI_Web_School / mutual / QW_Arena1 …) | 该仓根部 AGENTS.md 的入口协议块+角色路由节 | 取 ghcb(AGENTS.md 内钉 SHA 命令)→ `ghcb next ` 找卡;无卡不开工,新想法走该仓 intent 表单 | | 治理仓 .github | [AGENTS.md](../AGENTS.md)(本仓契约) | 治理变更走 C1(§2,无需卡);卡工作照入口协议块 | | 治理仓 CI-Workflows | 该仓 AGENTS.md | workflow/pipeline 变更=C1 面(.github/ 路径):PR 引 ADR + owner review | -| 治理仓 archive | `runs/README.md` | 运行报告只追加(append-only);ADR 落 `adr/` + 更新 INDEX.yaml | +| 治理仓 archive | `runs/README.md` | 运行报告只追加(append-only);ADR 落 `adr/` + 更新 INDEX.yaml;证据账本判定层=`evidence/`(独立复算/写入脚本居 archive 仓 `scripts/verify_evidence.py` 等——月度 checkpoint 由 .github 仓 `evidence-checkpoint.yml` 调度,#469) | | 支撑仓 cnb-bridge | 仓内 `REMOVAL.md` + `accounts.yaml` | 池运维 owner 面;派单一律经 .github 仓 `cnb-dispatch` 工作流(key 不入上下文) | | 支撑仓 arbiter / holdout | —(owner 直管) | 你不直接调用(见 §2「conductor/arbiter」);holdout 对 agent 只读 | | 发现 bug | [.github/ISSUE_TEMPLATE/bug.yml](../.github/ISSUE_TEMPLATE/bug.yml)(org 级继承,各仓可用) | 提交即机器复现(B1–B5,ADR-0064):reproduced → 修复合入 → fixed → done;处理 issues 的完整指引=[ROLE-ACCEPT.md](agent/ROLE-ACCEPT.md) | diff --git a/governance/cost-check.sh b/governance/cost-check.sh index b12add0..8614a38 100644 --- a/governance/cost-check.sh +++ b/governance/cost-check.sh @@ -228,11 +228,15 @@ wave_channel_check() { fi fi out=$(python3 "$DIR/wave_schema.py" wave-check --cards "$cards" --ledger-dir "$led" 2>/dev/null) || rc=$? - if [[ $rc -eq 2 ]]; then - printf 'INFRA\twave-check 执行失败(参数/环境 rc=2)\n'; return 0 + # rc 兜底(#470 方案 C):0=正常 | 4=超限(期望红,rows 已出交由下方解析); + # 其余任何退出码(含未处理异常 rc=1、环境 rc=2/3)= 预算面不可信 → INFRA, + # 不静默通过(fail-closed:未来同类命名错不再吞掉执法) + if [[ $rc -ne 0 && $rc -ne 4 ]]; then + printf 'INFRA\twave-check 异常退出 rc=%d(fail-closed:预算面不可信,不静默通过)\n' "$rc"; return 0 fi python3 -c 'import json,sys -rows = json.loads(sys.stdin.read() or "[]") +s = sys.stdin.read().strip() +rows = json.loads(s) if s else [] bad = [r for r in rows if r.get("error")] if bad: print("INFRA\t波次卡块非法(预算面盲区,fail-closed):" + "; ".join(r["card"] + " " + r["error"] for r in bad)) @@ -246,6 +250,9 @@ nb = sum(1 for r in rows if not r.get("error")) extra = "" if warn: extra = ";warn 超限(只报告不判定):" + ", ".join(r["card"] + ":" + "+".join(r["exceeded_dims"]) for r in warn) +unef = [(r["card"], "+".join(r["unenforced_dims"])) for r in rows if r.get("unenforced_dims")] +if unef: # #472:无账本源的声明维度——摘要可见,不静默 + extra += ";未执法维度(账本无源,可见声明):" + ", ".join(c + ":" + d for c, d in unef) print("WAVE-OK\t预算卡 %d 张对账无 hard-stop 超限%s" % (nb, extra))' <<<"$out" } # @w2c3-wave-channel-end diff --git a/governance/evidence-query.sh b/governance/evidence-query.sh index f9ebf79..7272580 100644 --- a/governance/evidence-query.sh +++ b/governance/evidence-query.sh @@ -64,13 +64,20 @@ PYEOF SRC_METER="$TMP/metering"; mkdir -p "$SRC_METER" if "$GH" api "repos/Cloudbird-Software/CI-Workflows/contents?ref=metering-ledger" >"$TMP/list.json" 2>"$TMP/api.err"; then - python3 - "$TMP/list.json" "$SRC_METER" <<'PYEOF' -import base64, json, sys + # #471 修复:目录列表条目不含 content 字段(base64 正文)——只取文件名清单, + # 逐文件走 fetch_file(单文件 contents API 含 content;404=并发删除按缺席跳过) + python3 - "$TMP/list.json" > "$TMP/meter-files.txt" <<'PYEOF' +import json, sys for ent in json.load(open(sys.argv[1], encoding="utf-8")): - if ent["type"] == "file" and ent["name"].startswith("shadow-evidence-") and ent["name"].endswith(".jsonl"): - open(f"{sys.argv[2]}/{ent['name']}", "w", encoding="utf-8", newline="\n").write( - base64.b64decode(ent["content"]).decode("utf-8")) + name = ent.get("name") or "" + if ent.get("type") == "file" and name.startswith("shadow-evidence-") and name.endswith(".jsonl"): + print(name) PYEOF + while IFS= read -r mname; do + [[ -n "$mname" ]] || continue + fetch_file "Cloudbird-Software/CI-Workflows" "metering-ledger" "$mname" "$SRC_METER/$mname" \ + || { echo "WARN: metering $mname 并发缺席(404)——跳过" >&2; true; } + done < "$TMP/meter-files.txt" else # 同 fetch_file:按 "HTTP 404" 状态码判源缺席(报文变体猜谜必漏——见上方注释) if ! grep -q 'HTTP 404' "$TMP/api.err" 2>/dev/null; then diff --git a/governance/policy/retention.yaml b/governance/policy/retention.yaml new file mode 100644 index 0000000..273b580 --- /dev/null +++ b/governance/policy/retention.yaml @@ -0,0 +1,33 @@ +version: 1 +# 证据账本保留策略声明(IR-0006 BUDGET-02 / AC-3;#468 补件) +# +# 声明位=governance/policy/(可 drift 对账、红队可审,ADR-0082);调整走 +# C1(PR 引 ADR)。执行面脚本=archive 仓 blob-store.sh(sweep/retention 词表 +# 已过 test-blob-store.sh)——本文件是声明面真源,与执行面分离(声明≠实现, +# drift-check 对账两者一致性)。 +tiers: + adjudication: # 判定层(archive 仓 evidence/ledger.jsonl——hash 链账本) + tier: hot + retention: 90d + storage: archive-jsonl + note: 统一判定层真源;链验证 verify_evidence.py(archive 仓 scripts/) + trajectory: # 轨迹层(内网 blob——完整轨迹/大对象) + tier: warm + retention: 1y + storage: self-cloud-blob + note: blob-store.sh sweep 执行面;词表 30d/90d/180d/1y/3y/forever + discard: # 丢弃层(GitHub 事件面——runner 销毁即弃,仅 digest 入链) + tier: digest-only + retention: 30d + storage: github-native + note: 日常 15min 轮影子随 runner 销毁;仅 drill 持久化轮入对应 *-ledger 分支 +checkpoint: + cadence: monthly # BEH-02 月度节奏:月末链头 hash+月度汇总提交 git + cron: "33 7 1 * *" # UTC 每月 1 号 07:33(与 sli-weekly/env-drift 错峰) + workflow: .github/workflows/evidence-checkpoint.yml + artifact: evidence/checkpoints/YYYY-MM.json # archive 仓 + verification: archive 仓 scripts/verify_evidence.py 独立复算(链断=红,fail-closed) + freshness_window_days: 40 # 最新 checkpoint 超 40 天=红(防月度跑飞,#469 2B) +enforcement: + sweep_frequency: daily # blob-store.sh sweep 建议日度(轨迹层过期回收) + growth_soft_cap_mb: 200 # BUDGET-01 判定层年增长软上限(gzip 后) diff --git a/governance/tests/test-cost-wave-channel.sh b/governance/tests/test-cost-wave-channel.sh index c374317..69e64da 100644 --- a/governance/tests/test-cost-wave-channel.sh +++ b/governance/tests/test-cost-wave-channel.sh @@ -119,6 +119,45 @@ if [[ "${WOUT%%$'\t'*}" == "INFRA" ]] && grep -q "卡清单拉取失败" <<<"$WO pass "卡清单拉取失败 → INFRA(预算面不可知)" else fail "清单失败应 INFRA,得到:$WOUT"; fi +# ---- 5b) #470/#472 回归:wallclock_sec 超限 → WAVE-EXCEEDED;human_minutes → 可见未执法 ---- +cat >"$TMP/cards-wc.json" <<'EOF' +[ + {"number": 504, "body": "## budget(波次预算)\nwallclock_sec: 7200\non_exceed: hard-stop"}, + {"number": 506, "body": "## budget(波次预算)\nhuman_minutes: 1\non_exceed: hard-stop"} +] +EOF +mkdir -p "$TMP/ledger-wc" +cat >"$TMP/ledger-wc/shadow-evidence-unified.jsonl" <<'EOF' +{"ts":"2026-08-29T04:00:00Z","kind":"cost","action":"cost.dispatch","verdict":"pass","subject":{"card":"Cloudbird-Software/.github#504","tenant":"t1"},"cost":{"tokens":1000,"usd":0.1,"wall_sec":8000.0},"seq":1,"prev_hash":null,"hash":"dd"} +{"ts":"2026-08-29T05:00:00Z","kind":"cost","action":"cost.dispatch","verdict":"pass","subject":{"card":"Cloudbird-Software/.github#506","tenant":"t1"},"cost":{"tokens":900000,"usd":99.0,"wall_sec":8000.0},"seq":2,"prev_hash":"dd","hash":"ee"} +EOF +COST_WAVE_CARDS_FILE="$TMP/cards-wc.json" COST_WAVE_LEDGER_DIR="$TMP/ledger-wc" run_chan +if [[ "${WOUT%%$'\t'*}" == "WAVE-EXCEEDED" ]] && grep -q '"wallclock_sec"' <<<"$WOUT"; then + pass "wallclock_sec 超限 → WAVE-EXCEEDED 含超限维(#470:执法面可达)" +else fail "wallclock 超限应 WAVE-EXCEEDED 含维度,得到:$WOUT"; fi + +cat >"$TMP/cards-hm.json" <<'EOF' +[ + {"number": 506, "body": "## budget(波次预算)\nhuman_minutes: 1\non_exceed: hard-stop"} +] +EOF +COST_WAVE_CARDS_FILE="$TMP/cards-hm.json" COST_WAVE_LEDGER_DIR="$TMP/ledger-wc" run_chan +if [[ "${WOUT%%$'\t'*}" == "WAVE-OK" ]] && grep -q "未执法维度" <<<"$WOUT" && grep -q "human_minutes" <<<"$WOUT"; then + pass "human_minutes 声明 → WAVE-OK+摘要可见未执法维度(#472:不静默)" +else fail "human_minutes 应 WAVE-OK+可见未执法,得到:$WOUT"; fi + +# ---- 5c) #470 兜底:wave-check 异常退出(任何非 0/4 rc)→ INFRA(fail-closed) ---- +mkdir -p "$TMP/bin-wc" +printf '#!/usr/bin/env bash\nexit 1\n' >"$TMP/bin-wc/python3"; chmod +x "$TMP/bin-wc/python3" +# 前置桩 python3 使 wave-check 退出 1(未处理异常语义);调用后复原 PATH +PREVPATH="$PATH" +PATH="$TMP/bin-wc:$PATH" +COST_WAVE_CARDS_FILE="$TMP/cards-hm.json" COST_WAVE_LEDGER_DIR="$TMP/ledger-wc" run_chan +PATH="$PREVPATH" +if [[ "${WOUT%%$'\t'*}" == "INFRA" ]] && grep -q "rc=1" <<<"$WOUT"; then + pass "wave-check 异常退出 → INFRA 兜底(#470 方案 C:预算面不可信不静默)" +else fail "异常退出应 INFRA rc=1,得到:$WOUT"; fi + # ---- 6) 集成段:全脚本(桩 gh 记录调用日志,吸收全部写操作)—— 波次超限 → 硬停三件套 ---- # 桩 gh:billing=0 用量(数值已按 --jq 口径)、熔断变量 404、issue/pr 清单空; # 全部调用落 $GHSTUB_LOG 供三件套调用路径断言(BEH-07:置变量+P0 issue)。 diff --git a/governance/tests/test-evidence-shadow.sh b/governance/tests/test-evidence-shadow.sh index 048b808..c7fb442 100644 --- a/governance/tests/test-evidence-shadow.sh +++ b/governance/tests/test-evidence-shadow.sh @@ -200,8 +200,11 @@ python3 - "$SHADOW" "$DSHADOW" "$BUTLER_SHADOW" "$ESHADOW" "$TICKETS" "$FEISHU" import base64, json, sys shadow, dshadow, bshadow, eshadow, tickets, feishu, envd, fixdir = sys.argv[1:9] b64 = lambda p: base64.b64encode(open(p, "rb").read()).decode() -json.dump([{"type": "file", "name": "shadow-evidence-2026-W35.jsonl", "content": b64(shadow)}], +# #471:metering 源逐文件拉取(目录列表条目不含 content——单文件 API 含)—— +# 目录列表 fixture 只保留名字(真实形态),正文走单文件 fixture +json.dump([{"type": "file", "name": "shadow-evidence-2026-W35.jsonl"}], open(f"{fixdir}/metering-list.json", "w")) +json.dump({"type": "file", "content": b64(shadow)}, open(f"{fixdir}/metering-file.json", "w")) json.dump({"type": "file", "content": b64(dshadow)}, open(f"{fixdir}/drill.json", "w")) json.dump({"type": "file", "content": b64(bshadow)}, open(f"{fixdir}/butler.json", "w")) json.dump({"type": "file", "content": b64(eshadow)}, open(f"{fixdir}/elev.json", "w")) @@ -218,6 +221,8 @@ F="${GH_STUB_FIXTURES:?}" case "$url" in "repos/Cloudbird-Software/CI-Workflows/contents?ref=metering-ledger") cat "$F/metering-list.json" ;; + "repos/Cloudbird-Software/CI-Workflows/contents/shadow-evidence-2026-W35.jsonl?ref=metering-ledger") + cat "$F/metering-file.json" ;; "repos/Cloudbird-Software/.github/contents/governance/drill/shadow-evidence.jsonl?ref=drill-ledger") if [[ -f "$F/drill-tampered.json" ]]; then cat "$F/drill-tampered.json"; else cat "$F/drill.json"; fi ;; "repos/Cloudbird-Software/.github/contents/governance/butler/shadow-evidence.jsonl?ref=butler-ledger") diff --git a/governance/tests/test-feedback-edge.sh b/governance/tests/test-feedback-edge.sh index ddd2b56..7f9421f 100644 --- a/governance/tests/test-feedback-edge.sh +++ b/governance/tests/test-feedback-edge.sh @@ -87,7 +87,10 @@ cat > "$TMP/dash-p.txt" <<'EOF' "cost": {"per_ir_usd": 25.8}}} ``` EOF -env -u GH_TOKEN -u GITHUB_TOKEN python3 "$EDGE" --dry-run --dashboard-file "$TMP/dash-p.txt" 2>&1 | grep -q "^PENDING needs-human-p90-stop" \ +# #471 修复:grep -q 命中即关读端 → 上游 SIGPIPE → pipefail 误报红(环境竞态); +# 改为先落文件再 grep——判定面与输出流解耦 +env -u GH_TOKEN -u GITHUB_TOKEN python3 "$EDGE" --dry-run --dashboard-file "$TMP/dash-p.txt" >"$TMP/outp.txt" 2>&1 +grep -q "^PENDING needs-human-p90-stop" "$TMP/outp.txt" \ && ok "pending 标注字串 → 诚实跳过(字串口径同判)" || bad "pending 字串误判" # ---- 去重:open 候选已存在 → DUPLICATE 跳过(RB-B2) ---- @@ -111,8 +114,8 @@ cat > "$TMP/dash-cost.txt" <<'EOF' "cost": {"per_ir_usd": 80.5}}} ``` EOF -env -u GH_TOKEN -u GITHUB_TOKEN python3 "$EDGE" --dry-run --dashboard-file "$TMP/dash-cost.txt" 2>&1 \ - | grep -q "^SIGNAL per-ir-cost value=80.5 gt 60.0" \ +env -u GH_TOKEN -u GITHUB_TOKEN python3 "$EDGE" --dry-run --dashboard-file "$TMP/dash-cost.txt" >"$TMP/outc.txt" 2>&1 +grep -q "^SIGNAL per-ir-cost value=80.5 gt 60.0" "$TMP/outc.txt" \ && ok "用量面信号越阈(内联阈值 gt)" || bad "用量越阈漏检" # ---- 负向:policy 非法 → exit 2 fail-closed(三形态) ---- diff --git a/governance/tests/test-wave-schema.sh b/governance/tests/test-wave-schema.sh index 6264e2e..9458e9f 100644 --- a/governance/tests/test-wave-schema.sh +++ b/governance/tests/test-wave-schema.sh @@ -141,6 +141,46 @@ OUT=$(python3 "$WS" wave-check --cards "$TMP/cards.json" --ledger-dir "$TMP/no-s [[ $RC -eq 0 ]] && grep -q '"exceeded_dims": \[\]' <<<"$OUT" \ && pass "账本目录缺失 → 零用量不误熔断" || fail "空账本误判(rc=$RC)" +# ---- #470 回归:wallclock_sec 超限 → exit 4(键名映射 wall_sec 修复面) ---- +cat >"$TMP/cards-wc.json" <<'EOF' +[ + {"number": 504, "body": "## budget(波次预算)\nwallclock_sec: 7200\non_exceed: hard-stop"}, + {"number": 505, "body": "## budget(波次预算)\nusd: 1.0\nwallclock_sec: 7200\non_exceed: hard-stop"} +] +EOF +mkdir -p "$TMP/ledger-wc" +cat >"$TMP/ledger-wc/shadow-evidence-2026-W36.jsonl" <<'EOF' +{"ts":"2026-08-29T04:00:00Z","kind":"cost","action":"cost.dispatch","verdict":"pass","subject":{"card":"Cloudbird-Software/.github#504","tenant":"t1"},"actor":{"identity":"x","role":"bot","model":null},"cost":{"tokens":1000,"usd":0.1,"wall_sec":8000.0},"seq":1,"prev_hash":null,"hash":"dd"} +{"ts":"2026-08-29T05:00:00Z","kind":"cost","action":"cost.dispatch","verdict":"pass","subject":{"card":"Cloudbird-Software/.github#505","tenant":"t1"},"actor":{"identity":"x","role":"bot","model":null},"cost":{"tokens":1000,"usd":99.0,"wall_sec":8000.0},"seq":2,"prev_hash":"dd","hash":"ee"} +EOF +OUT=$(python3 "$WS" wave-check --cards "$TMP/cards-wc.json" --ledger-dir "$TMP/ledger-wc" 2>"$TMP/wc2.err"); RC=$? +[[ $RC -eq 4 ]] && pass "wallclock_sec 超限 → exit 4(#470:KeyError 修复+维度执法可达)" \ + || fail "wallclock_sec 超限应 exit 4(rc=$RC,stderr:$(head -2 "$TMP/wc2.err" 2>/dev/null))" +python3 - "$OUT" <<'PYEOF' && pass "wallclock+usd 混合卡:两维同报不互吞(#470:usd 超限不再被 wallclock 键错崩掉)" || fail "混合卡断言" +import json, sys +rows = {r["card"]: r for r in json.loads(sys.argv[1])} +c504 = rows["Cloudbird-Software/.github#504"] +assert c504["exceeded_dims"] == ["wallclock_sec"], c504 +c505 = rows["Cloudbird-Software/.github#505"] +assert c505["exceeded_dims"] == ["usd", "wallclock_sec"], c505 +PYEOF + +# ---- #472 回归:human_minutes 声明 → 行级 unenforced_dims 可见(不静默) ---- +cat >"$TMP/cards-hm.json" <<'EOF' +[ + {"number": 506, "body": "## budget(波次预算)\nhuman_minutes: 1\non_exceed: hard-stop"} +] +EOF +OUT=$(python3 "$WS" wave-check --cards "$TMP/cards-hm.json" --ledger-dir "$TMP/ledger-wc" 2>/dev/null); RC=$? +[[ $RC -eq 0 ]] && python3 - "$OUT" <<'PYEOF' && pass "human_minutes → unenforced_dims 行级可见(#472:账本无源不静默放行)" || fail "human_minutes 可见断言" +import json, sys +rows = json.loads(sys.argv[1]) +r = rows[0] +assert r["exceeded_dims"] == [], r +assert r.get("unenforced_dims") == ["human_minutes"], r +PYEOF +[[ $? -eq 0 ]] || fail "human_minutes 轮 rc=$RC(应 0——无执法面不误红)" + echo "----------------------------------------" if [[ $FAILS -eq 0 ]]; then echo "test-wave-schema: PASS"; exit 0; fi echo "test-wave-schema: $FAILS 处失败"; exit 1 diff --git a/governance/wave_schema.py b/governance/wave_schema.py index 265c650..8b12b4a 100644 --- a/governance/wave_schema.py +++ b/governance/wave_schema.py @@ -53,6 +53,12 @@ BUDGET_KEYS = ("usd", "tokens", "wallclock_sec", "human_minutes") ON_EXCEED = ("hard-stop", "warn") +# 可执法维度 → 账本聚合键映射(#470:声明侧 wallclock_sec vs 账本侧 wall_sec +# 键名不一致曾致 KeyError→执法不可达;此处单点映射,逐维判定不再依赖书写顺序) +ENFORCEABLE = {"usd": "usd", "tokens": "tokens", "wallclock_sec": "wall_sec"} +# 无账本源的声明维度(#472:human_minutes 暂无采集点——行级 unenforced_dims +# 可见声明,不静默放行也不假装判定;补源后移入 ENFORCEABLE) +UNENFORCED_KEYS = ("human_minutes",) SECTION_RE = re.compile(r"^#{2,6}\s*(budget|capabilities|evidence)\b[^\n]*$", re.MULTILINE) CAP_RE = re.compile(r"^(org-secret:[A-Z0-9_]+|vault:[A-Za-z0-9/_.-]+)$") @@ -151,8 +157,9 @@ def wave_check(cards_file: str, ledger_dir: str) -> tuple: """逐卡对账。返回 (rows, exceeded_any)。 聚合口径:subject.card 精确匹配;tenant 归因分离(AC-9b 多租户计量分离); - cost.{usd,tokens,wall_sec} 求和(无 cost 字段记 0)。human_minutes 无账本源 - ——报告不判定(备注承载)。 + cost.{usd,tokens,wall_sec} 求和(无 cost 字段记 0)。可执法维度见 + ENFORCEABLE(声明键→账本聚合键映射);human_minutes 无账本源——行级 + unenforced_dims 可见声明(#472:不静默放行,不假装判定)。 """ with open(cards_file, encoding="utf-8") as f: cards = json.load(f) @@ -188,13 +195,18 @@ def wave_check(cards_file: str, ledger_dir: str) -> tuple: "tokens": sum(t["tokens"] for t in ten.values()), "wall_sec": sum(t["wall_sec"] for t in ten.values())} on_exceed = budget.get("on_exceed", "hard-stop") - exceeded_dims = [k for k in BUDGET_KEYS[:3] - if k in budget and agg[k] > float(budget[k])] + exceeded_dims = [k for k in ENFORCEABLE + if k in budget and agg[ENFORCEABLE[k]] > float(budget[k])] + unenforced_dims = [k for k in UNENFORCED_KEYS + if k in budget and k not in ENFORCEABLE] if exceeded_dims and on_exceed == "hard-stop": exceeded = True - rows.append({"card": card_ref, "budget": budget, "usage_by_tenant": ten, - "usage_total": agg, "exceeded_dims": exceeded_dims, - "on_exceed": on_exceed}) + row = {"card": card_ref, "budget": budget, "usage_by_tenant": ten, + "usage_total": agg, "exceeded_dims": exceeded_dims, + "on_exceed": on_exceed} + if unenforced_dims: # 无账本源的声明维度——可见,不静默(#472) + row["unenforced_dims"] = unenforced_dims + rows.append(row) return rows, exceeded diff --git a/specs/IR-0006/acceptance.md b/specs/IR-0006/acceptance.md index 3f69ca3..c8baf60 100644 --- a/specs/IR-0006/acceptance.md +++ b/specs/IR-0006/acceptance.md @@ -38,7 +38,7 @@ spec PR:.github#403(adversary:survived 红队审计通过);背书 ADR: |---|---|---|---| | 1 | 18 章节落位表+词汇归并表,spec PR 红队 survived | specs/IR-0006/absorption-map.md(落位表"已覆盖映射/吸收后退役"两类+词汇归并);.github#403 merged,IR#402 带 adversary:survived | ✅ | | 2 | ADR-0103 落 archive/adr/ 并入 INDEX.yaml(risk_class=参数包选择器,裁决仍硬谓词+shadow) | archive PR #38/#39 merged;archive/adr/0103*;宪法 §5 未动(调和非回退) | ✅ | -| 3 | 统一判定层账本(三层/payload 指针/月度 checkpoint/hash 链/tenant/三源同 schema 查询) | archive evidence/ledger.jsonl(链尾 841c63dcaab5)+checkpoints/2026-08.json;**本轮实测**:scripts/verify_evidence.py 独立复算 OK(3 条链完整+checkpoint×1 对账一致);governance/evidence-query.sh 七源(metering/drill/butler/elevation/tickets/feishu/env)一条命令 | ✅ | +| 3 | 统一判定层账本(三层/payload 指针/月度 checkpoint/hash 链/tenant/三源同 schema 查询) | archive evidence/ledger.jsonl(链尾 841c63dcaab5)+checkpoints/2026-08.json;**本轮实测**:archive 仓 scripts/verify_evidence.py 独立复算 OK(3 条链完整+checkpoint×1 对账一致——脚本居 archive 仓,导航见 docs/NAVIGATION.md §1 治理仓 archive 行,#469 2A);governance/evidence-query.sh 七源(metering/drill/butler/elevation/tickets/feishu/env)一条命令 | ✅ | | 4 | providers.yaml 含 self-cloud-pool/vault;env-defs 建仓申报 REPOS.yaml,期望态/实况可对账 | .github#427(providers+governance/assets-register.yaml)+#428(REPOS.yaml 申报);.github#452 env-drift 引擎(run 33257903305 漂移检出→issue #453 自动开→消除自动关,GM-1 实走) | ✅ | | 5 | PM 凭证收敛为 cloudbrid-agent 短令牌,个人 PAT 退出日常 | .github#446(gh-app-token 上收+应急回退通道 24h 窗口文档化,ADR-0044);本会话起全程 GH_TOKEN=gh-app-token.sh 代签 | ✅ | | 6 | 飞书多维表格看板(label 唯一真源,drop & rebuild) | .github#447-#451;feishu-ledger 分支(影子账本链验通过);run 33254361404:人工违规改表被下轮投影纠正+drift 告警入账本(INV-05 实证);飞书 API 数字字段字符串怪癖/batch POST 形态两实测缺陷修复(#450/#451) | ✅ | @@ -47,7 +47,7 @@ spec PR:.github#403(adversary:survived 红队审计通过);背书 ADR: | 9 | holdout 扩展 eval registry+非劣性 eval gate,首个 optimization 波次走通 | holdout#9(eval-quad 四元组 pin)+.github#458;W5-OPT-1 波次(waves.yaml 注册):run 33263909046 基线/候选同 harness 评测+非劣性裁决 GREEN;fail-closed 活体:run 33263613945 ts 缺字段被 verify_evidence 拦截红→#460 修复重跑绿 | ✅ | | 10 | 决策语料 append-only 落 archive 开始累积 | archive decisions/ledger.jsonl(链式 hash+机械校验;首条真实记录 ADR-0103 决策情境,五段齐备) | ✅ | -**十条全绿。** +**十条全绿。**(修订 #473:AC-9/AC-3/AC-4 的后验缺陷见残留第 6 条——登记+修复+回归后方可称全绿,原断言系收口时未扫缺陷面的漏登) ## 冷上下文六问复测(a-f,主链路零假链) @@ -74,8 +74,16 @@ spec PR:.github#403(adversary:survived 红队审计通过);背书 ADR: 2. **R5 骨架未立项**(IR 非目标:外输三件套另行立项)——conformance/ 产物(32 卡语料+四列评审+晋级账本)已为外输预留底料。 3. **仪表盘 pending 项**:user_results 全产品仓 pending(埋点未落)、holdout_gap 检测面未建——按 ADR-0073 决策 7 诚实 pending,不造数。 4. **飞书数字字段字符串怪癖**已修(#450)但飞书 API 形态漂移风险常在——feishu-drill 每日演练=回归面。 -5. **main 直推事件(2026-08-29 17:36 UTC)**:owner 侧另一会话将"一人公司治理体系诊断与落地"巨型提交(15ac9c1)直推 main——绕过全部门禁(治理文件一致性未验);其携带 holdout/ gitlink 致 CI checkout 全红(#438 同款缺陷),由本 PR 修复(git rm --cached holdout)。直推豁免未登记——建议 owner 醒后按 ADR-0093 同款追认或回退。 +5. **main 直推事件(2026-08-29 17:36 UTC)**:owner 侧另一会话将"一人公司治理体系诊断与落地"巨型提交(15ac9c1)直推 main——绕过全部门禁(治理文件一致性未验);其携带 holdout/ gitlink 致 CI checkout 全红(#438 同款缺陷),由 PR #467 修复(git rm --cached holdout)。直推豁免未登记——建议 owner 醒后按 ADR-0093 同款追认或回退。 +6. **AC 级缺陷(独立验收发现,2026-08-30 第二轮复核 #473 登记——本节为后补,原报告漏登)**: + - **#470(P0/SEV:HIGH,命中 AC-9/BEH-07)**:wave_schema 键名不匹配(wallclock_sec vs wall_sec)→ KeyError → wallclock 维度预算执法不可达。**已修**(ENFORCEABLE 映射+cost-check rc 兜底,本修复 PR)。 + - **#472(P1,命中 AC-9/BUDGET-03)**:human_minutes 无账本源且被 BUDGET_KEYS[:3] 静默排除。**已修**(行级 unenforced_dims 可见声明,本修复 PR;补源完整执法另行立项)。 + - **#471(命中 AC-4/W6-M2 自测)**:evidence-query metering 源 KeyError(目录列表无 content 字段)→ metering 恒 0 条;test-feedback-edge SIGPIPE 竞态红被 gate for 循环吞掉。**已修**(逐文件拉取+先落文件再 grep+gate 逐测试判定,本修复 PR)。 + - **#469(GAP,命中 AC-3/BEH-02)**:verify_evidence.py 实居 archive 仓(本仓导航缺失,2A)+ checkpoint 月度无调度(2B)。**已修**(spec blastRadius/NAVIGATION 导航登记+evidence-checkpoint.yml 月度 cron+40 天新鲜度守卫,本修复 PR)。 + - **#468(GAP,命中 AC-3/BUDGET-02)**:三级保留策略声明位缺失。**已修**(governance/policy/retention.yaml 落盘,本修复 PR)。 + - 防复发:gate 新增 acceptance 收口守卫(验收报告合并时其 IR 项下不得有未豁免的 open bug/P0 issue——#473 建议处置 4)。 ## 结论 -IR#402 十条期望变化全部有运行时证据支撑,冷上下文六问复测通过,20/20 子卡 done。T9 谓词满足,IR-0006 可收口(state:done)。 +IR#402 十条期望变化全部有运行时证据支撑,冷上下文六问复测通过,20/20 子卡 done。T9 谓词满足,IR-0006 收口(state:done)。 +**修订(2026-08-30,#473)**:原「十条全绿」结论与独立验收发现的 AC 级缺陷并存(AC-9 #470/#472、AC-3 #469/#468、AC-4 #471)——系收口时未做缺陷面扫描的漏登。上表结论以「缺陷登记+修复合入+回归实测绿」为准(残留第 6 条逐项登记),acceptance 收口守卫已机械化(gate)。此后验收报告合并=缺陷面已清或已登记的机器判定,不再依赖单方自述。 diff --git a/specs/IR-0006/spec.md b/specs/IR-0006/spec.md index 27daa1f..c3f0ae3 100644 --- a/specs/IR-0006/spec.md +++ b/specs/IR-0006/spec.md @@ -56,6 +56,7 @@ blastRadius: path: governance/(providers.yaml、REPOS.yaml、policy/ 账本条款、ISSUE_TEMPLATE 卡模板) - repo: Cloudbird-Software/archive path: adr/(ADR-0103)、evidence/(判定层账本)、runs/、决策语料 + # 独立复算/写入/checkpoint 脚本居本仓 archive:scripts/{verify_evidence,write_evidence,verify_decisions}.py(#469 2A 导航登记) - repo: Cloudbird-Software/CI-Workflows path: pipeline/metering/(三源对齐、tenant tag 注入) - repo: Cloudbird-Software/