---
status: active
superseded_by: ""
---

# workflow-discovery — Design

> **狀態**：草案，待確認。對應 `requirements.md`。
> **建立**：2026-06-27（issue #8）

---

## 1. 設計總綱

三件事，全部把能力落在 API（cypher-executor + KBDB），CLI/MCP 只暴露：

1. **強制 description**：部署端點驗證 description 非空 → 否則 422/400 擋下（兩條路徑共用同一驗證）。
2. **可搜的 metadata**：workflow metadata 同時寫進一個 **embeddable entry**（吃 issue #7 的 `/entries/search` 語意 + 降級）。
3. **search_workflow 工具**：薄殼呼叫 cypher 的 workflow 搜尋端點，後者轉發 KBDB `/entries/search`，限本租戶。

---

## 2. 核心決策 Q1：metadata 存哪（record vs entry vs 雙寫）

| 方案 | 做法 | 優 | 劣 |
|------|------|----|----|
| **A. records 表加 q 搜尋** | 給 `/records/search` 加 `q=` LIKE 掃 slots | 改動小 | records 吃不到 #7 的 Vectorize 語意；要另接一套語意＝重造 #7 已有的輪子 |
| **B. 改存 entry（取代 record）** | workflow metadata 改寫進 `entries`（`entry_type=workflow`），description 進可 embed 欄位 | 直接吃 #7 語意 + 降級閉環，零重造 | 既有 `u6u_list_workflows` 讀 record 要改；migration 成本 |
| **C. 雙寫（record + entry）** ⭐ | 部署時**同時**寫 workflow_metadata record（保既有 list/get 不動）**＋** 一個 `entry_type=workflow` 的 embeddable entry（給搜尋） | list/get 零回歸；搜尋吃 #7 語意；兩者解耦各司其職 | 一份資料兩處；要保證一致（部署原子性） |

**推薦：C（雙寫）**，理由：
- **不回歸**：`u6u_list_workflows` / `u6u_get_workflow` 繼續讀 record，完全不動（降低框架級風險，守 C1）。
- **吃現成語意**：搜尋走 entry → 直接複用 #7 的 `/entries/search?mode=semantic` + 降級 + capability_hint，**零重造**。
- **解耦**：record＝「我有哪些 workflow」（精確列舉，list 場景）；entry＝「找做某事的 workflow」（語意檢索，search 場景）。職責不同、欄位不同，本就該分。
- 一致性風險可控：雙寫在同一個部署 API handler 內，entry 寫失敗不阻塞部署但回 warning（fire-and-forget + 可補建），對齊 #7 `embedOnWrite` 的 `waitUntil` 非阻塞慣例。

> **被否的 A**：records 加 q 只能 LIKE，要語意還是得自己接 Vectorize＝把 #7 在 entries 做好的事在 records 再做一遍。違反「不重造輪子」。
> **被否的 B**：取代 record 會逼 list/get 一起改，框架級風險放大，不值得。

---

## 3. R1：強制 description（落 API）

### 3.1 統一驗證點

> ⚠️ **實作期發現（2026-06-27，已讀 code）**：`POST /workflows/deploy` **在 cypher-executor 根本不存在**（route 清單只有 `/workflows/:name/executions`、`/workflows/resume`）。MCP `u6u_deploy_workflow.ts:22` 打 `http://cypher-executor/workflows/deploy` → **目前必 404**。意味 MCP 部署路徑現狀是壞的 / 從未端到端驗過。這影響「兩條路徑」的定義，見 §3.1a 待決。

description 強制**落在 cypher-executor 的部署端點**。實際部署端點有三個：
- `POST /webhooks`（token 式匿名）：description optional。
- `POST /webhooks/named`（CLI `acr push`，具名）：description optional → **改必填**。
- `POST /workflows/deploy`（MCP 目標）：**不存在（404）**。

