# Spec: 商機工作收件匣與產品需求搜尋

> Status: `approved`
> Status note: 使用者 2026-08-11「批准」
> Source: `docs/product/opportunity-inbox/requirements.md`（approved 2026-08-11）
> Parent specs: `docs/product/demand-radar/spec.md`、`docs/product/brand-product-radar/spec.md`
> Last updated: `2026-08-12`

## 1. 摘要

本規格把現有「今日商機」與「全部結果」合併成一個商機工作收件匣，以獨立的 `review_state` 管理待處理、已完成與已移除，不改變既有加入 CRM、判定合格或拒絕等業務狀態。移除是可復原的軟移除，且保留來源 tombstone，確保下一次巡邏不會把同一內容重新建立成新商機。

搜尋改為「產品真相 → 需求地圖 → 多組使用者語言 → 去重與前處理 → 輕量排序 → AI 深度判定」。預設推薦分數以產品痛點適配為主，另支援來源發布時間等排序。所有付費動作都沿用既有 meter、平台代付與 BYOK 規則，加入執行前預估、每次巡邏成本上限、階段用量明細，以及同候選同產品輸入版本不重複判定與扣點的契約。

## 2. 名詞與實體

| 名詞 | 定義 |
|------|------|
| **OpportunityStatus** | 既有業務／判定狀態：`judging / qualified / rejected / accepted / dismissed`；`accepted` 仍表示已加入 CRM |
| **ReviewState** | 新增的工作收件匣狀態：`pending / completed / removed`，與 OpportunityStatus 分開 |
| **Soft removal** | 將 ReviewState 設為 `removed` 並保存原因、時間與操作者；資料與來源識別仍保留 |
| **Source tombstone** | 已移除商機保留的 owner＋來源唯一識別；後續命中只合併來源，不建立新卡、不自動恢復 |
| **DemandMap** | 一個產品可檢查的需求地圖：困擾語言、情境、期望結果、解法意圖與排除訊號 |
| **DemandInputVersion** | 由受眾、痛點、情境、能力、匹配與排除內容決定的產品搜尋輸入版本；純名稱或顯示欄位修改不改版 |
| **DemandMapVersion** | 需求地圖每次有效儲存的版本，供搜尋計畫與判定防重使用 |
| **QueryPlan** | 從目前 DemandMap 產生、符合來源短詞限制的多組搜尋語言與排除語言 |
| **CandidateDisposition** | 前處理結果：`pass / review / reject`；reject 不進 AI 深判，review 可低順位保留或抽樣深判 |
| **DemandEvidence** | 候選原文中表達問題、求助、求推薦、比較或想達成結果的短片段 |
| **PriorityScore** | `priority_score`，0–100；決定收件匣推薦順序，與既有 `intent_score`、`product_fit_score` 分開 |
| **Billable action** | 會使用平台代付搜尋或 AI provider 的動作；純 UI、規則前處理與已保存結果讀取不是付費動作 |
| **CostPreview** | 付費動作執行前的點數估計，含固定最低值、可能最高值、BYOK 模式及估計依據 |
| **CreditCeiling** | 單次手動工作或每日自動巡邏最多可新增消耗的平台點數；不得超額後再通知 |

## 3. 狀態機與不變量

### 3.1 工作收件匣狀態

```text
pending   --> completed : 使用者標示完成，或成功加入 CRM
completed --> pending   : 使用者恢復待處理
pending   --> removed   : 使用者移除並選原因
completed --> removed   : 使用者移除並選原因
removed   --> pending|completed : 復原到移除前狀態
```

- 每筆 Opportunity 恰有一個 ReviewState。
- `completed` 只代表使用者已處理，不代表適合／成交／加入 CRM，不形成負向訓練訊號。
- `removed` 必須保存 `removal_reason`；`other` 必須另有非空白說明。原因 enum：`pain_mismatch / provider_or_ad / stale / already_solved / duplicate / other / legacy_unknown`。
- 進入 `removed` 時保存 `previous_review_state`、`removed_at`、`removed_by`；復原回到 `previous_review_state`，缺值時回 `pending`，並保存最近一次移除紀錄供稽核。
- 更新成目前相同狀態是 idempotent success，不重複寫操作、不產生用量。
- ReviewState 不得直接改寫 OpportunityStatus。既有 `accept` 成功後仍設 `accepted`、綁 Contact，另外把 ReviewState 設為 `completed`。
- 舊 `dismiss` 行為保留相容，但改為設定 OpportunityStatus=`dismissed` 且 ReviewState=`removed`；reason 空白的舊呼叫保存 `legacy_unknown`。

