Mirror copy for gu-log SP-251. Original by Thariq (@trq212): Know your unknowns. English | 繁中
← 所有範例
了解你的未知 · 實作前

可調整的計畫

不要依執行順序排列計畫,而是依照有多大可能會修改每個部分來排序——這樣值得你關注的決策會浮上來,機械性的工作則沉到底部。

Prompt
用 HTML 寫一份註解匯出的實作計畫,但開頭放我最可能想調整的決策:資料模型變更、新的 type interface、以及任何面向使用者的部分。把機械性的重構藏到最底下——那部分我相信你。
Claude 產出的成品

實作計畫:註解匯出

Acme · 將審閱的註解匯出為可分享的 PDF 或 CSV · 分支 feat/annotation-export

工作量~2.5 人天
異動檔案13 個(3 個新增)
風險低至中
資料庫遷移1 個,純新增
↑ 要調整 ↓ 信任我
這份計畫按「你可能會調整的程度」排序,而非執行順序。從上往下讀 = 最值得你關注的優先。A 區包含所有判斷性決策;B 區是建置順序;C 區是機械性工作,可以完全跳過。
A

你可能會想修改的決策

高調整可能性

三個我做了判斷、但你可能不同意的領域。每個標記的選擇都顯示我考慮過的替代方案——可以切換比較。

A1 · 資料模型 — 新增 annotation_exports

annotation_exports 新增
iduuid pk
review_iduuid → reviews
requested_byuuid → users
statusenum(queued, rendering, ready, failed)
optionsjsonb
snapshot ①jsonb
① 沒有 snapshot 欄位 — renderer 在渲染時即時 JOIN annotations
artifact_url ②text null
rendered_attimestamptz null
② 不儲存產出物 — GET /exports/:id/download 按需渲染
created_attimestamptz
選擇 ① 快照 vs. 即時 join
計畫的選擇 — 反正規化快照
在請求匯出時,將註解 payload 複製到 snapshot jsonb。匯出是該審閱當下的記錄:後續的編輯、解決、刪除都不會改寫歷史。
  • 即使審閱被歸檔,匯出仍然有效。
  • Renderer 只讀一筆——不會在 annotations + annotation_replies 產生 N+1 問題。

成本:重度審閱約 ~40 KB/row;快照可能相對即時討論串變得過時。

替代方案 — 渲染時即時 join
跳過快照;renderer 在產生檔案時才 join annotations。匯出永遠反映當前狀態。
  • 沒有重複資料,沒有過時問題。
  • 重新下載同一份匯出可能會產生不同的檔案——對稽核用途來說很意外。

選這個如果:匯出是工作文件,不是記錄。一句話告訴我:「用即時 join。」

選擇 ② 儲存產出物 vs. 按需渲染
計畫的選擇 — 渲染一次,存到 blob storage
Worker 渲染 PDF/CSV 一次,然後寫入 artifact_url。下載會轉向 signed URL——便宜、可快取,而且可以分享給沒有 Acme 帳號的審閱者。

成本:需要管理 blob 生命週期;我會加一個 30 天 TTL 清理(C 區,項目 6)。

替代方案 — 每次下載都渲染
不儲存檔案:GET /exports/:id/download 串流一個全新的渲染。零儲存,但大型審閱每次下載約 2–4 秒,而且沒有可分享的公開連結。

選這個如果:儲存/合規規定讓儲存副本變得麻煩。

A2 · 新的 type interface

// src/types/exports.ts (新增)
export interface ExportRequest {
reviewId: ReviewId;
format: 'pdf' | 'csv';1
range: 'all' | 'unresolved' | TimecodeRange;2
includeReplies: boolean; // 預設 true
includeDrawings: boolean; // 預設 false3
}
 
export interface AnnotationSnapshot {
id: AnnotationId;
timecode: string; // "00:04:12.08"
author: { id: UserId; name: string };
body: string;
state: 'open' | 'resolved';4
replies: ReplySnapshot[];
}
1先做 PDF + CSV。之後要加 'xlsx''srt' 只需一個 union member + 一個 renderer。
2TimecodeRange 讓製作人可以只匯出某一場景的筆記。如果沒人要這個,拿掉可以省約半天。
3繪圖標記會點陣化到 PDF。預設關閉,因為會讓渲染時間增加三倍。
4刻意把內部的 5 狀態機在匯出時簡化為 2 狀態。如果審閱者需要看到 'wont_fix',告訴我。

A3 · UX 流程

步驟 1
匯出按鈕
審閱工具列溢出選單中的新項目,放在「複製分享連結」旁邊。
步驟 2
選項彈窗
格式、範圍、包含回覆、包含繪圖。重用設定裡的 ModalForm
步驟 3
這份計畫最弱的部分
背景任務 + toast
匯出在 worker 渲染;使用者收到「我們會通知你」的 toast,可以繼續瀏覽。
步驟 4
完成通知
鈴鐺通知 + 下載連結;也會列在審閱 → 匯出分頁。

為什麼步驟 3 最弱:大部分匯出會在 3 秒內完成,所以 fire-and-forget 的 toast 可能感覺小題大做——但 400 個註解加繪圖的審閱要約 20 秒,太長不能 block。混合方式(最多等 4 秒,然後 fallback 到通知)體驗更好,但會多約半天工時。你決定。

B

執行順序

建置順序

我實際會用的建置順序。每一步都在 CI 綠燈落地;步驟 5 翻 flag 之前不會有任何使用者可見的東西上線。

  1. 1Migration + model。 建立 annotation_exports,接 Sequelize model、factory、fixtures。0.5d
  2. 2Types + request validation。 A2 的所有內容,加上 POST /api/reviews/:id/exports 的 zod schema。0.25d
  3. 3Renderer。 先做 CSV(簡單),再透過現有的 @acme/render service 做 PDF。1d
  4. 4Worker job + blob 上傳。 Queue 到 exports.render;重試 ×3 然後 status = failed0.25d
  5. 5UI:選單項目、彈窗、toast、匯出分頁。 放在 flag export_annotations 後面。0.5d
  6. 6測試、文件、flag 對 Acme 團隊開放。0.25d
C

機械性工作(相信我)

低調整可能性

這個功能需要的重構和接線。這裡沒有判斷性決策——刻意收起來。

無聊但必要 8 個任務 · 總共約 0.5d,已含在上面的估時
  • AnnotationSerializerapi/annotations.ts 抽到 lib/serializers/,讓 export worker 可以重用。純搬移,沒有行為變更。
  • 完成 CommentMarkerAnnotationMarker 的重新命名——還有 3 個檔案 import deprecated alias。
  • annotation_exports 加到 fixture loader 和 CI database-reset script。
  • workers/index.ts 註冊 exports.render,並加到 dead-letter alert 清單。
  • flags.yaml 建立 feature flag export_annotations,預設關閉。
  • jobs/cleanup.ts 加一個每日 TTL 清理過期 export blob(只在選擇 ② 維持現狀時需要)。
  • 擴充 openapi.yaml 加上兩個新 endpoint,並重新產生 client。
  • 把 timecode 格式化 helper 從 player/utils.ts 移到 lib/time.ts——PDF renderer 需要它,但不該 import player bundle。

調整這三件事

你可以發送的最高槓桿回覆。複製一個、編輯、發送——我會修改計畫。

「選擇 ① 改成即時 join——這裡的匯出是工作文件,不是記錄。」
「把 TimecodeRange 從 ExportRequest 拿掉;沒人要求過依場景匯出。」
「步驟 3 用混合方式:最多等 4 秒 inline,然後 fallback 到通知。」