zudo-test-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

フェイルクローズなライブ証明と証拠契約

ライブなリリース証明に対する 3 つの別々の判定 -- 実行・証拠の完全性・クリーンアップ -- と、それぞれを支える規律。手組みの終端オブジェクトではなく実際のライフサイクルフィクスチャ、閉じた理由コードを持つバージョン付きでスクラブ安全な証拠契約、シグナルとして扱うタイミングのサンプルと pass-on-retry、独立した always-run のゲートとしてのクリーンアップ。

1 つのライブ証明に 3 つの判定

このガイドのデプロイ済みティアに対するテストは、原則として読み取り専用です。プレビューティアは共有状態に対して読み取り専用、本番は決して変更しない。このページが扱うのは、その原則では覆えない唯一のケース -- 証明したい操作そのものが 書き込み である場合です。パブリッシングプロダクトのリリースウォークスルーは、サイトを割り当て、実際のビルドを走らせ、非公開プレビューを公開し、それを昇格させ、公開バイトを配信し、ソースと突き合わせなければなりません。読み取り専用の手段では、それが動くことを証明できません。そこでこの証明は、資格情報付きのゲートとしてプレビューティア -- ティアの表が読み取り専用と定める、デプロイ済みで本番同等のステージング環境 -- に対して走ります。最初の書き込みの前に他のあらゆる環境を拒否する mutation fence の内側で、自分で作って自分で片付ける使い捨ての generation の上で、です。本番は立ち入り禁止のままであり、フェンスが本番に届くことはありません。このページが緩めるのはプレビューティアの読み取り専用の行であり、それも意図的に、以下の義務を引き受けることでのみ緩めます。

このような証明は、ライブな変更の長い連なりとそれに続くクリーンアップであり、1 回の実行結果には独立した 3 つの部分があります。これらを 1 つの成否ビットに潰すと、プロダクトの失敗と壊れた計器を区別する能力を失います。

軸問い何がそれを証明するか何に依存してはならないか
実行(execution)プロダクト操作は成功したかライフサイクルが終端状態に到達したこと。さもなくば閉じた failure kind証拠やクリーンアップがたまたま動いたかどうか
証拠(evidence)保存したものは完全で信頼できるかスクラブ済みの証拠が、名前付きの問題なしにバージョン付き契約としてパースできることプロダクトが成功したかどうか
復旧(recovery)クリーンアップは役目を果たしたかalways-run の teardown ステップが自前の判定を報告すること。プロセス内 teardown との一致は証拠の軸で検査される実行が成功・失敗・キャンセルのいずれだったか

実例:あるライブ実行はビルドに失敗し、すべてを片付け、根本原因の完全な痕跡を保存しました -- そして証拠準備ジョブは「証拠が不完全」と報告しました。preparer は契約違反にもライフサイクルの失敗にも同じ <label>-unproven コードを push し、全チェックの和集合から 1 つの boolean を導いていたので、プロダクト操作の失敗と証拠ファイルの欠落が同じメッセージになっていたのです。それとは別に、redact された teardown のセンチネル 1 つ(後述)が、成功していた recovery を complete: false に変えました。バンドルを読んで「クリーンアップは動いたか?」に答えるには、手作業で調査をやり直すしかありませんでした。

修正はジョブを緑にすることではありません。どの 軸が失敗してもジョブは赤になります -- 実行の失敗なら証明ステップが、名前付きの証拠の問題や不完全な recovery なら preparer が。変わるのは、アップロードされる証拠が軸を名指しすることです。

terminalState: failed                      # the proof step's verdict -- red on execution failure
proofOutcome:
  failureKind: timeout                     # closed enum, projected from the failure code
  unconditionalRecovery:
    attempted: true
    complete: true                         # from the always-run recovery file; disagreement with the proof is a named issue
validation:
  issues: []                               # the scrubbed intermediates parsed clean