### 3.2 舊資料讀取規則

缺 `review_state` 的既有 Opportunity 依下列順序讀取：

```text
status=accepted  -> completed
status=dismissed -> removed(removal_reason=legacy_unknown)
其他             -> pending
```

- 第一次狀態寫入時持久化映射結果；列表讀取不可要求一次性全庫遷移才可用。
- `dismissed` 舊資料復原時只恢復 ReviewState；既有 OpportunityStatus 保持 `dismissed`，避免竄改歷史業務狀態。若需要重新成為 qualified，仍走既有人工 override。

### 3.3 去重與 tombstone

- 沿用「同 owner＋同來源內容識別只一筆 Opportunity」。來源識別優先用正規化 external ID，其次正規化 permalink。
- 查到既有 `removed` 商機時，只合併新的 watch／term／產品匹配來源與最近命中時間；不得建立新 Opportunity、不得自動改回 pending、不得重複深判。
- `duplicate` 移除原因可保存 `duplicate_of_opportunity_id`；被指向的主卡不存在或不屬於同 owner 時拒絕操作。
- tombstone 沿用既有資料保留政策；P0 不提供不可逆硬刪除 API。

## 4. 搜尋、前處理與排名契約

### 4.1 DemandMap 結構

每個產品目前版本只有一份生效 DemandMap：

```text
DemandMap = {
  product_id, demand_input_version, map_version, state,
  pain_phrases[], scenario_phrases[], desired_outcomes[],
  solution_signals[], exclusion_signals[],
  source_basis[], custom_phrases[], ai_enriched_at?, updated_at
}
```

- `state` 為 `ready / incomplete / stale`。
- 每個 phrase 保存 `text`、`kind`、`basis_kind`、`basis_text`、`origin=product|user|ai`、`enabled`；不得只保存無法追溯來源的模型輸出。
- 系統從 Brand／Product 真相建立免費 baseline；不得為了開頁或每日巡邏自動呼叫 AI。
- 使用者手動編輯與啟停 phrase 不扣點，每次有效儲存增加 map_version。
- AI enrichment 是明確的手動付費動作；成功後新增 AI phrases 並保存來源依據，不覆寫使用者自訂內容。
- 產品受眾、痛點、情境、能力、匹配或排除內容實質變更時，DemandInputVersion 改變；舊 AI enrichment 失效，免費 baseline 以新產品真相重建，使用者自訂內容保留但標示待確認。
- 純品牌／產品名稱、圖片或顯示順序修改不改 DemandInputVersion，也不允許重新扣同候選深判點數。
- baseline 缺少痛點且缺少情境／期望結果時為 `incomplete`；可保存但不能啟動新的產品巡邏，畫面引導補產品資料。

### 4.2 QueryPlan

1. 只從 enabled DemandMap phrases 產生符合 Threads 短詞契約的 1–6 組查詢；每組至少包含困擾／情境／期望結果之一，產品名不得單獨成組。
2. solution signals 用於提升求助、求推薦、比較與購買意圖召回；exclusion signals 傳給既有排除流程。
3. 同義表達分散在不同查詢組，結果在 owner 層統一去重；增加 query 組數不得繞過單次 CostPreview 與 CreditCeiling。
4. 每次 Sweep 保存實際使用的 demand_input_version、map_version 與 query plan 摘要，使結果可重現。
5. 使用者手動修改 watch terms 時仍可使用，但 UI 顯示它們與 DemandMap 的關聯；純產品名 term 只能輔助召回，不能提供排名證據。

### 4.3 候選前處理

候選依序經過下列不呼叫 AI provider 的階段；每階段保存計數與 machine reason：

1. **正規化與去重：** 正規化 external ID／permalink，同一來源只保留資訊最完整的一筆。
2. **基本有效性：** 必須有可用文字與來源；語言不支援、時間未知不直接 reject，但留下風險。
3. **硬排除：** 命中產品排除訊號，或明確為同業供給、廣告、品牌公告、抽獎／互追雜訊時 disposition=`reject`。
4. **證據抽取：** 從原文擷取 DemandEvidence；找不到任何問題、求助、比較、求推薦或期望結果訊號時 disposition 至少降為 `review`。
5. **產品證據：** 用 DemandMap 對困擾、情境、期望結果做輕量比對；產品名／品牌名命中不計分。
6. **分流：** 同時具備 DemandEvidence 與至少一項產品證據為 `pass`；只有其中一項為 `review`；兩者皆無為 `reject`。