#### 3.1a 待決：MCP 部署路徑怎麼收斂（新發現，需拍板）
兩個方向：
- **方向 ① MCP 改打 `/webhooks/named`**（推薦）：MCP deploy 不再打死端點，改打 CLI 同一條 `/webhooks/named`（傳 name + graph + description）。→ **CLI/MCP 真正共用一條部署端點**（rule 07 薄殼一致的最強形態），死端點順手清掉，description 強制只需做在 `/webhooks/named` 一處。
- **方向 ② 新建 `/workflows/deploy`**：補上這條 route 給 MCP 專用。→ 但會變成「兩條部署端點各做一次 description 強制」，且與 `/webhooks/named` 職責重疊（都是部署具名 workflow），違反「能力只實作一次」。

> **leo 拍板（2026-06-27 issue #8）：方向①** ✅。CLI/MCP 共用一條部署端點＝rule 07 最強形態；list 改讀 `GET /webhooks/named`（現成端點，回歸可控）。

**強制規則**：缺/空白 trim 後為空 → `400 { error: '<§3.2 定位訊息>', requires: 'description' }`。驗證是 API 行為（R1.3），薄殼不判空（守 rule 07）。

#### 3.1b 方向①實作期再發現：YAML→部署編排被寫在 CLI 介面層（藏的薄殼債）

讀 code 發現「MCP 改打 /webhooks/named」不是換 URL 就好——**`/webhooks/named` 吃的是 graph 物件，不是 YAML**。把 YAML 變 graph 的編排目前在 **CLI `push.ts` 介面層**做了一整段：
1. `loadWorkflowYaml` + `parseTriplets`（解析 YAML flow → triplets）
2. `POST /cypher/search`（triplets → 執行圖 graph）
3. config 套節點、組 `{id, name, nodes, edges}` graph
4. `POST /webhooks/named`（傳 graph）

而 MCP `u6u_deploy_workflow` 吃 `yaml_content` 字串、期待一個**吃 YAML 的 server 端點**（打 `/workflows/deploy` 傳 `Content-Type: application/yaml`）——這端點從來不存在（故 404）。

**張力**：方向① 若讓 MCP 在介面層複製 CLI 那 4 步編排 → **兩個薄殼各做一次 YAML→graph 編排 = 違 rule 07「能力只實作一次」**（正是薄殼原則要防的）。

**收斂選項（待總管定，§3.1c）**：
- **①-a 最小修**：MCP 介面層複製 CLI 的 YAML→graph 步驟，打 /webhooks/named。→ 快，但複製編排＝技術債（與方向①初衷「薄殼統一」自相矛盾）。
- **①-b 編排下沉（正解）** ⭐：新增 API 端點 `POST /workflows/deploy`（這次**真的建**，但吃 YAML、職責是「YAML→graph→部署」完整編排），CLI 和 MCP 都只傳 YAML。→ 真正一處編排，兩薄殼都瘦。但要把 CLI push.ts 的編排搬進 API（較大重構，且碰 exposure_consent 流程）。
- **①-c 折中**：先 ①-a 讓 MCP 通（解 404、issue #8 範圍內），①-b 編排下沉另開 issue（薄殼債獨立追）。

> 註：①-b 的 `/workflows/deploy` 與被否的「方向②」不同——②是「為 MCP 另開一條重複端點」，①-b 是「把編排下沉成唯一真相端點，CLI 也改用它」。前者增重複，後者消重複。

### 3.2 description 來源（Q2 定案：操盤 CC 據實生成、用戶可改）

> leo 翻案，但守住誠實精神。目標用戶 low-code，**不知道要填 description**；強迫填一個不懂的欄位＝違北極星「不增加用戶負擔」。調和關鍵＝分清兩種「生成」：

- ✅ **操盤的 CC 據實生成**：CC 剛幫用戶建這工作流，**最懂它串了什麼、做什麼** → 由 CC 寫一句真實的「這做什麼」（leo 例：「呼叫可以 Upsert Google Sheets」）。這是**真描述非假裝**，完全不違 mindset §7。
- ❌ **介面層機械塞佔位**（從 name 複製、塞 `workflow_xxx` 預設）＝假描述，**仍禁**（§3.2 原本防的就是這個，保留）。