クリーンアップの証拠が完全な失敗ビルドは、証拠が存在しない実行と区別できなければなりません。このページの残りは、各軸がどうやって判定を獲得するかです。

ここでの「フェイルクローズ」の意味

「未実行」「skip」「読めない」「不完全」を「成功」と区別できない結果は、証明ではありません。 不在と曖昧さは赤として読みます。このガイドには既に、別の名前でいくつもの実例があります -- pass-by-skip、握りつぶされた待機がサイレントなグリーンになること、デプロイ後スモークテストの answered ラッチと「失敗しえないチェックは走っていないチェックと区別がつかない」というルール、そして終了コードではなく件数をアサートすること。ライブ証明はそこに自前の実例を加えます。未知または曖昧な子プロセスの終了は platform loss であり、決して quota の判定にも成功にもなりません。欠落した、あるいはパースできない証拠ファイルは名前付きの unproven 問題です。資格情報が未プロビジョニングならゲートは skip ではなく失敗します。キャンセルされた実行は「失敗しなかった」ではなく「走らなかった」です。

フェイルクローズが拘束するのは 必須 の証拠ソース -- 契約が名前を挙げているもの -- だけです。あらゆる observer を必須の集合に取り込んでよい、という許可ではありません。冗長な observer のテレメトリも依然として読みますし、それがエラーを報告したときの内容には依然として拘束力があります。決して許されないのは、それが観測対象である実際の結果より上位に立つことです。ブラウザが既に届けた終端イベントよりも先に診断コールバックが着くことを要求すれば、動いているプロダクトが赤い実行に変わります。これは、このあと観測側の時間と課金された時間のあいだに引く区別と同じです -- 1 つの実行を測った 2 つの値が別々の問いに答えており、プロダクトのものはそのうち 1 つだけです。これのテストレベルでの形がクロスコンテキストの終端証拠です。

実際のライフサイクルを通す。終端オブジェクトを手組みしない

実例:あるリリースウォークスルーは ready なビルド、ready なプレビュー、ready なデプロイメント、バイト単位で正しい公開出力に到達し、そのあと厳密なパリティゲートで失敗しました。このゲートはアーティファクトのオーナー向け射影(projection)をビルドリクエストと比較します。その中には不変の値が 1 つあります -- その generation を獲得したリクエストのバージョン、つまり どの リクエストが ready なバイトを生成したかの唯一の証明です。この値はワーカーのビルド claim、すなわち lease のサブレコード上に置かれていました。ready 遷移の仕事は claim をクリアして output を書くことであり、その output にこのフィールドは含まれていませんでした。成功したアーティファクトはすべて null を射影し、ゲートは完全に成功した実行を 1 つの潰れたコードで拒否し、根本原因は、すでに片付けられた行に対する手作業の読み取り専用クエリで証明されました。

ユニットテストは 2 層とも、別々の理由で緑でした。ゲートのテストは、比較器に手組みの「ready」観測 -- すでにそのフィールドを持ち、期待値がハードコードされたもの -- を与えていました。実際のステートマシンが ready 後に決して出さない状態です。これはパイプラインではなく比較器をテストしていました。アダプターのライフサイクルテストは実際の claim → start → ready 遷移を 確かに 通していましたが、status・attempt・worker id しか(toMatchObject で)アサートしておらず、フィールドは通過はしても観測されませんでした。一方の層は間違った対象を見ており、もう一方はフィールドを見ていなかったのです。

手組みの終端 DTO は、作者が入っていると期待したものをそのまま持ちます。 実際の終端遷移がフィールドを落としたことを示すことはできません。フィクスチャはその遷移を通らないからです。実際のステートマシンを走らせ、その実際の射影を取り、それを実際のゲートに渡してください。リテラルは、実際の正例ベースラインから導いた負例のために取っておきます。このバグの背後にある構造的ルールはこうです。来歴(provenance)は、終端遷移より長生きするレコードに保存する。 成功遷移がサブレコード(claim、lease、lock)をクリアするなら、その上の全フィールドを列挙し、null にする前にどれを引き継ぐかを明示的に決めてください。魅力的に見える誤った修正 -- 欠けた値を読み取り時にレコードの可変バージョンカウンターから埋め戻す -- は、どのリクエストも持ったことのない数値を刻印していたでしょう。これは自己一致のライブシステム版です。期待する答えを符号化したフィクスチャは、その答えと食い違えません。

