# Plan: 品牌 × 產品商機雷達

> Status: `approved`  
> Status note: 使用者 2026-08-10「批准」  
> Source: `docs/product/brand-product-radar/spec.md`（approved 2026-08-10）  
> Requirements: `docs/product/brand-product-radar/requirements.md`（approved 2026-08-10）  
> Parent capability: `docs/product/demand-radar/`（58/58 tasks 已完成）  
> Last updated: `2026-08-10`

## 1. 交付策略

1. **先保住既有雷達。** 新欄位與新行為全部向後相容；舊 RadarWatch 讀作 generic、舊 Opportunity 顯示未指定產品。P0 不做猜測回填，也不先改掉既有去重唯一性。
2. **嚴格遵守 A → B → C → D。** Phase A 只鋪契約、索引與相容測試；Phase B 先以 mock 把選產品、證據、多產品去重、全部結果與主推產品操作走通；mock 驗收通過後才寫 Phase C 真 API 業務；Phase D 再把產品歸屬帶進回覆、CRM、成交與品質校準。
3. **先驗證互動，再擴後端。** 多產品合併後的卡片資訊量最大，也是最容易做錯的地方。前端必須先證明使用者能回答「這個人適合哪個產品、為什麼、是否值得現在聯絡」。
4. **擴充既有實體，不平行建系統。** RadarWatch、Opportunity、RadarSweep、Contact、OutcomeEvent 都原地擴充；同 owner＋external identity 的 Opportunity 唯一語意不變。
5. **判定與持久化分層。** 先完成可純測試的產品適配規則，再接 AI；AI 不可用時仍須輸出完整四維理由，否則整筆 match 失敗，不回空成功。
6. **準確度是上線閘門。** P0 live 串接後，以代表性真實樣本檢查前十筆至少八筆同時具備需求與產品適配證據；未達標只回到召回／判定調校，不先擴 P1。

所有 task 預估以 **100–200 行生產碼變更**為主；較大能力在 tasks 階段再拆，避免一題同時跨契約、後端、前端與測試。

## 2. 里程碑

### M0 — Phase A：契約、相容與資料地基

- **目標：** 先讓跨層型別、API 契約、索引方向與舊資料讀法固定，尚未完成的業務明確回未就緒，不產生成功空資料。
- **完成定義：**
  - `generate/api/*.api` 原地擴充 RadarWatch、Opportunity、ProductMatch、Sweep counters、產品篩選與主推產品操作，再執行 `make gen-api`；不手寫 handler／routes。
  - 新增 Brand／Product 關聯與 ProductMatch 查詢所需 migration；保留 owner＋external identity 唯一索引，先以測試資料確認多產品不會產生第二筆 Opportunity。
  - 前端 domain types、Radar repository 介面與錯誤型別對齊 spec；mock／live 共用同一介面。
  - 建立舊 watch／舊 opportunity 相容 fixture：缺新欄位時分別讀為 generic／未指定產品。
  - 新 API 尚未實作時回既有明確 not-ready error；不得讓 goctl 產生的空 logic 回 102000。
- **建議 task 區間：** **T700–T706**
- **驗證：** `make gen-api`、後端 compile、前端 type/build；spec 驗收 #1、#10、#13 的契約層案例。

### M1 — Phase B：產品型雷達設定（frontend + mock）

- **目標：** 不接真 API，先完成「選 Brand → 選 Product → 看資料完整度 → 取得需求詞 → 建立產品型 watch」操作。
- **完成定義：**
  - 新增具狀態的 mock Radar repository，可建立 product／generic watch、一次性補綁、pause／resume／archive，並模擬 ownership、關聯錯誤與 product unavailable。
  - `/app/radar/watches` 加 Brand／Product 串接選擇、產品內容完整度、include／exclude 建議依據、短詞驗證與名稱快照。
  - 新 UI 不提供 generic 新建入口；既有 generic 卡片顯示「補上產品」，補綁後不可更換。
  - product watch 不被 ServiceProfile 必填卡住；generic watch 維持原門檻。
  - `/app/brands` 刪 Product 前，顯示受影響 watch 數量與「將暫停、不刪歷史」的結果。
  - mock contract test 與頁面互動測試覆蓋成功、空品牌、錯誤品牌關係、產品失效及舊資料。