**落地規則**：
- **YAML `description:` 為主**：CC 寫 workflow YAML 時就據實填好 description（既有慣例，`registry/examples` 已示範）。
- description **強制非空仍落 API**（R1 不變，§3.1 驗證點不動）。
- 部署若收到空 description：**不是擋下逼用戶手填**，而是回明確訊息要求**操盤 CC 據實補一句**再部署（CC 是 description 的生成者，用戶可改）。錯誤訊息對齊此定位：「description 必填：請操盤的 AI 據實寫一句『這工作流能做什麼』（如「呼叫可 Upsert Google Sheets」），用戶可再改。」
- **內容定位**：一句「這工作流**能做什麼**」，**不是寫文章**。供語意搜尋命中，要點是「做什麼」可被自然語言意圖匹配。
- **仍禁**：介面層（CLI/MCP TS）機械從 name 生成佔位（守 rule 07 + 誠實，hook 7.x 範圍）。「據實生成」是操盤 CC 在寫 YAML 當下做的，不是介面層 deploy 時自動補。

---

## 4. R2：可搜的 entry + search_workflow

### 4.1 部署時寫 embeddable entry（雙寫的 entry 那半）
部署成功後（record 寫完），cypher 端再寫一個 entry 到 KBDB：

```
POST /kbdb/entries   （經 cypher proxy，租戶隔離注 owner_id）
{
  entry_type: "workflow",
  page_name: <workflow name>,
  content: <description>,                    // 被 embed 的主體
  metadata: {
    embed: true,                             // #7 精耕開關：只有標 true 才進 Vectorize
    workflow_id, name, deployed_at
  }
}
```

- `metadata.embed:true` → 命中 #7 的 `embedOnWrite` 精耕條件，description 進 Vectorize（若模組開）。
- 模組沒開 → entry 照樣存（D1），語意搜降級成 LIKE 仍能命中 content/name（不假綠）。
- owner_id 由 cypher proxy 強制注入本租戶（沿用既有隔離，C4/R2.4）。

### 4.2 search_workflow 工具（薄殼）
新 MCP 工具 `search_workflow(query)`，形態抄 `u6u_search_components`：

```
search_workflow(query) →
  cypher GET /workflows/search?q=<query>&mode=semantic
    → KBDB /entries/search?q=&owner_id=&source=&mode=semantic&entry_type=workflow
      → mode=semantic 開：Vectorize 語意命中
      → 沒開：降級 keyword(LIKE) + capability_hint「叫 CC 幫你開語義查詢」
```

- cypher 加一條 `GET /workflows/search`（薄轉發到 KBDB `/entries/search`，限 `entry_type=workflow` + 本租戶 owner_id）。
- MCP 工具把結果格式化（含 capability_hint 透傳給 AI，AI 看到就能主動問用戶開 Vectorize → R2.3）。
- 命名：MCP 既有工具是 `u6u_*` 前綴（`u6u_search_components`）。本工具命名 `u6u_search_workflows`（複數對齊 `u6u_list_workflows`），對外描述「用自然語言找現成工作流」。

### 4.3 Vectorize 開啟（R2.3，不新造）
完全沿用 #7：`acr init` 問 / `kbdb_embed:true` + `acr update` → deploy 建 Vectorize index。AI 從 search 回傳的 `capability_hint` 得知未開 → 主動問用戶要不要開。本 SDD **不碰** Vectorize 開關機制，只消費它。

---

## 5. R3：既有工作流回填

**推薦：下次部署強制 + 一次性 backfill 端點（提示式，不自動跑）**

