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:
Executable
+54
@@ -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"
|
||||
Executable
+42
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user