- **建議 task 區間：** **T710–T718**
- **驗證：** spec 驗收 #1、#7、#9、#10、#13 的 mock 場景；前端 build／test／lint／token check。

### M2 — Phase B：商機證據、去重與全部結果（frontend + mock）

- **目標：** 用足量 mock 資料確認「一張商機卡＋多個產品匹配＋一個主推產品」真的看得懂，並容納較舊與低適配結果。
- **完成定義：**
  - `/app/radar/today` 加 Brand／Product／fit band 篩選；卡片顯示主推產品、商機分、產品適配分、需求證據、風險與其他匹配數。
  - 四維 ProductFitReason 可展開；產品篩選時優先展示該產品證據，但不暗中改 PrimaryProduct。
  - 支援自動主推與人工主推 mock；人工指定後，新出現的更高分 match 不覆蓋。
  - 新增 `/app/radar/opportunities` 全部結果：可看 stale／weak／excluded／未指定產品，預設分數排序。
  - Explore／manual import 面板先選 Brand／Product，mock 統計分清新商機、合併 match 與截斷。
  - CRM 卡片／時間軸與商機回覆面板能顯示產品歸屬；尚不改真 CRM 行為。
  - 至少準備：同貼文雙產品、20 天舊文、高分但 excluded、無品牌名但痛點吻合、純產品名命中、舊 generic 六組 fixture。
- **建議 task 區間：** **T720–T729**
- **驗證：** spec 驗收 #2–#6、#8、#11、#14 的 mock 場景；人工 UX 走查可在三分鐘內回答產品與證據。
- **硬閘門：** M2 未經使用者確認操作與資訊密度前，**不得進 M3 大寫業務後端**。

### M3 — Phase C：後端領域與持久化

- **目標：** 建立 product watch、ProductMatch、PrimaryProduct 與舊資料相容的可信領域模型及 repository 行為。
- **完成定義：**
  - RadarWatch 加 context mode、Brand／Product IDs、名稱快照、綁定時間與 pause reason；驗證 generic／product 不變量。
  - Opportunity 加 ProductMatch 集合、PrimaryProduct 與人工 override；`matched_service` 保留給 generic／舊資料。
  - memory／Mongo repository 支援產品篩選、穩定排序、同 ProductMatch 合併 watch／term，以及主推產品 guarded 更新。
  - 既有 owner＋external identity 唯一鍵保留；同 ProductMatch 在競態下不得重複。
  - Brand／Product owner 與父子關係由後端重新驗證，不信任 request IDs。
  - 舊文件不需批次回填即可正常 list／detail／CRM 使用。
- **建議 task 區間：** **T730–T739**
- **驗證：** domain／repository 單元測試；spec 驗收 #1、#4–#7、#10、#12、#13。

### M4 — Phase C：產品上下文、適配判定與原子去重

- **目標：** 把 Brand／Product 真相接入建議詞與判定，並在多 watch／多 worker 下正確合併 ProductMatch。
- **完成定義：**
  - 共用產品上下文讀取器產 snapshot；來源無效時回明確原因，不跨 owner、不自動改綁。
  - 關鍵字建議以品牌受眾、產品痛點、情境、標籤、能力與排除詞產生，回 basis；仍通過 Threads 短詞規則。
  - 產品適配判定完整輸出 pain 35／scenario 25／audience 20／capability 20、四維理由、公開 excerpt、product basis、risks 與 exclude reason。
  - eligible／strong／possible／weak 規則與主推產品自動選擇固定；人工 override 後不自動換主推。
  - 新貼文建立 Opportunity＋ProductMatch；既有貼文新產品只新增 match；同產品再命中只合併來源且不重複計費。
  - stale 仍保存產品證據但不進今日；產品名單純命中不可成為 eligible。
  - AI 不可用時的規則降級仍需四維完整；不完整結果不持久化並記失敗。
