<!-- 公開用の匿名化コピー。原証拠は evidence/ 配下（非公開）。変換規則は evidence_public/CONVERSION_RECORD.md を参照。内容の実行結果は変えていない。 -->

# 記事B 最小実験 実行記録（改訂版 rev1）

- 実行日時: 2026-09-18 23:36 JST
- 実行環境（実測）: macOS 26.6.2 / Python 3.9.6 (/usr/bin/python3) / SQLite 3.51.0
- 注意: Python 3.9 は 2025-10-31 に公式サポートが終了している（devguide.python.org/versions、2026-09-18確認）。ここでは既存環境で追加インストールなしに実行した事実を記録するもので、公開教材で推奨する環境ではない。サポート中の版（3.12/3.13 など）での実行は本記録では未確認。
- 実行場所: examples/sample-b/（外部接続なし、メモリ上の新規SQLiteのみ。本番・共有・外部DBへの接続なし）
- 原証拠（初版 2026-09-18 22:20 JST）は evidence/sample-b/run_log.md にそのまま保存。本記録はそれを上書きしない。
- v1 の失敗は教材のために意図的に入れた不備によるもの。実際のAI出力を観測した結果ではない。
- 期待結果: v1 原版8件は T2〜T7 の6件が失敗、追加4件は T9・T12 が失敗し T11 はスキップ。v2 は12件すべて成功。変異4種はすべて検知。

### 1. v1（説明用の失敗例）× 原版8件（検査強化後）

```text
$ python3 test_approval.py approval_v1 --suite original
=== 検証対象: approval_v1 / スイート: original ===
test_T1_normal_approval (__main__.OriginalScenarios)
割当承認者が提出済みの依頼を承認 → 承認済み。履歴に承認・鈴木・固定時刻が1件（REQ-003, 005） ... ok
test_T2_not_assigned_approver (__main__.OriginalScenarios)
割当外の承認者（高橋）が承認 → 拒否。依頼・履歴の全行が不変（REQ-003） ... FAIL
test_T3_self_approval (__main__.OriginalScenarios)
依頼者=承認者の異常データを本人が承認 → 拒否。全行不変（REQ-003） ... FAIL
test_T4_wrong_state_draft (__main__.OriginalScenarios)
下書きの依頼を割当承認者が承認 → 拒否。全行不変（REQ-003） ... FAIL
test_T5_duplicate_approval (__main__.OriginalScenarios)
同じ承認が2回届く → 2回目は拒否。1回目の後から何も変わらない。承認履歴は1件（REQ-006） ... FAIL
test_T6_partial_failure_rolls_back (__main__.OriginalScenarios)
状態更新の直後に失敗 → 状態と履歴の両方が元どおり（REQ-007） ... FAIL
test_T7_send_back_requires_reason (__main__.OriginalScenarios)
理由なしの差戻し → 拒否。全行不変（REQ-004） ... FAIL
test_T8_send_back_with_reason (__main__.OriginalScenarios)
理由ありの差戻し → 差戻し。履歴に差戻し・鈴木・固定時刻・理由が1件（REQ-004, 005） ... ok

======================================================================
FAIL: test_T2_not_assigned_approver (__main__.OriginalScenarios)
割当外の承認者（高橋）が承認 → 拒否。依頼・履歴の全行が不変（REQ-003）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 62, in test_T2_not_assigned_approver
    self.assertFalse(r.ok)
AssertionError: True is not false

======================================================================
FAIL: test_T3_self_approval (__main__.OriginalScenarios)
依頼者=承認者の異常データを本人が承認 → 拒否。全行不変（REQ-003）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 70, in test_T3_self_approval
    self.assertFalse(r.ok)
AssertionError: True is not false

======================================================================
FAIL: test_T4_wrong_state_draft (__main__.OriginalScenarios)
下書きの依頼を割当承認者が承認 → 拒否。全行不変（REQ-003）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 78, in test_T4_wrong_state_draft
    self.assertFalse(r.ok)
AssertionError: True is not false

======================================================================
FAIL: test_T5_duplicate_approval (__main__.OriginalScenarios)
同じ承認が2回届く → 2回目は拒否。1回目の後から何も変わらない。承認履歴は1件（REQ-006）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 88, in test_T5_duplicate_approval
    self.assertFalse(r2.ok)
AssertionError: True is not false

======================================================================
FAIL: test_T6_partial_failure_rolls_back (__main__.OriginalScenarios)
状態更新の直後に失敗 → 状態と履歴の両方が元どおり（REQ-007）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 104, in test_T6_partial_failure_rolls_back
    self.assertUnchanged(before, "途中失敗なのに状態変更または履歴が残っている")
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 40, in assertUnchanged
    self.assertEqual(full_snapshot(self.conn), before, msg)
AssertionError: Tuples differ: ([('R[48 chars]室', '承認済み', 'u-suzuki'), ('REQ-A02', 'u-sato',[582 chars]ne)]) != ([('R[48 chars]室', '提出済み', 'u-suzuki'), ('REQ-A02', 'u-sato',[582 chars]ne)])

First differing element 0:
[('RE[47 chars]室', '承認済み', 'u-suzuki'), ('REQ-A02', 'u-sato',[235 chars]in')]
[('RE[47 chars]室', '提出済み', 'u-suzuki'), ('REQ-A02', 'u-sato',[235 chars]in')]

Diff is 932 characters long. Set self.maxDiff to None to see it. : 途中失敗なのに状態変更または履歴が残っている

======================================================================
FAIL: test_T7_send_back_requires_reason (__main__.OriginalScenarios)
理由なしの差戻し → 拒否。全行不変（REQ-004）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 111, in test_T7_send_back_requires_reason
    self.assertFalse(r.ok)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 8 tests in 0.004s

FAILED (failures=6)
結果: 実行 8 件 / 失敗 6 / エラー 0 / スキップ 0
終了コード: 1
```