- reject 候選不進 AI 深度判定，也不建立高／中／低商機；Sweep 保留原因計數，不保存作者敏感資訊的額外副本。
- review 候選可按輕量分數進入低順位結果或在 CreditCeiling 內抽樣深判；不得標為高順位。
- 已存在且已有同 DemandInputVersion＋DemandMapVersion 有效判定的候選直接復用，不再呼叫 AI。

### 4.4 AI 深度判定

- 只對 `pass` 的高價值候選及部分 `review` 不確定候選執行，順序以輕量產品證據、需求意圖與新鮮度排列。
- 輸出必須包含結構化 DemandEvidence、產品適配證據、排除風險與各分數；所有正向重要理由都須引用候選原文片段及 DemandMap／產品依據。
- AI 不得用品牌名、產品名或搜尋詞本身補成痛點證據；不得推測作者未公開的身分、預算或敏感屬性。
- 結構不完整、證據無法在原文找到或產品依據不存在時，該判定失敗且不得保存為有效結果；工作可在剩餘 CreditCeiling 內繼續下一筆。
- 判定結果為不適合但 provider 已成功完成有效結構化工作，仍正常計點並保存拒絕原因，避免日後重判重扣。

### 4.5 推薦分數與硬門檻

```text
priority_score = pain_fit(0..45)
               + demand_intent(0..25)
               + evidence_quality(0..15)
               + freshness(0..15)
```

| 因子 | 行為定義 |
|------|----------|
| `pain_fit` | 原文對 Product 痛點、情境或期望結果的適配；至少一項有可引用證據才可大於 0 |
| `demand_intent` | 是否正在求助、求推薦、比較方案、表達想改善或準備採取行動 |
| `evidence_quality` | 引文是否直接、完整且同時支持需求與產品適配；只有推測時為 0 |
| `freshness` | 沿用來源 posted_at 時效衰減；未知時間為 0，不得視為最新 |

```text
priority_band = high   if score >= 75 and hard gates pass
                review if score 45..74 and hard gates pass
                low    otherwise but has basic demand + product evidence
```

高順位 hard gates：

- 至少一段 DemandEvidence。
- 至少一項痛點、情境或期望結果的產品證據。
- 非 provider／廣告／公告／雜訊，且沒有產品硬排除。
- 不能只靠品牌名、產品名或 matched term。
- 來源與引文可回看；證據不足只能進 review／low。

排序 enum：

| sort | 排序 |
|------|------|
| `recommended` | priority_score desc → product_fit_score desc → posted_at desc → id asc |
| `newest` | 已知 posted_at desc → priority_score desc；未知時間置底 |
| `oldest` | 已知 posted_at asc → priority_score desc；未知時間置底 |
| `product_fit` | product_fit_score desc → priority_score desc → posted_at desc |
| `demand_intent` | demand_intent desc → priority_score desc → posted_at desc |

- 所有分頁排序必須穩定，最後以 id tie-break；禁止前端只對單頁資料重排。
- 舊 `sort=score` 相容映射為 `recommended`，舊 `sort=posted` 相容映射為 `newest`。

## 5. 扣點與配額契約

### 5.1 免費與付費邊界

| 動作 | 平台點數 |
|------|----------|
| 開頁、列表、詳情、篩選、排序 | 0 |
| 完成、移除、復原、選移除原因 | 0 |
| baseline DemandMap 建立、手動編輯、QueryPlan 規則產生 | 0 |
| 去重、硬排除、證據抽取、輕量排序 | 0 |
| API path 搜尋 | 沿用既有 `web_search` 單次成本與 `radar.*` source |
| crawler path 搜尋 | 沿用既有 crawler／BYOK 計量行為，不臆造平台搜尋點數 |
| DemandMap AI enrichment | 沿用既有 AI meter，以獨立 `radar.demand_map` source 歸因 |
| AI 深度判定 | 沿用既有 AI research／判定 meter，以 `radar.judge` source 歸因 |
| 回覆生成 | Retain 既有 meter 與按需生成規則 |