期待値は呼び出し直前ではなく作成時に固定する

最初の修正は、それ自体がトートロジーな回帰テストを伴って出荷されました。テスト対象の遷移の 直前 に、実装がまさにコピーしようとしていたのと同じ可変バージョンフィールドからレコードの期待する authority を取得していたのです -- そのためこのアサーションは、実装と一致することしかできませんでした。誤った挙動(start/renew がすでに進めた楽観的並行制御のバージョンを刻印し直す)に対して緑であり、正しい挙動に対しては赤になっていたでしょう。統合レビューがこれを捕まえました。lease 期限切れ後の replacement claim は、ずっと authority を上書きしていたのです。

// tautological: the expectation is read from the same mutable field the code is about to copy
const expected = artifact.version;          // already advanced by start/renew
artifact = reclaim(artifact);
expect(artifact.claim.authority).toBe(expected);   // green for the bug, red for the fix

// anchored: mint the expectation once at the origin event, assert it after every transition
const ensured = ensure(request);            // idempotent create-or-lookup of the artifact record
const authority = ensured.version;          // captured before the first claim
claimed = claim(ensured);                   expect(claimed.claim.authority).toBe(authority);
started = start(claimed);                   expect(started.claim.authority).toBe(authority);
clock.advance(halfLease); renewed = renew(started);  expect(renewed.claim.authority).toBe(authority);
clock.advance(halfLease); renewed = renew(renewed);  expect(renewed.claim.authority).toBe(authority);
ready = markReady(renewed);                 expect(ready.output.authority).toBe(authority);
expect(toOwnerProjection(ready).authority).toBe(authority);

あるフィールドが変更の主題なら、そのレコードに触れるすべてのアサーションでそのフィールドを名指ししてください。すでに信頼していたフィールドだけの toMatchObject こそが、ライフサイクルの層が緑のままだった理由です。

ライフサイクルフィクスチャが網羅すべきもの

最終的に欠落を捕まえた回帰テストは短いものです。ensure(アーティファクトレコードの冪等な create-or-lookup)→ claim → start → renew を時計を進めながら 2 回 → ready → オーナー射影を読む → それを実際のゲート評価器に渡して空の理由リストを期待する。区別が宿るのは、その背後にある適合性マトリクスです。

  • 同一 generation の継続か、新しい generation の開始か。 lease 期限切れ後の replacement claim は、同じリクエストを新しいワーカーが担うものです。直前の claim の authority を 保持 します。retry・rearm・collect してからの rehydrate は新しい generation です。claim と output の両方をクリアし、次の claim が新しい authority を発行するようにし、再ビルドされた generation の authority は collect されたものと異なっていなければなりません。

  • 終端コマンドの冪等なリプレイは、最初のレスポンスと同じ保存済み authority を返します。

  • レガシーレコードは不在を明示的な null にデコードし、null は下流で独立した「missing」コードとしてフェイルクローズします -- 決して成功にはならず、mismatch と同じコードにもなりません。

  • 不正な値(ゼロ、負数、小数、安全な整数範囲超え、文字列、boolean、オブジェクト)は あらゆる 読み取り経路で corruption エラーを上げます。保存された生の行を改変して読み戻すことでテストします。

  • 射影の優先順位は明示的です。保存済みの終端 output が最初、次にアクティブな claim、可変バージョンはレコードがまだ queued の場合のみ、それ以外は null。不変の authority を可変バージョンフィールドから埋め戻してはいけません。

