thread-master/docs/product/brand-product-radar/spec.md

340 lines
24 KiB
Markdown
Raw Normal View History

2026-08-13 02:22:24 +00:00
# Spec: 品牌 × 產品商機雷達
> Status: `approved`
> Status note: 使用者 2026-08-10「批准」
> Source: `docs/product/brand-product-radar/requirements.md`approved 2026-08-10
> Parent spec: `docs/product/demand-radar/spec.md`
> Last updated: `2026-08-10`
## 1. 摘要
本規格把既有 RadarWatch、每日立即巡邏、Opportunity、回覆與 CRM 串上 BrandProduct。產品型雷達以一個品牌下的一個產品為上下文同一公開內容仍只建立一筆 Opportunity但可累積多個產品匹配並選出唯一主推產品。
P0 不另建第二套商機系統:保留既有商機五問與 0100 分數,新增可獨立解釋的產品適配分數、證據與風險;產品型結果預設按商機分數排序。舊通用雷達與舊商機保持可用,逐步補上產品關聯。
## 2. 名詞與實體
| 名詞 | 定義 |
|------|------|
| **通用雷達generic watch** | 舊式 RadarWatch沒有 BrandProduct 關聯,沿用 ServiceProfile 判定服務匹配 |
| **產品型雷達product watch** | 綁定一個 Brand 與該 Brand 下的一個 Product 的 RadarWatch |
| **ProductContextSnapshot** | 判定當下使用的品牌/產品識別與版本快照,只保留解釋歷史所需資訊 |
| **ProductMatch** | 一筆 Opportunity 對一項 Product 的適配結果;同產品在同一商機中最多一筆 |
| **ProductFitReason** | 產品適配的一個維度分數、理由與公開貼文證據 |
| **PrimaryProduct** | 商機目前主推的唯一產品;必須存在於該商機的 ProductMatch 中 |
| **產品適配分數** | `product_fit_score`0100與既有 `intent_score` 分開呈現 |
| **可跟進產品匹配** | 適配分數至少 45、有痛點或使用情境的正向證據且沒有產品硬排除 |
沿用且不重新定義Brand、Product、ServiceProfile、RadarWatch 狀態、RadarSweep、Opportunity 狀態、Contact、FollowUp、ReplyVariant、Job、Usage、Notification。
## 3. 核心行為契約
### 3.1 RadarWatch 上下文
```text
generic --(使用者補選合法 Brand + Product)--> product
product --X--> generic
product(A) --X--> product(B)
```
- `context_mode``generic | product`
- `generic``brand_id`、`product_id` 皆空;`product` 時兩者皆必填,且 Product 必須屬於該 Brand 與目前會員。
- `generic` active watch 維持既有 ServiceProfile 必填;`product` active watch 不要求 ServiceProfileBrandProduct 才是產品適配的必要上下文。
- product watch 有自己的 `regions[]` 時優先使用;留空且 ServiceProfile 存在時沿用其地區remote兩者都沒有時視為「未限制地區」不得把未知地區硬判為不符。
- 新前端流程一律建立 `product` watch。既有 API 呼叫未帶兩個識別欄位時仍可建立 `generic`,維持相容性,但 UI 不主動提供此入口。
-`generic` watch 可做一次性補綁;第一次成功巡邏後,產品關聯不可原地更換。要改產品時封存或暫停舊 watch再以相同搜尋詞建立新 watch避免歷史統計混義。
- watch 保存品牌名稱、產品名稱與綁定時間快照。列表優先顯示目前名稱;來源已刪除時顯示快照。
### 3.2 產品適配評分
產品型判定除既有五問外,必須輸出以下四個維度;四維滿分合計 100
| 維度 | 滿分 | 判定來源 |
|------|------|----------|
| `pain` | 35 | 貼文問題是否對應 Product `pain_points` |
| `scenario` | 25 | 貼文使用/購買情境是否對應 `product_context``match_tags` |
| `audience` | 20 | 公開文字是否支持 Brand `target_audience`;不確定給 0不得猜測 |
| `capability` | 20 | Product `provider_capability_terms` 是否確實能處理該需求 |
每個維度都產生一筆 `ProductFitReason`
- `dimension`:上述四值之一。
- `score`0 到該維度滿分。
- `reason`:人話說明。
- `candidate_excerpt`正分時必填必須是候選公開文字中的短片段0 分可留空。
- `product_basis`:對應的品牌受眾、產品痛點、情境、標籤或能力詞;不得生成來源資料中不存在的產品承諾。
另輸出 `risks[]`。若候選文字明確命中 Product `provider_exclude_terms`,或判定為同業叫賣/品牌公告/雜訊,該 ProductMatch 設 `excluded=true` 並附 `exclude_reason`
```text
product_fit_score = pain + scenario + audience + capability
product_fit_band = strong if score >= 75
possible if score 45..74
weak if score < 45
eligible = score >= 45
AND (pain > 0 OR scenario > 0)
AND excluded = false
```
- 單純命中搜尋詞或產品名,不算 `pain``scenario` 證據。
- 產品型 Opportunity 的既有五問 `fit` 維度,取主推 ProductMatch 分數等比例換算為 010其他四問及 8050 分級門檻不變。
- `intent_score` 仍是今日排序的主要分數;`product_fit_score` 獨立顯示,作為同分排序與解釋依據。
### 3.3 同貼文多產品合併與主推產品
- Opportunity 的唯一語意仍是「同一會員+同一來源內容識別」;不因 watch 或 product 不同而複製商機。
- ProductMatch 的唯一語意是「同一 Opportunity同一 Product」。同產品再次命中只合併 `watch_ids[]``matched_terms[]`,不新增重複項目。
- 首個 eligible ProductMatch 成為 PrimaryProduct後續出現更高適配分數時自動改成較高者。
- 分數相同時依 `strong` 優先、首次匹配時間較早、最後以 `product_id` 穩定排序。
- 使用者手動選定主推產品後寫入 override之後新增 ProductMatch 不再自動改主推,直到使用者再次選擇。
- PrimaryProduct 改變時,以該 ProductMatch 重新換算既有 `fit` 維度與 `intent_score`,其他四維理由不重跑;變更須留操作者與時間。
- ProductMatch 使用判定當下的品牌/產品版本快照。產品日後修改不回寫舊 match同貼文重複命中也只合併來源不重算舊快照。只有先前尚無該 ProductMatch 時才執行一次適配判定。
### 3.4 時效與顯示
- 既有時效分數不變24 小時內最高、2472 小時遞減、72 小時至 14 天低分。
- 超過 14 天仍以 `stale` 原因保留為 `rejected`,但產品型判定仍保存 ProductMatch 與證據;不得只因過舊就完全不入庫。
- 「今日商機」只顯示未逾 14 天、既有狀態允許顯示,且至少有一個 eligible ProductMatch 的產品型 Opportunity。
- 「全部結果」可看 stale、weak、excluded 與通用商機;預設仍依 `intent_score` 高到低,其次主推產品適配分數高到低、`posted_at` 新到舊。
- rejected、weak、excluded 預設不混入今日高/中/低統計,但篩選後可檢查其理由。
### 3.5 Watch 與來源生命週期
沿用 `active ↔ paused → archived`,新增 `pause_reason`
```text
active --> paused(product_unavailable) : 綁定 Brand/Product 不存在或不可用
paused(product_unavailable) --X--> active : 不允許直接恢復
paused(product_unavailable) --> archived : 保留歷史
```
- 使用者手動暫停使用 `pause_reason=user`
- 刪除 Product 時,同一會員下所有綁定該 Product 的 active watch 先轉 paused歷史保留禁止自動改綁。
- Brand 仍沿用「有產品不可刪」;若因資料異常找不到 Brand相關 watch 依同樣規則暫停。
- `product_unavailable` watch 可複製搜尋設定建立新產品 watch不能原地換產品後恢復。
- 排程發現無效產品上下文時,必須 guarded 暫停 watch、記錄原因並通知不建立成功空 Sweep。
## 4. 使用者流程
### 4.1 建立產品型每日雷達
1. 使用者進入雷達訂閱頁,先選 Brand產品選單只列該 Brand 下、目前會員可用的 Product。
2. 選 Product 後顯示上下文完整度:品牌受眾、產品情境、痛點至少一項、能力詞。缺品牌或產品本體直接阻擋;內容不足可繼續,但提示結果信心會降低。
3. 使用者要求建議詞。系統只讀所選 BrandProduct回傳 includeexclude 建議、理由與依據種類。
4. 使用者增刪建議,沿用 Threads 短詞規則驗證後啟用。
5. 建立成功後沿用既有首巡與每日 UTC 22:00 排程;卡片顯示 Brand、Product、狀態與最近巡邏時間。
### 4.2 每日巡邏
1. worker 取得 active watch 後,重新讀取同 owner 的 BrandProduct驗證歸屬。
2. 找不到或關聯錯誤時按 §3.5 暫停並通知。
3. 讀取最新 BrandProduct 組成 ProductContextSnapshot再以 watch 搜尋詞 fan-out 取得候選。
4. 對全新公開內容執行既有商機五問與產品適配判定;對已存在 Opportunity、但尚無此 ProductMatch 的內容,只新增該產品適配判定,不重建商機。
5. 對已存在且已有同 ProductMatch 的內容,只合併 watchterm不重複計 AI 判定用量。
6. 新 ProductMatch 不占用「每日新商機筆數」;新 Opportunity 才占用。AI 適配判定仍受既有點數 gate。
7. Sweep 另外記錄適配評估數、合併數與因產品適配不足未進今日頁的數量。
### 4.3 立即探索
1. 使用者先選 BrandProduct再輸入或採用 16 組短詞。
2. 立即探索使用與每日 watch 相同的上下文、去重、評分、配額與 ProductMatch 合併規則。
3. 沒選 BrandProduct 時,舊 API 相容路徑可繼續做 generic explore新 UI 不提供 generic 預設。
4. 結果回報搜尋命中、判定、新增商機、合併產品匹配、截斷與用量,不把「合併」誤算成新商機。
### 4.4 查看與篩選商機
1. 今日頁預設跨所有產品、去重後按 §3.4 排序;提供 Brand、Product、適配程度與商機分級篩選。
2. 選定 Product 篩選時,以「是否存在該 ProductMatch」決定是否入列卡片優先展示被篩選產品的證據與適配分數但不暗中更改儲存的 PrimaryProduct。
3. 卡片顯示主推 BrandProduct、商機分數、產品適配分數、需求摘要、至少一個證據、風險、其他匹配產品數與原文。
4. 展開「為什麼適合」顯示四維理由;展開其他產品可比較分數與證據。
5. 使用者可把任一 eligible ProductMatch 設為主推產品weakexcluded 不可設為主推,除非先以既有人工覆寫機制確認並留下理由。
6. 「全部結果」提供 staleweakexcluded未指定產品篩選結果多但不混淆可立即聯絡的今日商機。
### 4.5 回覆、CRM 與成交
1. 生成回覆時使用 Opportunity 的 PrimaryProduct 快照、Brand 語境及 ServiceProfile 的營運限制。
2. 沒有 PrimaryProduct 的舊商機沿用既有 ServiceProfile 回覆;畫面明確標示「未指定產品」。
3. 加入名單後ContactTouch 保存該次主推 `brand_id`、`product_id` 與 opportunity 關聯;同一聯絡人可在不同時間對應不同產品。
4. 主推產品後續改變,不改寫已送出回覆或既有 ContactTouch新的回覆與接觸使用新主推產品。
5. 成交回報保存成交當下主推產品;既有 OutcomeEvent 行為不變,額外攜帶產品歸屬供後續 P1 統計。
### 4.6 舊資料過渡
1.`context_mode` 的既有 watch 讀作 `generic`;缺 ProductMatch 的既有 Opportunity 讀作「未指定產品」。不需一次性猜測回填。
2. watch 列表提供「補上產品」;只允許為 generic watch 執行一次。
3. 舊商機可繼續接受、回覆、進 CRM 與成交;不因未指定產品而變成不可用。
4. `matched_service` 保留給 generic 與舊資料;產品型商機不再以它代表產品,欄位暫不移除。
## 5. 介面契約
### 5.1 共通規則與 Auth matrix
- 所有下列 `/api/v1/radar/**` 與既有 BrandProduct 操作只接受會員 `Authorization: Bearer <member JWT>`
- AI provider tokenBYOK key 只能由既有 provider 設定與內部 AI 呼叫使用,不得當會員 JWT也不得回傳至前端。
- 成功 envelope`code=102000`、`message`、`data`、`error`;失敗不得回 102000 空資料。
- 列表:`page``pageSize`,回 `pagination``list`。
- 所有時間為 UTC unix nanoseconds `int64`
### 5.2 HTTP / API
既有路徑原地擴充;欄位均以 `generate/api/*.api` 為契約真相,再由 goctl 產生。表內只列本 run 有變化的介面。
| Method | Path | 用途 | 本 run 主要 requestresponse 變更 |
|--------|------|------|------------------------------------|
| GET | `/api/v1/radar/watches` | 列 watch | filter 加 `context_mode`、`brand_id`、`product_id`item 加上下文、名稱快照、`pause_reason` |
| POST | `/api/v1/radar/watches` | 建 watch | req 可帶成對 `brand_id``product_id`response 加 `context_mode` 與上下文 |
| PUT | `/api/v1/radar/watches/:id` | 改搜尋設定 | 只改 termsexcluderegions不得更換產品 |
| POST | `/api/v1/radar/watches/:id/assign-product` | 舊 watch 一次性補綁 | req `brand_id`、`product_id`;只允許 generic 且尚未補綁 |
| POST | `/api/v1/radar/watches/suggest` | 產品需求詞建議 | req 加 `brand_id`、`product_id`item 加 `basis_kind`、`basis_text` |
| POST | `/api/v1/radar/watches/:id/sweep` | 手動巡 | 行為加產品上下文驗證;無效時明確失敗/暫停 |
| GET | `/api/v1/radar/today` | 今日商機 | query 可帶 `brand_id`、`product_id`、`fit_band`Opportunity item 加產品欄位 |
| GET | `/api/v1/radar/opportunities` | 全部結果 | filter 加 `brand_id`、`product_id`、`fit_band`、`match_state`、`sort` |
| GET | `/api/v1/radar/opportunities/:id` | 商機詳情 | 回完整 `product_matches[]`、PrimaryProduct 與 override |
| PUT | `/api/v1/radar/opportunities/:id/primary-product` | 改主推產品 | req `product_id`、`reason`;回更新後 Opportunity |
| POST | `/api/v1/radar/explore` | 立即探索 | req 加成對 `brand_id``product_id`response 加 `matched_count`、`merged_count` |
| POST | `/api/v1/radar/import` | 手動匯入 | req 可帶成對 BrandProduct使用同一適配判定 |
| GET | `/api/v1/radar/sweeps` | 巡邏紀錄 | item 加 `match_evaluated_count`、`match_merged_count`、`fit_rejected_count` |
| DELETE | `/api/v1/scout/products/:id` | 刪 Product | 刪除前同步暫停相關 active watch任一步失敗不得假裝刪除成功 |
主要輸出形狀:
```text
RadarWatchPublic += {
context_mode, brand_id?, product_id?,
brand_name_snapshot?, product_label_snapshot?, context_bound_at?, pause_reason?
}
OpportunityPublic += {
primary_brand_id?, primary_product_id?,
primary_brand_name?, primary_product_label?,
primary_product_fit_score?, primary_product_fit_band?,
primary_product_overridden?, product_matches[]
}
ProductMatchPublic = {
brand_id, product_id, brand_name_snapshot, product_label_snapshot,
brand_updated_at, product_updated_at,
product_fit_score, product_fit_band, eligible, excluded,
exclude_reason?, reasons[4], risks[], watch_ids[], matched_terms[], matched_at
}
```
### 5.3 錯誤語意
| 情境 | 行為 |
|------|------|
| 只帶 Brand 或只帶 Product | validation error不得降級成 generic |
| BrandProduct 不屬於會員或關係不符 | 對外使用 not foundforbidden 的既有安全語意,不洩漏他人資料 |
| 對 product watch 更換 BrandProduct | validation error提示建立新 watch |
| 重複替 generic watch 補綁 | conflict回目前綁定不覆寫 |
| product_unavailable watch 恢復 | conflict提示封存複製到新產品 |
| 主推產品不在 ProductMatch | validation error |
| 主推 weakexcluded match 且無覆寫理由 | validation error |
| ProductMatch 判定缺四維理由 | 判定失敗,不保存不完整 matchSweep 記失敗數 |
| 產品來源失效 | watch 暫停+通知;不得成功空巡 |
| 全為 staleweakexcluded | today 正常回 0`empty_reason=no_eligible_product_match`;全部結果仍可查 |
### 5.4 前端頁面與互動
| 路由 | 行為變更 |
|------|----------|
| `/app/radar/watches` | Brand → Product 串接選擇、資料完整度、產品詞建議、generic 補綁、產品失效提示 |
| `/app/radar/today` | BrandProduct適配篩選、產品證據卡、多匹配展開、改主推產品 |
| `/app/radar/opportunities` | 新增全部結果視圖,包含 staleweakexcluded未指定產品與分數排序 |
| `/app/brands` | Product 刪除前顯示會暫停的 radar 數量與後果;不重做產品編輯器 |
| `/app/crm` | 聯絡人與接觸時間軸顯示來源 BrandProduct既有階段操作不變 |
- mock 與 live 必須實作同一 Radar repository 介面與相同錯誤/空狀態。
- 沿用 Harbor Desk、既有 UI 元件與 `radar.css`;不新增 UI framework不用 emoji 作主 icon。
- BrandProduct 名稱只是導覽資訊;證據與風險才是商機卡的主要解釋。
### 5.5 Job / 背景任務
| Job | Retain變更 | 成功結果 |
|-----|--------------|----------|
| `radar_sweep` | 保留每日 UTC 22:00、手動首巡、guarded 更新與 Redis `workerID` lock加入產品上下文與 ProductMatch 合併 | 新 Opportunity 或合併 ProductMatchSweep 統計完整 |
| `radar_followup_scan` | Retain | 行為不變;通知與 Contact 歷史可帶產品歸屬 |
| `scout_scan` | Retain | 不改 Scout 掃描行為;只復用 BrandProduct 真相 |
## 6. 資料保存與不變量
| 實體 | 新增/保留欄位 | 生命週期與不變量 |
|------|---------------|------------------|
| RadarWatch | context mode、BrandProduct IDs、名稱快照、綁定時間、pause reason | product 關聯不可原地更換;封存保留 |
| Opportunity | PrimaryProduct、override、`product_matches[]`;保留 matched_service | 同 ownerexternal identity 唯一;舊資料可無產品 |
| ProductMatch | 來源版本快照、四維理由、分數、band、eligible、exclude、watchterm 來源 | 同 OpportunityProduct 唯一;判定後不可因產品編輯回寫 |
| ContactTouch | brand_id、product_id、opportunity_id | 寫入後不隨主推產品改變 |
| OutcomeEvent | product_id可空向後相容 | 以成交當下主推產品保存 |
| RadarSweep | match evaluatedmergedfit rejected counts | 與既有 Sweep 一起保存、可追查 |
必須成立:
1. ProductMatch 的 Brand 必須與 Product 所屬 Brand 相同,且兩者 owner 與 Opportunity owner 相同。
2. PrimaryProduct 必須指向 eligible ProductMatch若人工覆寫 weakexcluded則必須同時存在理由與稽核紀錄。
3. 同一產品 match 的 `watch_ids`、`matched_terms` 去重;同一 Opportunity 的 ProductMatch 不重複。
4. ownerexternal identity 的既有唯一性不變;多 worker 競態時結果仍是一筆 Opportunity。
5. API 列表預設排序穩定:`intent_score desc`、primary fit desc、`posted_at desc`、`id asc`。
## 7. 與現況對照Retain / Replace / Remove
| 能力 | Retain | Replace | Remove | 備註 |
|------|--------|---------|--------|------|
| 每日排程、首巡、立即探索 | ✓ | | | 同一 pipeline 加產品上下文 |
| RadarWatch 狀態與配額 | ✓ | | | 新增 context 與 pause reason |
| Threads 短詞 fan-out | ✓ | | | 建議詞改由選定產品產生 |
| ServiceProfile 地區remoteforbidden | ✓ | | | product watch 可選用generic watch 仍必填 |
| active watch 一律要求 ServiceProfile | | ✓ | | 只對 generic 保留product 改以 BrandProduct 為門檻 |
| ServiceProfile 作為產品 fit 來源 | | ✓ | | product watch 改用 BrandProductgeneric 保留舊行為 |
| `matched_service` | ✓ | | | 舊generic 相容,產品型不再用它冒充 Product |
| 重複貼文只合併 matched terms | | ✓ | | 改為同產品合併來源、不同產品新增 ProductMatch |
| Opportunity ownerexternal identity 去重 | ✓ | | | 核心去重不變 |
| 五問分數與高/中/低門檻 | ✓ | | | fit 10 分由 PrimaryProduct 換算 |
| >14 天不進今日頁 | ✓ | | | 仍保存產品適配供全部結果查看 |
| 今日頁只看即時商機 | ✓ | | | 加 eligible product gate |
| 全部結果頁 | | ✓ | | 從只有 API 查詢補成可操作前端視圖 |
| 回覆、CRM、FollowUp、成交 | ✓ | | | 附加產品歸屬,不重造狀態機 |
| BrandProduct CRUD | ✓ | | | Product 刪除增加 radar 暫停副作用 |
| 既有欄位/舊資料 | ✓ | | | P0 無硬刪欄位、無猜測回填 |
本 run 不移除任何既有對外路徑或持久欄位。
## 8. 非功能契約
- **權限:** 每次 watch、opportunity、product match、primary product 變更都以 JWT owner 驗證;不可只信 request 中的 brand_idproduct_id。
- **一致性:** Product 刪除與 watch 暫停必須具備可重試的一致結果;部分失敗不得回刪除成功。
- **併發:** 同一貼文被多 watchworker 同時命中時,需以 guardedatomic 語意保證單 Opportunity、單 ProductMatch。
- **效能:** 列表解析 BrandProduct 名稱不得逐筆查詢;`pageSize` 沿用既有限制,產品篩選與排序在合理索引下完成。
- **成本:** 同一 `(opportunity, product)` 不重複計適配判定;新增 ProductMatch 可計一次既有 `ai_research``source=radar.product_fit`,不新增 meter。Product 更新不觸發舊商機重算。
- **可觀測性:** Sweep 可區分搜尋命中、商機判定、新商機、適配評估、合併、適配不足、截斷與失敗。
- **隱私:** prompt 與 UI 只使用公開貼文、會員自己的 BrandProductServiceProfile禁止輸出推測的敏感屬性。
- **降級:** AI 適配不可用時可用明確的規則結果,但仍須四維理由與證據;無法產出完整契約時該 match 失敗,不以空理由成功。
## 9. 驗收場景Given / When / Then
1. **Given** 會員有 Brand A 下的 Product A1 與 A2**When** 建立 A1 watch**Then** watch 回傳 product context且不能綁 A2 所屬以外的 Brand。
2. **Given** Product 有痛點但沒有品牌名出現在貼文,**When** 貼文明確描述該痛點,**Then** 可形成含原文 excerpt 的 ProductMatch不要求命中品牌名。
3. **Given** 貼文只提到產品同名詞但無需求/痛點,**When** 判定,**Then** 不得只憑詞命中成為 eligible match。
4. **Given** 同一貼文先被 A1 watch 命中、後被 A2 watch 命中,**When** 兩次巡邏完成,**Then** 只有一筆 Opportunity、兩筆 ProductMatch且不重複 Contact。
5. **Given** A2 分數高於 A1 且沒有人工 override**When** A2 match 合併,**Then** A2 成為 PrimaryProductintent fit 維度按 A2 換算。
6. **Given** 使用者手動指定 A1**When** 後續加入更高分 A2 match**Then** PrimaryProduct 仍是 A1並保留 override 紀錄。
7. **Given** Product 更新名稱與痛點,**When** 查看舊商機,**Then** 顯示判定時快照與舊證據;下一次新商機才使用新版資料。
8. **Given** 一筆 20 天前但產品高度適配的內容,**When** 巡邏完成,**Then** 今日頁不顯示、全部結果可見 stale 與 ProductMatch 理由。
9. **Given** Product 被刪除,**When** 刪除完成,**Then** 相關 active watch 已暫停並通知,歷史 OpportunityContactOutcomeEvent 仍可看。
10. **Given** 舊 generic watch 與舊 Opportunity**When** 升級部署,**Then** 原巡邏、回覆與 CRM 可繼續,且 UI 顯示未指定產品而非猜測關聯。
11. **Given** 今日沒有 eligible match 但有 weakstale 結果,**When** 開今日頁,**Then** 顯示 `no_eligible_product_match` 與前往全部結果的動作,不顯示模糊空白頁。
12. **Given** 兩個 worker 同時處理同一貼文與同一 Product**When** 都完成,**Then** 只存在一筆 Opportunity 與一筆該 ProductMatchwatchterm 集合無重複。
13. **Given** 另一會員的 BrandProduct ID**When** 嘗試建立 watch 或改主推產品,**Then** 請求失敗且不洩漏對方名稱與內容。
14. **Given** 產品型商機生成回覆,**When** 檢查 prompt 與輸出,**Then** 使用 PrimaryProduct 快照與正確 Brand且不引用其他品牌資料。
## 10. 開放規格問題
P0 無阻擋問題。以下保留到 P1P2不影響本規格批准
1. 產品回饋訊號要採明確正負樣本,或由 CRM 行為推導權重。
2. 產品自己的理想使用者欄位何時取代/覆蓋 Brand `target_audience`
3. 跨來源同一人的商機是否允許人工合併P0 仍不做身分推測。
## 11. 批准
- [x] Spec 已批准2026-08-10使用者「批准」