- 不新增第五種 usage meter；`radar.*` source 只用於歸因。
- BYOK 的平台 credits 一律為 0，但 count、source、工作統計仍增加。

### 5.2 CostPreview 與執行確認

CostPreview 回傳：

```text
{
  action, key_mode,
  fixed_credits, min_credits, max_credits,
  search_calls, max_ai_candidates,
  remaining_credits, estimate_basis, expires_at
}
```

- 取得 preview 本身不扣點。
- 手動搜尋／巡邏、AI enrichment、批次深判與回覆生成在 UI 觸發前取得 preview；固定成本顯示確值，候選數未知時顯示範圍，不假裝精準。
- 使用者確認時送出 `preview_id` 與 `credit_ceiling`。preview 過期、產品／query plan 改版或目前餘額不足時，執行前拒絕並要求重新確認。
- 單筆既有固定成本的動作仍可由既有用量 metadata 顯示；若沒有變動成本，不必多一個確認步驟，但按鈕附近必須標示點數。

### 5.3 實際計點、防重與失敗

1. provider 呼叫前通過會員額度、meter soft cap、preview 及 CreditCeiling gate。
2. provider 未被呼叫，或呼叫未產生既有計量規則認定的有效工作，不 commit 點數。
3. 搜尋 provider 成功返回（即使 0 hit）後，commit 搜尋點數。
4. AI 成功返回可驗證的完整判定後，commit 該候選判定點數；結果 rejected 仍計點。
5. AI 輸出結構無效或不可驗證時，沿用既有失敗退點語意，不把失敗結果加入有效判定快取。
6. 有效判定唯一鍵為 owner＋來源識別＋product_id＋demand_input_version＋map_version＋judge_kind；命中即復用，重試、跨 watch 或跨 query 不再扣。
7. Job 中斷重跑時，逐筆跳過已 commit 且已有有效結果的唯一鍵；不得只靠整批成功旗標防重。
8. 達 CreditCeiling 或方案點數上限時停止新的 provider 呼叫，已完成結果保留，剩餘候選記 `budget_deferred_count`；不得把工作標成完整成功。

### 5.4 自動巡邏成本上限

- P0 沿用方案既有雷達與 AI 配額作預設 CreditCeiling，不新增每產品自訂額度；自訂額度列 P1。
- 每日排程不逐次詢問，但開始前仍建立系統 CostPreview 快照供對帳。
- 多個 watch 共用 owner 當日剩餘額度；排程不得因先處理某個 watch 而突破會員方案上限。
- 點數不足不刪除、不封存 watch；該 Sweep 顯示 `partial_budget` 或 `blocked_budget`，並保留未判定數量。

## 6. 使用者流程（行為）

### 6.1 第一次進入商機收件匣

1. 使用者進 `/app/radar/opportunities`，預設 `review_state=pending`、`time_scope=today`、`sort=recommended`。
2. 頁首只顯示待決定／已處理／已移除、時間範圍與排序；品牌、產品、意向、匹配狀態收進「更多篩選」。
3. 卡片首層顯示問題摘要、適合產品、單段最強證據、priority score／band、posted_at 與三個明確決策：「加入名單並追蹤／只標示已處理／不適合」。
4. 「加入名單並追蹤」成功後 OpportunityStatus=`accepted` 且 ReviewState=`completed`，顯示前往該 Contact 的入口；「只標示已處理」不建立 Contact。
5. 使用者展開完整判定後仍可直接加入名單或只標示已處理，不必關閉詳情回卡片操作。
6. 無結果時依 no watch、尚未巡邏、全部被前處理排除、點數不足、巡邏失敗、篩選後為空分別提示，不回一個無解釋的空白頁。

### 6.2 完成、移除與復原

1. 按「只標示已處理」立即將卡片移出 pending，但不建立 Contact；操作失敗則回復原畫面並顯示後端訊息。
2. 按移除先選原因；`other` 需輸入說明。成功後顯示可復原提示，不觸發 AI。
3. 已完成／已移除 tab 使用同一列表與排序契約；復原後依 previous_review_state 回到對應 tab。
4. 加入 CRM 成功後 ReviewState 自動 completed，並回傳 Contact 入口；只標示已處理不會自動建立 Contact。兩種整理動作皆不扣點。

### 6.3 檢查需求地圖與手動探索

