Day 16 結尾,我替今天寫了三個驗收條件:
匯出的是整份 draft(產品決策加上 Builder 的狀態),不含 Project Specification 和 AI Coding Prompt;匯入要驗證格式;壞資料不能讓 App crash。
今天就照這三個條件,做 draft import/export。
聽起來很簡單:一個按鈕下載 JSON,一個按鈕上傳 JSON。
最快的做法,是把目前整個 app state 直接轉成 JSON 再下載:
current app state
↓
JSON.stringify
↓
download
但這樣會把畫面上那份 Project Specification 和 AI Coding Prompt 也一起包進檔案。
所以今天真正要決定的是:
什麼東西可以放進 draft 檔,什麼東西不能放進去?
這條資料流是 Day 11、Day 12 定下來的,Day 14 開始寫進程式:
使用者確認的產品決策
↓
Canonical Project Model
↓
Project Specification
↓
AI Coding Prompt
Project Specification 和 AI Coding Prompt 當然可以被使用者帶走。
Spec Preview 本來就有「匯出 Markdown」。
Prompt Preview 也本來就有 Copy Prompt。
但那是 output action。
使用者要拿走輸出結果,沒有問題。
今天處理的是另一件事:
draft 檔裡要不要放 generated output?
我的答案是不要。
因為 draft 是來源資料。
Project Specification 和 AI Coding Prompt 是從來源資料產生出來的結果。
如果 draft 檔裡同時放了來源資料和產生後的結果,之後匯入時就會出現兩份真相:
匯出檔會比現在這版 App 活得更久。
ClarifyBuild 的 Project Specification 18 個章節還沒有完全補齊,generator 之後也可能調整。到那時,同一份 draft 可能會產生更完整的 Spec。
如果檔案裡還帶著今天產生的舊 Spec,它就會變成第二個版本的真相。
所以 Day 17 的規則很直接:
draft 檔裡不放 generated output。
匯出時,如果使用者現在停在 Spec Preview 或 Prompt Preview,檔案裡的 stage 也會被改存成 specification_setup。
匯入後,使用者要從目前 draft 重新產生 Project Specification,再重新產生 AI Coding Prompt。
這多了一步。
但這一步是在保護來源資料的邊界。
今天匯出的檔案叫 clarifybuild-draft.json。
它放的是兩類資料。
第一類是使用者已經做出的產品決策,也就是 Canonical Project Model 裡的內容:idea、target user、problem、goal、platform、features、scope、Must Have feature specification、Target Project User Flow、project constraints、tech stack decision。
第二類是 Builder 需要恢復流程的 Interaction State:目前 stage、activeGoal、Candidate 是否仍等待確認、scopeBaseline、needsReview、optional details 是否已經提供過。
這些 Interaction State 不算產品決策。
但少了它們,匯入後的 draft 會看起來完整,流程語意卻已經壞掉。
Day 16 那個「研究生」Candidate 也一樣:
系統猜到 target user 可能是研究生,匯出時它是 needs_confirmation,匯入後也必須仍然是 needs_confirmation。
這對應的是 Day 10 就鎖定的規則:
Inference ≠ Resolved。
系統猜到的,不等於使用者確認的。
所以 export 存的是:
Canonical Project Model + Interaction State。
今天沒有讓 import 直接相信任意 JSON。
匯出的檔案會帶一個很小的格式邊界:
kind: "clarifybuild.draft"
version: 1
exportedAt: ...
kind 用來說這是一份 ClarifyBuild draft。
version 用來說這是目前支援的 draft file 格式。
exportedAt 只是給人看的匯出時間;目前驗證只確認它是一個字串,不拿它做版本判斷。
這些欄位讓 import 可以先擋掉明顯不對的檔案。
例如不是 ClarifyBuild draft、版本不支援、或根本不是一份合法 draft shape。
沒有這層邊界,import 就會變成「能 JSON.parse 就先塞進 state」。
Day 16 我留了一題:
壞資料要不要提示使用者?
localStorage 還原失敗時怎麼處理,今天沒動。
如果瀏覽器裡存著壞資料,目前仍然是安靜地回到乾淨的 Builder。不過它和匯入共用同一套驗證,今天補強的檢查,還原時也一起套用。
但 draft import 不一樣。
匯入是使用者主動給 ClarifyBuild 一個檔案。
如果失敗,使用者需要知道。
所以今天的匯入流程是:
先讀檔。
再驗證。
驗證通過,才替換目前 state。
驗證失敗,就顯示錯誤 toast,而且目前 draft 不會被取代。
這裡特別補強了幾個會讓 App 出問題的形狀:
null
{ aiMayDecide, aiMustAsk }
activeGoal 不是 ClarifyBuild 認得的形狀null
我也把 App 的 commit 順序改成先 render,再寫入 localStorage。
這樣即使之後有漏網的壞資料穿過驗證,只要 render 失敗,就不會先把壞 draft 寫進 localStorage。
匯入 action 也會保留原本 state。
如果匯入後 render 失敗,就回到原本 draft,並提示「已保留目前 draft」。
這不改 ClarifyBuild 的產品邏輯。
它只是讓外部檔案進來時,先經過一道比較可信的門。
今天有一個測試我特別想補。
它測的是一個故意刁難的情境:
如果一份匯入檔案裡偷塞了
generatedSpec和generatedPrompt,ClarifyBuild 會不會採用?
理想答案是:不會。
測試檔案可以故意長這樣:
generatedSpec: "# stale generated spec"
generatedPrompt: "stale generated prompt"
匯入後:
generatedSpec: ""
generatedPrompt: ""
這只是產品的資料邊界,跟資安無關。
外部檔案只能帶回 draft。
不能帶回一份聲稱已經產生好的 Spec 或 Prompt。
因為 Spec 和 Prompt 必須由目前 draft 重新產生。
這個測試守的是:
generated output 不能跨過 import/export 邊界,變成來源資料。
UI 上今天只加了兩個小入口:匯出 Draft 和 匯入 Draft。
匯出會下載 clarifybuild-draft.json。
匯入會讓使用者選一個 JSON 檔。
如果格式正確,就替換目前 draft,並提示要重新產生輸出。
這裡有一個刻意沒做的地方:
匯入前確認。
目前匯入一份合法 draft,會直接取代現在的 draft。
而且 ClarifyBuild 每次變更都會寫進 localStorage,被取代的舊 draft 回不來。
所以真正給使用者用時,應該要有一個確認步驟:
匯入會取代目前 draft,是否繼續?
Day 17 先不做這個。
原因是今天的最小切片是把資料邊界接上:匯出合法 draft、匯入合法 draft、擋掉壞資料、不恢復 generated output。
確認對話、拖拉匯入、更好的錯誤提示,可以接在這條線後面做。
Day 17 新增 3 個測試,並補強既有 storage invalid-shape 測試。
新增測試涵蓋五種情境。
第一,匯出的 draft 可以再匯入。匯入後 stage 回到 Specification Setup,needsReview 還在,Candidate 和 scopeBaseline 也仍然維持原本語意。
第二,匯出的 JSON 裡根本沒有 generatedSpec 和 generatedPrompt 這兩個欄位。Day 16 我只是把這兩個欄位存成空字串;今天乾脆不寫,匯出檔和 localStorage 都一樣。
第三,壞檔案會被拒絕。壞 JSON、unknown kind、unsupported version、draft 為 null,匯入都會回傳 null。更細的巢狀 shape 由補強後的 storage invalid-shape 測試守著,因為 import 和 localStorage 還原共用同一套驗證。
第四,即使匯入檔案硬塞 generated output,還原後仍然是空的。
第五,流程裡選「不指定功能」的 step,featureId 是 null,load 和 import 都要接受。
這個是我補強驗證時踩到的。我一度把 featureId 驗成一定要是字串,結果這種合法 draft 匯入會失敗,重新整理後還會被清空。
測試 fixture 每個 step 都有 feature,所以沒抓到,是用真實流程一步一步重放才發現。補強驗證的時候,也要反過來確認它沒有擋掉合法資料。
實作集中在 src/storage/draftStorage.js:新增 createDraftExport 和 importDraft,並讓 localStorage 還原與檔案匯入共用同一套驗證與還原邏輯。
src/main.js 和 src/ui/render.js 接上匯出與匯入入口,也調整了前面說的 commit 順序和匯入失敗時的還原。
Clarification Engine、Scope Engine、Spec Generator、Prompt Assembler 今天都沒動。
目前所有測試都通過:原本 16 個,加上今天的 3 個,現在是 19 個。
回頭看 Day 16 的三個條件:draft 匯出時不含 Project Specification 和 AI Coding Prompt;匯入時先驗證格式;壞資料不會讓 App crash:先被驗證擋下,就算漏網,也不會蓋掉目前的 draft。
現在 ClarifyBuild 可以把 Canonical Project Model 和 Interaction State 匯出成 draft 檔,也可以在匯入時先驗證格式、拒絕壞資料、清空 generated output,讓使用者從目前 draft 重新產生 Spec 和 Prompt。
這一步讓 draft 不再只困在 localStorage 裡。
但它也沒有讓 Project Specification 或 AI Coding Prompt 趁機混進 draft 檔。
所以 Day 17 看起來是在做 import/export。
其實還是在守同一件事:
Draft 可以被保存、移動、恢復;但來源資料不能混淆。
明天 Day 18,我應該要補 browser-level test。
因為 Day 15 到 Day 17 已經改了不少真實 UI 互動:structured setup editor、draft export/import、generated output lifecycle。
Node tests 守住了資料規則。
但使用者是不是真的能在瀏覽器裡一路走過 MVP、匯出、匯入、再重新產生輸出,還需要一條瀏覽器層級的測試來確認。
Day 17 完成。
明天見。