手書きのフィクスチャが安全なのは、それが模倣する契約を自分が所有しているときだけです -- 定期再試験がレポーターのフィクスチャに引く境界と同じものです。他人の遷移が生み出す終端状態は、あなたが所有する契約ではありません。

証拠はバージョン付きの契約であり、スクラブ後に検証する

証拠の 保持 -- 記録したトレースは、アップロードしなければ証拠にならない、failure() || cancelled() でアップロードする -- は別のページで扱っています。このセクションは証拠の 形 についてです。ライブ証明はスクラバーを通して証拠を保持するので、スクラバーと検証器はそれぞれ単体では正しくても、その合成が壊れていることがあるからです。

実例:証明はスクラブ済みの中間ファイルを書き、always-run の recovery ステップが 2 つ目を書き、資格情報を持たない preparer が両方を読み直して検証し、バンドル全体をもう一度スクラブして、そのバンドルだけをアップロードします。後のハードニングコミットがスクラバーのキー名ルール -- 部分文字列マッチ -- を広げ、source や marker といった語にも当たるようにしました。teardown ブロックは保持(retention)のセンチネルを、名前に source を含むキーの下に記録していました。拡張後、そのキーの値はディスク上で redaction トークンに置き換えられ、検証器はそれが固定のセンチネルに等しいことを依然として要求し、バンドルは成功していた recovery に対して retention-unproven と complete: false を報告しました。スクラバーのユニットテスト(シークレットを redact するか?)も検証器のユニットテスト(正しい証明を受理するか?)もどちらも緑でした。検証器のフィクスチャは本番のスクラバーではなく素の JSON.stringify で書かれており、スクラブ済みの証明を検証器に与える唯一のテストは、赤になるのが前提の経路に置かれ、特定の 2 つのコードが ない ことしかアサートしていなかった -- 3 つ目には気づけません。

Warning

自明に見える 2 つの修正は、どちらも間違いです。 スクラバーにパス例外を足すことは、実行より長生きするアーティファクトから資格情報を締め出す唯一のガードを弱めます。検証器にリテラルの redaction トークンを代替値として受理させることは、その値が 証拠そのものでない ときにだけ健全です -- ここでは値(保持された/作られなかった)こそが 証拠であり、その例外は証拠を破壊していたでしょう。

スクラブ前に、スクラブ安全なサブ契約へ射影する

スクラバーのキー名パターンを証拠契約の一部として扱ってください。それに当たりえないキー名を選び、パターンが広がるたびに保持する全キーを点検し直します。そのうえで、内部の teardown 状態を、汎用スクラバーが走る 前に 専用のバージョン付きサブ契約へ射影し、スクラバーには手を触れません。

// applied to the internal teardown result before scrubbing; copies only closed-enum fields
teardown: {
  preview: "revoked" | "absent" | "unproven",
  site:    "tombstoned" | "absent" | "unproven",
  retention: {
    contract: "<product>/live-proof-retention-evidence",
    version: 1,
    frozenBundleRetention: "retained-under-policy" | "not-created-or-unconfirmed",
    artifactRetention:     "retained-under-policy" | "not-created-or-unconfirmed",
  },
}
// any input outside these enums -> { preview: "unproven", site: "unproven", retention: null }
// extra keys on the input are dropped, never copied

検証器はそれから厳密にパースします。正確なキー集合、証明ファイルと recovery ファイルの 両方 での contract と version の一致、enum への所属、そして逸脱があれば null と名前付きの <label>-retention-contract-unproven 問題。安全なキーの下にある敵対的な文字列は契約違反であって、許容される mismatch ではありません。次に相互検証します。各センチネルを独立した実行状態と突き合わせ(ソースは本当に公開されたか? アーティファクトは作られたか? 実行の識別子は状態ファイルと一致するか?)、証明と recovery を突き合わせる -- 両者は同じ version と同じセンチネルを持たなければならず、不一致は証拠の軸における名前付きの proof-recovery-retention-mismatch 問題であって、recovery の判定を黙って格下げすることは決してありません。同じ規律がバンドル全体に及びます。boolean のチェックオブジェクトは正確なキー集合で検証し(スクラブ語を含むチェックのキーは例外扱いではなく改名されました)、タイミングと件数はドメイン上限付きの bounded safe-integer フィルタを通し、phase 名は短い anchored regex で制限します。

