# MECE横断監査レポート — デグレ/技術負債 観点（2026-06-29）

**作成ブランチ**: `claude/content-verification-undx76`
**目的**: 「デグレ無し・技術負債なし」の観点で、コードベース全体を **MECE な6観点**で監査した結果。各指摘は `file:line` の証拠付き。別エンジニアが Cursor に本ファイルを読ませて確認・修正に着手できるよう構成。
**前提**: 実コードは未変更（本書は調査結果の提示）。`vendor/` 不在環境のため `php artisan route:list` 等は静的解析で代替。**[要再現確認]** は本番DB等の実環境で最終確認が必要なもの。

## このファイルの使い方（Cursor連携）
1. 別エンジニアが本ファイルの GitHub URL を Cursor に渡す。
2. 末尾の「**Cursor 再チェック用プロンプト**」を Cursor に貼り、各指摘を実コードで再検証 → 採否判断。
3. 採用分から修正。修正後は `docs/funnel-spec.md`／§11 チェックリストを更新（リポジトリ運用ルール）。

---

## 0. 重大度別サマリ（先に直すべき順）

### 🔴 HIGH（デグレ／実害・優先対応）
| ID | 観点 | 一言 | 場所 | 確証 |
|---|---|---|---|---|
| H1 | パリティ | **公開ページのセクション余白が保存後に消える**。公開レンダラ `preview.blade.php` は section の `margin_*` を読まず `padding_*` のみ。エディタは読込時に padding→margin 移行＋padding空に → 保存で公開の縦余白が崩れる | `preview.blade.php:268-275`（padding のみ）／`editor.blade.php:1402-1443`(migrateSectionPaddingToMargin) | ✅本レビューで裏取り |
| H2 | ルート | **未定義ルート参照で500**。`password.update/confirm/request/email`・`verification.resend` が未定義。該当 auth ビューが描画されると RouteNotFound | `resources/views/auth/passwords/{reset,confirm,email}.blade.php`・`auth/verify.blade.php` | ✅監査で確認 |
| H3 | DB | **`partners`/`partner_campaigns` のマイグレーションが存在しない**。`/partner` は新規DB/復元で起動時クラッシュ | `routes/web.php:309-311`＋`app/Models/Partner*.php`（create migration 皆無） | ✅本レビューで裏取り（本番に手動作成の可能性は[要再現確認]） |
| H4 | セキュリティ | **SSRF**。画像ミラーが取り込みHTML内の画像URLを検証なしGET。内部IP/クラウドメタデータ(169.254.169.254)到達可。DNS解決後IP検証・リダイレクト再検証なし | `FunnelPageImportImageMirrorService.php:266-274`／`FunnelPageImportUrlValidator.php:42-57` | ✅監査で確認 |
| H5 | セキュリティ | **秘密情報の平文保存**。`DeliveryAccount` の `smtp_pass`・`line_channel_secret`・`line_login_channel_secret`・`line_access_token` に encrypted cast なし | `app/Models/DeliveryAccount.php`（$casts） | ✅監査で確認 |
| H6 | テスト/運用 | **本番DB破壊リスク**。`phpunit.xml` でテストDBが分離されておらず `.env` のMySQLを向く（sqlite無効）。`RefreshDatabase` 混入で全テーブル消失（CLAUDE.mdに過去事故記載） | `phpunit.xml:24-26` | ✅監査で確認 |
| H7 | 運用/冪等 | **決済・Webhookの二重実行耐性なし**。①購入 `markPaid` がロック/トランザクション無しで二重付与し得る ②LINE Webテに冪等キー無し（重複INSERT＋auto-reply二重） ③`scenario_mail_deliveries` の updateOrCreate キーに unique/index 無し（二重cronで重複行） | `OrderPaymentFulfillmentService.php:17-27`／`Api/LineWebhookController.php:71-74`／`MailDeliveryStateService.php:90-94,193-197` | ✅監査で確認 |
| H8 | 性能 | **会員ダッシュボードN+1**。コース毎に `memberHasAccess`/`progressSummary`/`isCompleted` をループ発行。コース・受講生増で線形劣化 | `BundleCourseOfferService.php:99-100,218`／`MemberPortalController.php:710,774` | ✅監査で確認 |