### 2. v1 × 追加4件

```text
$ python3 test_approval.py approval_v1 --suite additional
=== 検証対象: approval_v1 / スイート: additional ===
test_T10_foreign_key_enforced (__main__.AdditionalChecks)
存在しない依頼IDの履歴を直接追加 → 保存先が拒否する（宣言した関係を接続が守っている） ... ok
test_T11_sql_state_guard (__main__.AdditionalChecks)
処理側の状態確認を通り抜けた場合でも、UPDATE の状態条件と更新件数の検査で止まる（REQ-003/006 の二重の守り）。 ... skipped 'この版には処理側の確認関数がない'
test_T12_autocommit_connection_refused (__main__.AdditionalChecks)
自動確定モードの接続 → 変更前に拒否。全行不変（取消を保証できない設定では処理しない） ... FAIL
test_T9_assignee_without_approver_role (__main__.AdditionalChecks)
承認者でない運用管理者が割当先になっている異常データ → 拒否。全行不変（REQ-003：役割と割当は別条件） ... FAIL

======================================================================
FAIL: test_T12_autocommit_connection_refused (__main__.AdditionalChecks)
自動確定モードの接続 → 変更前に拒否。全行不変（取消を保証できない設定では処理しない）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 171, in test_T12_autocommit_connection_refused
    self.assertFalse(r.ok)
AssertionError: True is not false

======================================================================
FAIL: test_T9_assignee_without_approver_role (__main__.AdditionalChecks)
承認者でない運用管理者が割当先になっている異常データ → 拒否。全行不変（REQ-003：役割と割当は別条件）
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<PROJECT_ROOT>/examples/sample-b/test_approval.py", line 134, in test_T9_assignee_without_approver_role
    self.assertFalse(r.ok)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 4 tests in 0.003s

FAILED (failures=2, skipped=1)
結果: 実行 4 件 / 失敗 2 / エラー 0 / スキップ 1
終了コード: 1
```

### 3. v2（修正後）× 原版8件（検査強化後）