- **建議 task 區間：** **T740–T749**
- **驗證：** scorer table tests、AI structured-output tests、repository concurrency tests；spec 驗收 #2–#8、#12。

### M5 — Phase C：每日／立即巡、查詢與產品生命週期 API

- **目標：** 將 M3／M4 接回每日巡、立即探索、手動匯入、today／全部結果與 Product 刪除，形成完整真 API。
- **完成定義：**
  - `radar_sweep` 每次重新驗證 Brand／Product；無效時 guarded 暫停 watch、通知並記原因，不建立成功空 Sweep。
  - 每日、首巡、立即探索、手動匯入共用同一 ProductMatch merge；只有新 Opportunity 占每日筆數，新增 match 不被配額誤算成新卡。
  - today 支援 Brand／Product／fit band；全部結果支援 stale／weak／excluded／generic 與指定排序；filtered product 使用自己的 match 作展示／次排序。
  - 主推產品 API 驗證 match、eligible／override reason、owner 並留 audit。
  - Sweep 回報 match evaluated／merged／fit rejected，Explore 回報 created／merged／truncated，數量語意不混淆。
  - 刪 Product 前冪等暫停相關 active watch；任何步驟失敗不得回刪除成功，歷史保留。
  - product_unavailable watch 不可直接 resume，可複製設定建立新產品 watch。
- **建議 task 區間：** **T750–T759**
- **驗證：** radar／scout logic integration tests；spec 驗收 #4、#8–#13；失敗路徑確認非 102000 空資料。

### M6 — Phase C：live repository 接線與 P0 真實閉環

- **目標：** 用 live repository 替換 Phase B mock 資料，保持畫面與錯誤行為一致，完成可操作的 P0。
- **完成定義：**
  - Radar live mapper 支援所有新增欄位、filters、assign product、primary product、explore／import counters；缺選用欄位不丟失舊資料。
  - M1／M2 頁面切到 live 後操作與 mock 一致；loading、not-ready、forbidden、product unavailable、no eligible match 狀態均有明確 UI。
  - `/app/radar/opportunities` 與 today 的 URL filter／返回狀態穩定，重新整理不改查詢意圖。
  - Product 刪除後 watch 頁立即反映 pause reason；舊 Opportunity 仍顯示快照名稱。
  - 完成跨層 integration：建產品 watch → 巡邏 → 同貼文合併兩 ProductMatch → 改主推 → today／全部結果正確。
- **建議 task 區間：** **T760–T769**
- **驗證：** backend integration＋frontend repository contract／page tests；spec §9 #1–#13 全路徑，前後端 build 綠。

### M7 — Phase D：回覆／CRM 歸屬、品質閘與回歸

- **目標：** 把產品上下文帶完整個成交閉環，驗證準確度、併發、成本與既有功能沒有退化。
- **完成定義：**
  - 新回覆使用 PrimaryProduct snapshot＋正確 Brand＋選用 ServiceProfile 限制；已送出回覆不隨主推產品修改。
  - ContactTouch 保存接觸當下 Brand／Product；同一 Contact 可有不同產品；舊 touch 相容。
  - 成交 OutcomeEvent 保存成交當下 product_id；舊 outcome 可空；不新增成交帳與自動成交判定。
  - 建立代表性人工樣本集，記錄需求證據、產品適配與誤判原因；高排名前十至少八筆合格才通過。
  - 多 worker／重試測試證明單 Opportunity、單 ProductMatch、不重複 AI 計費；Sweep counters 可對帳。
  - 回歸 generic radar、Scout 三模式、Outbox、AccountHealth、CRM、成果歸因、方案配額與舊資料。
  - 更新 testing／gaps 文件，明列 P1 回饋學習、產品轉換統計與 P2 多產品 watch 未做。
- **建議 task 區間：** **T770–T779**
- **驗證：** spec 驗收 #4、#12、#14；成功標準全表；全 repo backend／frontend tests、build、lint、token check。