### 🟠 MEDIUM（負債・計画的対応）
| ID | 観点 | 一言 | 場所 |
|---|---|---|---|
| M1 | パリティ | preview スタイルが3実装に分裂（`preview.blade.php`=実体／`_preview_sections.blade.php`=死蔵／`preview.blade.php.new`=残骸）。§6不変条件のコメントが実レンダラを指していない | `funnel/page/preview.blade.php` 他 |
| M2 | パリティ | セクション背景 `w100` の background-position が editor=center / 公開=left top で乖離（取込ロゴ/ヒーローでズレ） | `editor.blade.php:1465,6906` vs `FunnelSectionBackgroundStyle.php:35,60` |
| M3 | パリティ | テキスト系デフォルト値が editor/公開で不一致（headline 既定: 2rem/700/#222 vs 36px/600/#212529）。プロパティ欠落の取込ページで食い違い | `editor.blade.php:6149-6154` vs `_preview_element.blade.php:130,143` |
| M4 | パリティ | SP上書きのカバレッジ差（公開は `padding_*_sp`・`br sp-hidden` も出力、editorは font-size のみ） | `editor.blade.php:2076-2093` vs `_text_sp_font_styles.blade.php:53-82` |
| M5 | DB | FK型不整合6件（`scenario_id`/`site_id` が bigint だが参照先は INT 等）。FK未付与で現状migrateは通るが将来FK追加で詰まる | `scenario_labels`/`scenario_line_auto_replies`/`membership_site_lesson_comments`/`membership_site_labels`/`member_lesson_completions` の各create |
| M6 | セキュリティ | テレコムwebhookに署名/HMAC検証なし（token＋clientip照合のみ、clientip空ならスキップ）。token漏洩で偽決済通知の余地 | `TelecomCreditWebhookService.php:42-51,76-82` |
| M7 | セキュリティ | UTAGE/URL取り込みHTML/CSSのサニタイズ不完全（`on*`属性・`javascript:`・`</style>`ブレイクアウト残存）。取込元は半信頼の外部サイト | `FunnelPageUrlImportService.php:256-270`／`preview.blade.php`(custom_css raw) |
| M8 | セキュリティ | `access_code`(契約閲覧PW) 平文。`User`/`Operator` の `$fillable` に管理系カラム（plan/status/perm_*）含む（現状実害なし・将来事故予防） | `ContractInstanceSigner`／`User.php`／`Operator.php` |
| M9 | デッドコード | `AbTestController`(8メソッド)＋`AbTestVariant`/`AbTestSession` モデルが死蔵（対応テーブルも無し）。A/Bは `FunnelPageController@abtest` のスタブ | `app/Http/Controllers/AbTestController.php` |
| M10 | 巨大ファイル | `editor.blade.php` **7,568行**／`UtagePageImportService.php` **3,077行**／`ContractInstanceController.php` **1,192行**。スタイル計算のJS手書き複製が上記パリティ乖離の温床 | 各ファイル |
| M11 | 性能 | `subscribers` の `LOWER(email) NOT LIKE '%@line.local'` はindex無効でフルスキャン。読者一覧検索も前後一致OR | `Subscriber.php:48`／`SubscriberController.php:27-31` |
| M12 | 運用 | cron(`schedule:run`)/`queue:work` 停止時に配信・リマインダ・付与が無言で停止（死活監視なし）。`Cache::lock` がドライバ依存（array/multi-nodeで二重送信） | `Kernel.php:15-16`／各Service |

### ⚪ 衛生（リスク小・確実に整理可）
| ID | 一言 | 場所 |
|---|---|---|
| C1 | アプリの**二重コピー** `_deliveries/`（30ファイル・古い `edit.blade.php`/`FunnelComment.php` 等含む）がコミット済み | `_deliveries/` |
| C2 | ビルド成果物 `utage-align-changes-20260327.tar.gz` が**ルートと_deliveries内で二重コミット**／壊れた絶対symlink `_deliveries/latest.tar.gz` | ルート・`_deliveries/` |
| C3 | 旧エディタ `funnel/page-editor.blade.php`（96KB・死蔵、`media.list` 未定義参照含む） | `resources/views/funnel/page-editor.blade.php` |
| C4 | 死蔵ビュー: `_preview_sections.blade.php`／`preview.blade.php.new`／`event/show.blade.php`／`member/partials/_course_thumbnail.blade.php` | 各 |
| C5 | 作業文書 `manus_report.md`・**0バイト空ファイル「メール管理」**がコミット済み | ルート |
| C6 | `.gitignore` 不足: `_deliveries/`・`*.tar.gz` 未除外 | `.gitignore` |

> **TODO/FIXMEマーカーは実質ゼロ**、`.env`/`vendor`/`node_modules` の誤コミットは無し、`$request->all()` 一括代入は0件、公開ルートの認可（`/p`・`/member`・`/buy`・`/contract`）は概ね良好 — これらは健全。

---

## 1. パリティ（エディタ↔プレビュー↔公開） — デグレ温床