scrub → parse → scrub がバイト単位で同一であることを証明する。そしてその理由

scrub(raw) -> parse -> scrub  ==  scrub(raw)

理由は上のパイプラインにあります。preparer は、埋め込まれた中間ファイルを検証した あとで バンドル全体をもう一度スクラブします。冪等でないルールは、検証済みの内容を検証後に書き換えてしまいます。この性質は、検証器のフィクスチャを本番のスクラバーで書き出すのと同じテストの中で、文字列の等価性で証明してください -- 通る べき経路で、無関係な理由で拒否される経路だけではなく -- そして、いくつかのコードが無いことではなく、結果全体(コマンドが resolve すること、問題リストの全体)をアサートします。汎用の redaction ルールを変更したら、マージ前に persist → re-read → validate を緑のフィクスチャでエンドツーエンドに走らせ直してください。

閉じた、漏れない理由コード

上で失敗したパリティゲートは、8 つの述語の論理和に 1 つのコードを返していました。if (a || b || c ...) throw new Error("preview-not-ready")。boolean の OR は構造的に情報を失い、最初のグループで throw すれば後のグループは評価すらされません。根本原因に手作業のクエリが必要だったのはそのためです。

  • すべての述語を純粋関数で評価し、集合全体を返す。 ソートと重複排除をし、エクスポートされた閉じた enum から引きます。ランナーは結合したコードで 1 回だけ throw します。観測した値は決してコードに現れません -- id、ハッシュ、パス、オリジン、DTO の断片はなし。

  • 「missing」と「mismatch」は別のコード。 !== はレガシーの null と誤った値を混同します。ドメインは両者を別に扱うので、enum もそうでなければなりません。

  • enum をテーブルでテストする:[理由, 改変する入力パス, 敵対的な値, 連動する理由?]。各行は評価器がちょうどその集合を返すことをアサートします。2 つの述語が 1 つの入力を読むなら、独立を装わず、その行で連動する集合を宣言します。行をまたいで和集合を集め、エクスポートされた enum と等しいことをアサートします -- 出力しえないメンバーがあればスイートは落ちます。敵対的な文字列を注入し、シリアライズされた理由にも throw されたメッセージにも現れないことをアサートします。リファクタリングが報告だけを変えたと証明できるよう、strict-success テストを残します。

  • 外部ツールの出力は phase コードに還元する。 作者が書いた stderr、サードパーティ CLI のメッセージ、パス、タイトル、ソーステキスト、識別子、ダイジェスト、URL、アカウント情報は一時的なジョブログに留めます。失敗コードと理由は、閉じた信頼済み集合のメンバーであるときにだけ証拠へ渡り、未知のものはコピーされず捨てられます。自由記述の出力を読む分類器は、分類対象そのものに欺かれえます -- 実際に、あらゆる stabilizer のエラーメッセージがたまたま「resource limit、retryable」と分類されるトークンを含んでいたことがありました。

タイミングのサンプルはテールの証明ではない

デフレイキングレシピのルール 3はすでに、サンプルではなく elapsed < budget をアサートせよと言っており、根本原因カタログは競合するデッドラインや kill 期限を超えて合算されるステージ予算を扱っています。ライブ証明はそれより難しい版を加えます。デッドラインはプロダクトのものであり、それを数回通過しても、テールについては何も証明しません。