## 3. 依賴與硬閘門

```text
M0 Phase A 契約地基
 └─► M1 Phase B watch mock
      └─► M2 Phase B results mock ──【使用者 UX 批准】
           └─► M3 Phase C domain/repository
                └─► M4 Phase C product fit/merge
                     └─► M5 Phase C pipeline/API
                          └─► M6 Phase C live UI ──【P0 live 驗收】
                               └─► M7 Phase D CRM/品質/回歸
```

| 關係 | 原因 |
|------|------|
| M0 → M1 | mock 必須依批准後的共用型別與錯誤契約，不自行發明另一套 shape |
| M1 → M2 | 結果 fixture 必須來自可選定的 Brand／Product 與 watch 上下文 |
| M2 → M3 | **硬性：** 前端＋mock 操作未通過，不大寫業務後端 |
| M3 → M4 | 適配判定需要可原子保存、合併與查詢的 ProductMatch 模型 |
| M4 → M5 | 每日／立即巡必須共用已驗證的 fit 與 merge，不在 handler 重寫判定 |
| M5 → M6 | live 前端需有完整 today／list／primary／lifecycle API |
| M6 → M7 | CRM 與回覆深化建立在已可操作的 P0 商機閉環上 |
| M5 → Product CRUD | 刪 Product 的 watch 暫停副作用需要 RadarWatch repository 能力 |
| M7 → 既有 OutcomeEvent | 只加產品歸屬，不新增成交資料來源 |

## 4. 風險與緩解

| 風險 | 緩解 |
|------|------|
| 多產品卡片資訊過載 | M2 先以 mock 驗證；卡片只露一段主要證據，四維與其他產品按需展開；未通過 UX 閘不進後端 |
| 同貼文競態產生重複 ProductMatch | 保留 Opportunity 唯一鍵；M3 repository 定義原子 merge 與 guarded primary；M4／M7 加多 worker 測試 |
| ProductMatch 陣列查詢／排序變慢 | M0 先驗證索引與代表性資料 explain；列表只回分頁；必要的主要產品欄位獨立保存，不在 request 逐筆掃全品牌 |
| Product 更新後歷史漂移 | match 保存名稱與版本快照；舊 match 不重算；新 Opportunity 才讀最新版 |
| Product 刪除只做一半 | 採可重試的「先暫停 watch，再刪 Product」；部分失敗不回成功，重試不重複副作用 |
| 跨 owner ID 洩漏 | 每個入口都從 JWT owner 重查 Brand／Product；錯誤不回對方名稱；M3/M5 有越權 integration test |
| AI 用關鍵字腦補成受眾 | 正分必須有公開 excerpt 與 product basis；未知受眾給 0；禁止敏感屬性推測 |
| 搜尋量變多但品質變差 | 搜尋詞只負責召回；eligible 必須有痛點／情境證據；M7 用前十至少八筆的人工樣本閘門 |
| stale 全被丟掉又「完全找不到」 | stale 保存並進全部結果，今日頁維持及時性；空狀態連到全部結果 |
| 每日上限導致既有貼文無法補 ProductMatch | 配額只限制新 Opportunity；既有卡片新增 match 仍可處理，AI 點數 gate 另算 |
| goctl 產出空 logic 被誤當成功 | M0 每個新增能力先回明確 not-ready；業務只落 `internal/logic/**`／`internal/module/**`，不手寫 handler／routes |
| Phase B mock 與 live 漂移 | 同一 repository interface＋contract tests；M6 同一組 fixture 對 mock／live mapper 驗證 |

## 5. 刻意不做（本 plan 週期內）

- Requirements P1：用適合／不適合、CRM 行為自動調整產品詞權重。
- Requirements P1：產品維度漏斗、成交率／金額比較與自動調校建議。
- Requirements P2：一個 watch 同時服務多個產品、產品專屬理想使用者欄位、跨來源擴張。
- 批次猜測舊 watch／opportunity 的 Brand／Product 關聯。
- 因 Product 更新而批次重算舊商機。
- 自動留言、私訊、自動改主推產品的人工 override、自動成交。
- 重做 Brand／Product 編輯器、Radar 排程、Threads 抓取、CRM 狀態機、計費或 AI provider 抽象。
- 新增 usage meter；產品適配沿用 `ai_research`＋`source=radar.product_fit`。