`docs/funnel-spec.md §6` は「`editor.blade.php` と `_preview_sections.blade.php` は同じロジック」を不変条件としているが、**実際に公開・管理プレビューで描画されるのは `preview.blade.php`**（`FunnelPageController:458`・`PublicFunnelPageController:36`・`PublicCustomDomainRootController:82`）であり、`_preview_sections.blade.php` は**どこからも live 参照されていない死蔵**。＝不変条件の前提が崩れている。

- **H1（最重要）**: `preview.blade.php` のセクション余白は `padding_top/bottom/side`（+legacy `padding`）のみ（:268-275）。`margin_*` を出力しない。エディタは読込時 `migrateSectionPaddingToMargin` で padding→margin に移し padding を空にする（:1402-1443）。→ legacy/取込ページや保存後のページで **editor=margin / 公開=padding(=0)** となり、**公開ページのセクション縦余白が消失**。
  - 対応案: `preview.blade.php` のセクション div に margin 出力を追加（暫定）／恒久的には公開も `_preview_sections` を `@include` して**単一実装化**。
- **M1-M4**: 上表の通り（3実装分裂・w100位置・タイポ既定値・SPカバレッジ）。
- **page_content 正規化の未保存リスク**: `card_blocks` を持つUTAGE取込ページは editor が複数セクションへ展開、公開は `utage-card-stack` 1セクションで描画。**取込直後〜初回保存まで構造が食い違う**。

---

## 2. ルート/参照整合性

- **H2**: `password.*`・`verification.resend` 未定義（`Auth::routes()` 不使用）。auth配下ビューが残っているため、表示されれば500。**使うならルート定義／使わないならビュー削除**。
- M9関連: `funnel.page.abtest.variant.update` 未定義は死蔵 `AbTestController`/`abtest_edit.blade.php` 起因。
- C3: 旧 `page-editor.blade.php` 内 `media.list`（正は `media.api.list`）未定義 — ただし死蔵ファイル。
- **健全**: 現行 `editor.blade.php` の fetch 系（`media.api.*`・`funnel.page.update`・`scenario-usage-check`・`*-registration-routes`）は全て定義済みに解決。ルート→コントローラメソッドの欠落は抜き取り範囲で無し。
- [要確認]: `site.course.show`・`account.show/destroy`・各 `group.*` は静的に参照0だが、JS動的URL組み立ての可能性があり**未確認**（実機確認推奨）。

---

## 3. DB/マイグレーション整合

- **H3**: `partners`/`partner_campaigns` の create マイグレーションが**リポジトリに皆無**（baseline含む）。`/partner` 機能は空DB/復元で確実に落ちる。**[要再現確認]**: 本番に手動作成された可能性。リポジトリ正本化のため create マイグレーションを新設すべき。
- **H7-③**: `scenario_mail_deliveries` の `updateOrCreate` キー `(source_type,subscriber_id,step_message_id)` / `(…,scenario_broadcast_id)` に **unique/index 無し** → 二重cronで重複行＋検索フルスキャン。
- **M5**: FK型不整合6件（`scenario_labels`/`scenario_line_auto_replies` の `scenario_id`=bigint vs `scenarios.id`=INT 等）。FK未付与で現状OKだが、`events.scenario_id` を後からINTにMODIFY+FK付与した前例（`2026_05_12_120025`）どおり、将来必ず詰まる。型を揃えるのが安全。
- **健全**: hasTable/hasColumnガードは baseline 以降で一貫し空DBで通る。FK制約付きの主要キーは型一致。

---

## 4. セキュリティ

- **H4（SSRF・実害）**: `FunnelPageImportImageMirrorService::downloadAndStore`(:266-274) が画像URLを検証なしGET（弾くのは data:/blob:/storage: のみ）。`FunnelPageImportUrlValidator`(:42-57) もDNS解決後IP検証なし＝内部IPを指すドメインは素通り。両経路でリダイレクト毎の再検証なし。→ **DNS解決後の全IP判定＋IPピン留め＋リダイレクト再検証の共通fetchラッパ**を新設し両経路へ。
- **H5（秘密情報・実害）**: `DeliveryAccount` のSMTP/LINEシークレットが平文。→ `$casts` に `encrypted` 追加＋既存行の再暗号化マイグレーション。
- **M6**: テレコムwebhook署名検証なし＋clientip空スキップ。→ 署名 or 送信元IP allowlist、空スキップ廃止。
- **M7**: UTAGE/URL取り込みHTML/CSSのホワイトリストサニタイズ（`on*`・`javascript:`・`</style>`中和）。
- **M8**: access_code 暗号化、User/Operator の `$fillable` 整理。
- **許容（管理者信頼前提）**: 会員レッスン本文・お知らせ・固定ページ・フォーム設定の raw 出力（運営者入力）。訪問者/会員/LINE由来の入力は全て `nl2br(e())` でエスケープ済み（安全）。LINE webhook署名検証あり（良好）。