1. 使用者從雷達設定或收件匣次要入口選 Brand／Product，查看免費 baseline DemandMap 與資料完整度。
2. 可免費編輯、啟停 phrases；儲存後 preview 顯示下一次探索使用的新 map version。
3. 若選 AI enrichment，先顯示 CostPreview，確認後執行；失敗不覆寫目前有效地圖。
4. 手動探索前顯示 QueryPlan 摘要、預估搜尋次數、最多 AI 候選與點數範圍；使用者確認 ceiling 後才排程。
5. 完成頁顯示 hit、dedupe、prefilter reject／review／pass、AI judge、created／merged、budget deferred 與實際點數。

### 6.4 每日巡邏

1. worker 驗證 watch、產品與 DemandMap；incomplete 時阻擋並通知補產品資料。
2. 保存系統 preview 與版本，執行 QueryPlan 搜尋。
3. 依 §4.3 前處理，先復用已有判定，再依輕量順位及剩餘 ceiling 選 AI 深判候選。
4. 每完成一個可計點階段即記錄可續跑進度；中斷後從未完成候選繼續。
5. 建立／合併 Opportunity 時保存 priority factors、證據、版本與 ReviewState=pending；命中 removed tombstone 只合併來源。
6. 完成時寫完整 funnel 與點數；部分完成必須標 partial 並顯示未處理數。

## 7. 介面契約

### 7.1 共通規則與 Auth

- 所有 `/api/v1/radar/**` 只接受會員 `Authorization: Bearer <member JWT>`；owner 隔離沿用既有安全語意。
- AI provider token／BYOK key 只供內部 provider 使用，不得出現在 CostPreview、Sweep 或 Opportunity response。
- 成功 envelope：`code=102000`、`message`、`data`、`error`；失敗不得回成功空資料。
- 列表 request `page / pageSize`，response `pagination + list`。
- 所有時間均為 UTC unix nanoseconds `int64`。
- 後端契約只由 `generate/api/*.api` 修改並以 goctl 生成 handler／route；業務行為落在 logic／module。

### 7.2 HTTP / API

| Method | Path | 用途 | 主要 request／response |
|--------|------|------|-----------------------|
| GET | `/api/v1/radar/opportunities` | 單一收件匣列表 | query 加 `review_state`、`time_scope`、`sort`；item 加 review、priority、evidence |
| GET | `/api/v1/radar/opportunities/:id` | 商機詳情 | 回完整 review audit、priority factors、DemandEvidence 與產品依據 |
| PUT | `/api/v1/radar/opportunities/:id/review-state` | 完成、移除、復原 | req `state`、`removal_reason?`、`removal_note?`、`duplicate_of_opportunity_id?`；回 Opportunity |
| POST | `/api/v1/radar/opportunities/:id/accept` | 加入 CRM | Retain；成功時另設 review_state=completed |
| POST | `/api/v1/radar/opportunities/:id/dismiss` | 舊客戶相容 | Retain；映射 review_state=removed，保存 reason 或 legacy_unknown |
| GET | `/api/v1/radar/products/:productId/demand-map` | 讀目前需求地圖 | 回完整度、input／map version、phrases 與 stale 資訊 |
| PUT | `/api/v1/radar/products/:productId/demand-map` | 免費人工調整 | req enabled／custom phrases 與基礎版本；衝突時要求重讀 |
| POST | `/api/v1/radar/products/:productId/demand-map/enrich` | AI 擴充 | req `preview_id`、`credit_ceiling`；回新 map 與 credits_used |
| GET | `/api/v1/radar/cost-preview` | 付費動作預估 | query `action`、watch／product／opportunity context；回 CostPreview |
| POST | `/api/v1/radar/watches/:id/sweep` | 手動巡邏 | req 加 `preview_id`、`credit_ceiling`；回 job／sweep 識別 |
| POST | `/api/v1/radar/explore` | 手動探索 | req 加 map version、preview、ceiling；回 funnel 統計與實際耗點 |
| GET | `/api/v1/radar/sweeps` | 巡邏紀錄 | item 加版本、funnel counts、credit breakdown、budget state |
| GET | `/api/v1/radar/today` | 舊頁相容 | Retain 暫時；只回 pending＋today，同新列表排序，之後導覽移除 |

`OpportunityPublic` 增量：