```text
$ python3 test_approval.py approval_v2 --suite original
=== 検証対象: approval_v2 / スイート: original ===
test_T1_normal_approval (__main__.OriginalScenarios)
割当承認者が提出済みの依頼を承認 → 承認済み。履歴に承認・鈴木・固定時刻が1件（REQ-003, 005） ... ok
test_T2_not_assigned_approver (__main__.OriginalScenarios)
割当外の承認者（高橋）が承認 → 拒否。依頼・履歴の全行が不変（REQ-003） ... ok
test_T3_self_approval (__main__.OriginalScenarios)
依頼者=承認者の異常データを本人が承認 → 拒否。全行不変（REQ-003） ... ok
test_T4_wrong_state_draft (__main__.OriginalScenarios)
下書きの依頼を割当承認者が承認 → 拒否。全行不変（REQ-003） ... ok
test_T5_duplicate_approval (__main__.OriginalScenarios)
同じ承認が2回届く → 2回目は拒否。1回目の後から何も変わらない。承認履歴は1件（REQ-006） ... ok
test_T6_partial_failure_rolls_back (__main__.OriginalScenarios)
状態更新の直後に失敗 → 状態と履歴の両方が元どおり（REQ-007） ... ok
test_T7_send_back_requires_reason (__main__.OriginalScenarios)
理由なしの差戻し → 拒否。全行不変（REQ-004） ... ok
test_T8_send_back_with_reason (__main__.OriginalScenarios)
理由ありの差戻し → 差戻し。履歴に差戻し・鈴木・固定時刻・理由が1件（REQ-004, 005） ... ok

----------------------------------------------------------------------
Ran 8 tests in 0.003s

OK
結果: 実行 8 件 / 失敗 0 / エラー 0 / スキップ 0
終了コード: 0
```

### 4. v2 × 追加4件

```text
$ python3 test_approval.py approval_v2 --suite additional
=== 検証対象: approval_v2 / スイート: additional ===
test_T10_foreign_key_enforced (__main__.AdditionalChecks)
存在しない依頼IDの履歴を直接追加 → 保存先が拒否する（宣言した関係を接続が守っている） ... ok
test_T11_sql_state_guard (__main__.AdditionalChecks)
処理側の状態確認を通り抜けた場合でも、UPDATE の状態条件と更新件数の検査で止まる（REQ-003/006 の二重の守り）。 ... ok
test_T12_autocommit_connection_refused (__main__.AdditionalChecks)
自動確定モードの接続 → 変更前に拒否。全行不変（取消を保証できない設定では処理しない） ... ok
test_T9_assignee_without_approver_role (__main__.AdditionalChecks)
承認者でない運用管理者が割当先になっている異常データ → 拒否。全行不変（REQ-003：役割と割当は別条件） ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.002s

OK
結果: 実行 4 件 / 失敗 0 / エラー 0 / スキップ 0
終了コード: 0
```

### 5. 変異確認（故意の誤変更4種を一時コピーで検知できるか）

```text
$ python3 mutation_check.py
変異版は一時フォルダ <TMP> に作り、終了時に削除する

| 変異（故意の誤変更） | 原版8件（強化後） | 追加4件 | 検知したテスト | 判定 |
|---|---|---|---|---|
| sql_guard_removed | 実行 8 件 / 失敗 0 / エラー 0 / スキップ 0 | 実行 4 件 / 失敗 1 / エラー 0 / スキップ 0 | test_T11_sql_state_guard | 検知 |
| rejection_deletes_history | 実行 8 件 / 失敗 1 / エラー 0 / スキップ 0 | 実行 4 件 / 失敗 0 / エラー 0 / スキップ 0 | test_T2_not_assigned_approver | 検知 |
| empty_reason_adds_history | 実行 8 件 / 失敗 1 / エラー 0 / スキップ 0 | 実行 4 件 / 失敗 0 / エラー 0 / スキップ 0 | test_T7_send_back_requires_reason | 検知 |
| sendback_wrong_actor | 実行 8 件 / 失敗 1 / エラー 0 / スキップ 0 | 実行 4 件 / 失敗 0 / エラー 0 / スキップ 0 | test_T8_send_back_with_reason | 検知 |

すべて検知
終了コード: 0
```