---

## 5. テスト/運用/性能

- **H6**: `phpunit.xml` のDB未分離（最優先・構造的事故防止）。→ `DB_CONNECTION=sqlite`/`:memory:` を有効化。
- **H7-①②**: 購入 `markPaid` を `lockForUpdate`+`DB::transaction` で囲う。LINE webhook に冪等キー（event_id unique）＋処理済み判定。
- **H8/M11**: 会員ダッシュN+1 の一括取得化、`@line.local` 判定のフラグカラム化。
- **テスト空白（負債）**: 購入Webhook→付与、契約フロー、会員ログイン/ドリップ、LINE Webhook受信、ステップ/一斉送信パイプライン に Featureテスト無し。UTAGE取り込みのみ手厚い（偏り）。
- **M12（運用）**: cron/worker 死活監視（最終実行時刻ヘルスチェック）、`Cache::lock` をDB/redisストアに固定。
- **良い例（参考）**: リマインダは `scenario_reminder_id` で `alreadyHandled` 冪等。

---

## 6. デッドコード/重複/衛生

- 即削除可（確証高・低リスク）: `funnel/page-editor.blade.php`・`event/show.blade.php`・`member/partials/_course_thumbnail.blade.php`・`preview.blade.php.new`・`_preview_sections.blade.php`（※H1の一本化と併せて判断）・ルート2つの `*.tar.gz`・`_deliveries/`・`manus_report.md`・空ファイル「メール管理」。
- 方針確認のうえ: `AbTestController`＋`AbTestVariant`/`AbTestSession`＋`FunnelPage` relation（A/B不採用なら）。
- `.gitignore` 追記: `_deliveries/`・`*.tar.gz`。
- 重複の一本化（中期）: セクションスタイル算出のJS(editor)/PHP(Support)二重実装。

---

## 推奨の取り込み順

1. **構造的事故の封じ込め（即）**: H6（phpunit DB分離）。誤実行での本番破壊を先に止める。
2. **公開デグレの解消**: H1（公開セクション余白）→ H2（auth未定義ルート）→ H3（partnersマイグレーション）。ユーザー影響/起動クラッシュ系。
3. **実害セキュリティ**: H4（SSRF）→ H5（シークレット暗号化）→ M6（webhook署名）。
4. **冪等性/整合**: H7（決済/Webhook二重実行・mail_deliveries index）→ M5（FK型）。
5. **性能**: H8/M11（N+1・LIKE）。
6. **衛生/デッドコード**: C1-C6・M9（低リスク・まとめて）。
7. **中期リファクタ**: M10（巨大ファイル分割）→ パリティ単一実装化（M1-M4）。

> 各項目とも、修正時は該当の `docs/funnel-spec.md` 節と §11 受け入れチェックリストを更新する（リポジトリ運用ルール §12）。

---

## Cursor 再チェック用プロンプト（コピペ可）

> 以下を Cursor のチャットに貼り、本リポジトリに対して実行してください。各指摘を**実コードで再検証**し、誤検出を除外したうえで修正方針を提案させます。

```
あなたはこのLaravelリポジトリのレビュアです。docs/audit-degradation-techdebt-20260629.md の各指摘(H1..H8, M1..M12, C1..C6)を、実コードを開いて1件ずつ検証してください。各件について:
1) 指摘が現在のコードで再現するか（該当 file:line を引用）
2) [要再現確認] や [未確認] の項目は、確認手順（実行すべきartisanコマンドやgrep）を提示
3) 真であれば最小diffの修正方針、偽なら誤検出の根拠
を表で出力。特に次のHIGHを最優先で検証:
- H1: resources/views/funnel/page/preview.blade.php がセクションの margin_top/bottom を出力するか（padding のみでないか）。editor.blade.php の migrateSectionPaddingToMargin と突合。
- H2: password.update/confirm/request/email・verification.resend のルート定義有無（routes/*・RouteServiceProvider・Auth::routes）。
- H3: database/migrations に partners / partner_campaigns の Schema::create があるか。
- H4: FunnelPageImportImageMirrorService の画像取得が内部IP/DNS解決後IPを検証するか。
- H6: phpunit.xml がテスト用DB(sqlite/:memory:)に分離されているか。
- H7: OrderPaymentFulfillmentService::markPaid のロック/トランザクション、LINE webhookの冪等キー、scenario_mail_deliveries のunique/index。
修正に着手する場合は1件ずつ別コミットにし、docs/funnel-spec.md と §11 チェックリストも更新すること。
```

---

*本書は静的解析と一部実コード確認に基づく。HIGH項目のうち H1/H3 は本調査で直接裏取り済み。`[要再現確認]` は本番/実DB環境での最終確認を要する。*
