Add evidence gates: facts registry, evidence tooling, and precheck checks

Why: declarations were passing as capability; hand-transcribed outputs flowed
through cards and PR bodies as if they were execution evidence, and stale
external facts (e.g. the dead git-panel host) kept being trusted until burned.

Evidence: guidelines draft.10 (PR Development-Guidelines#4, merged af199ee)
pinned here; policy/facts.md F1-F4 each re-verified on 2026-10-05 before
registration (endpoint version, token HTTP 200 via header only, SSH ls-remote,
dead-host negative probe); scripts syntax-checked (bash -n); tool self-tests
and make test recorded in the task card via the new make evidence.

Impact: new AGENTS.md section (SELF/REPLAY/GATE, tool-written evidence,
replay-on-review, zero-trust facts, honest-empty); make evidence/facts-verify;
precheck gains [hooks]/[facts], task-sync gains stale-evidence-HEAD warning;
task-start cards now embed the evidence table; gitlink advances to draft.10.

Refs: task card .task/feature-evidence-driven-gates.md; guidelines CHANGELOG
0.1.0-draft.10.
This commit is contained in:
2026-10-05 10:47:27 +00:00
parent fb99968922
commit ba83a17c11
8 changed files with 211 additions and 8 deletions
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# 证据写入工具:执行命令,把(命令、被测 HEAD、退出码、输出 sha256、尾部摘录、时间、来源)
# 追加到当前分支任务卡的「## 证据(Evidence)」表。规范依据:AGENTS.md「证据与验证门禁」、
# guidelines standards/verification.md「声明与证据来源」——转写/手抄的输出视为无效证据。
# 用法:make evidence CMD="<单行命令>" [SRC=SELF|REPLAY|GATE] [TAIL=15]
# 退出码 = 被执行命令的退出码(失败可见),证据行无论成败都会写入。
set -uo pipefail
CMD="${CMD:-}"; SRC="${SRC:-SELF}"; TAILN="${TAIL:-15}"
[ -n "$CMD" ] || { echo 'usage: make evidence CMD="<command>" [SRC=SELF|REPLAY|GATE] [TAIL=15]' >&2; exit 2; }
case "$SRC" in SELF|REPLAY|GATE) ;; *) echo "ERROR: SRC 须为 SELF|REPLAY|GATE(当前:$SRC)" >&2; exit 2 ;; esac
case "$TAILN" in ''|*[!0-9]*) TAILN=15 ;; esac
case "$CMD" in *$'\n'*) echo 'ERROR: CMD 须为单行命令' >&2; exit 2 ;; esac
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[ -n "$branch" ] || { echo "ERROR: 不在 git 仓库内" >&2; exit 1; }
slug=$(echo "$branch" | tr '/' '-')
card=".task/${slug}.md"
if [ ! -f "$card" ]; then
# 回退:按卡片「分支:」字段匹配(与 task.sh cmd_sync 同款口径)
for pc in .task/*.md; do
[ -f "$pc" ] || continue
if [ "$(sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$pc" | head -1)" = "$branch" ]; then
card=$pc; break
fi
done
fi
[ -f "$card" ] || { echo "ERROR: 找不到分支 ${branch} 的任务卡(.task/${slug}.md)" >&2; exit 1; }
head=$(git rev-parse HEAD)
short_head=$(echo "$head" | cut -c1-10)
ts=$(date -u +%FT%TZ)
tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT
bash -c "$CMD" >"$tmp" 2>&1; rc=$?
digest=$(sha256sum "$tmp" | cut -c1-16)
# 脱敏(baseline 硬边界 2):token 风格的 40 位 hex / Authorization 头不打进任务卡。
sanitize() {
sed -E -e 's/(token[ =:]+)[0-9a-fA-F]{40}/\1<redacted>/g' \
-e 's/(Authorization: *token +)[^ ]+/\1<redacted>/Ig' \
-e 's/(access_token[=:]+)[0-9a-fA-F]{40}/\1<redacted>/Ig'
}
if ! grep -q '^## 证据(Evidence)' "$card"; then
printf '\n## 证据(Evidence)\n\n<!-- make evidence CMD="…" 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->\n| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |\n|---|---|---|---|---|---|\n' >> "$card"
fi
cmd_cell=$(echo "$CMD" | sed -e 's/|/\\|/g' -e 's/`/‘/g')
printf '| %s | `%s` | %s | %d | %s | %s |\n' "$ts" "$cmd_cell" "$short_head" "$rc" "$SRC" "$digest" >> "$card"
printf '\n<!-- evidence:run %s -->\n```\n$ %s\n[exit %d | HEAD %s | sha256:%s] 输出尾部 %s 行:\n%s\n```\n' \
"$ts" "$CMD" "$rc" "$short_head" "$digest" "$TAILN" "$(tail -n "$TAILN" "$tmp" | sanitize)" >> "$card"
echo "[evidence] card=$card cmd=\"${CMD}\" exit=$rc src=$SRC head=$short_head sha256=$digest"
exit "$rc"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# 外部事实登记表逐条重验:对 policy/facts.md 每条事实执行其验证命令并报告 PASS/FAIL。
# 规则依据:AGENTS.md「证据与验证门禁」(外部事实零信任);verification.md「声明与证据来源」。
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
set -uo pipefail
F="${FACTS:-policy/facts.md}"
[ -f "$F" ] || { echo "ERROR: 事实登记表 $F 不存在" >&2; exit 1; }
python3 - "$F" "${1:-}" <<'PY'
import re, subprocess, sys, datetime
path, update = sys.argv[1], sys.argv[2] == "--update"
lines = open(path, encoding="utf-8").read().splitlines(keepends=True)
today = datetime.date.today().isoformat()
total = fails = 0
for i, line in enumerate(lines):
if not re.match(r"^\|\s*F\d+\s*\|", line):
continue
raw = line.strip().strip("|").replace("\\|", "\x00")
cells = [c.strip().replace("\x00", "|") for c in raw.split("|")]
if len(cells) < 4:
print(f"[row {i+1}] SKIP 格式不符(列数<4)", file=sys.stderr)
continue
fid, cmd = cells[0], cells[2].strip("`")
total += 1
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=90)
ok = r.returncode == 0
except subprocess.TimeoutExpired:
ok = False
print(f"[{fid}] TIMEOUT >90s", file=sys.stderr)
print(f"[{fid}] {'PASS' if ok else 'FAIL'} :: {cmd[:80]}")
if not ok:
fails += 1
elif update and len(cells) >= 4 and re.match(r"\d{4}-\d{2}-\d{2}", cells[3]):
cells[3] = today
lines[i] = "| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |\n"
if update:
open(path, "w", encoding="utf-8").writelines(lines)
print(f"[facts-verify] total={total} pass={total - fails} fail={fails}"
+ (";last_verified 已刷新为 " + today if update and total else ""))
sys.exit(1 if fails else 0)
PY
+43 -1
View File
@@ -59,6 +59,34 @@ cmd_precheck() {
fi
done < <(git for-each-ref --format='%(refname:short) %(committerdate:unix)' "refs/remotes/origin/feature/card-close-*")
[ -n "$stale" ] && echo "[cards] 闭合分支超过 24 小时未合入:$(echo $stale)"
# [hooks] 声明≠能力:core.hooksPath 未指向 scripts/hooks 时,"钩子已启用"只是声明。
local hp; hp=$(git config core.hooksPath 2>/dev/null || true)
if [ "$hp" = "scripts/hooks" ] && [ -x scripts/hooks/pre-commit ]; then
echo "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)"
else
echo "[hooks] WARN core.hooksPath='${hp:-未设置}'——本地钩子未生效,先 make setup;本检出提交将绕过本地闸(远端准入仍在)"
fi
# [facts] 外部事实登记表过期项(policy/facts.md;引用前先跑其验证命令,AGENTS.md 证据与验证门禁)。
if [ -f policy/facts.md ]; then
local fid lv ttl expired="" ftotal=0
while IFS='|' read -r _ fid _ _ lv ttl _; do
fid=$(echo "${fid:-}" | tr -d ' '); case "$fid" in F[0-9]*) ;; *) continue ;; esac
ftotal=$((ftotal+1))
lv=$(echo "$lv" | tr -d ' '); ttl=$(echo "$ttl" | tr -d ' ')
case "$lv" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) expired="$expired ${fid}(日期格式)"; continue ;; esac
case "$ttl" in ''|*[!0-9]*) ttl=30 ;; esac
if [ $(( $(date +%s) - $(date -d "$lv" +%s) )) -gt $(( ttl*86400 )) ]; then
expired="$expired ${fid}(last=${lv},ttl=${ttl}d)"
fi
done < <(grep -E '^\| *F[0-9]+ ' policy/facts.md || true)
if [ -n "$expired" ]; then
echo "[facts] WARN 过期事实:$(echo $expired)——引用前先 make facts-verify 重验并更新登记表"
else
echo "[facts] policy/facts.md ${ftotal} 条事实均在有效期内"
fi
else
echo "[facts] WARN policy/facts.md 不存在——外部事实登记表缺失(AGENTS.md 证据与验证门禁)"
fi
echo "[required-reading] core/baseline.md, workflows/delivery.md §1$([ "$dirty" -gt 0 ] && echo ", standards/collaboration.md(树脏=他人或未收尾现场,不得写入)")"
echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)"
if [ "$dirty" -gt 0 ] || [ "${sub_drift:-0}" -gt 0 ]; then
@@ -120,10 +148,16 @@ cmd_start() {
|---|---|---|---|
| 启动 | ${rmain} | $(git -C "$wt" rev-parse HEAD) | $(date -u +%FT%TZ) |
## 证据(Evidence)
<!-- make evidence CMD="…" 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 结果与验证(完成后填写)
- 结果:
- 验证:
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
- 影响与剩余项:
EOF
git -C "$wt" add .task && git -C "$wt" commit -q -m "Record the task card for ${slug}" && echo "[card] $card(已随分支提交)"
@@ -155,6 +189,14 @@ cmd_sync() {
printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
git add "$card"
git commit -q -m "Record the sync checkpoint" || true
# [evidence] 同步改变了被测对象:卡片证据表中旧 HEAD 的行自动失效,关键检查须重跑。
local eh stale_ev="" short_head
short_head=$(echo "$head" | cut -c1-10)
while IFS='|' read -r _ _ _ eh _ _ _; do
eh=$(echo "$eh" | tr -d ' `')
[ -n "$eh" ] && [ "$eh" != "$short_head" ] && stale_ev="$stale_ev $eh"
done < <(grep -E '^\| [0-9]{4}-[0-9]{2}-[0-9]{2}T[^|]*\|' "$card" 2>/dev/null || true)
[ -n "$stale_ev" ] && echo "[evidence] WARN 证据表含旧被测 HEAD:$(echo $stale_ev | tr ' ' '\n' | sort -u | tr '\n' ' ')(≠ 当前 $short_head)——同步后关键检查须重跑并以 make evidence 重新写入"
else
echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2
fi