簿記3級ノート全26章のうち24章を、純HTMLからeurekapu-nuxt4のVueコンポーネントに一日で移植した。朝に6章を6並列で起動し、昼に9章を9並列で追加、夕方に9章を9並列で仕上げた。途中で calcNetIncome の重複宣言バグを掘り当て、JournalExample.vue の貸方合計に当期純利益が乗っていない致命バグを潰し、Write権限拒否で止まったサブエージェントを手動で救済し、別セッションでブランチを取り違えて reset --soft で巻き戻し、最後にE2Eのフレーキーを expect.toPass で恒久対策した。
やったこと(時系列)
純HTMLで書いていた簿記3級ノートをVueコンポーネント化する作業は、前日までで2章だけ手作業で済ませていた。残り24章を一日で仕上げるには並列化しかない。memo/2026-05-02/ に章ごとの移植仕様メモを置き、サブエージェントに食わせる前提で章リストを優先度A・B・Cに分けた。
朝のうちに優先度A(6章)を6並列で起動した。
- merchandise(商品売買)
- notes(手形・電子記録債権)
- fixed-assets(固定資産)
- closing-adjustments-1〜3(決算整理仕訳の3章)
並列起動から30分ほどで6章すべてのVue化が返ってきた。コミット前の pnpm test でいきなり赤が出る。
SyntaxError: Identifier 'calcNetIncome' has already been declared
useFinancialStatements.ts を開くと、calcNetIncome が同じファイル内で2回 const 宣言されていた。前日までの手作業移植で混入した pre-existing バグで、テストが走るのは今回が初めてだったため発覚した。後段のクロージングロジックが同名で再宣言していたので、片方を calcNetIncomeForBS に分離して解決。
バグ2: 「負債純資産の合計に当期純利益が乗っていない」
優先度Aの章をブラウザで開いてみたら、貸借対照表の合計が一致しない。負債純資産側に当期純利益が加算されていない。JournalExample.vue の sectionTotals 計算ロジックで、liab のみを集計していて eq(純資産)と当期純利益を取りこぼしていた。
// Before: 当期純利益が消える
const liabTotal = computed(() =>
rows.value.filter(r => r.section === 'liab').reduce((s, r) => s + r.amount, 0)
)
// After: liab + eq + 当期純利益
const liabEqTotal = computed(() =>
rows.value
.filter(r => r.section === 'liab' || r.section === 'eq')
.reduce((s, r) => s + r.amount, 0) + netIncome.value
)
このバグは前日までのHTML版でも同じ式だったが、HTML版は当期純利益を別表示にしていたため気づかなかった。Vue化で残高試算表を統合表示に変えた瞬間に表面化した、典型的な「移植で炙り出される潜在バグ」だ。
優先度B(9章)の9並列移植
Aが片付いたところで、優先度Bの9章を9並列で起動した。
- intro-double-entry(複式簿記入門)
- dividends(配当金)
- depreciation-detailed(減価償却の詳細)
- 社会保険関連(給与・法定福利費)
- advanced-topics(応用論点)
- equity(純資産)
- bad-debts-detailed(貸倒引当金の詳細)
- ほか2章
9個中7個は無事に返ってきたが、dividends と depreciation-detailed の2エージェントが Write 権限拒否で BLOCKED になった。サブエージェントに渡す権限スコープが章ディレクトリ単位で切られており、新規ファイル作成パスが許可リストから外れていたのが原因。リトライしても同じところで止まるので、出力されたコード片を読んで自分で apps/web/app/components/notes/3kyu/dividends/ 配下に直接書き込んで救済した。
ブランチ取り違え→reset --soft で復旧
優先度Bを15時頃にコミットしようとしたら、別セッションで開いていた feat/journal-example-fix ブランチに自分が立っていた。気づかずに git add . まで進めかけて手が止まる。
git status
# On branch feat/journal-example-fix ← 違う!
git reset --soft HEAD でステージを戻し、新ブランチ feat/3kyu-notes-9chapters を切ってから再コミット。reset --soft を選んだのは、作業ツリーを保ちつつインデックスだけ巻き戻したかったから。--hard を打っていたら午前の修正がすべて飛んでいた。
優先度C(9章)の9並列移植とPR #14
夕方に優先度Cの9章を9並列で投入した。
- trial-balance-detailed(試算表の詳細)
- closing-overview(決算手続きの全体像)
- ledgers(総勘定元帳)
- mock-exam(模擬試験)
- evidence(証憑類)
- ほか4章
ここまでで累計24章/26章の移植が完了。pnpm test でテスト700件すべてグリーン。最後に「SVG見切れ」「グリッドテーブル化漏れ」「フォントサイズ不一致」「レスポンシブ対応漏れ」「ARIA属性」「キーボード操作」の6観点をサブエージェントに網羅レビューさせ、検出された不具合を7並列で一気に修正した。
PR #14 を feat/3kyu-notes-9chapters から立てて、CIを回す。E2Eで1件だけ赤が出た。
TimeoutError: page.click: Timeout 30000ms exceeded.
- waiting for selector "[data-testid='journal-submit']"
ローカルで再実行すると緑。Playwrightのフレーキー特有のパターンだ。とりあえず Squash and merge で取り込み、フレーキーは別PRで潰すことにした。
PR #15: networkidle → load + expect.toPass の恒久対策
PR #15 でE2Eのフレーキーを根治する。元の実装は await page.waitForLoadState('networkidle') の後に即 click していたが、Nuxt のハイドレートが完了する前に click が走ると要素が登録されておらず空振りする。
Codex GPT-5.5 にレビューさせたら、こう返ってきた。
networkidleは SSE や long-polling があるページで永遠に待つことがある。loadに変えて、その後expect(...).toPass({ timeout: 5000 })で click をリトライする方が確実。
指摘通りに修正。
// Before: ハイドレート前に click が走って空振り
await page.goto('/notes/3kyu/merchandise')
await page.waitForLoadState('networkidle')
await page.click('[data-testid="journal-submit"]')
// After: load + expect.toPass で click 自体を retry
await page.goto('/notes/3kyu/merchandise')
await page.waitForLoadState('load')
await expect(async () => {
await page.click('[data-testid="journal-submit"]', { timeout: 1000 })
}).toPass({ timeout: 5000 })
expect.toPass は中の処理を timeout 内で何度もリトライしてくれる。click が空振りしても次のリトライで拾える。これでローカル20回連続グリーン、CIも安定して通った。
学び
移植は潜在バグを炙り出す。 当期純利益が貸方合計に乗っていなかったバグは、HTML版で別表示にしていたから気づかなかった。Vue化で表示を統合した瞬間に、計算式の欠落が画面の数字として現れた。リファクタリングの副産物として既存バグが発掘されるパターンは、これからも繰り返し起きる。
Write拒否は手動救済の方が早い。 サブエージェントの権限スコープを後から広げて再起動するより、生成済みのコード片を自分で貼り付ける方が3倍速い。9並列のうち2個が落ちても、残り7個の出力を待つ間に手で救済できる。
networkidle は罠。 Nuxt のように継続的にイベントが飛ぶページでは networkidle が永遠に待ち続けることがある。load + expect.toPass の組み合わせを今後のE2Eテンプレに採用する。
reset --soft は命綱。 ブランチ取り違えに気づいた瞬間に --hard を打っていたら午前中の修正がすべて消えた。インデックスだけ巻き戻したいときは必ず --soft を選ぶ。
明日やること
- 残り2章(cash-detail, journal-rules)をVue化して26章コンプリート
-
JournalExample.vueのsectionTotals計算にスナップショットテストを追加して当期純利益バグの再発を防ぐ - E2Eテンプレに
load+expect.toPassパターンをまとめて、他のテストファイルにも横展開 - サブエージェントの権限スコープを章ディレクトリではなく
apps/web/app/components/notes/配下全体に広げて Write 拒否の再発を防ぐ