iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

昨天 Day 15,我把 ClarifyBuild 的 Specification Setup 從容易出錯的文字格式,改成比較明確的 row-based editor。

也就是讓使用者不用記:

前提 | 當使用者 | 系統要 | 最後應該 | Important rules

而是可以一格一格填:

  • Precondition(可留空)
  • Trigger
  • System behavior
  • Result
  • Important rules(可留空)

Target Project User Flow 也從每行文字,改成可以新增、移除、選擇 View / Action 的結構化輸入。

Day 15 做完之後,ClarifyBuild 已經比較像一個真的 Builder。

我在 Day 15 的最後留了一個問題:

當 Specification Setup 互動開始變多,UI 程式本身是不是也該拆小?

還寫了一句:Day 16 很可能會先把 Scope Review 和 Specification Setup 的 UI rendering 拆開。

但今天真的打開專案,看起來合理的方向其實不只這一個:

  • 繼續拆 UI
  • 補 browser happy path test
  • 擴充 Project Specification 的章節完整度
  • 做 draft import/export
  • 做更完整的 responsive QA

最後我沒有照昨天預告的先拆 UI,也沒有先做 draft import/export。

我選的是一個很小、很不華麗,但我覺得現在很必要的切點:

把目前已經存在的 localStorage draft lifecycle,變成明確、可測、而且明顯壞掉的 draft 資料不會把 App 弄壞。

換句話說,今天沒有做新功能。

我想確認的是:

使用者辛苦填好的 draft,重新整理之後可以回來;但不該被保存的東西,也不能偷偷變成產品真相。


Day 16 一開始先確認 Day 15 到底做到哪裡

昨天我預告的是拆 UI。

但動手之前,我先看了 docs/DAY15_STATUS.md 列的下一步,想確認專案現在到底做到哪裡。

才發現一件重要的事:

localStorage 其實已經在 Day 14 的架構裡存在了。

Day 14 我寫過一條跨層規則,叫 Storage / Generated Output lifecycle,講的就是它。

專案裡已經有 src/storage/draftStorage.js。

而且 main.js 一開始也已經會:

loadDraft() ?? createBuilderState()

也就是說,App 啟動時會先嘗試從 localStorage 還原 draft;如果沒有資料,才建立新的 Builder State。

所以 Day 16 不應該重做 persistence。

這就是為什麼我今天一開始沒有急著寫功能。

但 storage 已經存在,不代表它已經可以信任。

如果我只是照昨天預告先拆 UI,很可能就直接略過它:它已經在跑了,可不可信,我卻還沒確認過。

在 ClarifyBuild 這個專案裡,storage 尤其不能只是「能用就好」。

因為前面 Day 10 到 Day 14 已經鎖定很多核心規則。

例如:

  • Candidate 只能先進 needs_confirmation
  • Blocking Dimensions resolved 後才能進 Scope Review
  • Scope 只能存在單一 features[]
  • Project Specification 是 generated output
  • AI Coding Prompt 也是 generated output
  • Specification Setup 資料遇到 upstream change 要保留,但標記 needsReview

如果 Day 16 只是為了「存資料」而重新設計資料流,反而會把前面鎖住的規格弄亂。

所以今天真正要處理的是:

既然 storage 已經存在,它是不是足夠可信?


存進 localStorage 的,其實是產品語意

localStorage 看起來很簡單。

把資料轉成 JSON,塞進瀏覽器,下一次打開再讀出來。

大概像這樣:

save draft
↓
refresh page
↓
load draft

但在 ClarifyBuild 裡,事情沒有這麼單純。

因為 ClarifyBuild 存下來的,不只是「輸入框裡有什麼文字」。

還有 Builder 自己需要記住的狀態:

  • 使用者目前走到哪個 Builder stage
  • 某個 requirement 是 unknown、needs confirmation,還是 resolved
  • Candidate 是不是還在等待使用者確認
  • scopeBaseline:最近一次確認 Scope 時,每個功能當時的 Scope 快照,之後 Scope 有沒有被改動,就是拿它來比
  • needsReview:上游被改動之後,先前填好的 Specification Setup 不會被刪掉,只會被標成「需要重新確認」

這些狀態如果恢復錯了,壞掉的就是產品邏輯。

例如,假設 Candidate Detection 發現:

targetUser candidate = 研究生

這個狀態應該是:

needs_confirmation

也就是系統猜到了,但還沒得到使用者確認。

如果重新整理頁面之後,它偷偷變成:

resolved

那就違反了 ClarifyBuild 很早就鎖定的規則:

AI 可以提出候選答案,但不能直接替使用者完成產品決策。

所以 Day 16 的 storage 測試,第一個要守的是:

資料恢復後,產品語意有沒有被改掉。


另一個更容易出錯的地方:generated output

存進 localStorage 的,主要是程式裡的 draft,再加上使用者目前走到哪個 stage。

draft 裡放著兩類東西。

第一類,是使用者真正確認過的產品決策,也就是 Canonical Project Model 的部分。

例如:

  • target user
  • problem
  • goal
  • platform
  • features 與 scope
  • behavior specification
  • constraints
  • tech stack decision

第二類,是 ClarifyBuild 自己需要記住的狀態,也就是 Interaction State。

例如某個 requirement 現在是 needs confirmation 還是 resolved、系統猜的 Candidate、needsReview、scopeBaseline。

這兩類要一起存。少了第一類,使用者的決定就不見了;少了第二類,App 就不知道使用者做到哪裡、哪些還沒確認。

但有兩樣東西不在 draft 裡:

  • Project Specification
  • AI Coding Prompt

它們是從 draft 推導出來的 generated output。很重要,但不是來源資料。

這條規則 Day 14 已經寫過:Generated Output 不能變成第二份真相。

那時候規則已經在程式裡,但 storage 這一段還沒有測試。今天補上。

如果今天把 generated Spec 也存進 localStorage,然後下次打開 App 直接拿它當真相,就會出現一個問題:

使用者看到的 Spec,可能不是目前 draft 重新產生出來的 Spec。

這會讓 ClarifyBuild 變得很危險。

因為使用者可能已經回到前面修改了需求或範圍,但畫面上還留著舊的 Spec。

如果這個舊 Spec 又被拿去產生 Prompt,Coding Agent 拿到的就不是最新決策。

所以 Day 16 要保護的第二個邊界是:

可以保存 draft,但不能把 generated Spec / Prompt 保存成來源資料。

目前的做法是:

如果使用者在 Spec Preview 或 Prompt Preview 時重新整理頁面,下次回來會被帶回 Specification Setup。

也就是回到來源資料所在的位置。

Project Specification 和 AI Coding Prompt 會清空。

使用者要重新產生。

這看起來比「直接回到預覽頁」多一步。

但這一步是有意義的。

因為它確保使用者看到的 generated output,一定是從目前 draft 重新產生出來的,而不是某個被 cache 住的舊結果。


今天實際改了什麼

Day 16 的實作範圍很小。

主要改在 src/storage/draftStorage.js。

原本的 storage module 已經有:

  • saveDraft
  • loadDraft
  • clearDraft

今天我沒有改它的產品定位。

它仍然只負責保存目前 draft lifecycle。

但我加了兩件事。

第一,讓 storage 可以被注入。

也就是原本在瀏覽器裡使用 localStorage,但測試時可以給它一個 memory storage。

這樣就不用為了測 storage lifecycle 馬上引入 browser test dependency。

第二,讀取時加入基本的 payload validation。

如果 localStorage 裡的資料是壞掉的 JSON,或 stage 是未知值,或 draft 裡面少了後面流程一定會讀的欄位,就不要硬讀。

例如 requirements 是空的、少了 targetUser、features 裡混進 null,或 status 是不認識的值。

前幾種會讓程式直接丟出錯誤。

不認識的 status 雖然不會 crash,卻會讓 Builder 問不出下一題,也進不了 Scope Review。

所以驗證失敗,就回傳 null。

App 看到 null 之後,就會照原本流程建立新的 Builder State。

這還不是完整的 migration framework,舊版本相容的問題也留到之後。

今天先守住一個底線:

壞掉的 draft 資料不應該讓整個 Builder crash。


測試比功能本身更重要

今天新增的測試在 test/draftStorage.test.js。

它們不是在確認 localStorage API 能不能用。

現在的 saveDraft / loadDraft 對 draft 幾乎是原樣存取,沒有做任何轉換。

所以這些測試比較像護欄:之後不管我在讀取時加 migration、補預設值,還是整理資料格式,只要不小心動到這幾條規則,測試就會先叫。

我補了幾個情境。

第一,draft 可以 round-trip。

也就是保存後再讀回來,該在的東西都還在,意思也沒有被改掉。

包括:

  • Candidate 還是 needs_confirmation
  • Candidate 的內容(研究生)還在
  • needsReview 還在
  • scopeBaseline 還在

這裡特別重要的是 Candidate。

「研究生」只是系統猜的,還不是使用者的決定。

它可以被保存,但不能因為存了又讀回來,就變成 resolved。