実例:新しい隔離コンテナ内のホステッドビルドが、ハードな wall-clock 上限を超え続けていました。唯一のシグナルは、6 つの逐次 phase を包む外側の timeout ラッパーからの phase を知らない終了コードでした -- そのため最初の対処はホットスポットでない phase から作業を動かし、次の実行は同じように失敗しました。phase の帰属ができるようになったあと、新しい 2 回の診断実行は上限のおよそ 55〜65% で完了し、issue は「再現しない」として閉じられました。次の新しいフルライフサイクル実行は、まさにその phase で、上限を約 15% 超えて 期限切れになりました。原因は隠れたコールド作業でした。新しいコンテナのたびに CSS エンジンがネイティブアドオンを温めるための使い捨てのフルコンパイルを走らせ(新しい temp ディレクトリには warm マーカーが存在しません)、それから本物のコンパイルを走らせていた -- 変動する負荷の下での 2 回のコンパイルという、温まったローカルやコンテナのタイミングには決して現れない広がりです。構造的な修正はこのウォームアップを取り除き、操作回数で証明され、新しい 3 回の実行を通過しました。後日の新しい実行は、1 回の有界リトライのあと、同じ phase で再び期限切れになりました。残った 1 回のコンパイルでさえ、新しいコンテナではサンプルが上限ではなく可能性しか記述できない程度の分散を持っているのです。

この連なりの中には、さらに 2 つの証拠の喪失が隠れていました。成功した診断実行はコンテナ内に phase ごとのタイミングを 確かに 持っていましたが、成功経路の exact-key 射影 -- 密輸防止のガード -- が新しいフィールドを黙って落とし、オペレーターはホットスポットを推測するのではなく立ち止まりました。その後レビューが、失敗経路も DTO を手で組み直して同じく落としていることを見つけました。証拠フィールドを追加したら、それが残るべき場所に残ることを示す正のテストを追加してください。 そしてオーケストレーション側(「observer」)の時間は、デッドライン内で課金された時間より 3 分の 1 から半分ほど長くかかっていました。この 2 つは別の問いに答えます。プロダクトの上限と比較できるのは課金された合計だけであり、observer 側のオーバーヘッドは証明自身のタイムアウトに対して数えます -- 両者を混同すると、プロダクトにはないマージンを報告するか、遅い dispatch をプロダクトのせいにするかのどちらかになります。

サンプルの代わりになるもの

  1. デッドラインを phase に帰属させる。 課金される連なり全体にわたる単一の単調な絶対デッドライン(phase は残り予算だけを受け取り、クリーンアップの猶予は別に制限されて決して作業時間に数えない)と、失敗 enum の中の閉じた phase ごとの理由 -- wall-time-<phase>。これを 信頼できる ものにします。特権を持つスーパーバイザーだけが phase・タイミング・理由を主張し、作者側の子プロセスには書けないチャネルで伝えます。子の stdout・stderr・終了コードは数えて捨てます。封筒(envelope)をフィールド横断で検証し -- 理由は phase と一致、phase は終了ステータスと一致、phase ごとの所要時間の和は合計と一致、終端 phase 以降はゼロ -- 封筒が欠けている、または矛盾していれば platform loss として扱い、決して quota の判定にはしません。

  2. 最適化には、取り除いた操作を示させる。 決定論的な操作回数の回帰テスト(ウォームアップ/ビルドの組がコンパイル 2 回ではなく 1 回になる)を、ユニット形式 かつ 実際の本番イメージに対して、イメージ側が干渉できないハーネスで。ローカルのタイミングはこの種の作業を積極的に隠します。

  3. 実行を計測したイメージを固定する。 実行可能な挙動が変わるたびにランタイムの authority -- すべての実行がハンドシェイクで照合するイメージやランタイムのバージョン -- をローテーションし、古いものが拒否されることを証明します。あるイメージで測ったタイミングが別のイメージの手柄にならないように。

  4. 新しい、隔離された、リトライなしの実行でゲートする -- 少なくとも 3 回、まさにその候補イメージ上で、それぞれ上限に対してマージンのある目標値以下で -- そしてリリース後もそのゲートを維持します。分散は残るからです。

