diff --git a/CONTEXT.md b/CONTEXT.md index 98d8c2d..86c1318 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -51,3 +51,7 @@ _Avoid_: Large file、413 file、upload too large **Move Rejection**: Sync 把本機改名轉成 server move 時,server 以非 retryable、非認證的錯誤拒絕(例如 `409 VERSION_CONFLICT`、`409 PATH_CONFLICT`);該組改名本輪略過並以 path error 回報,不改用 upload+delete,也不讓整輪停止。 _Avoid_: Move failure、move conflict、409 + +**Search Scope**: +Drive 搜尋的範圍:指定 library ID 時只搜該 library;省略時搜 Workspace 內所有未刪除的 library。Search Cursor 綁定 query、Search Scope 與 `path_prefix`,任一不同即為不同搜尋。 +_Avoid_: Global search、all-libraries search、workspace mode diff --git a/README.md b/README.md index 488586b..1dfdbca 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Official TypeScript SDK and CLI for [wspc.ai](https://wspc.ai). ## Roadmap +- [`wspc drive search` 支援 Workspace scope 與 `--path-prefix`](docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-design.md) - [Drive sync 明確回報超過單檔上限的檔案](docs/superpowers/specs/2026-09-18-drive-oversized-file-design.md) - [Drive watch 不再重傳被永久拒收的檔案](docs/superpowers/specs/2026-09-18-drive-permanent-upload-rejection-design.md) - [移除未使用的 occurrence-time-zone parser marker](docs/superpowers/specs/2026-09-09-occurrence-time-zone-marker-removal-design.md) diff --git a/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-brief.md b/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-brief.md new file mode 100644 index 0000000..8438ffb --- /dev/null +++ b/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-brief.md @@ -0,0 +1,100 @@ +# Product Review Brief:`wspc drive search` 支援 Workspace scope 與 `--path-prefix` + +來源 Todo:`tod_01M4A48GD4EMGXY7R3MAD1X7KE`。Backend 權威 spec:[Drive search findability](https://github.com/sadcoderlabs/wspc/blob/main/docs/superpowers/specs/2026-10-07-drive-search-findability-design.md)(wspc#1302,2026-10-07 已部署 production)。 + +## Goal and Problem + +**受影響的使用者**:用 `wspc drive search` 找 Drive 檔案的 CLI 使用者,以及透過 CLI 操作 Drive 的 agent。 + +**未被滿足的需求**:backend 已經可以一次搜尋整個 Workspace、用 `path_prefix` 限定資料夾,但 published `@wspc/cli` 0.14.0 的 `drive search` 仍是舊介面: + +- `` 是必要 positional,無法省略 library 搜整個 Workspace。 +- 沒有 `--path-prefix`。 +- `--cursor` 說明仍寫「same query and library」,沒有提到 `path_prefix` 與 v1 cursor 失效。 + +結果是使用者只能逐 library 搜,要跨 library 或限定資料夾就得自己打 HTTP;wspc 的 landing 文件也只能寫「待外部 wspc-cli 發佈」並附 HTTP example。 + +**證據(2026-10-07 本機查證)**: + +| 事實 | 來源 | +| --- | --- | +| Live `https://api.wspc.ai/openapi.json` 新增 `GET /drive/search`(operationId `drive_workspace_search`),兩個 search endpoint 都有 `path_prefix`,response 每筆帶 `library_id`。 | live OpenAPI readback | +| 與 committed `spec/openapi.json` 的差異只有這兩個 path 與 `DriveSearchResponse` schema,沒有其他 drift。 | 兩份 spec 的 path/schema 比對 | +| 兩個 endpoint 的 `x-cli.command` 都是 `drive search`,差別只在 per-library 那個有 `positional: ["id"]`。 | live OpenAPI `x-cli` | +| codegen 以 command path 決定輸出檔名,兩個 op 都寫到 `src/generated/cli/drive/search.ts`,後者覆蓋前者;`index.ts` 仍 import 兩個名字。 | `tools/cli-codegen/main.ts:230-245`,在 scratchpad 對 live spec 乾跑 | +| 對 live spec 跑 `sync-spec → generate → typecheck` 會失敗:`index.ts(39,10): error TS2305: Module '"./drive/search.js"' has no exported member 'driveSearchCommand'`。 | 同上乾跑 | +| `Release` workflow 每次都先 `sync-spec` 再 `generate` 再 `typecheck`,所以在修好之前任何一次 release 都會中止。 | `.github/workflows/release.yml:40-52` | + +**最小有用的 milestone**:讓 codegen 能處理這對同名 command,讓 `wspc drive search` 支援省略 library 與 `--path-prefix`,同步 live spec,恢復 release 可用。 + +**限制**: + +- 既有 `wspc drive search --query ...` 用法不能壞(script 相容)。 +- `src/generated/` 不手改;要改 generator 或 handwritten layer。 +- `x-cli` metadata 由 wspc backend repo 決定,本 repo 只能消費。 +- `path_prefix` 語意固定:純字串前綴、不解讀萬用字元、空字串視同未提供,與 `drive manifest get --path-prefix` 相同。 +- 每個查詢詞至少 3 個字元,短詞回空結果而非錯誤;舊 cursor 回 `VALIDATION_ERROR`。這些是 server 行為,CLI 只需正確轉述。 + +## User Stories + +- 身為 CLI 使用者,我想要 `wspc drive search --query 日本旅遊` 不帶 library 就能搜整個 Workspace,以便不用先 `drive library ls` 再逐一搜。 +- 身為 agent,我想要 `wspc drive search --query 日本旅遊 --path-prefix travel/` 只回 `travel/` 底下的檔案,以便縮小候選再 `drive file` 讀取。 +- 身為 CLI 使用者,我想要 `--cursor` 續頁在 Workspace scope 加 `--path-prefix` 下照常運作,並且在 `--help` 看到正確的 cursor 規則。 +- 身為既有 script 的作者,我想要 `wspc drive search --query ...` 升級後行為不變。 +- 身為 maintainer,我想要 release workflow 在 live spec 同步後仍能通過 typecheck,以便之後的 release 不再被這對 endpoint 卡住。 + +## Experience + +CLI 介面,無 GUI。預期的 help: + +``` +Usage: wspc drive search [options] [id] + +Search drive text across the Workspace + +Arguments: + id library ID; omit to search every library in the Workspace + +Options: + --query query + --limit limit + --cursor Opaque Search Cursor from next_cursor; reuse with the same query, library and path_prefix. No TTL. + --path-prefix path_prefix + -h, --help display help for command +``` + +| 狀態 | 行為 | +| --- | --- | +| Happy(Workspace) | `wspc drive search --query 日本旅遊` 呼叫 `GET /drive/search`,list 顯示 `library_id`、`path`、`snippet` 三欄;`--json` 原樣輸出 `{ results, next_cursor? }`。 | +| Happy(per-library) | `wspc drive search lib_x --query 日本旅遊 --path-prefix travel/` 呼叫 `GET /drive/libraries/lib_x/search?...&path_prefix=travel/`,顯示欄位依該 endpoint 的 `x-cli.display`(`path`、`snippet`)。 | +| 續頁 | 有 `next_cursor` 時沿用既有 pagination footer;使用者以 `--cursor` 搭配相同 query/scope/prefix 續頁。 | +| Empty | 無命中(含詞少於 3 字元)顯示 `no matches`,exit 0。 | +| Failure:舊或不符的 cursor | server 回 400 `VALIDATION_ERROR`,CLI 以既有格式印 `HTTP 400: {...}`,exit 1;help 已說明要重新搜尋。 | +| Failure:library 不存在/已刪除/他人 Workspace | server 回 404 `NOT_FOUND`,CLI 印 `HTTP 404: {...}`,exit 1。 | +| Failure:缺 `--query` | 與現在相同,由 server 回 400;CLI 不另做前置檢查。 | +| Permission | 沿用既有 bearer auth 與 `--account`;`x-cb-drive` consistency bookmark 由現有 fetch wrapper 依 response header 處理,不需新邏輯。 | +| Accessibility | 純文字輸出,沿用既有 pretty/JSON 切換。 | + +## Product Acceptance Criteria + +1. `wspc drive search --query <詞>` 不帶 library 可以執行,回傳跨 library 的結果,pretty 輸出有 `library_id` 欄。 +2. `wspc drive search --query <詞>` 維持 0.14.0 的行為與輸出欄位。 +3. 兩種 scope 都接受 `--path-prefix `,並原樣送到 server 的 `path_prefix`。 +4. `wspc drive search --help` 顯示 `[id]` 為可省略、`--path-prefix` 存在,且 `--cursor` 的說明提到 `path_prefix`。 +5. `--cursor` 在 Workspace scope 搭配 `--path-prefix` 可續頁;舊 cursor 得到 server 的 `VALIDATION_ERROR` 而非 CLI 自行報錯。 +6. `spec/openapi.json` 與 `src/generated/` 更新到 live production spec,且 `typecheck`、`test`、`build` 通過。 +7. 之後對 live spec 跑 `sync-spec → generate` 不再產生 drift 或 typecheck 失敗。 + +## 已定案的決策(2026-10-07 核准) + +- CLI 以 `wspc drive search [id]` 表達 scope:positional `id` 改為 optional,省略即 Workspace scope,有給即 per-library。既有 `drive search ` 用法不變。 +- codegen 新增「成對 command」規則:同一個 `x-cli.command` 對應兩個 operation,且恰好一個有 positional path param、另一個沒有時,合成一個 command;其他形式的重名直接讓 codegen 報錯中止。help、flag、description 仍由 spec 驅動。 +- wspc landing 文件 `cli.md` 的 search 段落在 CLI release 後,於 wspc repo 另開 Todo 更新;本 milestone 不跨 repo 開 PR。 + +## Out of Scope + +- 不替 `--query` 做 3 字元前置檢查,也不做 cursor 格式檢查:皆為 server contract,CLI 只轉述。 +- 不改 `drive manifest get`、`drive file` 等其他 Drive command。 +- 不處理 `x-cli` metadata 本身(屬 wspc backend repo)。 +- 不在本 milestone 觸發 npm release;release 仍由使用者手動在 Actions 執行。 +- 不加 `--library ` 別名;若之後使用者反映 positional 不直覺再評估。 diff --git a/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-design.md b/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-design.md new file mode 100644 index 0000000..eea706d --- /dev/null +++ b/docs/superpowers/specs/2026-10-07-drive-search-workspace-scope-design.md @@ -0,0 +1,178 @@ +# `wspc drive search` 支援 Workspace scope 與 `--path-prefix` + +產品決策依據:[Product Review Brief](2026-10-07-drive-search-workspace-scope-brief.md)(2026-10-07 核准)。Backend 權威 spec:[Drive search findability](https://github.com/sadcoderlabs/wspc/blob/main/docs/superpowers/specs/2026-10-07-drive-search-findability-design.md)。來源 Todo:`tod_01M4A48GD4EMGXY7R3MAD1X7KE`。 + +## Problem Statement + +身為用 `wspc drive search` 找 Drive 檔案的使用者或 agent,我想要省略 library 就搜整個 Workspace、用 `--path-prefix` 限定資料夾,以便不必先列 library、逐一搜尋,或自己打 HTTP。 + +2026-10-07 的現況: + +- wspc backend 已部署 `GET /drive/search`(operationId `drive_workspace_search`),兩個 search endpoint 都接受 `path_prefix`,每筆結果帶 `library_id`,Search Cursor 升 v2。 +- published `@wspc/cli` 0.14.0 的 `drive search` 仍要求 ``、沒有 `--path-prefix`、`--cursor` 說明過時。 +- live OpenAPI 中兩個 endpoint 的 `x-cli.command` 都是 `drive search`,只差 per-library 那個有 `positional: ["id"]`。codegen 以 command path 當輸出檔名,兩個 op 都寫到 `src/generated/cli/drive/search.ts`,後者蓋掉前者,`index.ts` 仍 import 兩個名字。對 live spec 跑 `sync-spec → generate → typecheck` 會得到 `index.ts(39,10): error TS2305: Module '"./drive/search.js"' has no exported member 'driveSearchCommand'`。 +- `Release` workflow 每次都先 `sync-spec` 再 `generate` 再 `typecheck`,所以修好前任何一次 release 都會中止。 + +限制:既有 `wspc drive search --query ...` 不能壞;`src/generated/` 不手改;`x-cli` metadata 由 backend repo 決定;`path_prefix`、3 字元規則、cursor 失效都是 server 行為,CLI 只轉述。 + +## Solution + +### 使用者介面 + +`wspc drive search [id] --query [--limit ] [--cursor ] [--path-prefix ]` + +| 情境 | 行為 | +| --- | --- | +| 省略 `[id]` | 呼叫 `GET /drive/search`(SDK `driveWorkspaceSearch`),render kind `drive_workspace_search`,display 依該 op 的 `x-cli.display`(欄位 `library_id`、`path`、`snippet`)。 | +| 給 `[id]` | 呼叫 `GET /drive/libraries/{id}/search`(SDK `driveSearch`),render kind `drive_search`,display 依該 op 的 `x-cli.display`(欄位 `path`、`snippet`),與 0.14.0 相同。 | +| `--path-prefix` | 兩種 scope 都原樣放進 query `path_prefix`;未給就不送。 | +| `--query`、`--limit`、`--cursor` | 原樣送出;缺 `--query` 由 server 回 400,CLI 不做前置檢查。 | +| 無命中 | 顯示 `no matches`,exit 0。 | +| server 4xx | 沿用 `runSdkCommand` 的 `HTTP : {...}` 輸出,exit 1。 | +| `--help` | summary 與 description 取自沒有 positional 的 op(`drive_workspace_search`);argument 說明為 `library ID; omit to search every library in the Workspace`;flag 說明來自 spec 的 parameter description。 | +| `x-cb-drive` bookmark | 由既有 consistency fetch wrapper 依 response header 處理,不需新邏輯。 | + +### codegen 成對 command 規則 + +- 定義:兩個 operation 的 `x-cli.command` 相同,且恰好一個有 positional path param(`x-cli.positional` 或 path param 自動補上的)、另一個完全沒有 path param,稱為一組 Paired Command。 +- Paired Command 只產生一個檔案(命名與現在相同,`drive/search.ts`),export 名稱取有 positional 的 op(`driveSearchCommand`),檔頭註解列出兩個 operationId。 +- positional 以 `[name]` 宣告為 optional。action 內 `name === undefined` 時呼叫無 positional 的 op,否則呼叫有 positional 的 op;兩條分支各自帶自己的 `kind` 與 `display`。 +- query/body flag 以兩個 op 的 query/body field 聯集產生;本案兩者 query 相同。 +- 不符合 Paired Command 定義的重名(例如三個 op 同名、兩個都有或都沒有 positional),codegen 以明確錯誤中止,不得 silent overwrite。 +- `index.ts` 對 Paired Command 只 import、註冊一個 command。 + +### Spec 同步 + +`spec/openapi.json`、`src/generated/sdk/`、`src/generated/cli/` 以 `npm run sync-spec && npm run generate` 更新到 live production spec,regenerated diff 一併提交。 + +## Acceptance Criteria + +所有自動化測試在本機與 CI 以 `npm test` 執行,mock generated SDK 與 auth loader,不需真實帳號。 + +| 編號 | 情境與預期結果 | 時機、環境與佐證 | +| --- | --- | --- | +| AC1 | `driveSearchCommand.parseAsync(["node","search","--query","日本旅遊"])` 呼叫 mocked `driveWorkspaceSearch` 一次、`driveSearch` 零次;input 無 `path`,`query.query === "日本旅遊"`;render context `kind === "drive_workspace_search"` 且 `display.columns` 含 `library_id`。 | pre-merge automated,`test/generated/drive.test.ts`。 | +| AC2 | 同 command 帶 `lib_123 --query 日本旅遊 --path-prefix travel/`:呼叫 `driveSearch` 一次、`driveWorkspaceSearch` 零次;`path.id === "lib_123"`,`query.path_prefix === "travel/"`;render kind `drive_search`,columns 為 `["path","snippet"]`。 | pre-merge automated,同上。 | +| AC3 | 省略 id 並帶 `--path-prefix travel/ --cursor c2`:`driveWorkspaceSearch` 收到 `query.path_prefix === "travel/"` 與 `query.cursor === "c2"`。未給 `--path-prefix` 時 `query.path_prefix === undefined`。 | pre-merge automated,同上。 | +| AC4 | `emitCommand` 對一組 Paired Command 輸入:輸出含 `.argument("[id]"`、`--path-prefix `、兩個 `operation:`(`driveSearch`、`driveWorkspaceSearch`)、`kind: "drive_search"` 與 `kind: "drive_workspace_search"`、`id === undefined` 分支;對非 Paired 的重名輸入,codegen 主流程丟出含兩個 operationId 的錯誤。 | pre-merge automated,`tools/cli-codegen/emit.test.ts`、`tools/cli-codegen/main.test.ts`。 | +| AC5 | `node dist/cli.js drive search --help` 輸出含 `[id]`、`--path-prefix `、`--cursor` 說明含 `path_prefix` 字樣。 | pre-merge automated(`test/cli-root.test.ts` 或新 smoke test 以 `buildProgram` 取 help text)或 PR 描述貼 `npm run build` 後的輸出。 | +| AC6 | `spec/openapi.json` 含 `/drive/search`;`src/generated/cli/index.ts` 對 `drive search` 只有一個 import;`npm run typecheck`、`npm test`、`npm run build` 通過。 | pre-merge,CI `pr.yml`。 | +| AC7 | 在乾淨 checkout 執行 `npm run sync-spec && npm run generate && git status --porcelain spec src/generated` 輸出為空(live spec 未再變動時),且 `npm run typecheck` 通過。 | pre-merge manual,PR 描述貼 readback;若 live spec 在實作期間又變動,貼出 diff 的 path 清單並說明。 | +| AC8 | Live 驗收:以已登入帳號執行 `node dist/cli.js drive search --query <本人 Workspace 中存在的 3 字元以上詞>`,回傳含 `library_id` 的結果;`node dist/cli.js drive search --query <同一詞> --path-prefix <存在的資料夾/>` 只回該前綴下的 path;以第一頁 `next_cursor` 加 `--cursor` 續頁成功;以任意舊字串當 `--cursor` 得到 `HTTP 400` 且 body 含 `VALIDATION_ERROR`。 | pre-merge manual,PR 描述貼 transcript(去除 token 與個人內容,只留欄位與 status)。 | +| AC9 | Release 後 `npx -y -p @wspc/cli@latest wspc drive search --help` 顯示 `[id]` 與 `--path-prefix`。 | post-release manual;release 由使用者手動觸發,不是 merge gate。 | + +## Implementation Decisions + +### 取捨順序與決策權 + +優先序:既有 `drive search ` 相容 > generated output 由 spec 驅動 > codegen 規則通用性 > 程式碼量。 + +| 限制 | 一行理由 | +| --- | --- | +| 不手寫 `drive search` command、不加本地 operationId skip 清單 | help 與 flag 文字會與 spec 脫鉤,之後 backend 改 description 不會跟上。 | +| 非 Paired 的重名必須報錯 | 今天的 silent overwrite 就是沒有這個 guard 造成的。 | +| per-library 分支維持 `kind: "drive_search"` 與原 display | 0.14.0 行為不變,既有 renderer/script 不受影響。 | +| CLI 不做 3 字元或 cursor 前置檢查 | server contract 已定義,重複實作只會製造不一致。 | + +判斷詞定義: + +- 「Paired Command」:同名兩個 op,恰好一個有 path param、另一個沒有。例:`drive_search`(`/drive/libraries/{id}/search`)與 `drive_workspace_search`(`/drive/search`)。反例:兩個 op 都有 path param 但 param 名不同;三個 op 同名。 +- 「相容」:`wspc drive search lib_x --query q` 在 0.14.0 與本版送出相同 HTTP request(除新增的 optional `path_prefix` 未給時不送),render kind 與 columns 相同。 + +實作者自行決定: + +- Paired Command 在 `main.ts` 的分組方式(先收集再成對,或在迴圈內查表)。 +- `emitCommand` 的輸入形狀(例如 `fallback?: { operationId, display?, queryFields }`)與分支程式碼排版。 +- 檔頭註解格式、測試檔拆分與 fixture 命名。 + +決定後回報: + +- 若兩個 op 的 query field 聯集需要去重或衝突處理(本案不需要),說明採用的規則。 +- AC5 採自動化或 PR 描述貼輸出。 + +停下來問: + +- live spec 在實作期間出現 search 以外的 drift,且 regenerate 後 typecheck 或既有測試失敗。 +- 需要改 `x-cli` 語意、`runSdkCommand` 介面、或 `src/handwritten/output` 的 renderer 分派。 +- 發現 live spec 再出現第二組同名 command 但不符合 Paired 定義。 + +### 實作地圖 + +要改的檔案: + +| 檔案 | 改動 | +| --- | --- | +| `tools/cli-codegen/main.ts` | 收集所有符合條件的 op 後,依 `x-cli.command` 分組;對 Paired Command 以有 positional 的 op 為主、另一個為 fallback 呼叫 `emitCommand` 一次;其他重名 throw。`emitted` 對 Paired 只 push 一筆。 | +| `tools/cli-codegen/emit.ts` | `EmitInput` 新增 fallback 欄位;positional 有 fallback 時以 `[name]` 宣告;action 內依 `name === undefined` 分支呼叫兩個 operation;import 兩個 SDK 函式;summary/description/help 取 fallback op。 | +| `tools/cli-codegen/emit.test.ts`、`tools/cli-codegen/main.test.ts` | AC4。 | +| `test/generated/drive.test.ts` | AC1–AC3,沿用現有 `vi.mock` SDK 與 `loadCommands()` 模式,mock 加入 `driveSearch`、`driveWorkspaceSearch`。 | +| `spec/openapi.json`、`src/generated/sdk/*`、`src/generated/cli/drive/search.ts`、`src/generated/cli/index.ts` | `npm run sync-spec && npm run generate` 產物,不手改。 | +| `README.md` | Commands 表加一列 `wspc drive search [id] --query [--path-prefix ]`;Roadmap 已連結本 spec。 | +| `CONTEXT.md` | 已加入 `Search Scope` 詞條。 | + +不要碰:`src/handwritten/commands/drive/*`(search 不走 handwritten)、`src/handwritten/output/render.ts`、`src/handwritten/auth/consistency-fetch.ts`、`scripts/sync-spec.ts`、`.github/workflows/*`。 + +要擴充的既有測試與 fixture:`test/generated/drive.test.ts` 的 mock 與 `loadCommands()`;`tools/cli-codegen/emit.test.ts` 既有 `emitCommand` 輸入樣式(見 `invite accept`、`todo show` 案例)。 + +允許的附帶改動:regenerate 後 `src/generated/sdk/types.gen.ts` 內與 search 無關但由同一份 live spec 帶出的型別差異(AC7 讀回時列出);`src/version.ts` 由 `sync-spec` 重建但維持不 commit。 + +### 已查證事實與已知陷阱 + +| 事實 | 來源 | +| --- | --- | +| live spec 與 committed spec 的差異只有 `/drive/search`(新增)、`/drive/libraries/{id}/search`(改動)與 `DriveSearchResponse` schema。 | 2026-10-07 兩份 spec path/schema 集合比對。 | +| 兩個 op 的 `x-cli`:`drive_workspace_search` 無 positional,`display.columns = ["library_id","path","snippet"]`;`drive_search` `positional: ["id"]`,`display.columns = ["path","snippet"]`。 | live OpenAPI `x-cli`。 | +| 兩個 op 的 query 參數相同:`query`(required)、`limit`、`cursor`、`path_prefix`。 | live OpenAPI parameters。 | +| codegen 輸出檔名為 `x-cli.command` 的 path,同名會覆蓋;`buildTree` 的 leafVarName 以最後一個為準。 | `tools/cli-codegen/main.ts:230-245`、`:142-156`。 | +| 對 live spec 乾跑 `generate → typecheck` 的失敗訊息:`src/generated/cli/index.ts(39,10): error TS2305: Module '"./drive/search.js"' has no exported member 'driveSearchCommand'`。 | 2026-10-07 scratchpad 乾跑。 | +| `Release` workflow 依序 `sync-spec`、`generate`、`typecheck`、`build`、`test`,再 commit drift 與 bump。 | `.github/workflows/release.yml:40-70`。 | +| `pr.yml` 跑 `typecheck`、`test`、`build`,不跑 `sync-spec`。 | `.github/workflows/pr.yml`。 | +| `x-cb-drive` bookmark 由 `consistency-fetch.ts` 依 response header 名稱處理,與 path 無關。 | `src/handwritten/auth/consistency-fetch.ts:5-12`。 | +| commander 的 optional argument `[id]` 在省略時 action 收到 `undefined`。 | commander 既有行為;`drive bind [path]` 已使用。 | +| `drive manifest get --path-prefix` 由 codegen 自動從 `path_prefix` query 產生 kebab flag,不需 `x-cli.options`。 | `src/generated/cli/drive/manifest/get.ts:15`。 | + +已知陷阱: + +| 症狀 | 處理 | +| --- | --- | +| 先 `sync-spec` 再改 generator 會讓 repo 在中途處於 typecheck 失敗狀態。 | 先改 generator 與測試(用 fixture 模擬 pair),再 `sync-spec && generate`,同一個 PR 內提交。 | +| `test/generated/drive.test.ts` 的 `vi.mock` 列舉 SDK 函式;缺 `driveWorkspaceSearch` 時 import 會得到 undefined 而非錯誤。 | mock 兩個 search 函式,並斷言未被呼叫的那個 call count 為 0。 | +| `src/version.ts` 被 `sync-spec` 改寫。 | 已 gitignore,不加入 commit。 | +| live spec 在實作期間再變動。 | AC7 讀回時列出 path 差異;只有 search 以外的差異造成失敗才停下來問。 | + +實作前 live readback: + +1. `curl -s https://api.wspc.ai/openapi.json | python3 -c 'import json,sys;s=json.load(sys.stdin);print(sorted(s["paths"]["/drive/search"]["get"]["x-cli"]), s["paths"]["/drive/libraries/{id}/search"]["get"]["x-cli"].get("positional"))'`,預期 `/drive/search` 無 `positional`、per-library 為 `["id"]`。若 upstream 已改成不同 command 名或 positional 形狀,停下來問。 + +### 後續方向 + +Paired Command 規則以「恰好一個有 path param」為界;若 backend 之後出現同名三層 scope(例如 org/workspace/library),再擴充為多 fallback。 + +## Testing Decisions + +| 準則 | 驗證方式 | +| --- | --- | +| AC1–AC3 | `test/generated/drive.test.ts`,mock SDK 與 render,斷言 operation 呼叫次數、input、render context。 | +| AC4 | `tools/cli-codegen/emit.test.ts` 字串斷言;`main.test.ts` 對分組/報錯函式的單元測試(需把分組邏輯抽成可 import 的純函式)。 | +| AC5 | 以 `buildProgram()` 或 `dist/cli.js` 取 help 文字斷言;若實作者改採 PR 描述貼輸出,說明原因。 | +| AC6 | CI `pr.yml`。 | +| AC7 | 手動 readback,PR 描述。無法自動化:PR CI 不跑 `sync-spec`,避免 CI 依賴 live API。 | +| AC8 | 手動 live transcript,PR 描述。無法自動化:需真實帳號與資料。 | +| AC9 | post-release 手動;release 為手動 workflow。 | + +實作切片(每片先寫會紅的測試): + +1. **codegen 分組與報錯**:`main.test.ts` 先寫「同名兩個 op 恰一個有 path param → 回傳一組 pair」與「不符合 → throw 含兩個 operationId」(紅:函式不存在);在 `main.ts` 抽出純函式並接上主流程;done = 兩個測試綠,既有 `main.test.ts` 綠。 +2. **emit 合成 command**:`emit.test.ts` 先寫 AC4 的字串斷言(紅:無 `[id]`、無第二個 operation);改 `emit.ts`;done = AC4 綠,既有 `emit.test.ts` 與 `test/cli-codegen.test.ts` 綠。 +3. **spec 同步與 regenerate**:`npm run sync-spec && npm run generate`;done = `src/generated/cli/drive/search.ts` 為合成版本、`index.ts` 單一 import、`npm run typecheck` 綠。 +4. **generated command 行為**:`test/generated/drive.test.ts` 先寫 AC1–AC3(紅:在切片 3 前 import 失敗或無 `--path-prefix`);done = AC1–AC3 綠。 +5. **help 與文件**:AC5 測試或輸出;README Commands 表;done = AC5、AC6 綠。 +6. **手動驗收與 PR**:AC7 readback 與 AC8 transcript 寫入 PR 描述;merge 後 AC9 由使用者 release 後確認。 + +## Out of Scope + +- `--library ` flag 別名:若使用者反映 optional positional 不直覺再加。 +- CLI 端 3 字元檢查與 cursor 格式檢查:server contract 已覆蓋。 +- wspc landing `cli.md` 的 search 段落:CLI release 後在 wspc repo 另開 Todo。 +- 觸發 npm release:由使用者手動執行 `Release` workflow。 +- 處理 search 以外的 live spec drift:目前沒有;若出現,另開 Todo。