検査の台本を 1 本足したら、目録が三か所にあった。一つの契約を一か所に置くまで
Adding one test script revealed the catalog lived in three places: putting one contract in one place
D29-3-T57-single-source-of-truth・技術・日本語入力でプレイする仕組み
新しく足した検査の台本は、毎回の回帰テストでちゃんと回っていました。
それなのに、台本の棚卸しは「どの検査にも載っていない台本がある」と赤を出し続け、その赤を誰も見ていませんでした。
台本の目録が、三つのスクリプトに別々に書いてあったからです。
この回の位置と、前後の回#
区切り: T50〜T57 失敗の名前(8 回のうち 8 回目)
先に読むとよい回: T37(台本の中に期待を書く)・T38(今の出力を写して固定する)
次に読む回: T58(通った台詞だけを採る)
名前 — single source of truth#
- 正式名
- single source of truth(略 SSOT。単一の正・信頼できる唯一の情報源)
- 初出
- 確かめられませんでした(情報システムの設計の語、と説明する解説はあるが、著者・年は見つからない)
- 正式名
- Don't Repeat Yourself(略 DRY。繰り返すな)
- 初出
- Andy Hunt, Dave Thomas, "The Pragmatic Programmer", Addison-Wesley, 1999
- 正式名
- shotgun surgery(散弾銃の手術)
- 出てくる場所
- Martin Fowler, "Refactoring: Improving the Design of Existing Code", Addison-Wesley, 1999(Kent Beck ほか寄稿)の、悪臭(bad smells)の目録
- 語を作った人
- 確かめられませんでした
三つの名前の一般的な意味#
single source of truth(単一の正・信頼できる唯一の情報源)とは、あるデータを 1 か所でだけ保存・編集し、ほかの場所はそこを指す、という情報システムの設計の考え方です。
DRY は、コードの字面の重複の話だと思われがちです。
元の定義はこうです。
「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.」
重複を禁じている対象は「知識」です。
Thomas 自身が、データベースのスキーマ、テスト計画、ビルドの仕組み、文書まで含む、と説明しています。
守れていると、ある要素を変えたときに、論理的に関係のない要素を変えなくて済みます。
破れている状態は、冗談まじりに WET(write everything twice)と呼ばれます。
逆向きの注意もよく言われます。
字面が同じでも、別の知識がたまたま同じ形をしているだけなら、まとめると後で困る。
見るべきは「この二か所は、いつも一緒に変わるか」です。
SSOT は、同じことをデータの側から言った語です。
あるデータは 1 か所でだけ保存・編集し、ほかの場所はそこを指す。
食い違ったら、その 1 か所が勝つ。
全部を一つの道具に入れる、という意味ではありません。
データの種類ごとに、信頼できる置き場が 1 つずつ決まっていればよい、という説明が多いです。
shotgun surgery は、違反したときに見える症状の名前です。
一つの変更のために、あちこちのクラスやファイルを少しずつ直す羽目になる。
直す場所が散っているので、どれかを忘れます。
教科書の処方は、散った責務を一か所へ寄せることです(Move Method・Move Field のような手で)。
三つを一列に並べると、こうなります。
DRY は原則、SSOT はその原則が守られた状態、shotgun surgery は破れたときの痛み。
うちで起きたこと(2026-08-11〜08-17) — single source of truth#
霧走町では、検査を「台本」で書きます。
プレイヤーの入力を 1 行ずつ並べたテキストを CLI の adv-sim に流し、最後まで解けるか、取り違えが無いかを見ます(T37・T38)。
2026-08-17 まで、台本は Scripts/ の中に adv-*.txt という名前で置いていました。
作品の区別は、ファイル名の接頭辞だけです。
そして「どの台本を、どの検査が、どの合格ラインで回すか」が、三か所に書いてありました。
一つ目、毎回の回帰テスト adv-loop.ps1 の中の、台本ごとの 1 行。
PlayCheck 'adv-ch1-verdict' 'adv-ch1-verdict.txt' @('--max-leak',$MaxLeak,'--require-clear','--max-miss-run',0)
二つ目、補助の検査 adv-verify-aux.ps1 の中の、棚卸しの分類表。
三つ目、第一章の検査 adv-verify-ch1.ps1 が持つ一覧。
分類表はこういう形でした(当時の commit から抜粋)。
$Classified = @{
# adv-loop.ps1 が回す
'adv-ch1-verdict' = 'loop'; 'adv-ch1-route-b' = 'loop'; ...
# adv-verify-ch1.ps1 が回す
'adv-demo-playthrough' = 'verify-ch1'; ...
# このスクリプトが回す
'adv-rescue-check' = 'aux'; ...
'adv-shot-bg' = 'not-a-test: 背景6枚を順に出す撮影用(ff37b4a で追加。登録漏れ)。...'
}
棚卸しは、Scripts/ にある adv-*.txt のうち、この表に無いものを数えて、1 本でもあれば FAIL にします。
新しく置いた台本が、誰にも回されずに溜まるのを止める歯止めです。
表の最後の行の注記に「登録漏れ」とあるとおり、漏れは前にも一度起きていました。
2026-08-11、第一章の岬蓮まわりの台本 adv-misaki-grounds.txt を足しました。
adv-loop.ps1 には 1 行足したので、毎回の回帰テストでは回っています。
分類表には足していません。
棚卸しは正しく FAIL を出しました。
ところが、その FAIL は 2 日間、誰にも見られませんでした。
棚卸しは補助の検査の中にあり、補助の検査を束ねて回すのは全体の入口 adv-gate.ps1 だけです。
08-11 から 08-13 まで、作業は adv-loop.ps1 を単独で回して緑を見ていました。
08-13 の記録(Docs/Reports/2026/08/13/06)は、原因を「08/11 以降、誰も adv-gate.ps1 を通していない」と書いています。
台本を 1 本足すという一つの変更に、三か所の手当てが要る。
これが shotgun surgery の形そのものです。
そして、知っていることが一つ(この台本は誰が回すか)なのに、置き場が三つある。DRY の違反です。
直したこと(2026-08-17・4cf9a0cb)#
規約を一行足しました。
「Scripts/ に作品名を書かない。作品に属するものは Game/adv-<work>/ の中だけ。区別はフォルダでつける。ファイル名の接頭辞では区別しない。」
台本 32 本を Scripts/adv-<work>-.txt から Game/adv-<work>/runs/.txt へ移し、目録を runs/suite.json として作品のフォルダに置きました。
汎用のランナー(adv-loop.ps1・adv-verify-work.ps1・adv-gate.ps1・adv-route-map.mjs)からは作品名と台本名の直書きを外し、-Work <dir> で作品を受け取るようにしました。
いまの suite.json の 1 件はこうです。
{
"id": "verdict",
"route": { "key": "A", "label": "通しA — 章末の推理決定まで", "expect": "clear" },
"cmds": "runs/verdict.txt",
"runBy": "loop",
"why": "受け入れ条件1(05 §1)通しA — 標準ルート。決め手=血痕+時刻",
"gates": ["--require-clear", "--max-leak", "0", "--max-miss-run", "0"]
}
runBy が「誰が回すか」です。
loop は汎用のランナー、check は作品ごとの検査 checks/*.ps1、selfplay は自然文だけで遊ばせる自己プレイ、ue は UE の実機、not-a-test は判定を持たないと決めたもの(理由を why に書く)。
gates は adv-sim play にそのまま渡す引数で、合格ラインもここに書きます。
ツールの内側に埋めないのは、緩めたら git diff に出るようにするためです。
棚卸しは、こう書き直しました(Scripts/adv-suite.ps1)。
function Get-AdvUnlistedRuns([string]$Work, $Suite) {
$dir = Join-Path $Work 'runs'
if (-not (Test-Path $dir)) { return @() }
$listed = @{}
if ($Suite) {
foreach ($r in $Suite.runs) { $listed[[IO.Path]::GetFileName($r.cmds)] = $true }
}
$out = @()
foreach ($f in Get-ChildItem $dir -Filter '*.txt' -File) {
if (-not $listed.ContainsKey($f.Name)) { $out += $f.Name }
}
return $out
}
いまは、これを毎回の回帰テスト adv-loop.ps1 の中で呼び、inventory という項目で PASS/FAIL を出しています。
単独で回すことの多い入口の中なので、08-11 のように「赤があるのに見ていない」は起きにくくなりました。
suite.json の冒頭の注記に、理由が残っています。
「目録を作品フォルダに置けば、台本を足した人が同じフォルダで目録も直す。」
変更が一つの場所に閉じる、という意味です。
今日(2026-10-29)の Game/adv/runs/ は、台本 46 本に対して suite.json の宣言が 46 件。
内訳は loop 27・check 10・not-a-test 6・ue 2・selfplay 1 です。
runs/suite.json を持つ作品フォルダは 16 あり、宣言の合計は 81 件です。
同じ型の、ほかの場所#
台本の目録だけではありませんでした。
- 目標の手数(2026-08-17)
クリアに要る手数の目標は、企画の検査 adv-plot-check.mjs の引数の既定値 70 にしか無く、作品の設計値なのに呼び出し方でしか決まっていませんでした。
企画のファイルの origin.targetBeats へ移しました(T59)。
制作の手引きはこう書いています。「設計値は必ず入力側のファイルに書く。引数の既定値は設計ではない。」
同じ手引きは、目標の手数・企画から逆算した手数・実測の手数を「3つは別物」とし、一致するかを検査にしています。どれかをファイルに書き写すと、書き写した瞬間に嘘になりうるからです。
- adv-sim の置き場所(2026-10-18・edcfb5b3)
CLI の実行ファイルの場所が、PowerShell と Node のスクリプト 21 本に直書きされていました。
Linux に切り替える分岐がある本と無い本が混ざっていた、と注記にあります。
PowerShell 側は adv-suite.ps1 の対応表(Get-AdvSimBuild)、Node 側は Scripts/lib/adv-sim-exe.mjs に寄せました。変わったファイルは 24 です。
- 規約そのもの(2026-09-05・e98e47dc)
別の開発 AI(Codex)が、エージェント向けの規約の矛盾を監査しました。
例えば、AI 初見プレイのスキルの中で、既定の人数が節によって 2 人と 3 人に割れていた(F06)。
「件数・閾値を書き写さない」と書いた文書が、同じ節に数字の表を写していた(F10)。
対処として CLAUDE.md を 181 行から 49 行にし、原則 6 を置きました。
「一つの契約は一か所で定義する。変えるときは旧文を置き換える(例外を追記しない)。日付つきレポートは記録であって現況ではない。」
テスト計画を一つの文書に決めたのも同じ原則です(T56)。
名前どおりでない所 — single source of truth#
SSOT は、ほぼ名前どおりです。
ただ「全部を一か所」ではなく「契約ごとに一か所」にしています。
台本の目録は作品の suite.json、事実は世界設定の文書、照合の閾値は thresholds.json、合格基準はテスト計画。
置き場の表を規約(CLAUDE.md の「置き場」)に持ち、混ぜない、と書いています。
DRY は、一か所だけ意図して破っています。
adv-sim の置き場所は、PowerShell と Node に 1 つずつ、2 つの定義があります。
Node 側のファイルには「PowerShell 側の正は Scripts/adv-suite.ps1 の Get-AdvSimBuild(対応表)で、ここはその写し」と書いてあります。
言語をまたいで一つの定義を読ませる仕組みを作るより、正と写しを名指しで書く方を選びました。
写しが 2 つになった時点で、また 21 本の形へ戻り始めます。
生成した写しも持っています。
企画から起こした作品データの生出力(baseline/)や、手の台帳(ledger.tsv)です。
これは重複ではなく派生物で、入力から作り直せます。
作り直す道具があることが、写しを持ってよい条件です(T60)。
弱点と、採らなかった案 — single source of truth#
弱点の一つ目。棚卸しが見ているのは「載っているか」までです。
08-13 の記録は「表に足すだけで実際は回していない、では意味がない」と書いていました。
runBy に check と書いた台本を、作品の checks/*.ps1 が本当に回しているかは、この棚卸しでは見ていません。
二つ目。目録が、無いものを指していても気づけません。
今日、Game/adv/runs/suite.json を読むと "ledger": "ledger.tsv" と宣言していますが、Game/adv に ledger.tsv はありません。git の履歴にも一度も現れません。
霧走町は、第一章を手で作ってから企画を起こした作品なので、企画から台帳を起こす工程を通っていないためです(T59)。
作品 1 本の回帰 adv-verify-work.ps1 は、台帳のファイルが無ければ SKIP と出して進みます。
一か所に置いた正が空を指している状態で、SKIP の行を読まなければ分かりません。
採らなかった案は 3 つです。
- Scripts/ に全作品の目録を 1 枚で持つ。前の形に戻るだけで、作品が増えるほど腐ります。
- ファイル名の接頭辞で作品を分ける。フォルダで分ければ、作品を丸ごと移しても検査が付いてきます。
- runs/ の .txt を全部、宣言なしで回す。目録が要らなくなりますが、合格ライン(gates)を置く場所と、「これは検査ではない」と理由つきで言う場所が無くなります。
検索語#
single source of truth
DRY principle Pragmatic Programmer
shotgun surgery code smell
町の人の返事は、変更のたびに台本で確かめ直しています。『霧走町 流氷に眠る』の第一章は、ブラウザで無料で歩けます。プロフィールのリンクから。