```text
{
  review_state, previous_review_state?, removal_reason?, removal_note?,
  removed_at?, removed_by?, last_matched_at?,
  priority_score, priority_band,
  priority_factors: { pain_fit, demand_intent, evidence_quality, freshness },
  demand_evidence[], demand_input_version?, demand_map_version?
}
```

`RadarSweepPublic` 增量：

```text
{
  demand_input_version?, demand_map_version?,
  hit_count, deduped_count,
  prefilter_pass_count, prefilter_review_count, prefilter_rejected_count,
  cached_judgment_count, judged_count,
  created_count, merged_count, tombstone_matched_count,
  budget_deferred_count,
  status: complete|partial_budget|blocked_budget|failed,
  credits_used,
  credit_breakdown: { search, demand_map, judge, reply }
}
```

### 7.3 列表 query 行為

| query | 值 | 預設／說明 |
|-------|----|-------------|
| `review_state` | pending／completed／removed | pending |
| `time_scope` | today／7d／all | today；與 from／to 不可同時使用 |
| `sort` | recommended／newest／oldest／product_fit／demand_intent | recommended |
| `brand_id`、`product_id` | owner 可見識別 | 沿用既有 product match filter |
| `priority_band` | high／review／low | 空表示全部 |
| `from`、`to` | unix ns UTC | 以 posted_at 篩選；未知時間只在 all 且無 from／to 時出現 |

- `time_scope=today` 以會員顯示時區的當地 00:00 換算成 UTC ns 範圍；資料儲存仍只用 UTC ns。
- time scope、filter、sort、page 寫入 URL，重新整理與返回頁面保持相同工作位置。

### 7.4 前端頁面 / 導覽

| 路由 | 目的 | 主要操作 |
|------|------|----------|
| `/app/radar/opportunities` | 唯一商機工作收件匣 | review tab、時間、排序、產品篩選、完成、移除、復原、詳情 |
| `/app/radar/today` | 舊連結相容 | redirect 到 opportunities 的 pending＋today，不再顯示第二套頁面 |
| `/app/radar/watches` | 雷達與需求地圖入口 | 查看完整度、編輯 map、AI enrichment、預覽成本、觸發巡邏 |
| `/app/usage` | 用量追溯 | 沿用既有 meter，按 `radar.*` source 查看消耗 |

- 導覽只保留一個「商機」入口，不同頁面不得重複出現同一工作清單。
- mock／live 實作同一 repository 介面、排序、狀態、錯誤與空結果。
- 沿用 Harbor Desk 與既有 UI 元件；不新增 UI framework。

### 7.5 Job / 背景任務

| Template type | Worker | 觸發 | 成功結果 |
|---------------|--------|------|----------|
| `radar_sweep` | 既有 radar worker | 每日排程、首巡、手動巡 | 按新 funnel 執行，guarded 更新完整或 partial 狀態與點數 |
| `radar_demand_map_enrich` | radar worker | 使用者確認付費 enrichment | 產生可驗證 phrases；成功才切換 map version 與 commit 用量 |

- Redis lock value 保持 `workerID`；只有持有者可續期／釋放。
- Job status 必須 guarded 更新；被取消或已終態的 job 不得被晚到 worker 覆寫。
- CostPreview 不是 Job，不得占用工作佇列或扣點。

### 7.6 錯誤語意

| 情境 | 行為 |
|------|------|
| 非 owner 讀寫商機、產品或需求地圖 | not found／forbidden 的既有安全語意，不洩漏存在性 |
| review state／removal reason／sort 不合法 | validation error；不做部分更新 |
| removed 未帶原因或 other 未帶 note | validation error；保持原狀態 |
| duplicate 指向不存在、自己或其他 owner | validation error／not found；保持原狀態 |
| DemandMap 基礎版本過期 | conflict；回目前版本摘要，要求重讀，不覆寫 |
| DemandMap incomplete | 新巡邏 blocked；回缺少哪些產品資訊，不回成功空資料 |
| preview 過期或 context 已改版 | conflict；要求重新預估與確認 |
| ceiling 小於固定成本 | 執行前 validation／quota error；provider 不被呼叫、不扣點 |
| 點數於批次中耗盡 | Sweep partial_budget；回已完成與 deferred counts |
| 全部候選被前處理排除 | Sweep complete 且 created=0；回各 reject reason counts，非無說明空成功 |
| provider 失敗 | 沿用 retry／退點規則；Sweep failed 或 partial，watch 不被刪除 |

