feat: screenshots モードで撮る story を絞り、撮らなかった story は baseline から引き継ぐ - #6
Conversation
788f30b to
61de923
Compare
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
eb2d1dc to
7200332
Compare
yupix
left a comment
There was a problem hiding this comment.
レビュー結果: APPROVE with Comments
実装は設計どおりで、select_stories 統一後も storybook モードの既存挙動(stats 不在 → 全撮影 / 破損 → exit 2)が維持されていることをコードとテストの両方で確認しました。base の main への retarget 済み・マージ済み PR #7 の stderr ログ / --json 変更との整合も確認済みです。コード本体にマージを止める欠陥は見つかりませんでした。
良い点(確認済み)
- 「空の only」と「capture_all」を型(
Selection)・JSON 契約・テストの三層で区別し切っている - fail-closed の分岐(
CorruptInput)が enum 1 個に集約され、両モードの食い違いが構造的に起きない - baseline / HEAD の commit SHA を計画へ焼き付けて CI 側で無効化検知させる設計は堅実
- plan が作る pending ビルドは baseline 選定に影響しない(baseline は承認時のみ昇格)ため、plan を 2 回実行しても baseline が汚れる事故は起きないことを確認した
指摘
-
🟡 README の CI レシピが、本文で禁じている曖昧さを再生産している
サンプルではonlyかつstory_idsが空の場合とcapture_allの場合のどちらも「空のstories.txt」になり、直後の本文で強調している両者の区別がレシピ上で消えます。CI 側が「空 = 撮らない」と解釈するとcapture_allで 1 枚も撮らない危険側に倒れえます。planの値をセンチネル(別ファイル/変数)で後段へ渡す形への書き換えを推奨します。あわせて、表で謳っている「未知のversionなら全撮影へ倒す」チェックもレシピに含めてください。 -
🟡 plan 由来の宙吊り pending ビルドに回収機構がない
--baseline-commitなしのvrt planは baseline SHA を得るためだけにscreenshotsビルド(pending)を作成します。plan 後に CI が失敗して finalize されなかった場合、そのビルドは pending のまま永久に残ります(backend に expiry/reaper 相当の機構がないことを確認済み)。baseline 選定には影響しないため正確性は壊れませんが、運用ノイズとして蓄積します。本 PR では README への明記のみで可、根本対応(古い pending の自動 fail、または baseline 取得専用の read-only エンドポイント)は後続 PR が妥当と考えます。 -
🔵 README の「
build_idもnullになる」は実装と不一致です(skip_serializing_ifによりキー自体が省略される)。jqでは実用上動きますが、厳密なスキーマ検証では食い違います。またstory_ids= キー省略 /reason= null 常在 /build_id= キー省略と、フィールドごとに方針が分かれているため、契約として揃えられるとより良いです。 -
🔵 機械可読出力の様式が subcommand 間で不揃いです(
upload --jsonは 1 行 JSON、planは pretty の複数行)。揃えるか、揃えない理由をコメントに残すことを推奨します。 -
🔵
cli/src/main.rsで、機能と無関係に既存の説明コメント(--waitなし時の終了コード方針、--jsonの error キー契約、flush_logsが失敗を終了コードに波及させない理由など)が数カ所削られています。復元を推奨します。 -
🔵
--baseline-commitに存在しない SHA を渡した場合、git diff失敗として exit 0 のcapture_allに倒れます。shallow clone と区別がつかないため fail-closed 自体は妥当ですが、設定ミスも黙って全撮影になります。SHA の存在確認(git cat-file -e相当)だけを exit 2 に分離する余地があります。
テストカバレッジ
19 件は要所(空 only vs capture_all、story_ids 省略、SHA 焼き付け、round-trip、storybook モードの挙動維持)をすべて固定しており実効性は高いです。一方すべてライブラリ層(plan.rs)止まりで、run_plan 本体(stdout の JSON 純度、--output の書き出し、引数不足時の exit 2、ビルド作成経路)は未テストです。CLI レベルの smoke テストを 1〜2 本追加できると盤石です。
Restore design-rationale comments removed during plan refactor, fail on invalid --baseline-commit instead of capture_all, improve README CI recipe with plan-kind sentinel and version guard, and add CLI smoke tests. Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
yupix
left a comment
There was a problem hiding this comment.
再レビュー結果: REQUEST CHANGES(修正 1 点のみ)
前回(2026-07-31)の指摘 6 件について、対応コミット(62e759c、dbbaaa5)を現物コードと突き合わせて検証しました。6 件すべて実体を伴って対応されています。 ただし、対応のために追加されたテストが PR head の CI(test チェック)を実際に落としており、この 1 点の解消までマージ不可と判断します。
前回指摘の対応状況(すべて検証済み)
| # | 指摘 | 状況 |
|---|---|---|
| 1 | README レシピが only 空 / capture_all の区別を潰す | 対応済み。PLAN_KIND センチネルファイル方式で区別が保存され、version ガードと未知値の capture_all への上書きも実装。シェルとしての正しさも確認 |
| 2 | 宙吊り pending ビルドの回収機構なし | 対応済み(README に残留の明記と --baseline-commit での回避、根本対応は後続 PR。前回の合意どおり) |
| 3 | build_id の null 記述と実装の不一致 |
対応済み(README を「キーを省略」に修正、story_ids / reason の方針も明記) |
| 4 | 機械出力の様式不揃い | 対応済み(揃えない理由を doc comment に明記。前回提示した選択肢のひとつ) |
| 5 | PR #7 由来の説明コメントの削除 | 対応済み(復元を確認) |
| 6 | --baseline-commit の typo が exit 0 の capture_all に化ける |
対応済み。明示指定時は即検証して exit 2、加えて OID 正規化と diff 終点の明示化まで踏み込んだ良い修正 |
| - | CLI smoke テストの推奨 | 対応済み(cli_smoke.rs 5 本 + fixtures 5 本追加) |
🔴 要修正: 新規テストが shallow clone の CI で落ちる
- 対象:
apps/backend/crates/cli/src/git.rsのresolve_commit_accepts_head_tilde_notation/changed_files_diffs_between_two_explicit_commits、tests/cli_smoke.rsのplan_normalizes_baseline_ref_to_full_oid/plan_diff_uses_explicit_commit_not_worktree_head - 原因: これらは
HEAD~1/HEAD~2に依存しますが、.github/workflows/backend-test.ymlのactions/checkoutはfetch-depth未指定(デフォルト 1)のため親コミットが存在せず、fatal: Needed a single revisionで失敗します。PR head の CI ログでtest result: FAILED. 17 passed; 2 failedを確認済みです。 - 修正案(いずれか):
- (a) 推奨: テスト内で一時ディレクトリに
git initして合成コミットを 2〜3 個作り、自給自足にする(実行環境に依存しない) - (b) 手軽:
backend-test.ymlにfetch-depth: 2以上を指定する(ただしテストがランナーの checkout 履歴に依存する構造は残る)
- (a) 推奨: テスト内で一時ディレクトリに
🔵 Low: 未使用関数と doc comment への内部識別子の混入
apps/backend/crates/cli/src/git.rs:57 の commit_exists は、dbbaaa5 の clippy 対応で呼び出し元が resolve_commit 直呼びに変わり、定義だけが残っています(pub のため dead_code 警告が出ません)。また doc comment 内の「cmd_591」は読者に意味の取れない内部作業由来の識別子です。関数の削除、またはコメントの書き直しを推奨します。
総評
対応の中身は「直したつもり」ではなく全件が実効性を伴っており、特に指摘 6 は要求以上の踏み込みです。上記 🔴 の 1 点(修正は軽微)が解消され次第、APPROVE 相当と判断します。
レビュー総評CLI( ただし、高2件を含む要修正があります。 【高】マージ前に必須1.
|
|
詳細なレビューをありがとうございます。 PR 本文の食い違い(中)三点とも本文の誤りでした。実装ではなく本文を直しました。
サーバー側でも形式検証または正規化を行うかは、別途検討します。 高2件いずれも対応します。 1 の ご指摘のとおり、直列化を screenshots 経路にしか通していませんでした。 2 の README は 中・低ロジックとテストの穴、および低・微の各項目も順に対応します。
migration をまとめる件も、未出荷なので一本に潰す方向で見ます。 対応後に改めてご確認をお願いします。 |
レビュー(二周目): 前回指摘の訂正と追加の発見前回のレビューコメントに対して敵対的な再検証と、一周目で手薄だった領域(認可境界・Unicode・git 出力パース・serde 互換)の追加レビューを行いました。まず前回コメントの訂正から。 前回指摘の訂正訂正 1: 【高1】pin_baseline_for_partial_render — トリガーの記述が不正確でした(High → Medium)「finalize リトライで 問題が成立するのは「baseline 昇格後に、新しい baseline の SHA を指定して同一ビルドへ finalize を再送した場合」に限られます。ただし構造上の指摘自体は事実のままです: 重大度を Medium に引き下げたうえで、修正推奨は前回どおり「pending 再確認 → SHA 照合 → pin → 遷移を finalize と同一トランザクション + build 行ロック内へ」です。 訂正 2: 【中】provenance の verify→read TOCTOU — セキュリティ問題ではありません(→ nit)provenance は無署名のローカルファイルであり、verify〜select の間にファイルを差し替えられる主体は provenance 自体を再計算して偽造できるため、read-once 化しても攻撃耐性は変わりません(README の脅威モデル「事故のみ」とも整合)。残るのは「ビルドプロセスが並行して stats を書き換えた場合に、検証した内容と選別に使った内容がずれる」という堅牢性の nit です。verify がバイト列を返して select がそれを parse する read-once 化は安価なので、余裕があれば、という位置づけに格下げします。 再検証で維持された指摘
追加の発見1. 【中】
|
screenshots モードはサーバーがレンダリングしないため、撮影は CI 側の テストランナーが行う。CLI が撮影を代行できない代わりに、撮る story の 選択計画だけを JSON で出力する `vrt plan` を追加する。 選択そのものは storybook モードの --only-changed と同じ turbosnap.rs を 通す。stats と index の読み込みと fail-closed の倒し方を plan.rs へ切り出し、 両モードから同じ経路を呼ぶ。両者の違いは、入力が壊れていたときに エラーへ倒すか全撮影へ倒すかだけにした。 計画には baseline と HEAD の commit SHA を焼き付ける。空の story_ids は 「影響のある既存 story は無い」であり、capture_all とは区別する。 baseline が無い、git 差分が取れない、stats が無い・壊れている、 index が壊れている、依存が更新された、変更がグラフ外のいずれでも capture_all へ倒す。 Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
Print the selection plan to stdout before attempting --output file writes so CI always receives the plan even when the output path fails. Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
vrt plan サンプルに set -euo pipefail と fail-closed の注意を追記する。 Assisted-by: multi-agent-shogun-aki-tweak
Restore design-rationale comments removed during plan refactor, fail on invalid --baseline-commit instead of capture_all, improve README CI recipe with plan-kind sentinel and version guard, and add CLI smoke tests. Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
Assisted-by: multi-agent-shogun-aki-tweak
Reject index JSON without extractable stories so corrupt input cannot become an empty only plan. Resolve baseline/head via rev-parse and diff between the same two OIDs recorded in the plan document. Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
Reverts the accidental /usr/bin/git hardcode introduced while debugging the shogun repo git wrapper in PATH. vrt must invoke git from PATH so Homebrew, Windows, and Nix installs keep working. Assisted-by: multi-agent-shogun-aki-tweak Co-authored-by: Cursor <cursoragent@cursor.com>
Pass the resolved --commit into resolve_only_story_ids so story selection uses changed_files(baseline, commit) instead of worktree HEAD. Replace shallow-checkout silent-return git/plan smoke tests with self-contained temp-repo fixtures and add a regression test for the upload path. Assisted-by: multi-agent-shogun-aki-tweak
Assisted-by: multi-agent-shogun-aki-tweak
Hyphen-prefixed revs from server JSON were parsed as git options. Add regression test with positive control and align README with --baseline-commit exit 2 behavior and --commit diff endpoint. Assisted-by: multi-agent-shogun-aki-tweak
Assisted-by: multi-agent-shogun-aki-tweak
Assisted-by: multi-agent-shogun-aki-tweak
「コマンドが成功した」ことと「成果物がその build で生成された」ことは別で ある。従来は build 前に残っていた別コミットの成果物が no-op 命令の「成功」 後にそのまま hash され、commit A の成果物に commit B の v2 provenance が 付けられた(`vrt stamp -- true` が exit 0)。 - build 開始前に旧 provenance と stats / index を削除(provenance::invalidate) - build 成功後に両ファイルが存在すること自体を「build 実行中に再生成された」 証明とする(no-op 命令は stats 不在で失敗する) - 副次効果として build が失敗した stamp は旧 provenance を残さない - storybook に HEAD 束縛の信頼できる build-time marker が無いため、cache-hit を証明付きで受け入れる形は取らず再生成を強制する - README: 塞いだ no-op 経路を「保証しない」から外し、塞げない「命令自身が 古い内容を書き戻す」ギャップだけを境界として残す Assisted-by: multi-agent-shogun-aki-tweak
Backend Test run 30705166763 の失敗は tests/github_integration.rs の installation_deleted_soft_deletes_row_and_unlinks_projects。 github_webhook.rs の deleted 処理は installation 行の update と projects の unlink を別ステートメントで行うため、テストが installation の deleted_at だけを待って project を GET すると未 unlink を観測しうる(本枝の diff は cli + README のみで server 側 0 行・同一 server コードの 9c07964 では緑)。 rerun 権限が無いため空 commit で再走行する。race 修正は follow-up として報告。 Assisted-by: multi-agent-shogun-aki-tweak
計画(selected_names / manifest_names)・アップロード・finalize の
captured_names の四経路が別々の規則(512 文字 vs trim + 255)で名前を
検証していた。計画・アップロード・finalize は名前の文字列一致で
突き合わせるため、計画側だけ緩いと「計画には載るのにアップロード
できない名前」ができ、そのビルドは永久に finalize できない。
- 規則を common::validation::ScreenshotName に一本化: 空でない・
前後空白なし(trim せず拒否)・255 バイト以内(バイトであって
文字数ではない)。名前を受け取る関数は String ではなくこの型を
要求し、検証の呼び忘れを型で防ぐ
- 255 バイトの根拠: DB は無制限 VARCHAR で制約源にならず、実分布は
story ID か PNG パス由来の名前。ファイル名へ書き戻せる 255 バイト
(ext4 / APFS の filename 上限)を採る。アップロード経路は初版から
この値ゆえ、超過名で完走した既存ビルドは存在しえない
- story ID(MAX_STORY_ID_LEN = 512)はスクリーンショット名とは別の
名前空間として残す。storybook モードの名前は {title}/{name} 生成後に
改めて ScreenshotName で検証される
- baseline 非空 × manifest 非空で交差ゼロの計画は命名規則ずれとして
attach を 400 で拒否(全エントリが removed の成功扱いで命名ずれが
隠れる 594 同型の穴)。README に別命名 baseline の作り直し手順を追記
- README「計画は stdout に必ず出る」を固定成功時の条件付きへ改め、
attach 失敗時(stdout 空のまま exit 2)の例外ポインタを追加
Assisted-by: multi-agent-shogun-aki-tweak
名前規則の適用範囲の列挙(計画・アップロード・finalize の captured_names)
から storybook モードのレンダリング経路({title}/{name} 生成後の検証)が
漏れていた。storybook 節へ、規則違反でビルドが failed になること・従来の
保存時 trim が無くなり前後空白付き title / story 名のビルドが今回から
失敗する破壊的変更であること・直し方(前後空白を除く)・trim を拒否へ
一本化した理由(絞り込み判定が未 trim の名前で baseline を引き永久不一致で
毎回撮り直しになる不整合)を追記した。
Assisted-by: multi-agent-shogun-aki-tweak
pin_baseline_for_partial_render はロックもトランザクションも持たず、 baseline_id の更新を遷移チェックより前に単独コミットしていた。 このためリトライで届いた 2 度目の finalize は、承認で baseline が 前進していると SHA 照合を通過して baseline_id を上書きし、本体は 409 で弾かれてもレンダリング・比較ジョブは以後 新しい baseline を 読んでいた——計画の根拠と比較相手のズレが別の入口から成立していた。 finalize_screenshots と同じく、build 行ロックを取った 1 トランザク ション内で「pending 再確認 → SHA 照合 → pin → 遷移」を行う形へ finalize_storybook に統合し、pending 再確認を pin より先に置いて 2 度目の finalize が baseline_id に触れる前に 409 で止まるようにする。 review_lock.rs のロック規約も storybook 経路を含む形へ改める。 統合テストは「2 度目の finalize が固定済み baseline を上書きできない」 ことを固定する(旧実装では baseline_id が B2 に化けて落ちる)。 Assisted-by: multi-agent-shogun-aki-tweak
vrt stamp の build_command は必須(後追い stamp は clap の usage エラーで 即死する)うえ、「build 後の後追い stamp」は本 README 自身が二度禁じた形 であり、レシピが自己矛盾していた。stamp に build を実行させる 1 行へ 差し替える。他のレシピ(クイックスタート・provenance 節・生 API 節)に 同じ形が無いことを通しで確認した。 Assisted-by: multi-agent-shogun-aki-tweak
provenance::verify が stats / index をハッシュ照合した後、 plan::select_stories が同じファイルを読み直していたため、照合した バイト列と選別に使うバイト列の同一性がプロセス内で保証されず、 その間の差し替え(TOCTOU)を内容ハッシュの束縛がすり抜けられた。 verify が読んだバイト列を Verification::Verified(VerifiedArtifacts) としてそのまま返し、選別は select_stories_from_verified がその値から パースする。plan / upload --only-changed の両経路とも読み直しを廃した。 ファイル読みの select_stories は fixture テスト用に残すが、パース以降は 同じ本体(select_stories_from_content)へ委譲するため実装は乖離しない。 Assisted-by: multi-agent-shogun-aki-tweak
manifest ∖ baseline ∖ selected の集合はアップロードも carry-forward の 引き継ぎ元も無く、比較結果のどこにも現れない。CI の選択ロジックに バグがあると、新規 story が added として報告されないまま レビュー可視性ゼロで PASS しうる。 attach_capture_plan は baseline entries を既に読んでいるため、 selected ⊇ manifest ∖ baseline の検査を足して 400 にする。既存 story の 絞り込みは baseline にあるので差集合に入らず、正当な部分撮影は弾かない。 統合テストは拒否と、同じ新規 story を選択すれば通ることの両側を固定する。 Assisted-by: multi-agent-shogun-aki-tweak
- uploads_are_serialized_...: 手書き JSON を CapturePlan の serialize から 生成する形に改め、保存形の変更にテストが追随するようにする - plan_attachment_takes_the_build_row_lock: 本物の添付 API が build 行 ロックでブロックされることを直接固定する(従来は自前トランザクション の再現のみで、attach 側からロック取得が消えても緑のままだった) - plan_narrows_when_worktree_matches...: provenance 無しの fixture では 実際は capture_all になるのに assert が SHA 2 つだけで通っていた。 実態どおり capture_all を固定する名前・assert に改め、実際に絞り込める stamp → plan の成功連鎖(plan="only")の smoke を新設する - 否定側 4 本を追加: only_story_ids で expected_baseline_commit_sha 省略 → 400 / storybook モードへの captured_names → 400 / 計画あり ビルドで finalize の SHA 不一致 → 400 / baseline の無いブランチへの plan 添付 → 409(いずれも実装・openapi に宣言済みだが未固定だった) Assisted-by: multi-agent-shogun-aki-tweak
captured_names は追加直後に次の migration で drop される履歴の遺物で、 ベクタ順(captured_names → capture_plan)とタイムスタンプ名の並びも 逆転していた。未出荷のため m20260802000000_build_capture_plan の 1 本に 統合し、IF NOT EXISTS / IF EXISTS で旧 2 本(改名前の旧名を含む)を 適用済みの開発 DB へ再適用しても落ちない形にする。 接頭辞は PR #9 の m20260801000000_build_approval_evidence と衝突しない m20260802000000 のままで、merge 後の適用順も #9 → 本 migration になる。 Assisted-by: multi-agent-shogun-aki-tweak
- 「baseline が動いた(再計画で解消)」の応答が plan 添付=409 / finalize 系=400 で割れていたのを 409 に統一する(storybook 部分 レンダリングの SHA 照合・全撮影時の照合の両方)。screenshots モードの 固定済み baseline とのクロスチェック不一致は、宣言と記録の食い違いで あって baseline の移動ではないため 400 のまま - vrt stamp が --stats-json / --index-json の解決値を build 前に無検証で 削除することを README に明記する(タイポで無関係なファイルが消える)。 パス検証は足さない——カスタムパスは成果物ディレクトリ外も正当であり、 形式で機械判定できる不正が無いため、明記と実行コマンド監査で扱う - vrt upload に --index-json を追加する。カスタム index で stamp した 成果物の検証・選別が stamp と同じ解決規則で通るようになり、 「解決パス違いによるハッシュ不一致」も help から辿れる - stamp の観測が build 前後の 2 点であることを README に明記し、 「build 中の checkout はここで検出され」の過剰主張を改める。 A→B→A の往復は検出できない旨を「保証しないこと」へ追加 - plan エンドポイントの 400 記述に名前規則(255 バイト・前後空白)・ ゼロ交差の命名ずれガード・新規名の選択漏れガードを追記 Assisted-by: multi-agent-shogun-aki-tweak
- vrt plan の --output 書き込みを stdout 出力より前へ移す。stdout を合図に ファイルを読む消費者が書き込み前のファイルを掴む隙間を無くす - 最新 baseline の直読み(created_at 降順・同時刻 id 降順)を tests/common の latest_baseline_of に一本化し、subset / render_flow の 重複実装を畳む - README の BUILD 変数に後段(アップロード先・finalize 先)での用途を明記 - 競合テストのコメント「ストレージは補償削除」に対応する実体アサートを 追加する(LOCAL_UPLOAD_DIR 配下の build ディレクトリが空であること) Assisted-by: multi-agent-shogun-aki-tweak
finalize / plan エンドポイントの応答記述変更(409 統一・400 記述の補強)を apps/frontend/openapi.json へ反映し、rustfmt の崩れを直す。 Assisted-by: multi-agent-shogun-aki-tweak
計画添付の selected ⊇ manifest ∖ baseline 検査(新規 story の選択漏れを 400 で拒否)を API 利用者向けの規則として README に載せる。 Assisted-by: multi-agent-shogun-aki-tweak
git の既定(core.quotepath=true)は非 ASCII / " / \ 入りのパスを C-quoting で出力し、その文字列は依存グラフのキーに一致しないため、 日本語ファイル名のリポジトリでは変更が常に「グラフ外 → capture_all」へ 倒れて差分選別が事実上無効だった。-z(NUL 区切り・quoting 無し)で受けて 生のパスを返す。行区切り + trim のパースは改行・前後空白入りのパス名を 壊すため廃止。 positive control: 既定 quotepath の C-quoting 出力が実ファイル名と一致 しないこと(壊れ方そのもの)をテスト内で固定し、日本語パス・改行入り パスが原文で返ること、日本語パスが story 選択まで到達することを追加。 Assisted-by: multi-agent-shogun-aki-tweak
equality が全経路バイト一致のため、Linux CI(NFC)で育てた baseline を macOS ランナー(APFS は NFD を返す)で参照すると、見た目同一の名前が NFC 側 removed / NFD 側 added に割れていた。parse の入口で NFC へ正規化し、 正規化後の形を保存・比較の正とする。 255 バイト上限は正規化**後**に当てる。DB・比較・ファイル名書き戻しに 現れるのは正規化後の形だけで、正規化前に当てると同じ見た目の名前が OS の返す表現次第で通ったり落ちたりする——この修正が消そうとしている 割れそのものが上限判定に残るためである。両方向(NFD 258B→NFC 129B は 通る / 合成除外文字 U+0958 の 255B→510B は落ちる)をテストで固定。 制御文字(Unicode Cc)は拒否する。NUL は Postgres の text/jsonb が U+0000 を拒否するため 400 ではなく 500 で落ち、ESC は名前が build_logs 経由で端末へ流れる経路があり ANSI エスケープ注入の芽になる。 Assisted-by: multi-agent-shogun-aki-tweak
- CLI の finalize が only_story_ids: None のとき expected_baseline_commit_sha を黙って捨てていた。サーバーの「screenshots + 計画あり + expected のみ」の 照合に構造的に到達できず、照合したつもりで送られていない footgun だった。 expected 単独でもボディを送る - 新設の AttachCapturePlanRequest に deny_unknown_fields。安全照合フィールドの typo が unknown field として黙殺され照合がサイレント無効化されるのを防ぐ。 既設の FinalizeBuildRequest には付けない——新旧混在デプロイで新しい CLI の フィールドを旧サーバーが一斉 400 にする後方互換破壊になるため。許容側も テストで固定 - finalize の 400 文言を分岐: 計画も pin もあるのに昇格元ビルドが消えて SHA を 辿れない場合に「requires a capture plan」と言わない - store_ci_screenshot の計画照合を HashSet 化(行ロック保持中の線形走査を回避) - CHANGELOG.md を新設し、trim→拒否・制御文字拒否・NFC 正規化の破壊的変更を 次回リリースノートへ転記する原稿として記載 Assisted-by: multi-agent-shogun-aki-tweak
2254a1c to
d4f667d
Compare
|
P1: public 配下の Markdown 変更を無条件に無視しないでください。 対象: apps/backend/crates/cli/src/turbosnap.rs:298-302 依存グラフ外の .md をすべて除外すると、story が public/copy.md のようなランタイム配信アセットを描画していても webpack stats には依存として現れず、そのファイルだけを変更した場合に seed が空になって Plan::Only([]) になります。結果としてサーバーが古い baseline を carry-forward し、見た目が変わっていても false PASS になります。 判定できない .md は CaptureAll に倒すか、除外対象を README 等の既知ドキュメントパスの allowlist に限定してください。public/*.md を変更した際に全撮影へ倒れる回帰テストもあると安全です。 注: 対象行がPR差分に含まれていないため、この指摘はインラインではなく会話欄へ投稿しています。 |
既知のルート文書だけを無視し、public配下を含む判定不能なMarkdown変更は全撮影へ倒す。CLI smoke用リポジトリではコミット署名を明示的に無効化する。 Assisted-by: multi-agent-shogun-aki-tweak
テスト用一時リポジトリの初期化 6 箇所(git.rs×3・main.rs×1・ cli_smoke.rs×2)が host の global gitconfig を継承しており、 commit.gpgsign や core.hooksPath が有効な環境では commit が落ちる。 lib に test_support モジュールを新設し、全箇所を init_test_repo へ寄せて リポジトリローカル設定(gpgsign/tag.gpgsign=false・hooksPath 無効化・ identity 明示・-b main)で遮断する。bin unit test と統合テストへは feature test-support の self dev-dependency で公開する。 重複していた git_in / git_output も同モジュールへ一本化した。 Assisted-by: multi-agent-shogun-aki-tweak
plan_without_credentials_exits_2 と plan_with_missing_baseline_commit_exits_2 が開発リポジトリの HEAD を git rev-parse で解決しており、git archive で 展開した .git 無しソースでは当該 2 件が落ちていた。init_linear_repo の 一時リポジトリ HEAD へ置き換えて自己完結させ、不要になった repo_root / git_rev_parse ヘルパーを削除する。 CI は .git ありの checkout で走るためこの性質を検証できない。検証手順 (git archive 展開下での cargo test -p vrt-cli)を README に残す。 Assisted-by: multi-agent-shogun-aki-tweak
何をしたか
screenshotsモードで「撮る story を絞る」を安全に成立させた。当初は CLI に選択計画を出す
vrt planを足すだけの変更だった。だがレビューを重ねるうちに、部分撮影が既存の比較契約と噛み合っていないことが分かり、
サーバー側の比較処理と成果物の検証にまで手を入れることになった。
本 PR は CLI から backend まで一続きの変更である。
なぜ CLI だけで終わらなかったか
screenshotsモードはサーバーがレンダリングしない。撮影は CI 側のテストランナーが行い、サーバーは送られてきた PNG を受け取るだけである。
この前提のもとで一部の story だけを撮って送ると、比較処理は撮らなかった story を
「消えた」と判定していた。
baseline が 100 件で今回撮ったのが 1 件なら、残り 99 件が
removedとして報告される。それを承認すれば baseline から消える。
つまり部分撮影を成立させるには、次の三つが揃っている必要があった。
3 が最も深く、
vrt planの入力であるpreview-stats.jsonとindex.jsonが通常 untracked であるため、commit と結び付いていないところまで遡った。
主な変更
撮る story を選ぶ(
vrt plan)依存グラフから影響のある story を割り出し、選択計画を JSON で出力する。
撮影は行わない。
CI ランナーがこの JSON を読み、
planがonlyのときだけ列挙された story を撮る。入力が読めない、あるいは意味的に壊れている場合は全撮影へ倒す。
storybookモードのvrt upload --only-changedも同じ選択器を通るので、選択のロジックは 1 本のままである。
撮らなかった story を引き継ぐ
比較処理に選択集合を渡し、集合に含まれない baseline entry は
removedではなく引き継ぎとして扱う。
storybookモードがonly_story_idsで行っている流用と同じ結果になる。削除された story は引き継がない
引き継ぎは「撮らなかっただけ」の story に限る。
現在の index から消えた story は
removedとして扱い、baseline に残り続けないようにした。選択集合をサーバー側に固定する
計画をビルドへ保存し、finalize と束縛する。
アップロード側の自己申告だけで検証すると、撮影が全滅したときに空集合どうしが一致して
通ってしまうためである。
計画が非空なのにアップロードが空という遷移は拒否する。
計画を添付した時点で baseline を固定する
計画を立てた時点の baseline と、比較を実行する時点の baseline がずれると、
選択の根拠と比較の相手が食い違う。
計画を添付する時に baseline を固定し、比較でもそれを使う。
固定はビルド作成時ではなく計画添付時である。
作成時に固定すると、作成から撮影開始までの間に進んだ baseline を取りこぼすためで、
利用者から見た保証は「撮影が始まる前に固定されている」ことになる。
計画を使わないビルドの意味は変えていない。
成果物を生成コミットに束縛する(
vrt stamp)vrt stamp -- <build command>の形でビルドコマンドを渡す。vrt が開始前に HEAD と worktree の clean を観測して古い provenance と成果物を削除し、
ビルドを実行し、成功後に同じ HEAD と clean を再確認してから provenance を書く。
これで「そのコミットでビルドが走り、成果物を作り直した」ことを vrt 自身の計器で確認できる。
build を所有しない旧形式(version 1)の provenance は全撮影へ倒す。
provenance が無い、あるいは合わない成果物での絞り込みは拒否する。
競合と再試行
計画の添付、アップロードの登録、finalize を同じロック規約で直列化した。
引き継ぎのコピーは決定的なキーと upsert で冪等にし、一時的な失敗は再試行へ戻す。
途中で止まった場合の孤児オブジェクトは補償削除で回収する。
出力契約
{ "version": 1, "plan": "only", "branch": "feat/x", "baseline_commit_sha": "<sha>", "head_commit_sha": "<sha>", "story_ids": ["button--primary"], "reason": null, "build_id": "<uuid>" }planはonlyとcapture_allの二値で、onlyかつstory_idsが空という状態も正当な結果として存在する。
「撮る story が無い」と「判定できなかった」を取り違えないよう、CI レシピでは
空ファイルではなくセンチネルで区別している。
コミット SHA は CLI 側で 40 桁の OID へ正規化してから送る。
サーバーは長さの検証のみで、baseline との照合は完全一致である。
CLI を通さず API を直接叩いて短縮形を送ると、同じコミットでも一致しない。
何を保証し、何を保証しないか
保証するのは、計画の根拠となった成果物がその commit で生成されたことと、
撮らなかった story が黙って消えないことである。
保証しないものは README に列挙した。
主なものは、ビルドコマンドが古い内容を書き戻す場合、storybook のキャッシュが
古い stats を出す場合、provenance が無署名であることの三つである。
脅威モデルは事故であって、悪意ある CI ランナーへの防御ではない。
レビューの読み方
コミット数が多いので、次の順に読むと筋が追いやすい。
crates/cli/src/plan.rsとturbosnap.rs— 何を撮るかの判断crates/cli/src/provenance.rsとmain.rsのstamp— 成果物をコミットに束縛する仕組みcrates/job/src/compare_build.rs— 引き継ぎと削除の判定crates/service/src/builds.rsとreview_lock.rs— 計画の固定と直列化README.md— CI レシピと保証の境界テスト
回帰テストは、修正前のコードで実際に落ちることを確認したうえで入れている。
git を使うテストは一時ディレクトリに合成コミットを作って自給自足にしてあり、
CI の shallow checkout でも本体の検証まで到達する。
競合の再現は、実トランザクションで行ロックを保持したまま後続の要求を投げ、
ロック待ちに入っていることを確かめる形にしている。