## 6. 驗證策略與指令

### 6.1 每個 milestone 都要跑

```bash
cd apps/backend
go test ./internal/module/radar/... ./internal/module/scout/... ./internal/logic/radar/ -count=1

cd apps/web
npm run test
npm run build
npm run lint
npm run check:tokens
```

### 6.2 契約或 API 變更後

```bash
cd apps/backend
make gen-api
go test ./... -count=1
make build
```

### 6.3 Phase C／D 整合閘

```bash
cd apps/backend
make test-integration
go test ./internal/module/radar/... -race -count=1

cd apps/web
npm run test -- RadarWatchesPage RadarTodayPage
```

### 6.4 Migration 驗證

只對獨立測試資料庫執行，不使用預設開發資料庫：

```bash
cd apps/backend
RADAR_TEST_MONGO_URL=mongodb://127.0.0.1:27017/thread_master_brand_product_test
MONGO_URL="$RADAR_TEST_MONGO_URL" make migrate-up
MONGO_URL="$RADAR_TEST_MONGO_URL" make init
```

### 6.5 里程碑驗收摘要

| M | 額外驗收 |
|---|----------|
| M0 | 舊 fixture 無新欄位仍可解碼；新增未實作 API 不回 102000 空資料；migration 可重複初始化 |
| M1 | product watch 不需 ServiceProfile；generic 仍維持舊門檻；錯誤產品關係在 mock 明確失敗 |
| M2 | 同貼文雙產品只一張卡；filter 展示指定產品但不改 primary；stale 可在全部結果找到；完成 UX 批准 |
| M3 | memory／Mongo 行為相同；競態只有一筆 Opportunity 與一個同產品 match；舊資料不回填也可用 |
| M4 | 四維理由完整；純產品名命中不 eligible；同產品重命中不重判／重計費 |
| M5 | 每日／explore／import 數量可對帳；Product 刪除暫停 watch；失敗不空成功 |
| M6 | mock 與 live 主要行為一致；跨層 P0 流程可展示；spec #1–#13 全過 |
| M7 | 前十至少八筆合格；回覆／ContactTouch／OutcomeEvent 產品歸屬正確；全回歸綠 |

## 7. Task 號段總表（供下一階段拆 task）

| 區間 | Phase / Milestone | 內容 |
|------|-------------------|------|
| T700–T706 | A / M0 | goctl 契約、migration／索引、FE types／repo、舊資料 fixture、not-ready guard |
| T710–T718 | B / M1 | mock Radar repo、Brand／Product 選擇、完整度、建議詞、generic 補綁、刪除影響 UI |
| T720–T729 | B / M2 | today 產品卡、四維證據、多產品／primary、全部結果、explore／import、CRM 標示、UX tests |
| T730–T739 | C / M3 | Watch／Opportunity／ProductMatch domain、memory／Mongo repository、ownership、相容讀取 |
| T740–T749 | C / M4 | context snapshot、建議詞、fit scorer、AI／fallback、atomic merge、primary arbitration、stale |
| T750–T759 | C / M5 | sweep／explore／import、today／list filters、primary API、counters、Product delete pause |
| T760–T769 | C / M6 | live repository／mapper、頁面接線、錯誤狀態、跨層 integration、P0 live 驗收 |
| T770–T779 | D / M7 | reply、ContactTouch、OutcomeEvent 產品歸屬、準確度、race／成本、回歸與缺口文件 |

正式拆 tasks 時，每一題都必須包含 Goal、Depends on、Inputs、Outputs（實際路徑＋行為）、Out of scope、Acceptance commands、對應 spec 驗收編號；不得把整個 milestone 塞成一題。

## 8. 批准

- [x] Plan 已批准（2026-08-10／使用者「批准」）