## 8. 與現況對照

| 能力 | Retain | Replace | Remove | 備註 |
|------|--------|---------|--------|------|
| Opportunity 與 owner＋來源去重 | ✓ |  |  | 加 tombstone 行為，不重建第二套實體 |
| `intent_score`、`product_fit_score` | ✓ |  |  | 保留顯示；新推薦排序用獨立 priority score |
| ProductMatch 與主推產品 | ✓ |  |  | DemandMap 提供更好的證據來源 |
| `accepted` 加入 CRM | ✓ |  |  | 成功後同步 review completed |
| `dismissed` 終態 | 相容讀寫 | ✓ |  | 新 UI 改用可復原 review removed |
| `RadarTodayPage` 獨立工作頁 |  |  | ✓ | 舊 URL redirect 到單一收件匣 |
| `RadarOpportunitiesPage` |  | ✓ |  | 改成唯一收件匣，不再只是封存查詢頁 |
| watch 手工 include／exclude terms | ✓ |  |  | QueryPlan 與 DemandMap 提供可追溯建議 |
| 目前短詞建議 | 相容 | ✓ |  | 升級為五類 DemandMap，不只回平面 terms |
| 基本 substring exclude／分類 | ✓ | ✓ |  | 成為完整、可觀測的免費前處理 funnel |
| 五問與產品適配 AI | ✓ | ✓ |  | 只處理 shortlist，輸出可驗證證據並防重扣 |
| 既有四 meter、BYOK、方案配額 | ✓ |  |  | 只加 `radar.*` source 與預估／明細 |
| 今日／全部兩套導覽 |  |  | ✓ | 時間改為同頁 scope |
| hard delete 商機 |  |  | ✓ | P0 明確不提供 |

## 9. 非功能

- **安全／權限：** Brand、Product、DemandMap、Opportunity、CostPreview、Sweep 全部以 owner 隔離；不得回傳 provider token、搜尋 session 或他人存在性。
- **一致性：** review state 更新、移除 audit 與 tombstone 必須同一成功邊界；accept 的 CRM binding 與 review completed 不得出現成功訊息但狀態未更新。
- **冪等性：** review state、provider 判定與用量 commit 各自有穩定冪等鍵；HTTP 重送及 Job 重跑不重複扣點。
- **可觀測性：** 每 Sweep 保存版本、funnel counts、各階段原因、點數 breakdown、partial／blocked 原因；日誌不得含 token 或完整敏感設定。
- **效能邊界：** 列表必須 server-side filter／sort／page；前處理應在 provider 深判前完成。候選與 AI 最大數受現有搜尋 limit、方案配額與 CreditCeiling 三重上限。
- **可解釋性：** 正向高順位理由必須能回指公開原文與產品／DemandMap 依據；找不到原文片段不得保存為高順位證據。
- **相容性：** 缺新欄位的舊資料可讀；舊 sort、today、accept、dismiss 呼叫有明確映射，不要求客戶端同日切換。

## 10. 資料保存（邏輯層）

| 實體 | 關鍵欄位 | 生命週期 |
|------|----------|----------|
| Opportunity | review state、previous state、removal fields、priority factors、demand evidence、版本、last matched | 既有 owner＋來源唯一；removed 保留 tombstone；不硬刪 |
| OpportunityReviewEvent | opportunity、from／to、reason／note、duplicate target、actor、at | append-only；完成、移除、復原皆留紀錄 |
| DemandMap | product、input／map version、state、五類 phrases、basis、origin | 一產品一個 current；舊版本保留供既有判定解釋與防重 |
| JudgmentResult | owner、來源、product、input／map version、kind、結果、usage event | 唯一鍵防重；有效結果可復用；失敗不當有效快取 |
| CostPreview | owner、action、context version、估計、ceiling、expiry | 短期保存；只可由同 owner 使用一次啟動工作，不扣點 |
| RadarSweep | query／版本、funnel counts、status、credit breakdown、deferred | 保留供成本、續跑與品質分析；時間 UTC ns |
| UsageEvent | 既有 meter、`radar.*` source、key mode、credits、count | Retain；不新增 meter；與 Sweep 可對帳 |

## 11. 驗收場景（Given / When / Then）