リトライは 1 回だけ、証拠に基づき、最初の試行を残す

pass-on-retry は成功ではなくトリアージのシグナルです -- ルール 2とリトライ予算がすでにそう言っています。ライブ証明に固有なのは、リトライをどう ゲート するかです。ちょうど 1 回、失敗した試行自身の証拠が閉じた許可リストにある信頼済みの retryable な理由を示し、かつ クリーンアップが証明されている -- 証明がその実行のためにデプロイした一時的な facade が消え、プレビューが revoke され、サイトが tombstone されている -- ときにだけ許可します。汎用の「terminal failure」コードは retryability を持たないので、リトライを起動してはいけません。リトライの前に、最初の試行のチェックポイントに対して recovery ステップを走らせ、その証拠を独立したファイルへ書き、2 回目の試行のものと同じように検証されるようバンドルに持ち込みます -- 最初の試行の証明を上書きするリトライは失敗理由をジョブログにしか残さず、2 番目の generation しかチェックポイントしないリトライは、最初の generation のクリーンアップをプロセス内の証拠だけで証明されたままにします。受け入れのサンプルは定義上リトライなしです。判定レベルのリトライと実行途中のセッション再発行は禁止し、証拠の中で 検証 します(verdictRetries: 0、reissued: false)。記録されない許容は、静かに広がりうる許容だからです。

クリーンアップは独立したゲート

always-run の形 -- シェルスクリプトの trap cleanup EXIT、アップロードの if: failure() || cancelled() -- は文書化済みです。ライブ証明はそれを層にして必要とし、さらにクリーンアップに 報告 させる必要があります。

teardown の 3 層

  1. プロセス内で、catch の中と、さらに finally の中で。 あらゆるライブな変更の前に、変更の識別子をアトミックに(temp ファイル+rename で)チェックポイントし、別の recover コマンドが識別子だけから teardown を再生できるようにします。recovery が必要とするのは、スクラブ済みの証拠が保持を許されているものだけでなければなりません -- 最初の teardown 経路はスクラバーが除去するハッシュに依存していたため、保持された証拠から始めた recovery は完了できませんでした。pending センチネルは変更が起きておらず何も片付けないことを意味します。teardown は observe-before-mutate かつ冪等です。まず終端状態を読み、まだ終端でないものだけを変更し、正確に良性のエラーコード 1 つだけを許容し、読み直し、テストでは 2 回走らせても unpublish と tombstone がちょうど 1 回ずつであることをアサートします。

  2. always-run の CI ステップでもう一度。 その出力ファイルは別に検証され、retention と実行の識別子についてプロセス内の teardown と 一致 しなければなりません。キャンセルされた実行は不明であって、成功ではありません。

  3. teardown が観測した以上のことを決して主張しない閉じた enum で報告する -- プレビューには revoked | absent | unproven、サイトには tombstoned | absent | unproven、正当に残りうる残存物には retained-under-policy | not-created-or-unconfirmed。証明が完全に所有するリソース(デプロイした一時的な facade など)は消えたとアサートできます。それ以外の論理的な teardown は削除ではありません -- 何が残り、どのポリシーの下にあるかを述べます。

- id: live-proof
  run: node scripts/live-proof.mjs --evidence out/proof.json
- if: always() && steps.live-proof.outcome != 'skipped'
  run: node scripts/live-proof.mjs recover --evidence out/recovery.json   # idempotent
- if: always()
  run: node scripts/prepare-evidence.mjs out/   # credential-free; validates, re-scrubs, bundles; non-zero on any named issue
- if: always()
  uses: actions/upload-artifact@v4
  with:
    path: out/evidence-bundle.json            # the doubly-scrubbed bundle only
    if-no-files-found: error                  # an absent bundle is itself red

使い捨ての状態は片付け、共有の状態は復元する

