3件処理したのに宇宙のお祭りの報告書が違う
目標
宇宙のお祭りのキャンセル作業の承認・監査・現在の行を観測し、顧客への説明と機械の判定が同じ根拠を持つ報告書を作ります。読み取り専用の収集と、既存のファイルを保全する発行をつなぎます。
なぜ重要なのか
同じ件数、通過したハッシュ、長い報告の文は、正しい対象と現在の状態の証拠ではありません。先に、Pythonのdict・list・例外・ファイル、SQLのトランザクション、前の承認バージョン・チャンク再開のレッスンを学習してください。想定所要時間は130分なので、期限が切れる前に+時間で延長してください。最大180分以内に終える必要があり、セッションが終わるとファイルが消えます。必要なコードは別に保管してください。
環境と共通契約
成果物は/root/evidence/worker.pyです。PostgreSQL 16・psycopg 3.2.3・Python 3がイメージに用意されています。追加のインストール・ネットワーク・権限なしで、postgresユーザーとして/rootに書き込みます。
採点ツールは、ローカルのlabdbの固有の一時スキーマに、次のテーブルと仮想のデータを作り、自分が作ったスキーマだけを片付けます。渡されたconのsearch_pathとDSNを使い、publicや運用DBは変更しません。テーブル・列の名前は下の固定の契約で、SQLのデータの値はパラメータで渡します。顧客・ID・観測時刻・スキーマをハードコードしないでください。
CREATE TABLE orders(id integer PRIMARY KEY,tenant text NOT NULL,
qty integer NOT NULL CHECK(qty BETWEEN 1 AND 1000),
state text NOT NULL CHECK(state IN ('pending','paid','cancelled')),
revision integer NOT NULL CHECK(revision>=0));
CREATE TABLE changes(change_id text PRIMARY KEY,tenant text NOT NULL,targets jsonb NOT NULL);
CREATE TABLE audit(change_id text REFERENCES changes(change_id),id integer NOT NULL,
previous_revision integer NOT NULL,new_revision integer NOT NULL,qty integer NOT NULL,
PRIMARY KEY(change_id,id));
change_id・tenantは、厳密なstrで、ASCIIの英数字・アンダースコア・ハイフンの1–64文字です。idは厳密なint 1–2147483647、qtyは厳密なint 1–1000、承認のrevisionは厳密なint 0–2147483646です。現在・監査のrevision・previous_revision・new_revisionは、厳密なint 0–2147483647です。boolは整数として許可しません。
targetsは、id・revision・qtyの3つのキーだけを持つ厳密なdictの、厳密なlistで、1–64個です。auditはid・previous_revision・new_revision・qty、currentはid・tenant・qty・state・revisionのキーを持つ厳密なdictのリストで、それぞれ0–64個です。各リストの中のIDの重複は禁止です。currentのstateはpending・paid・cancelledのどれかで、承認の範囲外のIDは禁止です。auditの承認の範囲外のIDは、形式のエラーではなく、分析する異常です。リストは、正規化するときにID順のディープコピーを作り、入力を変更しません。
借りたconはautocommit=True・Read Committedで、呼び出しの開始時にIDLEです。captureは、これを閉じず、成功・失敗のあとに、元の分離レベル・読み取り専用の設定・ロックと文の制限を復元して、IDLEに戻ります。別の外側のトランザクションで包まないでください。publishだけが接続を所有します。faultがあるときは、指定した文字列を引数にして呼び、元のエラーを隠しません。
証拠と分類の形式
evidenceは、format・change_id・tenant・targets・audit・current・snapshot・observed_atの8つのキーだけを持つ厳密なdictです。formatは、boolではないint 1です。snapshotは、PostgreSQLのpg_current_snapshot()のテキストで、4096文字以下の、10進数:10進数:任意のカンマ区切りの10進数のリスト、という形です。observed_atは、UTCのISO文字列で、最後がZであり、Pythonのdatetime.isoformat()をUTCで出力した形と同じです。例: 2026-09-13T01:02:03.123456Z。DBのtransaction_timestamp()を変換したもので、公的に認証された時刻という意味ではありません。
captureのtargetsは、planで正規化します。auditには該当するchange_idのものだけを、currentには承認IDに該当する現在の行だけを入れます。現在の行が別の顧客に変わっていても、その違いを見るには、該当するIDを観測する必要があります。もとの監査が壊れていても、直したり削除したりしません。ほかの変更の監査や、承認の範囲外の注文を混ぜません。
analyzeのすべてのID listは、昇順です。
- approved: 元の承認のID全体。
- committed: 承認のIDで、監査のprevious_revision=承認のrevision、new_revision=承認のrevision+1、qty=承認のqtyがすべて合っているID。
- invalid_audit: 承認の範囲外のID、または上の値が1つでも違う監査のID。committedには入れません。
- remaining: approvedからcommittedを引いたID。再実行できるという意味ではありません。
- matching: committedのうち、現在の行の顧客・qty・cancelled・revision=承認のrevision+1がすべて合っているID。
- missing: committedのうち、現在の行がないID。
- drifted: committedのうち、行はあるがmatchingではないID。
- decision: invalid_audit・missing・driftedが1つでもあればhold、そうでなくてremainingがあればincomplete、それ以外はcompleteです。これは観測時点の判定です。
顧客への説明とバンドルの契約
describeは、normalize・analyzeの結果だけから、次の順序の平文を返します。各行の終わりはLFで、最後の行のあとにもLFが1つあります。角括弧の位置には、実際の値を入れます。ID群は、空白なしのカンマ区切りで、空のリストの出力は、韓国語で「なし」を意味する語です。
변경: [change_id] / 고객: [tenant]
관측: [observed_at] / 스냅샷: [snapshot]
판정: [hold=보류, incomplete=미완료, complete=관측 시점 완료]
승인 ID: [approved]
확정 ID: [committed]
미확정 ID: [remaining]
현재 일치 ID: [matching]
후속 차이 ID: [drifted]
누락 ID: [missing]
감사 불일치 ID: [invalid_audit]
범위: 일관된 과거 관측이며 현재 상태·출처 인증을 보장하지 않습니다.
このコードブロックの韓国語の各行は、順に、変更と顧客、観測時刻とスナップショット、判定、承認ID、確定ID、未確定ID、現在一致ID、後続差異ID、欠落ID、監査不一致IDを示す見出しで、判定の括弧の中の韓国語は、保留・未完了・観測時点で完了という意味であり、最後の行の韓国語の文は「一貫した過去の観測であり、現在の状態や出所の認証は保証しません」という意味です。
バンドルは、payloadとsha256の2つのキーです。payloadは、format=1、scope=historical-snapshot-not-current-state-or-authenticity、正規化したevidence、分析のreport、平文のsummaryの5つのキーです。sha256は、payloadの正規バイト列に対する、小文字の64桁の16進数です。検証関数は、evidenceからpayloadを再計算して、ハッシュだけでなく、データ・分類・説明・範囲をすべて比べます。形式のバージョンのTrueは、1として認めません。別の観測時刻で一貫して新しく作ったバンドルが通りうるという事実が、この方式の、真偽の認証としての限界です。
ファイル出力の契約
出力のpathは、厳密なstrの絶対パスで、ユーザーが所有する既存のディレクトリの中の、通常のファイルの位置です。存在しないファイルは許可しますが、シンボリックリンク・ディレクトリ・相対パスはValueErrorです。親ディレクトリは自動で作りません。採点は、自分が作った一時の出力フォルダーを引数として渡すので、出力のパスもハードコードしないでください。受講生が自分で試すなら、/root/evidence/report.jsonのように、自分のフォルダーを使ってください。
送り先と同じフォルダーの固有の一時ファイルに、完成したバイト列を書き、flush・fsync・closeのあとで置き換えます。通常の例外では、一時ファイルを片付け、既存または新しい完成版を保全します。強制終了・電源障害や、別のプロセスが親のパスを入れ替える攻撃まで扱う、ファイルシステムのセキュリティの境界ではありません。信頼できる、自分が所有するフォルダーでだけ使ってください。
ステップ
- 承認の一覧の意味を固定します。Exceptionを継承するConflictと、plan(change_id,tenant,targets)を実装してください。共通の入力の契約を検証し、change_id・tenant・targetsだけを持つ新しいdictを返します。targetsはID昇順のディープコピーで、入力は変更しません。誤った入力はValueErrorです。
- 異なる時点が混ざらないように収集します。capture(con,change_id,fault=None)は、IDを検証してから、自分のトランザクションで、REPEATABLE READ READ ONLYと、ロック500ms・文2000msの上限を適用します。changesの該当する承認、該当するchange_idの監査、承認IDに該当する現在のordersを、順に収集します。各参照のあとに、after-plan・after-audit・after-currentのフックを呼びます。同じ観測のpg_current_snapshot()の表記と、transaction_timestamp()のUTCの文字列を入れて、下のevidenceの形式で返します。存在しない承認や、保存された承認の形式の破損は、Conflictです。
- 証拠の形式と重複を検査します。normalize(evidence)は、下の正確なキー・型・範囲・観測の表記を検証し、targets・audit・currentをID順に並べ替えたディープコピーを返します。重複したID、承認の範囲外のcurrent、未知の状態は、ValueErrorです。承認の範囲外のauditは、削除せず、あとで分析できるように保全します。
- 過去の確定と現在の違いを分けます。analyze(evidence)は、normalizeのあとで、下の分類の契約に従って、approved・committed・remaining・matching・drifted・missing・invalid_auditの昇順のID listと、decisionを返します。入力は変更しません。監査の件数を数えるだけにしたり、現在の値を自動で復元したりしません。
- 顧客への説明と機械の結果を、同じ根拠から作ります。canonical(value)は、UTF-8・ensure_ascii=False・sort_keys=True・separators=(カンマ,コロン)・allow_nan=Falseの、JSONのbytesを返します。describe(evidence)は、下の平文の形式で、分析の結果を説明します。bundle(evidence)は、正規化したevidence、analyzeのreport、describeのsummaryをpayloadに入れ、payloadの正規バイト列の小文字のSHA-256を、sha256に入れます。
- ハッシュを計算し直した偽りの結論も拒否します。verify_bundle(value)は、正確なバンドルの構造・format・scope・ハッシュの表記を検証し、payloadのハッシュを確認します。evidenceからバンドルを再作成して、既存のバンドルと、正規バイト列の全体を照合し、一致すれば、分析のreportの新しいdictを返します。バイト列、または結論・説明の不一致はConflict、誤った形式はValueErrorです。decode(raw)は、厳密なbytesの1–1048576個を、UTF-8のJSONとして解釈して、verify_bundleの結果を返し、重複したキー・有限でない定数・途中で切れたJSON・誤ったUTF-8を、ValueErrorで拒否します。
- ファイルの置き換えの失敗が、以前の結果を消さないようにします。export(path,evidence,fault=None)は、出力のパスを検証し、正規のバンドルを1MiB以下のbytesとして、先に完成させます。送り先と同じディレクトリの別の一時ファイルに書き、flush・fsyncのあとで閉じて、after-writeを呼びます。os.replaceで送り先を置き換えてから、after-replaceを呼び、バンドルのsha256を返します。置き換えの前のエラーは、既存のファイルを保全し、置き換えのあとのエラーは、新しい完成版を保全します。正常な場合も一般の例外の場合も、自分の一時ファイルだけを片付け、元のエラーを伝えます。
- DBの観測から報告の発行まで、つなぎます。publish(dsn,change_id,path,fault=None)は、IDと出力のパスを、接続の作成の前に検証します。psycopg.connect(dsn,autocommit=True,connect_timeout=2)で自分の接続を開いてcaptureし、成功・失敗のどちらでも閉じます。収集が成功したあと、DB接続を閉じた状態でexportを呼び、sha256を返します。収集・発行のフックとエラーをそのまま伝え、自動の再試行や、業務DBの変更は行いません。
参考
- 直接の診断: python3 -B /opt/lab/fixtures/evidence/check.py 8 /root/evidence/worker.py。数字を現在のステップに変えると、そのステップまで累積して検査します。1回の採点は40秒が上限です。
- 観測の間の別の接続のコミット、READ ONLYでの書き込みの拒否、同じ件数の別のID、その後のrevision、偽りの結論の再ハッシュ、ファイルの置き換えの前後のエラーを検査します。
- JSONをevalしないでください。duplicate keyはobject_pairs_hookで、NaNなどはparse_constantで拒否できます。UTF-8のデコードエラーと、JSONの解析エラーも、ValueErrorの系統です。
- 読み取り専用だからといって、認証や、顧客ごとのアクセス権限が生まれるわけではありません。自分に許可された仮想のデータだけを収集し、実際の個人情報・運用DB・外部の顧客への送付は行わないでください。
- このラボは、DBサーバーの終了・ファイルのプロセスの強制終了・電源障害への耐久性を検証しません。スナップショットの内部の一貫性、分類の契約、一般の例外のときのファイルの保全を検証します。
承認の一覧の意味を固定する
Exceptionを継承するConflictと、plan(change_id,tenant,targets)を実装してください。共通の入力の契約を検証し、change_id・tenant・targetsだけを持つ新しいdictを返してください。targetsはID昇順のディープコピーにし、入力は変更しないでください。誤った入力はValueErrorです。
同じ件数でも、同じIDの集合とは限りません。boolをintとして受け入れないでください。
異なる時点が混ざらないように収集する
capture(con,change_id,fault=None)は、IDを検証してから、自分のトランザクションで、REPEATABLE READ READ ONLYと、ロック500ms・文2000msの上限を適用してください。changesの該当する承認、該当するchange_idの監査、承認IDに該当する現在のordersを、順に収集してください。各参照のあとに、after-plan・after-audit・after-currentのフックを呼んでください。同じ観測のpg_current_snapshot()の表記と、transaction_timestamp()のUTCの文字列を入れて、下のevidenceの形式で返してください。存在しない承認や、保存された承認の形式の破損は、Conflictにしてください。
別の接続が、フックの間に実際にコミットします。複数のSELECTをBEGINだけで束ねると、デフォルトの分離レベルでは時点が混ざります。
証拠の形式と重複を検査する
normalize(evidence)は、下の正確なキー・型・範囲・観測の表記を検証し、targets・audit・currentをID順に並べ替えたディープコピーを返してください。重複したID、承認の範囲外のcurrent、未知の状態は、ValueErrorにしてください。承認の範囲外のauditは、削除せず、あとで分析できるように保全してください。
形式のエラーと、業務上の不一致を区別してください。承認の範囲外の監査は、隠す資料ではなく、報告する資料です。
過去の確定と現在の違いを分ける
analyze(evidence)は、normalizeのあとで、下の分類の契約に従って、approved・committed・remaining・matching・drifted・missing・invalid_auditの昇順のID listと、decisionを返してください。入力は変更しないでください。監査の件数を数えるだけにしたり、現在の値を自動で復元したりしないでください。
監査は過去の確定の根拠で、現在の行は、その後の違いを見る資料です。状態が同じでも、顧客・数量・revisionが違うことがあります。
顧客への説明と機械の結果を、同じ根拠から作る
canonical(value)は、UTF-8・ensure_ascii=False・sort_keys=True・separators=(カンマ,コロン)・allow_nan=Falseの、JSONのbytesを返してください。describe(evidence)は、下の平文の形式で、分析の結果を説明してください。bundle(evidence)は、正規化したevidence、analyzeのreport、describeのsummaryをpayloadに入れ、payloadの正規バイト列の小文字のSHA-256を、sha256に入れてください。
翻訳・空白・キーの順序が変わると、ハッシュも変わります。今回の形式の正規化の規則を、ほかのJSONの標準全体の保証だと呼ばないでください。
ハッシュを計算し直した偽りの結論も拒否する
verify_bundle(value)は、正確なバンドルの構造・format・scope・ハッシュの表記を検証し、payloadのハッシュを確認してください。evidenceからバンドルを再作成して、既存のバンドルと、正規バイト列の全体を照合し、一致すれば、分析のreportの新しいdictを返してください。バイト列、または結論・説明の不一致はConflict、誤った形式はValueErrorにしてください。decode(raw)は、厳密なbytesの1–1048576個を、UTF-8のJSONとして解釈して、verify_bundleの結果を返し、重複したキー・有限でない定数・途中で切れたJSON・誤ったUTF-8を、ValueErrorで拒否してください。
改ざんした結論のハッシュも、誰でも新しく計算できます。内部の照合が通っても、出所の認証や、現在のDBの確認ではありません。
ファイルの置き換えの失敗が、以前の結果を消さないようにする
export(path,evidence,fault=None)は、出力のパスを検証し、正規のバンドルを1MiB以下のbytesとして、先に完成させてください。送り先と同じディレクトリの別の一時ファイルに書き、flush・fsyncのあとで閉じて、after-writeを呼んでください。os.replaceで送り先を置き換えてから、after-replaceを呼び、バンドルのsha256を返してください。置き換えの前のエラーは、既存のファイルを保全し、置き換えのあとのエラーは、新しい完成版を保全してください。正常な場合も一般の例外の場合も、自分の一時ファイルだけを片付け、元のエラーを伝えてください。
送り先を先に書き込みモードで開くと、以前の報告書が切り詰められます。同じディレクトリで完成させてから、1回だけ置き換えてください。
DBの観測から報告の発行まで、つなぐ
publish(dsn,change_id,path,fault=None)は、IDと出力のパスを、接続の作成の前に検証してください。psycopg.connect(dsn,autocommit=True,connect_timeout=2)で自分の接続を開いてcaptureし、成功・失敗のどちらでも閉じてください。収集が成功したあと、DB接続を閉じた状態でexportを呼び、sha256を返してください。収集・発行のフックとエラーをそのまま伝え、自動の再試行や、業務DBの変更は行わないでください。
ファイル操作を待っている間、スナップショットを握り続けないでください。保存された報告書と、新しい観測の結論が違っても、どちらかを無理に合わせることはしません。