1. **Given** 既有 qualified 商機沒有 review_state，**When** 開啟收件匣，**Then** 它出現在 pending，且讀取不扣點。
2. **Given** pending 商機，**When** 使用者標完成，**Then** 它移到 completed、未建立 Contact、可免費恢復 pending。
3. **Given** pending 商機，**When** 以 pain_mismatch 移除，**Then** 它進 removed、保存原因；下次同來源命中只更新來源資訊，不新建、不重判、不扣判定點。
4. **Given** removed 商機先前為 completed，**When** 復原，**Then** 回 completed；移除 audit 仍可查。
5. **Given** 使用者按加入 CRM，**When** Contact binding 成功，**Then** OpportunityStatus=accepted 且 ReviewState=completed；只成功其中一半不得回成功。
6. **Given** 產品只有名稱、沒有痛點與情境，**When** 建 DemandMap，**Then** state=incomplete 且新巡邏被擋下，系統指出缺少資料。
7. **Given** 產品痛點有多種使用者口語，**When** QueryPlan 產生，**Then** 查詢含困擾／情境／結果語言，產品名不單獨成組。
8. **Given** 候選是同業廣告，**When** 前處理，**Then** disposition=reject、不呼叫 AI judge，Sweep reject count 增加。
9. **Given** 候選只有產品名命中，**When** 排名，**Then** pain fit 與 evidence quality 不因名稱得分，不能進 high。
10. **Given** 候選用不同說法表達同一產品痛點且引用可驗證，**When** 深判，**Then** 保存原文 evidence 與產品 basis，可依 priority 排到純關鍵字結果前。
11. **Given** 三筆相同 priority score，**When** 分頁查 recommended，**Then** 依 product fit、posted_at、id 穩定排序，跨頁無重複或跳筆。
12. **Given** posted_at 未知，**When** sort newest 或 oldest，**Then** 該筆都在已知時間之後，不冒充最新或最舊。
13. **Given** 手動探索尚未確認，**When** 取得 CostPreview，**Then** credits 不變；確認 ceiling 後才可啟動 provider 呼叫。
14. **Given** API 搜尋成功但 0 hit，**When** Sweep 完成，**Then** 搜尋點數如實計入、AI judge 為 0，畫面說明 no hit。
15. **Given** 前處理排除全部候選，**When** Sweep 完成，**Then** AI judge 點數為 0，並顯示各排除原因而非空白成功。
16. **Given** AI 完成結構有效但判定不適合，**When** 結果保存，**Then** 該次正常計點並快取 rejected 結果，重跑不再扣。
17. **Given** AI 回傳無法驗證的證據，**When** 判定失敗，**Then** 不建立有效 JudgmentResult、不 commit 該判定點，Sweep 顯示失敗。
18. **Given** Job 在第五筆後中斷，**When** 重跑，**Then** 前五筆從有效唯一鍵復用，僅未完成候選可能扣點。
19. **Given** CreditCeiling 在批次中用完，**When** 尚有候選，**Then** 停止新 provider 呼叫、保存已完成結果、status=partial_budget 並回 deferred count。
20. **Given** BYOK 會員執行相同流程，**When** 完成，**Then** 平台 credits_used=0，count、source 與 funnel 統計仍可查。
21. **Given** 產品只改名稱，**When** 同候選再命中，**Then** DemandInputVersion 不變，不允許重新扣點深判。
22. **Given** 產品痛點實質修改，**When** DemandMap 更新，**Then** input version 改變、舊 AI phrases 失效、使用者 custom phrases 待確認，重新深判前需新的 CostPreview。
23. **Given** 人工標註代表性資料，**When** 取 recommended 前十，**Then** 至少八筆同時具備需求與產品適配證據，且 provider／公告／純名稱命中不在 high。
24. **Given** 使用者從舊 `/app/radar/today` 進入，**When** 頁面載入，**Then** redirect 到 opportunities pending＋today，沒有第二份狀態或列表邏輯。

## 12. 開放規格問題

1. 無阻擋 P0 的開放問題。每產品自訂 CreditCeiling、批次操作與移除回饋調校均依 requirements 保持 P1，不在 P0 偷渡。

## 13. 批准

- [x] Spec 已批准（2026-08-11／使用者）
- 批准後下一步：建立 `plan.md`，依 Phase B mock 收件匣 → 契約 → Phase C live → Phase D 搜尋與品質閘拆里程碑；本輪不建立 tasks、不修改產品程式。