第二,generated output 不會被保存。

測試會故意讓 state 裡帶著一份已經產生好的 Spec 和 Prompt:

generatedSpec: "# Project Specification ..."
generatedPrompt: "..."

然後呼叫 saveDraft。

接著我直接檢查 storage 裡實際寫進去的 JSON,這兩個欄位已經是空字串。

用 loadDraft 讀回來,也一樣是空字串。

也就是說,Spec 和 Prompt 是「根本沒被寫進去」,而不是「寫進去了,讀的時候才忽略」。

第三,Preview stage 不直接恢復。

如果使用者原本停在 spec_preview 或 prompt_preview,重新整理後會回到 specification_setup。

原因前面講過:預覽畫面上的 Spec / Prompt 是 generated output,回來之後應該由目前的 draft 重新產生。

第四,明顯壞掉的 draft 資料要 fail safely。

例如:

  • JSON 壞掉
  • stage 不存在
  • draft 是 null
  • draft 裡面缺了必要的欄位

這些情況都應該回傳 null,讓 App 自己走乾淨的初始狀態。

目前遇到明顯壞掉的 draft 資料,只是安靜地回到全新的 Builder,不會告訴使用者發生了什麼事,壞掉的舊資料也會在 App 啟動時被新的空白 draft 直接蓋掉。

要不要提示、怎麼提示,可以留到 Day 17 做匯入驗證時一起想。

localStorage 本身讀寫失敗的情況,今天也還沒處理。


今天沒有做 import/export

其實 Day 15 狀態文件裡有提到一個可能方向:

Add draft import/export so users can preserve work beyond localStorage.

這件事很合理。

但我今天沒有做。

原因是 import/export 會把 storage 的邊界放大。

一旦使用者可以匯出 draft,再匯入 draft,就要回答更多問題:

  • 匯出的格式要不要有 version?(localStorage 裡我目前只寫了 version: 1,還沒用它判斷任何事)
  • 匯入時要不要驗證 schema?
  • 匯入壞資料時要怎麼提示?
  • 匯入後是否直接覆蓋目前 draft?
  • generated Spec / Prompt 要不要一起匯出?

其中最後一題尤其重要。

如果 Day 16 還沒把 generated output 邊界測清楚,就直接做 import/export,很容易把 Project Specification 也包進去,讓它看起來像來源資料的一部分。

所以今天先不做。

Day 16 只先補 storage boundary。

等這個邊界穩了,Day 17 再做 import/export 會比較自然。


Day 16 小結

今天我沒有拆 UI(src/ui/render.js 還是很大,先記著),也沒有做新功能。

我做的,是把已經存在的 localStorage draft lifecycle,補到可以信任:

第一,讓 storage 可以被注入,Node test 不用瀏覽器就能測。

第二,讀取時加上基本的 payload validation,明顯壞掉的 draft 資料會回傳 null,App 會從乾淨的初始狀態重新開始。

第三,補了 4 個 storage regression tests,涵蓋上面四個情境,守住兩個邊界和一條底線:draft 還原之後產品語意沒有被改掉,generated Spec / Prompt 不會被保存成真相,明顯壞掉的 draft 資料也不會讓 App crash。

目前所有測試都通過(原本 12 個,加上今天的 4 個),static server 也能正常回應首頁。

今天最重要的不是 localStorage。

而是這個問題:

在一個會把需求變成規格、再把規格變成 Prompt 的工具裡,什麼東西才有資格被保存成來源資料?

使用者確認的產品決策
↓
Canonical Project Model
↓
Project Specification
↓
AI Coding Prompt

這條線如果反過來,Spec 或 Prompt 開始變成來源資料,整個 Builder 就會慢慢失去可信度。

所以 ClarifyBuild 可以記住使用者做到哪裡,但不會記錯哪一層才是真相。

明天 Day 17,我打算做 draft import/export。

它要處理的是同一個問題往外延伸:使用者把 draft 帶出瀏覽器、再帶回來的時候,工具要怎麼保存進度,又不混淆真相來源?

匯出的是整份 draft(產品決策加上 Builder 的狀態),不含 Project Specification 和 AI Coding Prompt;匯入要驗證格式;壞資料不能讓 App crash。

Day 16 完成。

明天見。


上一篇
Day 15|不要先做漂亮,先讓規格輸入不容易壞掉
下一篇
Day 17|Draft 可以匯出了,但 Spec 和 Prompt 不能跟著放進去
系列文
AI 寫不好,可能是我沒說清楚:30 天打造 ClarifyBuild,讓 Vibe Coding 從需求開始 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言