- **下次部署強制**：R1 生效後，任何 re-deploy 都會被逼填 description → 自然回填（隨用隨補）。
- **一次性 backfill**：加 `POST /workflows/backfill-search-entries`（限本租戶），把現有 workflow_metadata records 中**已有 description 的**補寫成 entry（讓它們可搜）；**無 description 的**列出來回報「這些需要 re-deploy 補 description」，**不**自動編造描述（誠實）。
- backfill 是 **AI/人主動呼叫**（CLI/MCP 暴露），**不掛 cron/輪詢**（C2 flag 紅線）。
- 不破壞執行：entry 只供搜尋，workflow trigger 仍讀 WEBHOOKS KV / record，互不影響（R3.2）。

---

## 6. 薄殼一致性（rule 07）

| 能力 | API（真相） | MCP 薄殼 | CLI 薄殼 |
|------|------------|----------|----------|
| 強制 description | cypher 部署端點驗證 | （deploy 工具透傳錯誤）| （push 透傳錯誤）|
| 搜尋 workflow | cypher `/workflows/search` → KBDB `/entries/search` | `u6u_search_workflows`（本 SDD） | `acr workflow search`（R2.5，次階段，對稱補）|
| backfill | cypher `/workflows/backfill-search-entries` | 可選暴露 | 可選暴露 |

齊的單位是「能力」不是「端點」（decisions-summary 已釐清）：search 能力 MCP 先到位（AI 先用），CLI 對稱補可列次階段，不阻塞。

---

## 7. flag 安全自檢（C2）

- search_workflow＝AI 收到自然語言意圖時**主動 call 一次**，無排程、無 webhook 觸發、無跨 repo fan-out。✅
- backfill＝人/AI 主動呼叫一次，非 cron。✅
- 雙寫的 entry embed 走 `waitUntil` 非阻塞，是單次部署內的副作用，非輪詢。✅

---

## 8. 影響面（框架級，C1）

- **改 cypher-executor**：`/webhooks/named` + `/workflows/deploy` 加 description 強制；新增 `/workflows/search` + `/workflows/backfill-search-entries`；部署 handler 加雙寫 entry。
- **改 MCP**：`u6u_deploy_workflow` 抓並傳 description；新增 `u6u_search_workflows`。
- **改 KBDB**：可能不用改（複用 #7 `/entries/search` + `entry_type=workflow` 過濾；確認 `/entries/search` 支援 `entry_type` filter，若無則補一個 filter 參數——base 通用 filter，不寫死 workflow）。
- **self-hosted 影響**：description 強制對所有用戶生效 → 回填策略（R3）讓既有資產平滑過渡，不一刀斷。
- **不改**：workflow 執行/trigger 路徑、Vectorize 開關機制（複用 #7）、零件搜尋。

---

## 9. 拍板結果（leo 2026-06-27 issue #8，4 點全定）

1. **Q1 方案 C（雙寫）✅** — record 保 list/get 零回歸 + entry_type=workflow embeddable entry 吃 #7 語意，waitUntil 非阻塞。
2. **Q2 description「操盤 CC 據實生成、用戶可改」✅（leo 翻案）** — 非「介面層不生成」也非「逼用戶手填」。強制非空仍落 API；空時要求**操盤 CC 據實補一句**（非介面層機械塞佔位，仍禁佔位）。定位＝一句「能做什麼」非文章。詳見 §3.2。
3. **Q3 回填「下次部署強制 + 提示式 backfill」✅** — 無 desc 列出待 re-deploy、不自動編造、不掛 cron。
4. **Q4 補 base 通用 entry_type filter ✅** — 總管查證：`/entries/search` 目前**不支援** entry_type 過濾（只有 q/owner_id/source/mode），但 schema 有 entry_type 欄位 + 索引（`0001_base.sql:16-20`）。要改 **4 處**：`route`（entries.ts:43-77）/ `searchEntries` / `semanticSearch`（embed.ts）/ `kbdb-proxy`。**做成 base 通用 filter 不寫死 workflow**。

> **狀態：4 點拍定，已點頭實作。** 按 tasks.md Phase 1→6 推進，leo21c 端到端綠燈為完成標準。
