seeded-determinism-and-golden-vectors について
明示的なキーから生成結果を導き、実行やプラットフォームをまたいで再現可能にする開発スキルです。シード生成、生成器のバージョン管理、独立した実装で算出したゴールデンベクターを扱い、CI では再生成せず検証します。
主な機能
- 01時計を参照せずキーを注入
- 02仕様化したハッシュと混合処理でシードを導出
- 03独自の安定した疑似乱数生成器を使用
- 04生成器をバージョン管理し旧動作を保持
- 05ゴールデンベクターテーブルに指紋を固定
- 06ローカルでのみベクターを更新し CI は検証
活用シーン
- 01再現可能な日次コンテンツや選択結果の作成
- 02リリースをまたいだ生成結果の維持
- 03固定ベクターによるシード付きアルゴリズムの検証
- 04意図的な生成器変更のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill seeded-determinism-and-golden-vectorsインストール後、Claude Code で/seeded-determinism-and-golden-vectorsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/golden_vectors_test.dartseeded_generator.dartreferences/golden-vectors.mdseed-derivation.mdscripts/check-determinism-bans.sh詳細
シードに基づく決定性とゴールデンベクター
一部の出力はキーの純粋関数でなければなりません。ユーザー同士が比較する毎日のピック、手続き的に生成されるレイアウト、ほかの人が検証できる共有可能な結果などです。サーバーによる調整なしに、あらゆるデバイス、あらゆるタイムゾーン、来年リリースするプラットフォームビルドでも、同一の結果が得られる必要があります。これは「テストに合格する」よりも強い性質であり、それを維持するには独自の規律と、凍結されたオラクルが必要です。
testing-strategy は、純粋なロジックをどのようにテストするか(独立したオラクルと注入した Clock を使ったシード付きファズテスト)を担当します。このスキルは、そもそも出力を再現可能にするための要件と、それを固定するコミット済みのベクターテーブルを担当します。
ゴールデンベクターはゴールデン画像ではありません。 ベクターは純粋なジェネレーターのデータ指紋で、ウィジェットバインディングなしの dart test で検証します。UI をレンダリングした参照 PNG は widget-golden-and-a11y-testing の担当であり、それらを再ベースライン化する作業は run-goldens-rebaseline で行う別の儀式です。両者に共通するのは「ゴールデン」という言葉と、ゲートは検証対象を決して再生成してはならないというルールだけです。
絶対に守るルール
-
キーは注入し、読み取らないでください。 ジェネレーターには、日付文字列、ID、連番などのキーを引数として渡します。そこから到達可能なものが
DateTime.now()を呼び出してはなりません。実時間は、コンポジションルートで注入したClockを通じて、アプリに一度だけ取り込みます(このルールはvalue-objects-money-and-unitsが担当します)。理由:時計を読み取るジェネレーターはテストもゴールデン化もできず、バグレポートから出力を再現することもできません。 -
カレンダー日のキーは民事日付であり、時点ではありません。 「7月4日」は日付のみの値です。ISO 形式の
YYYY-MM-DD文字列や通算日数のように、日付として保存し、渡してください。DateTimeの時点として扱ってはいけません。イベントのタイムスタンプは引き続き UTC の時点ですが、日の識別子はそもそも時点ではありません。理由:時点を使うと参照のたびにタイムゾーンに依存するため、どのコンテンツが「今日のもの」かについてデバイス間で食い違いが生じます。 -
日付の定義は一つにし、比較する箇所すべてで使ってください。 コンテンツの導出に UTC 日を使いながら、連続記録、期限、「すでに見た」の確認に現地の午前0時を使うと、日付境界付近では毎日数時間にわたって両者が食い違います。ローカル時間か UTC かを意図的に選び、一度だけ名前を付け、すべての比較をその定義に通してください。
-
エントロピー源は一つだけです。導出したシードを使ってください。 生成経路のどこにも、環境依存の
Random()/Random.secure()を使ってはいけません。「無害」なタイブレークやシャッフルであっても同様です。乱数の生成はすべて、受け渡したシード付きジェネレーターに対する明示的な呼び出しでなければなりません。また、乱数を生成する順序は契約の一部です。2回の生成順序を入れ替えると、それ以降の出力がすべて変わります。 -
仕様で定めたハッシュとミックス処理を使ってシードを導出してください。 キー文字列をハッシュ化し(FNV-1a-64 は小さく、仕様が明確で、再実装も容易です)、機能ごとおよびモードごとのソルトを XOR で加えた後、結果をアバランチ/ミックス処理(SplitMix64)に通してから乱数を生成してください。理由:連続するキーは1バイトしか違わず、ミックス処理がないと、その相関によって連続する日の出力が目に見えて似てしまいます。これはユーザーが最初に気づくバグです。
references/seed-derivation.mdを参照してください。 -
PRNG は自前で用意し、仕様の定まっていないものに依存しないでください。
dart:mathのRandom(seed)は特定のランタイム内では再現可能ですが、そのアルゴリズムは実装の詳細であり、SDK バージョンやプラットフォームをまたぐ安定性は保証されていません。別のユーザーと比較する出力を、その保証に頼らせてはいけません。SplitMix64 は8行で書けます。自分で実装し、公表済みのベクターでテストし、凍結してください。 -
機能ごと、モードごとにソルトを使ってください。 同じ日付からシードを得る2つの機能が同じ乱数列を生成すると、出力に目に見える相関が生じます。機能ごとにソルト定数を一つ定め、リリース後は永久に固定してください。
-
派生コンテンツは再生成し、保存しないでください。 保存するのはキー、generatorVersion、そして結果(ユーザーが行ったこと)です。コンテンツ自体は
key → generator → contentであり、いつでも再構築できます。理由:保存すると信頼できる情報源が二重になり、履歴を移行できなくなります。派生した集計値も読み取り時に再計算します。このルールはpersistence-driftが管理します。 -
リリース済みのジェネレーターは凍結します。 ユーザーがあるキーの出力を目にした時点で、その出力がそのキーの意味になります。改善は、切り替えキーを設定した新しいバージョンとしてリリースし、それ以前のすべてを再現できるよう、古いバージョンもバイナリに残します。リリース済みのジェネレーターをその場で編集してはいけません。過去の内容をひそかに書き換え、保存済みの結果がすべて、もはや存在しないコンテンツを参照することになります。
-
ゴールデンベクターで契約を固定します。
(key, params) → fingerprintの行をまとめたコミット済みのテーブルと、UI に表示される派生メトリクスがあれば、それらも正確に検証します。成果物全体ではなく、フィンガープリント(正規シリアライズの安定したハッシュ)を保存します。これによりテーブルを小さく保て、実際の変更をレビュー担当者が確認できる1行の差分にできます。整数とフィンガープリントには==を使い、許容誤差を使うのは意図的に調整された浮動小数点値に限ります。 -
期待値はすべて独立したオラクルから算出します。 テスト対象の実装で生成したフィクスチャは、その実装のバグを固定してしまいます。これから導入しようとしているバグも例外ではありません。ベクターの行は、別個のよりシンプルで低速な実装か、手作業で導き出してください。(ファズループにおける独立したオラクルの原則は
testing-strategyが管理し、これはその凍結テーブル版です。) -
再生成は明示的に行い、レビューを受け、自動化しません。 ベクターを更新できるのは、ローカルで意図的に実行するコマンドだけです。その差分はプルリクエストで人が確認します。CI は検証しますが、正当化はしません。期待値を自動再生成するパイプラインは何も検査しないゲートです。CI で
--update-goldensを実行するのと同じ失敗です。 -
測定してからラベル付けし、証明として売り込まないでください。 生成された成果物に派生ラベル(難易度、品質ティア、サイズクラスなど)が付く場合、メトリクスを算出し、その値に対する凍結済みの閾値テーブルでラベルを決めます。対応関係が単調であることと、閾値が変更されていないことを検証してください。キャリブレーションを証明済みと説明してはならず、リリース済みのコンテンツに再度ラベルを付けてはいけません。
シードの導出
/// FNV-1a-64 over the key's UTF-8 bytes. Small, specified, trivially portable.
int fnv1a64(String key) {
var hash = 0xcbf29ce484222325;
for (final byte in utf8.encode(key)) {
hash = (hash ^ byte) * 0x100000001b3; // 64-bit wrap-around is intended
}
return hash;
}
/// The one entropy source. SplitMix64 — eight lines, published test vectors,
/// and yours forever, unlike an SDK-internal generator.
class SeededRng {
SeededRng(this._state);
int _state;
int nextInt64() {
_state += 0x9e3779b97f4a7c15;
var z = _state;
z = (z ^ (z >>> 30)) * 0xbf58476d1ce4e5b9;
z = (z ^ (z >>> 27)) * 0x94d049bb133111eb;
return z ^ (z >>> 31);
}
/// Uniform in [0, max). Draw order is part of the contract — never reorder.
int nextInt(int max) => nextInt64().toUnsigned(63) % max;
}
/// The only place the calendar meets a generator. `isoDay` is a civil date
/// ("2026-07-04") supplied by the caller — the generator never reads a clock.
SeededRng seedFor(String isoDay, {required int featureSalt, int modeSalt = 0}) =>
SeededRng(fnv1a64(isoDay) ^ featureSalt ^ modeSalt);
⚠️ アプリをウェブ向けにもビルドする場合、64ビット整数演算はこのようには動作しません。Dart の整数は、ウェブでは JavaScript の倍精度浮動小数点数にコンパイルされます。このコードをネイティブターゲットに限定するか、ハッシュと PRNG を32ビット単位で実装してください。references/seed-derivation.md を参照してください。
ゴールデンベクターテーブル
// One row per pinned key. Computed ONCE from an independent oracle (rule 11) and
// refreshed only by `dart run tool/update_vectors.dart` (rule 12).
const dailyPickVectors = <DailyPickVector>[
DailyPickVector('2026-01-01', 0x9f3ac1e0, 12, 'year boundary'),
DailyPickVector('2026-07-04', 0x2b7708d4, 3, 'mid-year'),
DailyPickVector('2026-12-31', 0x84ce11a9, 7, 'year end'),
];
test('the generator reproduces every frozen vector', () {
for (final v in dailyPickVectors) {
final pick = generateDailyPick(v.isoDay);
expect(pick.fingerprint(), equals(v.fingerprint), reason: v.note);
expect(pick.items.length, equals(v.itemCount), reason: v.note);
}
});
バグが潜む境界を網羅してください。サポートする最初と最後のキー、うるう日、月または年の繰り上がり、そして少なくとも1つの過去のキーを含めます。これにより、過去を書き換えるリグレッションがあれば、すぐに失敗します。
アンチパターン
- ジェネレーターやシードヘルパー内での
DateTime.now()— 出力の再現性が失われ、バグ報告から再生成できなくなります。 - 日付の識別に
DateTimeの時点を使用すること — 読み取りのたびにタイムゾーンに左右されます。 - UTC の日付からコンテンツを生成しながら、境界チェックでは現地時間の午前 0 時を使うこと — 1 日のうち数時間にわたって日付が 1 日ずれ、自分のタイムゾーンからは最も見つけにくい種類のバグになります。
- 「無害な」タイブレークに暗黙の
Random()を使うこと — 1 回呼び出すだけで、契約全体が崩れます。 - リファクタリング中に乱数の取得順を入れ替えること — 入れ替えた後の出力がすべて変わります。取得順は契約です。
- ミックス処理をせず、生のハッシュから直接シードを作ること — 連続する日付の出力が目に見えて似てしまいます。
- 2 つの機能で同じソルトを再利用すること — それぞれの出力に相関が生じます。
- ユーザーに見えるものに対して、
Random(seed)、identityHashCode、またはハッシュセットの反復順に依存すること — 安定性は文書化されて保証されていません。乱数を取得する前に、明示的なキーでソートしてください。 - 生成済みの成果物を保存すること —
(key, generatorVersion, outcome)を永続化し、再生成してください。 - リリース済みのジェネレーターをそのまま編集すること — 過去のキーが意味していた内容を書き換えてしまいます。バージョン管理された切り替えをリリースしてください。
- テスト対象の実装でベクターを生成すること — 実装のバグまで定着させてしまいます。
- ベクターテーブルに成果物全体を保存すること — 差分のレビューが困難になります。フィンガープリントを保存してください。
- CI でベクターを自動再生成すること — そうするとゲートが何も検証しなくなります。
- 調整済みのラベルを「証明済み」と呼ぶこと — メトリクスは測定値であり、ラベルは固定されたしきい値です。
完了の定義
- ジェネレーターはキーを引数として受け取り、その処理経路ではクロックも暗黙の乱数も読み取りません。
- 日付キーは暦日であり、日付に関わるすべての比較で同じ日付の定義を使用します。
- シードは
hash(key) ^ featureSalt ^ modeSaltをミックス処理に通し、その後、このリポジトリが所有する PRNG に渡します。 - ソルトは変更されない定数であり、機能/モードごとに 1 つずつあります。
- コンテンツは
(key, generatorVersion)から再生成され、永続化するのはキー、バージョン、結果だけです。 - すべての結果に
generatorVersionを保存します。ジェネレーターの変更は切り替えを伴う新しいバージョンとしてリリースし、以前のバージョンでも過去の内容を再現できる状態を保ちます。 - コミット済みのベクターテーブルには、フィンガープリントとユーザーに表示される派生メトリクスを固定し、範囲の境界、うるう日、過去のキーを網羅します。
- 期待値はすべて、テスト対象の実装ではなく独立したオラクルから取得されています。
- 再生成はレビュー対象となるローカルコマンドで行い、CI は検証のみを実行します。
- 決定性テストで、同じキーからの 2 回の実行がバイト単位で一致することをアサートします。これにより、暗黙の乱数が再導入されるとビルドが失敗します。
-
scripts/check-determinism-bans.shがジェネレーターのパッケージ/ディレクトリに対して成功します。 - 派生ラベルはすべて、単調性テストを備えた固定のしきい値テーブルに基づきます。リリース済みのコンテンツにラベルを付け直すことはありません。
関連スキル
- シード付きファズループ、独立オラクルの原則、
Clockの注入についてはtesting-strategyを参照してください。このスキルのテストも同じ形式です。 - 正規の保存方法と、注入する
Clockの境界についてはvalue-objects-money-and-unitsを参照してください。 - キー/バージョン/結果の保存と、派生集計値の再計算については
persistence-driftを参照してください。 - これらの禁止事項を機械的に適用できるよう、ジェネレーターを Flutter に依存しないパッケージに置く方法については
project-structure-and-packagesを参照してください。 - レンダリングされた UI の参照用画像については
widget-golden-and-a11y-testingを、再承認する手順についてはrun-goldens-rebaselineを参照してください。これらは、このベクターとは異なる層です。 - ベクターチェックと禁止事項の grep をゲートとして実行する方法については
ci-pipeline-and-gatesを参照してください。 - ジェネレーターが返す不変の値型については
dart3-idioms-and-coding-standardsを参照してください。
参考資料
- Dart API —
dart:mathRandom(シード値を指定した構築):https://api.dart.dev/stable/dart-math/Random-class.html - Dart — Web での数値(
intは JS の double、>>>、オーバーフロー):https://dart.dev/guides/language/numbers - Dart API —
identityHashCode:https://api.dart.dev/stable/dart-core/identityHashCode.html - FNV ハッシュ仕様:https://datatracker.ietf.org/doc/html/draft-eastlake-fnv
- SplitMix64(Steele、Lea、Flood — 「高速な分割可能疑似乱数生成器」):https://doi.org/10.1145/2660193.2660195
package:test:https://pub.dev/packages/test