fix(acceptance): 有効フラグは宣言であって証拠ではない——runtime は「走った記録」を要求する - #44
Conversation
`acceptance_evidence` は runtime 証拠が実行されたかを `runtime_smoke.enabled` の真偽値だけで判定していた。実プロジェクト実測: dev server も DB も起動せず `codd verify --runtime` を一度も叩かずに `enabled: true` を1行足すだけで、 runtime_evidence_not_executable 50→0・unbound_acceptance 61→20・finding 総数 180→89。証拠を用意するより宣言するほうが安いという逆インセンティブで、しかも 「証拠が無いこと」を捕まえるための検査自身が主張を証拠として受け取っていた。 規律を1つに戻す: **実行された runtime 義務だけが束縛になる**。 - codd/runtime_record.py(新規): runtime smoke ランナーが、有効かつ1つ以上の チェックが実際に走ったときだけ <codd-dir>/runtime_ledger.json を書く。全skip・ 無効では書かない(何も走っていない記録は記録ではない)。内容は recorded_at・ passed・各チェックの name/category/passed/skipped・config_digest(実行時点の runtime_smoke + runtime セクションのハッシュ)。欠損/破損は「証拠なし」であって 決して証拠ではない——acceptance_ledger.json と同じ規律。 - runtime_evidence_not_executed(新規 finding): 有効だが実行記録が無い/設定が 変わった/鮮度切れ/明示 runtime:<case> に応えるチェックが走っていない。 runtime_evidence_not_executable は無改変で「ステージが無効」のまま。穴が2つ、 remedy が2つ、finding も2つ。 - unbound_acceptance も復活する。未実行の義務は無効の義務と同じだけしか束縛しない。 - 明示/暗黙の非対称は runtime_obligations の既存設計を踏襲。明示 runtime:<case> は 同名の非skipチェックを要求し(print_sheet / Print Sheet / print-sheet は1つの名前)、 operation_flow.<id> 由来の暗黙義務は記録があれば足りる。落ちた実行も「実行済み」 として数える(Step 8 が既に赤で報告済み)。 - acceptance_evidence.runtime_execution_severity(新規・strict でも既定 amber)と runtime_max_age_hours(新規・既定 unset)。記録が無いのは「未証明」であって 「誤りの証明」ではなく、CoDD を上げただけで既存の緑を赤にしない。 未解決として明記: 実行証拠は実装内容には束縛していない(設定変更と鮮度でのみ 期限切れになる)。設計書の "What stays outside" に書いた。 テスト14本追加(tmp_path の合成プロジェクトのみ・FW/URL/案件語彙なし)。全件 PASS (7748 passed / skip 3 は dogfood コーパス不在の構造的skipで既往)。 Dogfood(実測した当の実プロジェクト・同一ツリー): enabled:true・記録なしで 180件 (not_executed 50・unbound 61)=ステージ無効時と同じ、記録を置くと 89件、記録後に runtime 設定を変えると再び 180件。 Closes #43 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2
CoDD Audit❌ Audit failed to run. Check workflow logs. |
Coverage Matrix ReportProject: /home/runner/work/codd-dev/codd-dev
Totals: 369 axes, 176 covered signals (47.70%), 193 unknown. |
…拠喪失の沈黙 Codex(gpt-5.6-sol/high・別血統)の read-only レビューが挙げた MUST 4件を閉じる。 1. 移行デッドロック(新規に作り込んでいた)。runtime だけで束縛されていた strict プロジェクトは、台帳が無い状態で unbound_acceptance が unbound_severity=red に なり、静的 verify が赤 → Step 8 に到達できない → 台帳が生まれない → 永久に赤。 ⇒ ステージが有効な間、runtime 実行待ちが原因の unbound は runtime_execution_severity(既定 amber)で出す。unbound は not_executed の 結果であって別の欠陥ではない(remedy も「走らせろ」の1つ)。ステージが無効な ときは従来どおり unbound_severity(走らせる経路そのものが無いので別の欠陥)。 2. 失敗実行が証拠になっていた。fail-fast の最初の失敗でも台帳は書かれ、record.passed は読むだけで判定に使っていなかったため、失敗した実行が後続の verify で緑の束縛 として読まれた。⇒ 暗黙義務は passed=True の実行だけが証明する(reason run_failed)。 明示 runtime:<case> は「その名前のチェックが passed」で判定するので、隣で別の チェックが落ちても巻き添えにしない。 3. config_digest が実行計画を表していなかった。①--runtime-base-url で別環境へ向けた 実行が、設定ファイル上の URL を検証した証拠として残っていた ⇒ 実効 target_url を 台帳に記録し、設定の dev_server.url と食い違えば target_changed。②report 出力先の 変更で失効していた ⇒ runtime_smoke.report を digest から除外。③merged 設定を ハッシュしていたため CoDD 更新で全プロジェクトの台帳が一斉失効しうる ⇒ プロジェクト 自身の codd.yaml の生セクションをハッシュする。 4. 台帳の書き込み失敗が沈黙していた ⇒ Step 8 が stderr に WARN を出す。FAIL には しない: 書けなかったのは成果物の喪失であって false green ではなく(次の verify が no_record を正直に報告する)、read-only チェックアウトを JSON 1本で落とすのは不均衡。 あわせて SHOULD も: 未来日付は stale 扱い(max(0) クランプ廃止)・台帳は temp+rename の アトミック書き込み・version 不一致と executed 0件の台帳は「証拠なし」・名前の正規化は 空白と `_-` のみに縮小(`.` `/` を畳むと a.b と ab が同一になる)。 テスト12本追加(合計26本)。全件 7760 passed / skip 3(dogfood コーパス不在の既往)。 MUST1 のテストは直前コミット 5dac942 に対して RED を実測。 Dogfood 再測(同一ツリー・enabled:true・未実行): 合計 180 件で不変、内訳が red 125→34・amber 55→146(unbound 61 = amber 41 + red 20)。宣言では finding は 1件も消えず、赤が「未証明」の琥珀に降りるだけ。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2
同じ検査器(Codex gpt-5.6-sol/high・read-only)の再レビューから、実在する穴だけを閉じる。
- 書き込み失敗時に**古い成功台帳が生き残っていた**(唯一の真の false green)。
昨日の緑+今日の記録不能=昨日の判定が今日の証拠として読まれる。⇒ 書けなかった
ときは古い台帳を削除する。削除も拒否されたら Step 8 を FAIL にしてファイル名を出す。
WARN で済ませるのは「今回の記録が消えただけ」の場合に限る。
- `report.fail_fast` を digest に戻した。最初の失敗の後を走らせるかを決める=実行計画
であって出力先ではない。除外は `report.log_to_file` と `report.file_path` だけ。
空になった report キーは落として、`report:` 無しと同じハッシュにする。
- target_url が空の台帳が、URL を設定したプロジェクトの証拠として通っていた。
CoDD が書く台帳は必ず target を持つので、空=別物として target_changed にする。
- `bool("false")` は True。手書きの `"passed": "false"` が合格として読まれるので、
真偽値は JSON の true だけを真とする(passed/skipped の両方)。
テスト5本追加(合計31本)。全件 7765 passed / skip 3(既往)。
意図的に採らなかった指摘は PR コメントに理由つきで残す(実装内容への束縛=本 PR で
明示的にスコープ外と書いた事項、--runtime-skip の部分実行、台帳の改竄耐性)。
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2
独立レビュー逐語(Codex gpt-5.6-sol / high・read-only・別血統)全文は kagemusha 側 1巡目(commit 5dac942 に対して)結論: 現状はマージ不可。特に移行デッドロックと、失敗・部分実行による false green が致命的です。
本番コードへの特定フレームワーク語彙の混入は見当たりません。pytest は読み取り専用環境で一時ファイルを作れず、収集前に停止したため実行結果は得られていません。 2巡目(MUST 4件の修正差分 5dac942..0881b76 に対して)MUST: (2) は部分実行について未解決です。runner.py:149 と runtime_record.py:335 は skip を成功扱いするため、「DB だけ pass、dev-server/e2e 等は MUST: (1) の severity 差し替えは新しい strict 回避路です。acceptance_evidence.py:260 で execution は strict でも既定 amber、dag/checks/acceptance_evidence.py:285 は no_record だけでなく run_failed/target_not_executed/stale 等すべてを pending に入れ、同:492 が unbound の赤も amber に落とします。結果、未束縛ACへ MUST: (3) の digest はまだ実行計画を表していません。runtime_record.py:48 は MUST: (4) は evidence-bearing 実行では WARN では不十分です。runtime_record.py:289 の置換失敗は既存の成功台帳を残し、cli.py:6193 は警告だけです。「次回は no_record」という文言も既存台帳があれば偽です。具体的には、旧 pass 台帳→今回 runtime が失敗→台帳更新も失敗、の後、plain verify は旧 pass を読み続けます。また新規台帳なし・全チェックskipでも同じ WARN 経路から exit 0 です。runtime obligation がある実行では、台帳を永続化できない/何も実行しなかった場合は FAIL が妥当です。 MUST: PR全体として、runtime 証拠が実装変更で失効しません。runtime_record.py:158 は runtime設定だけをハッシュし、freshness は既定無期限です。成功台帳の後でアプリコードだけ壊しても acceptance_evidence.py:496 は受理します。少なくとも実装/revision digest が必要です。 追加テストには、部分skip、全skipのCLI exit、strict宣言回避、fail_fast変更、target欠落version-1、旧台帳を残したwrite failure、実装変更後の失効が必要です。加えて runtime_record.py:246 の 対象pytestは、read-only環境に利用可能な一時ディレクトリがなく収集前に停止しました。 |
- ゲートは設定 URL を strip して比較するのに、ランナーは YAML の値をそのまま 記録していた。末尾に空白のある `url:` で target_changed の誤検知になるので、 記録側も strip する。 - 古い台帳を消せなかったときの FAIL メッセージが `codd/runtime_ledger.json` を ハードコードしていた。`.codd/` のプロジェクトに嘘のパスを出すので ledger_path() から引く。 全件 7765 passed / skip 3(既往)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2
レビュー処置のまとめ(マージ判断の材料)独立レビュー(Codex gpt-5.6-sol / high・read-only・別血統)を2巡。MUST 9件のうち 8件を修正、残り3件(うち1件は2巡連続の同一根)を理由つきで却下した。逐語は上のコメント。 却下の理由(a) severity 差し替えは strict の抜け道である(2巡連続・同一根) — 事実として正しい。
(b) 実行証拠が実装内容に束縛されていない — 正しい指摘だが、レビュー前に設計書 (c) 2巡目で実際に閉じたもの(すべて本 PR 内で修正済み)
状態
🤖 Generated with Claude Code |
`git show origin/main:<test files> | grep -c '^def test_'` との差分で実測。 7731 → 7765 の増分と一致する。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2
runtime_smoke.enabled: trueは宣言であって証拠ではない。それを証拠として読んでいたので、runtime を1回も走らせずに1行足すだけで finding が半減した。実測(この修正前・sokutei-platform 同一ツリー/要件書無改変)
dev server も DB も起動せず、
codd verify --runtimeを一度も叩かずにruntime_smoke.enabled: trueを追記しただけ:runtime_evidence_not_executableunbound_acceptance証拠を用意するより宣言するほうが安い、という逆インセンティブ。しかも「証拠が無いこと」を捕まえるための検査自身が、主張を証拠として受け取っていた。
原因
codd/acceptance_evidence.py:390-401runtime_smoke_enabled()— 真偽値だけを返し、実行の痕跡を読む経路が存在しない。codd/dag/checks/acceptance_evidence.py:208(旧) — 有効なら即return []。→ runtime 50→0。:145-155(旧) — 有効なら runtime 義務を「束縛済み」に入れる。→ unbound 61→20。codd/runtime_smoke/runner.py:144-163— 実行後に書くのはタイムスタンプ付き Markdown のみ(log_to_fileで無効化可)。検査が読める実行記録がそもそも生産されていなかった。修正
実行された runtime 義務だけが束縛になる。
codd/runtime_record.py(新規)— 実行台帳。ランナーが、有効かつ1つ以上のチェックが実際に走ったときだけ<codd-dir>/runtime_ledger.jsonを書く(全skip・無効では書かない)。recorded_at/passed/ 各チェックのname・category・passed・skipped/config_digest(実行時点のruntime_smoke+runtimeセクションのハッシュ)。欠損・破損は証拠なし、決して証拠ではない(acceptance_ledger.jsonと同じ規律)。commit して diff で読む成果物として扱う。runtime_evidence_not_executed(新規 finding): 有効だがno_record/config_changed/stale/target_not_executed。runtime_evidence_not_executableは無改変で「ステージが無効」のまま。穴が2つ、remedy が2つ、finding も2つ。unbound_acceptanceも復活。未実行の義務は無効の義務と同じだけしか束縛しない(このモジュールが無効側で既に適用していた規律)。runtime_obligationsの既存設計を踏襲。明示verified_by: runtime:<case>は同名の非skipチェックを要求(print_sheet/Print Sheet/print-sheetは1つの名前)。operation_flow.<id>由来の暗黙義務は、対応付けをプロジェクトが宣言していないので推測せず、記録があれば足りる。落ちた実行も「実行済み」(Step 8 が既に赤で報告済み・二重報告しない)。acceptance_evidence.runtime_execution_severity(strict でも既定 amber)とruntime_max_age_hours(既定 unset)。後方互換(★ここが読みどころ)
undeclared_numeric/reachabilityと同じ前例)= CoDD を上げただけでは赤にならない。config_digest)は常に期限切れにする。unbound_acceptanceは復活するが、ステージが有効な間はruntime_execution_severity(既定 amber)で出す。理由は移行デッドロック(独立レビュー MUST):codd verify --runtimeは静的 verify が通ってから Step 8 を走らせるので、unbound が赤いと台帳を作る手段そのものに到達できず、永久に赤になる。unbound は not_executed の結果であって別の欠陥ではない(remedy も「走らせろ」の1つ)。ステージが無効なときは従来どおりunbound_severity(走らせる経路自体が無いので別の欠陥)。verified_by: runtime:<何か>とenabled: trueを足すと、赤い unbound が琥珀に降りる。finding は1件も消えない(名前と remedy つきで両方出る)——main では 91件が消えていたので、そこからの改善であって後退ではない——が、ramp を拒否する strict プロジェクトはacceptance_evidence.runtime_execution_severity: redの1行で両方赤に戻せる。順序で解く筋(Step 8 到達性)は codd verify --runtime: 静的 verify が赤いと Step 8 に到達できない——「走らせろ」が remedy の finding を消せない #45 に分離した。runtime_evidence_not_executableのメッセージ・severity・発火条件は無改変。未解決(設計書に明記)
実行証拠は実装内容には束縛していない。runtime 設定が変わらないまま実装を書き換えても、記録は期限切れにならない。閉じるにはランナーとゲートの双方が計算できる content digest が要るが、ランナーは依存グラフを持たない。「台帳はステージが走ったことを証明するのであって、今日のコードに対して走ったことは証明しない」と
docs/design/acceptance-evidence-invariant.mdの "What stays outside" に書いた。Generality Gate(6問)
runtimeは既存語彙(runtime_smoke/verified_by: runtime:)。テストと検証
tests/dag/test_acceptance_evidence.py11 /tests/test_runtime_smoke.py3)。修正前のコードに対して14本すべて RED を実測してから実装。codd coverage checkPASS(176/369 = 47.70%)。codd dag verify(自己適用): 赤はcanon_integrity1件のみで、clean origin/main でも同一の10件canon_missingが出る既往(stash して実測)。本 PR はdocs/requirements/system-requirements.mdの該当行のみcanon acceptの digest に更新(codd canon acceptは既往の不具合で他10件を落とすため、その1行だけを手で反映)。acceptance_evidenceは amber 4件(reachability_unknown)。既存 AC-ACC-1 と同型で、CLI にルートが無いことに由来する。新設した AC-ACC-2〜4 が同じ型を3件増やす。Dogfood(実機・同一ツリー)
runtime_smoke無し(基準)enabled:true・未実行・origin/main のコードenabled:true・未実行・本 PRenabled:true+ 実行記録あり・本 PRruntime_execution_severity: red。独立レビューの処置(Codex gpt-5.6-sol / high・2巡・別血統)
逐語は下のコメント。MUST 9件のうち 8件を修正、3件(1件は MUST 重複)を理由つきで却下。
run_failed・明示 case は個別 pass 判定)config_digestが実行計画を表していないtarget_url記録/reportの出力先だけ除外/生 YAML をハッシュ)report.fail_fastを digest から外していたtarget_url空の台帳が通っていたtarget_changed)bool("false") == Truetrueだけを真とする)--runtime-skipの部分実行Closes #43
🤖 Generated with Claude Code
https://claude.ai/code/session_011xpq9A9SFvMUky6i6NLFz2