Skip to content

feat(rules): enforce artifact>testimony — RULES.md + evidence-check gate - #160

Open
sururu-k wants to merge 2 commits into
mainfrom
feat/anti-hallucination-rules
Open

feat(rules): enforce artifact>testimony — RULES.md + evidence-check gate#160
sururu-k wants to merge 2 commits into
mainfrom
feat/anti-hallucination-rules

Conversation

@sururu-k

Copy link
Copy Markdown
Collaborator

Summary

  • Make artifact > testimony the repo's single machine-checkable norm: RULES.md (6 rules + Evidence Block schema), pointed to by CLAUDE.md and the templates.
  • Enforce it mechanically, not by good intentions: scripts/evidence_check.py re-executes every RERUN recipe in a PR body, so a pasted or fabricated log cannot pass. This generalizes chore(pr-template): red-before-green acceptance gate for bug fixes #159's red-before-green box (template-only, self-reported) into a gate.
  • Ship the CI wiring at ci/evidence-check.yml for a maintainer to install (this token lacks GitHub workflow scope).

Type of change

  • CI / tooling
  • Documentation

Test plan — Evidence Blocks (dogfooding RULES.md)

$ python3 scripts/evidence_check.py --selftest | tail -1
selftest OK: honest PASS, fabricated FAIL, bare-claim FAIL
$ python3 -c "import sys;sys.path.insert(0,'scripts');import evidence_check as e;print([str(f) for f in e.check('x',changed_files=136)])"
['[FAIL] R6: 136 changed files exceeds size limit 40; ...']

Related issues / context

Refs #159 (red-before-green precursor), #134 (CI-green-but-broken), #118 (oversized PR).

Reviewer notes

…k gate

RULES.md makes the anti-hallucination principle the repo's single machine-checkable
norm (CLAUDE.md and the templates point to it). The enforcement is mechanical, not
a doc that can be ignored:

- scripts/evidence_check.py re-EXECUTES every '<!-- RERUN: <cmd> EXPECT <substr> -->'
  in a PR body, so a pasted or fabricated log cannot pass; VERIFIED claims need an
  adjacent raw-log fence (R1/R3/R5). 'UNVERIFIED' is a passing state (R2). Size gate
  fails >40 changed files without SPLIT-JUSTIFIED (R6). Ships with a red-before-green
  selftest: honest doc PASS, fabricated doc (RERUN->confirmed=0) FAIL.
- ci/evidence-check.yml runs the gate on every PR body; RERUN executes only for
  internal branches (fork PRs get structural checks). Shipped under ci/ because the
  authoring token lacks GitHub 'workflow' scope — a maintainer installs it to
  .github/workflows/. Until then the gate runs locally / pre-commit / manually.