「クリーンアップ」の下には、異なる 2 つの義務が隠れています。証明が作った使い捨てのリソースは、上記のとおり 論理的に 片付け、残存物を勘定します。証明が一時的に向け替える共有の表面 -- ルートのターゲット、デプロイ済みのランタイムバージョン -- は別の契約です。正確な事前状態をスナップショットし、切り替え前にそれをアサートし、あらゆる結果で復元し、読み戻し、その後センチネルホストに対して hidden(not-found)応答を probe します。前者を後者の語彙で記述してはいけません。「removed」も「restored」も主張であり、証拠は teardown が獲得していない主張をしてはなりません。

hidden-surface の probe -- 公開アセットが not-found を返す、プレビューが 古い cookie で not-found を返す -- は、記録された注釈ではなく、成功を主張する証明を検証するのに必須の入力です。このラウンドの最後の変更は、緑の証明に public-hidden probe を必須にすることでした。それまでは、公開面が消えたことを証明せずに証明が検証を通りえたのです。どの経路がそれを走らせるかについても同じく正直でいてください。このウォークスルーでは HTTP の読み戻し probe はプロセス内の成功経路で走り、always-run の recovery はコントロールプレーンの観測で teardown を証明していました。「recovery: complete」が起きていない probe を暗示するのに任せるのではなく、証拠の中でそう言ってください。

不在は赤

欠落した、あるいはパースできない recovery ファイルは名前付きの unproven 問題であって、skip されたチェックではありません。バンドルがなければバンドルのアップロードは失敗します。資格情報が未プロビジョニングならゲートは失敗します。そしてゲートが行うあらゆる比較には負のコントロールを持たせるべきです -- 新しいアサーションが壊れたデプロイに対して失敗することを証明することのライブシステム版です。

Tip

計器が識別できることを、その言うことを信じる前に証明してください。 パリティゲートは実行のたびに候補を摂動させ、比較が失敗することを要求します。ランタイムのハンドシェイクは、固定された各 authority を 1 つずつ古い値に改変して拒否を要求することでテストします -- 受理されれば、それ自体が stale-authority-accepted という名の失敗です。失敗しえないゲートは、走っていないゲートと区別がつきません。

ルールをまとめると

  1. 3 つの判定、1 ビットではなく。 実行・証拠の完全性・復旧は独立に計算され、保存される証拠の中で名指しされます。ゲートは赤のままでよいが、ファイルはどの軸が失敗したかを語ります。

  2. 実際の遷移を通す。終端オブジェクトを手組みしない。 期待する来歴はそれを作るイベントで固定し、あらゆる遷移のあとでアサートし、サブレコードをクリアする遷移より長生きするレコードに保存します。

  3. 証拠は、検証器がスクラブの あとに 読むバージョン付きの契約。 スクラブ前に衝突しないキー名の閉じた enum のサブ契約へ射影し、フィクスチャは本番のスクラバーで通る経路に書き、scrub → parse → scrub がバイト単位で同一であることを証明します。

  4. 理由コードは閉じたソート済みの集合で、「missing」は「mismatch」と別であり、enum 網羅性と敵対的な値の不在が証明されています。外部の出力は phase コードに還元します。

  5. サンプルはテールの証明ではない。 信頼できる封筒でデッドラインを phase に帰属させ、取り除いた操作を実際のイメージ上で回数によって証明し、実行を計測したイメージを固定し、マージンのある新しいリトライなしの実行でゲートします。

  6. リトライは 1 回だけ、証拠に基づき、クリーンアップが証明されたときに、最初の試行を残して。

  7. クリーンアップは 3 層で、独立した判定を持つ。 残存物は観測した以上を決して主張しない enum で勘定し、証明が向け替える共有の表面はスナップショットを取り、あらゆる結果で復元して読み戻し、hidden-surface の probe は緑の証明に必須の入力です。

  8. 不在は赤。 そしてあらゆる比較は負のコントロールを伴います。

次に読むべきもの

Revision History

作成更新