- PR template generalizes the #159 red-before-green box into Evidence/UNVERIFIED
  blocks (red-before-green is now rule 4, refute-don't-confirm).

Rule 5's non-deterministic cross-check path (independent subagent) is intentionally
not in this PR — deterministic CI gate first, staged rollout to avoid a #118-size change.

Refs: #159 (red-before-green precursor), #134 (CI-green-but-broken), #118 (oversized PR)
Makes RULES.md's claim true — the shared instruction layer now actually points
to it (CLAUDE.md working-rules section, bug-report evidence note), per the
original ask to embed the norm in CLAUDE.md / PR template / issue templates.
@grandchildrice

Copy link
Copy Markdown
Contributor

ブランチを手元に落として全部走らせました。以下、貼ってあるのは全部その実行結果です。環境は手元の Linux + uv、2426438d 時点。

再実行を軸に据えた設計は正しいと思います。RERUN を検証側が回し直すのは #159 の自己申告欄からの正当な一般化だし、UNVERIFIED を合格状態にしたのは捏造の誘因を消す判断として鋭い。--selftest を body チェックより先に置いた順番も効いていて、後述するとおり素朴な改竄は実際にこれで止まりました。

そのうえで、今のままだと止めたい失敗が通り抜けます。


1. #117 の本文がそのまま緑で通る

CI は --strict-claims なしで呼んでいるので、bare claim は WARN 止まりで exit 0 になります。

$ cat liar.md
## Summary
Fixed the bug.

## Test plan
I ran the full suite and everything passes. Both blocking issues addressed.

$ python3 scripts/evidence_check.py --body liar.md --changed-files 3   # ci/evidence-check.yml と同じ引数
[WARN] R3: bare claim without evidence (line 5): I ran the full suite and everything passes. Both blocking issues addressed.

PASS: 0 failing, 1 warnings, 0 ok
exit=0

本文が空でも同じです。

$ python3 scripts/evidence_check.py --body empty.md --changed-files 3
[WARN] -: no evidence markers found — nothing to enforce

PASS: 0 failing, 0 warnings, 0 ok
exit=0

証拠提出が opt-in なので、最短で緑にする方法が「何も書かない」になっています。

直し方

  • ci/evidence-check.yml の実行行に --strict-claims を足す。
  • check() の末尾、if not findings: の分岐を FAIL 側に倒す。ただし docs-only PR まで落とすと機能しないので、CLI に --require-evidence を足して、ワークフロー側で「差分に .py / .ts / prompts/ が含まれるときだけ渡す」形にする。判定は git diff --name-only "$base"...HEAD の結果をそのまま grep すれば足ります。

2. RERUN が終了コードを見ていない

run_rerun()p.returncode を失敗メッセージの中でしか使っていません。部分文字列一致だけなので、テンプレが公式例として載せている EXPECT passed が落ちているスイートで通ります。

$ cat demo/test_x.py
def test_ok(): assert True
def test_bad(): assert 1 == 2

$ uv run --with pytest python3 -m pytest demo -q
FAILED test_x.py::test_bad - assert 1 == 2
1 failed, 1 passed in 0.02s

1 failed, 1 passedpassed が含まれるので、これを RERUN に載せると通ります。ついでに echo で任意の claim を満たせることも確認しました。

$ python3 scripts/evidence_check.py --body weak.md
[OK] R1/R5: re-ran, found EXPECT 'passed': cd demo && uv run --with pytest python3 -m pytest . -q
[OK] R1/R5: re-ran, found EXPECT '"confirmed": 7': echo '{"confirmed": 7}'

PASS: 0 failing, 0 warnings, 2 ok
exit=0

赤にならない検証で緑を取るのは #134 そのものなので、それを止める道具の中にあるのはまずいです。

直し方

run_rerun() に rc 判定を入れて、既定を「rc == 0 かつ EXPECT 一致」にする。

out = (p.stdout or "") + (p.stderr or "")
if p.returncode != 0 and expect_rc is None:
    return Finding("FAIL", "R1/R5", f"RERUN exited rc={p.returncode}: {cmd}\n        {out.strip()[:300]!r}")
if expect_rc is not None and p.returncode != expect_rc:
    return Finding("FAIL", "R1/R5", f"RERUN rc={p.returncode}, expected {expect_rc}: {cmd}")

rc が非 0 になるのが正しいケース(pre-fix の赤ログなど)用に、マーカー側に EXPECT-RC を足す。

<!-- RERUN: uv run python3 -m pytest tests/test_foo.py -q EXPECT-RC 1 EXPECT 1 failed -->

テンプレの例は EXPECT passed をやめて EXPECT-RC 0 にしてください。823 passed のような具体数を EXPECT に書く手もありますが、テスト追加のたびに書き換えになるので rc のほうが実用的です。

3. 未編集のテンプレを出すと落ちる

$ python3 scripts/evidence_check.py --body .github/PULL_REQUEST_TEMPLATE.md --changed-files 3
[FAIL] R1/R3: EVIDENCE claim has no adjacent raw-log fence: "pytest suite is green"
[OK]   R1/R5: re-ran, found EXPECT 'passed': uv run python3 -m pytest tests/ -q
[FAIL] R1/R5: re-ran but EXPECT '<substring>' NOT in output (rc=2): <command>
        --- actual output (first 300 chars) ---
        '/nix/store/.../bin/sh: -c: line 1: syntax error near unexpected token `newline`\n... `<command>`'   ← nix のパスだけ省略、あとは verbatim
FAIL: 2 failing, 6 warnings, 2 ok

原因は2つあります。

プレースホルダの <!-- RERUN: <command> EXPECT <substring> --> がそのまま shell に渡っています。2行目を見ると、説明用に書いた uv run python3 -m pytest tests/ -q も実際に走っています。

もう1つは HTML コメントがネストしないことです。テンプレ 35行目の <!-- は 40行目の --> で閉じるので、41〜46行目は説明ではなく本物の Evidence Block として読まれます。GitHub 上でも見えています。

$ gh api /markdown --input md.json | sed 's/<[^>]*>//g'
Test plan — Evidence Blocks (RULES.md)

​ $ uv run python3 -m pytest tests/ -q 823 passed, 2 skipped ​

--&gt;

1 と組み合わさると勾配が逆を向きます。テンプレを正直に埋めると赤、マーカーを消すと緑です。

直し方

  • テンプレから live なマーカーを全部抜く。書式サンプルは RULES.md 側にだけ置いて、テンプレからはリンクする。1 の --require-evidence が入れば「マーカーを書かないと通らない」が担保されるので、テンプレにひな形を残す必要はありません。
  • パーサ側でも塞ぐ。_fenced_spans() は既にあるので、EVIDENCE.search / RERUN.findall をフェンス内の行を除いた本文に対して掛ける。今は RERUN.findall(text) が生テキスト全体を舐めているので、コードブロックに引用した RERUN 例まで実行されます。

4. CI に uv が無い

ci/evidence-check.ymlsetup-python@v5 だけです。この repo の他の Python ワークフローは uv を入れています。

$ sed -n '28,36p' .github/workflows/tests-on-push.yml
      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: "latest"
      - name: Install Python 3.11 via uv
        run: uv python install 3.11
      - name: Sync Python deps
        run: uv sync

未確認: 実際に GitHub Actions 上で走らせていないので、uv: command not found になるところまでは確認していません。ワークフローの中身から見て、uv run を含む RERUN は通らないはずです。

直し方 tests-on-push.yml の28〜35行をそのまま持ってくる。あわせて run_reruntimeout: int = 120 を CLI から変えられるようにしてください(--rerun-timeout、既定600くらい)。今の120秒だと uv run python3 -m pytest tests/ が入った時点で timeout FAIL になります。

未確認: git diff --name-only "origin/$base"...HEAD が checkout@v4 + fetch-depth 0 で通るかは試していません。もし origin/main が無いと CHANGED が空文字になって --changed-files "" で argparse が exit 2 になるので、CHANGED=${CHANGED:-0} を挟んでおくと安全です。

5. ゲートが PR 自身のチェックアウトから走る

actions/checkout@v4ref: が無いので、PR 側の scripts/evidence_check.py が PR を採点します。

素朴な改竄は --selftest が止めます。if expect in out:if True: に書き換えたら selftest が exit 1 になりました。この順番は効いています。

抜けるのは狙った改竄のほうです。DEFAULT_SIZE_LIMIT を 40 から 100000 に変えるだけだと、selftest は changed_files=3 でしか叩かないので素通りします。

targeted tamper: DEFAULT_SIZE_LIMIT 40 -> 100000 のみ
$ python3 scripts/evidence_check.py --selftest > /dev/null; echo $?
0
$ python3 scripts/evidence_check.py --body liar.md --changed-files 500; echo $?
[WARN] R3: bare claim without evidence (line 5): ...
PASS: 0 failing, 1 warnings, 0 ok
0

直し方 ゲート本体を base から取る。

- name: Fetch gate from base
  run: |
    git show "origin/${{ github.event.pull_request.base.ref }}:scripts/evidence_check.py" > /tmp/gate.py
- name: Enforce
  run: python3 /tmp/gate.py --body pr_body.md --changed-files "$CHANGED" --strict-claims

あわせて .github/CODEOWNERSscripts/evidence_check.pyRULES.md.github/workflows/evidence-check.yml を入れて、この3つの変更には必ずレビューが要る形にしてください。

6. マージしても何も強制されない状態で、RULES.md は強制されていると書いている

ワークフローは ci/evidence-check.yml に置いてあって .github/workflows/ ではないのに、RULES.md には現在形でこう書いてあります。

CI runs `evidence_check.py` on every PR body.

PR 本文の UNVERIFIED では正直に書いてあるので、同じ但し書きを RULES.md 側にも入れてください。「設置されるまで強制されない」と書くだけで済みます。

そのうえで、issue #41 が言っていた自己申告から CI 強制への移行は、以下3つが揃って初めて成立します。このPRだけだと #159 と同じ「欄があるだけ」の状態のままです。

  1. .github/workflows/evidence-check.yml に設置(workflow scope が要るので誰かに頼む)
  2. main の required status checks に evidence-check を追加
  3. enforce_admins: true

RULES.md の中身

7. 冒頭で、存在しない文書に優先順位を宣言している

If a rule here conflicts with a longer doc (LANDMINES.md, SESSION-START.md), this file wins
$ git ls-files | grep -i "landmine\|session-start"
$ git ls-tree -r origin/feat/anti-hallucination-rules | grep -i "landmine\|session-start"
$ git log --all --oneline --diff-filter=A -- '*LANDMINES*' '*SESSION-START*'
$ grep -rl "LANDMINES\|SESSION-START" --include="*.md" .
(4つとも0件)

この2つはこの repo に一度も存在したことがありません。リポジトリの状態についての裏取りなしの記述が、契約書の1段落目にあります。

直し方 括弧を落として If a rule here conflicts with a longer doc, this file wins. にする。

8. Evidence Block の唯一の手本が再現しない

L139-144 の例を verbatim で実行するとこうなります。

$ python3 experiments/paper_metrics/build_population_table.py --p04 cli/test/fixtures/sherlock-rq1/lodestar_fusaka/04_PARTIAL_*.json
build_population_table.py: error: provide --p03 and --p04 globs, or --selftest
exit=2

--p03 が必須です。貼られている {"adjudicated_reviewed": 37, "confirmed": 7, "disputed": 0} はこのコマンドからは出ません。しかも --p03 を足そうにも、その fixture ディレクトリに 03 はありません。

$ ls cli/test/fixtures/sherlock-rq1/lodestar_fusaka/ | sed 's/_W[0-9]*B[0-9]*_[0-9]*//' | sort -u
04_PARTIAL.json

RULES.md を自分のゲートに通すと落ちます。

$ python3 scripts/evidence_check.py --body RULES.md
[FAIL] R1/R3: EVIDENCE claim has no adjacent raw-log fence: "paper_metrics counts confirmed=7 on real lodestar 04"
[FAIL] R1/R5: re-ran but EXPECT '"confirmed": 7' NOT in output (rc=2)

人はルール本文よりも例をコピーするので、ここが一番効きます。

直し方

  • 例を、手元で実際に走らせて出力を貼れるコマンドに差し替える。python3 experiments/paper_metrics/build_population_table.py --selftest は exit 0 で通るのを確認したので、これでもいいです。あるいは 03/04 が両方ある cli/test/fixtures/sherlock-rq1/nethermind_fusaka/ を使う(そこで confirmed が何件になるかは走らせていません)。
  • そのうえで、ワークフローの enforce ステップに1行足して、RULES.md 自体を毎回ゲートに通す。
- name: Gate the rules doc itself
  run: python3 /tmp/gate.py --body RULES.md

これを入れておけば、二度と走らない例が RULES.md に入ることはなくなります。

9. #134 がルール上まったく捕まらない

#134 は証拠を出さなかった案件ではありません。テストはあったし、走ったし、CI は緑でした。壊れていたのは fixture がクライアントごとに別 ID で、見たかったケースを踏まなかったことです。

このスキームで #134 を書き直すと、著者は正直に RERUN: pytest EXPECT passed を書いて、緑になって、通ります。旗印にしている2例のうち片方がカバーできていません。

足りないのは「証拠があること」ではなく「証拠が偽のとき赤くなること」です。R4 は態度としては書いてありますが、Evidence Block のスキーマに落ちていないので強制手段がテンプレの欄しかなく、そこは #159 と同じ自己申告です。

直し方 Evidence Block に FALSIFIER を必須で足す。

<!-- EVIDENCE claim="phase03 が空 queue で早期 exit する" -->
```
$ uv run python3 -m pytest tests/test_phase03_early_exit.py -q
2 passed in 0.31s
```
<!-- RERUN: uv run python3 -m pytest tests/test_phase03_early_exit.py -q EXPECT-RC 0 -->
<!-- FALSIFIER: git stash && uv run python3 -m pytest tests/test_phase03_early_exit.py -q; git stash pop
     → 修正前は 1 failed(貼る) -->

FALSIFIER は「この主張が偽のとき、この確認はどう違う出力を出すか」を書く欄です。bug fix なら pre-fix の赤ログそのもの、新機能なら「この行をコメントアウトすると落ちる」でいい。red-before-green はこの特殊ケースとして自然に入るので、テンプレの専用欄は畳めます。

チェッカー側は、まず FALSIFIER の有無だけ見れば十分です(中身の自動実行は次の段階でいい)。無ければ FAIL、FALSIFIER-NA: 理由 があれば通す。

10. #117 型は原理的に捕まらない

#117 は Major 1 が未対応のまま both addressed と返ってきた件でした。Evidence Block は書かれた主張を検証する仕組みなので、書かれなかった主張については何も言いません。指摘5件のうち4件に完璧な証拠を付けて1件黙る、が満点で通ります。

証拠の質ではなく被覆の話なので、R1〜R6 のどれとも別の軸が要ります。

直し方 7つ目のルールとして足す。

7. 指摘には1対1で応答する。レビュー指摘・受入基準の各項目に、done / not done / rejected(+理由) のどれかを必ず付ける。
   「全部対応しました」は応答ではない。項目を1つずつ列挙して、それぞれに状態を書く。

テンプレ側は Reviewer notes の下に「指摘への応答」表を置いて、レビュー指摘の番号・状態・証拠へのリンクを1行ずつ書かせる形が実務的だと思います。

11. R1 の「同一ターン」は外から検証できない

同じターンかどうかはレビュアには分かりません。RULES.md 自身も「再実行するので鮮度は問題でなくなる」と書いていて、R1 は自分で自分を無効化しています。効かせたい性質は鮮度ではなく、証拠と対象コミットの対応のはずです。

あと「ターン」は AI 側の語彙なので、human and AI 両方の contract を名乗るなら R1 だけ読者が限定されています。

直し方

1. 証拠は commit を名指しする。Evidence Block には取得時の commit SHA を書き、その SHA で再現できること。
   記憶や過去の実行結果を根拠にしない。コードを触ったら証拠を取り直す。

12. R2 が万能の抜け道になっている

UNVERIFIED は常に PASS なので、全部 UNVERIFIED の PR が緑になります。正直を罰しない方向は変えないでほしいですが、対になる半分が要ります。

直し方 R2 に、UNVERIFIED にできない項目のリストを付ける。

2. 未確認は未確認と書いてよい。ただし PR 種別ごとの必須検証だけは例外で、これを UNVERIFIED にした PR は draft のままにする。
   - bug fix: FALSIFIER(修正前に落ちること)
   - schema 変更: 既存 PARTIAL の読み込みが壊れないこと
   - prompt 変更: 実データ1件での before/after 出力
   それ以外は自由に UNVERIFIED にしてよい。

チェッカー側は、PR テンプレの Type of change のチェック状態を読んで、bug fix にチェックがあるのに FALSIFIER が無ければ FAIL、で実装できます。

13. R5 は再実行と独立性を混同している

再実行が与えるのは再現性で、独立性ではありません。claim も RERUN も著者が書くので、著者の検証モデルが間違っていれば、第三者が独立に回し直しても著者の盲点をそのまま再現します。#134 がまさにそれです。

Enforcement 表が | 1, 3, 5 | evidence_check.py | と1行にまとめているので、R5 が実装済みに見えてしまっています。

直し方 表の R5 の行を分けて、mechanism を 未実装(deterministic gate の後) と書く。R5 の本文も、独立性の中身が「検証手段を PR ではなく issue の受入基準から導出すること」だと分かるように書き直す。

14. R6 は #118 の何が問題だったかとずれている

#118 の本質は136ファイルではなく、#27#29 を1本にまとめたことです(issue #41 にそう書いてあります)。ファイル数を閾値にすると、正当な機械的リネームは落ちて、関心事が2つ混ざった3ファイル PR は通ります。

直し方

6. 1 PR = 1 issue = 1つの受入基準セット。2つの issue を閉じる PR は分ける。
   ファイル数はあくまで目安で、40を超えたら理由を書く。

チェッカー側は、PR 本文から #\d+ を拾って、Closes/Fixes 付きが2件以上あれば FAIL でいけます。

15. Evidence Block に4つ目の部品が要る

claim と command を結ぶ根拠がありません。上の 2 で貼ったとおり echo で任意の claim が満たせます。

直し方 自然文の claim をやめて、claim を「コマンド + 期待」から生成する。書き手も楽になるし、自然文の claim 自体が testimony なので原則にも合います。互換のために claim= を残すなら、FALSIFIER を必須にすることで実質的に塞がります(echo に対する反証は書けないので)。

16. 規律クラスタが1行も入っていない

issue #41 の実測で self-merge・明文化ルールの逸脱が 5/24 = 21%、実行系の次に大きいクラスタでした。RULES.md は every human and every AI session の contract を名乗っているので、読み手は網羅と受け取ります。

直し方 末尾に一段落足す。

## この文書が扱わないもの
merge 規律(self-approve / self-merge / required check の迂回)は本文書の対象外で、
branch protection 側で担保する。文書で足りる問題ではない。

17. 4層目の norm doc になっている

「A norm doc alone gets ignored — that is the failure this repo keeps repeating」と書いた直後に、CLAUDE.md に9行の要約が重複しています。現時点で2箇所メンテです。

直し方 CLAUDE.md 側は1行のリンクだけにする。

## Working rules
[RULES.md](RULES.md) がこの repo の作業契約。長い説明とここが食い違ったら RULES.md が勝つ。

18. R3 のキーワード検出はこの repo だと誤検知する

confirmed|passes|verified は Phase 04 の語彙そのものです(CONFIRMED_VULNERABILITYconfirmed=7)。1 で --strict-claims を入れると、04 の出力を説明する PR が軒並み落ちます。RULES.md 自身を通したときにも6件出ていました。

直し方 ## Summary## Test plan セクション内に限定する。--strict-claims を実用にするには、これが前提になります。

19. gate 自体に pytest が無い

$ grep -rl "evidence_check" tests/
(0件)

この repo の pre-flight は uv run python3 -m pytest tests/ -v で、tests-on-push.yml が push ごとに回しています。--selftest はそこから呼ばれないので、workflow を設置するまでゲートのデグレは誰にも見えません。

直し方 tests/test_evidence_check.py を足して、HONEST / FABRICATED / CLAIM_NO_LOG の3ケースに加えて、上で出た rc 無視・echo・フェンス内マーカーのケースも入れる。既存の --selftest はそこから呼ぶ形にすれば二重管理になりません。

20. fork ゲーティングの説明が実装と違う

ワークフローのコメントに mirroring the actor gating the phase workflows already use とありますが、既存フェーズは actor の allowlist です。

$ grep -rn "github.actor" .github/workflows/ | head -3
.github/workflows/04-audit-review.yml:22:    if: ${{ github.actor == 'grandchildrice' || github.actor == 'hirorogo' }}
.github/workflows/01e-properties.yml:90:    if: ${{ github.actor == 'grandchildrice' || github.actor == 'hirorogo' }}
.github/workflows/02c-enrich-code.yml:63:    if: ${{ github.actor == 'grandchildrice' || github.actor == 'hirorogo' }}

このPRのは head.repo.full_name 比較なので、別物です。types: [edited] があるので、push 権限のある人は PR 本文を書き換えるだけで runner 上で任意コマンドが打てます。内部 repo なら許容範囲だと思いますが、コメントは実装に合わせてください。既存に揃えて actor allowlist も併用するなら、そのほうが一貫します。


優先順位

マージ前: 7, 8(捏造2件)→ 2, 9(赤にならない検証)→ 1, 3(緑の取り方が逆)→ 4

そのあと: 10, 12, 5, 6

後続で: 11, 13, 14, 15, 16, 17, 18, 19, 20

9 と 2 は同じ穴の表と裏です。FALSIFIER を足しても rc を見ないままだと echo で反証を偽装できるし、rc を見ても FALSIFIER が無ければ #134 は通ります。片方だけだと塞がりません。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants