Day 26 結尾,我留了一個問題:
這種 draft,要讓 Setup Status 照
isSpecificationSetupComplete()的規則顯示,還是在還原、匯入的時候就擋下來?
我也說過,這一天可能會動到產品程式,不會是純測試。
今天 Day 27 先回答這個問題,再補測試。
要處理的不一致是:
按鈕已經鎖住了,但 Setup Status 還在說完成。
Day 23 我寫過,按鈕用 isSpecificationSetupComplete() 判斷 disabled,Setup Status 四行則是在 render.js 裡各自算出來。
那時候兩邊一致。Day 26 才看到有一種情況會對不上。
這種 bug 很煩。
因為它不是資料被寫壞,也不是使用者按得下去。
它是畫面同時給使用者兩個互相矛盾的訊號:
產生 Project Specification 按鈕是 disabled。對使用者來說,這會變成一個很直覺的疑問:
你不是說完成了嗎?那為什麼不能產生?
今天處理的就是這個不一致。
ClarifyBuild 是我這系列正在做的小工具:它會把一個模糊的產品 idea,透過幾站釐清流程,整理成 Project Specification,最後再組成 AI Coding Prompt。
今天改了 scopeEngine.js 和 render.js,新增 1 條 browser test。
npm test 從 32 條變成 33 條,全過。
這個問題不是從正常表單操作發生的。
ClarifyBuild 在 Specification Setup 裡,本來就有幾個表單 guard。
例如功能規格要存起來時,會檢查:
技術設定如果選「我指定技術」,也一定要填至少一個項目。
所以如果使用者是一步一步從畫面輸入,通常不會產生這些怪資料。
但 ClarifyBuild 還有 draft restore 和 draft import。
也就是說,資料不一定永遠來自現在這一版表單。
它可能來自:
這些資料有可能「結構合法」,但「產品語意不完整」。
例如:
techStack.mode = "specified"
techStack.items = []
再例如 feature specification:
actor = " "
userValue = "快速找到需要的論文"
behaviors = [...]
這兩份資料的型別都沒壞。
mode 是字串,items 是陣列。
actor 也是 string。
壞掉的是意思。
你說你要指定技術,卻沒有指定任何技術。你說有 actor,但 trim 之後其實是空白。
Day 27 的問題,就藏在這個差異裡:
還原與匯入的驗證接受的是「結構能不能載入」,setup 完成條件判斷的是「產品語意能不能產生 Spec」。
這兩件事不能混在一起。
看到這種 invalid setup draft,直覺上有兩種做法。
第一種是:
Setup Status 跟著
isSpecificationSetupComplete()的嚴格規則走。
也就是 draft 可以載入,但只要它還不能產生 Spec,畫面就不要說完成。
第二種是:
restore / import 階段直接拒絕這些 draft。
看起來第二種也合理。
可是它有一個比較危險的地方:restore 和 import 的風險不一樣。
import 失敗時,ClarifyBuild 會保留目前 state,使用者只是看到「匯入失敗」,手上正在編輯的東西不會被換掉。
restore 是啟動時發生的。Day 16 我就寫過這個機制:
壞掉的舊資料也會在 App 啟動時被新的空白 draft 直接蓋掉。
那時候被蓋掉的是壞資料。
但如果現在讓 loadDraft() 變嚴格,被擋掉的就是「結構合法、只是還沒填完整」的 draft,也就是使用者填過的內容。
目前 main.js 是這樣拿初始 state:
let state = loadDraft() ?? createBuilderState();
如果 loadDraft() 回傳 null,app 就會用空白 state 開始。
啟動時的 commit() 再把它存回 localStorage。
原本那份 draft 就回不來了,也沒有任何提示。
那只擋 import 呢?
restore 和 import 目前共用同一個 isValidStoredDraft(),只擋匯入就得另外加一套匯入專用的規則。
而且匯入失敗的訊息是「檔案格式不符合 ClarifyBuild draft」,拿來講「內容還沒填完整」也不太準。
這不是 Day 27 想冒的風險。
所以今天選的是第一種做法:
不改還原與匯入的驗證,只修 Setup Status 的完成判斷。
Draft 可以被保留下來。
使用者可以看到自己原本填過的內容。
但畫面不能把不完整的 setup 說成完成。
這條路徑要保留下來:載入一份 actor 是空白的 draft,Setup Status 顯示 功能規格:0 / 1,按鈕鎖住。
把 actor 補上、按「儲存功能規格」,就變回 1 / 1,按鈕也解鎖。
使用者不用重填,只要補上缺的那一格。
Generate button 其實已經是對的。
它用的是:
isSpecificationSetupComplete(state.draft)
只要 setup 不完整,按鈕就 disabled。
問題在右側 Setup Status。
原本功能規格的完成數,是用比較粗的條件算:
有 specification
沒有 needsReview
basedOnRevision 等於 feature.revision
這會漏掉幾件事:
也就是說,一份 hand-edited draft 只要有 specification 物件,而且 revision 對得上,它就可能被 Status 算成完成。
可是 isSpecificationSetupComplete() 不會放過它。
所以畫面會變成:
功能規格:1 / 1
產生 Project Specification:disabled
技術設定也有類似問題。
原本 Status 是看:
Boolean(techStack.mode)
所以只要 mode 是 "specified",它就可能顯示完成。
但 specified 還必須搭配至少一個 item 才算完成。
所以這種 draft:
{
"mode": "specified",
"items": [],
"needsReview": false
}
Generate button 會鎖住。
但 Status 可能會說:
技術設定:完成
這就是 Day 27 要修的地方。
今天沒有在 render.js 裡再寫一套新的條件。
因為 Day 27 的重點就是:
UI Status 不應該和真正完成條件分家。
所以我把 feature specification 和 tech stack 的完成判斷抽到 scopeEngine.js:
export function isFeatureSpecificationComplete(feature) {
const spec = feature.specification;
return Boolean(
spec &&
spec.actor?.trim() &&
spec.userValue?.trim() &&
spec.basedOnRevision === feature.revision &&
spec.needsReview === false &&
spec.behaviors.length > 0 &&
spec.behaviors.every(
(behavior) =>
behavior.trigger?.trim() &&
behavior.systemBehavior?.trim() &&
behavior.result?.trim()
)
);
}
技術設定也拆成兩層:
export function isTechStackConfigured(techStack) {
return (
(techStack.mode === "specified" && techStack.items.length > 0) ||
techStack.mode === "ai_delegated"
);
}
export function isTechStackComplete(techStack) {
return isTechStackConfigured(techStack) && !techStack.needsReview;
}
mode 是 "specified",代表使用者要指定技術;items.length > 0,才代表真的有指定內容。
mode 是 "ai_delegated",代表使用者把技術選型交給 Coding Agent,所以不需要手動列出 items。
為什麼刻意拆成兩層?
如果只用 isTechStackComplete(),那「有設定但 needsReview」會被當成單純未完成。
但 Day 24 已經確定,過期資料要保留,而且要顯示「需要重新檢查」。
所以完整判斷是:
configured + needsReview -> 需要重新檢查
configured + !needsReview -> 完成
not configured -> 未完成
這樣也沒有改掉既有 needs-review 行為。
isSpecificationSetupComplete() 現在改成重用這些 helper:
const mustHave = getMustHaveFeatures(draft.features);
const featuresComplete = mustHave.every(isFeatureSpecificationComplete);
const flowComplete = draft.targetProjectUserFlow.steps.length > 0 && !draft.targetProjectUserFlow.needsReview;
const constraintsComplete = draft.projectConstraints.reviewed;
return featuresComplete && flowComplete && constraintsComplete && isTechStackComplete(draft.buildConfig.techStack);
Setup Status 四行裡,今天只有「功能規格」和「技術設定」改成共用 helper。
功能規格完成數變成:
const completeFeatureSpecCount = mustHave.filter(isFeatureSpecificationComplete).length;
技術設定變成:
formatReviewStatus(isTechStackConfigured(techStack), techStack.needsReview)
功能規格標題旁邊的 chip 也從原本只看 revision / needsReview,改成真的檢查內容是否完整。
所以如果 draft 裡有空白 actor,畫面會有這樣的變化:
| 右側 Setup Status | 功能規格標題旁的 chip | |
|---|---|---|
| 之前 | 功能規格:1 / 1 | 完成 |
| 現在 | 功能規格:0 / 1 | 未完成 |
今天做的,只是把畫面上的「完成」收回來,讓它符合真正可以產生 Spec 的條件。
「主要流程」和「專案限制」本來就跟 isSpecificationSetupComplete() 用一樣的條件,所以今天沒有動。
但它們還是在 render.js 裡各自寫一份。之後完成條件改了,這兩行要記得一起改。
這次比較適合補 browser test。
因為問題不是 predicate 算錯。
Day 25 已經測過 isSpecificationSetupComplete() 的每個條件。
問題是 UI 顯示用了另一套比較鬆的邏輯。
所以今天新增的測試放在 test/browserFlow.test.js。
測試名稱是:
browser restored setup status follows strict completion rules
它先放一份完整 draft 進 localStorage,確認正向情境:
1 / 1
然後它一個一個載入 hand-edited draft 形狀:
specified tech stack without items
blank feature-spec actor
blank feature-spec user value
empty behavior list
behavior missing result
每一種都確認:
這條測試測的是:
當一份結構合法但 setup 不完整的 draft 被 restore 到畫面時,使用者看到的完成狀態要和真正能不能 generate 一致。
這就是 Day 27 的產品承諾。
Day 26 結束時是:
npm test
tests 32
pass 32
fail 0
今天新增一條 browser test。
最後結果是:
npm test
tests 33
pass 33
fail 0
和 Day 26 一樣,我的執行環境第一次跑完整測試時,browser tests 會因為 local server 不能 bind 到 127.0.0.1 失敗。
放寬本機執行權限後,同一組測試通過。
Day 23 到 Day 26,守的是:
不完整或過期的 Specification Setup,不能產生 Project Specification。
Day 27 守的是另一件事:
不能產生的時候,畫面說的話要準。
這五天守的位置如下:
| Day | 守的位置 | 問題 |
|---|---|---|
| Day 23 | Browser button | setup 沒完成時,使用者不能按 |
| Day 24 | Browser flow | 前段需求改了,舊 setup 要重新檢查 |
| Day 25 | Predicate | isSpecificationSetupComplete() 對每個缺口都要回 false |
| Day 26 | App flow action | 直接呼叫 generateSpecFromState() 也不能產生 |
| Day 27 | Setup Status | 畫面不能把不能產生的 setup 說成完成 |
今天補的是最後那種很容易被低估的 UI bug。
它不一定會讓資料壞掉,也不一定會讓使用者走到錯誤下一步。
但它會讓使用者不相信畫面。
而 ClarifyBuild 這種工具,本質上是在幫使用者把模糊想法整理成清楚規格。
如果它自己的狀態文字都不清楚,就很諷刺。
所以 Day 27 沒有做大改。
只做一件事:
Status 顯示的完成,必須是真的完成。
這樣就好。
Day 24 以後一直有一個已知差異還沒處理:
目前程式裡的 needs-review 規則,和 Project_Spec_Schema_v0.1.md〈十四、失效與 Needs Review 規則〉不完全一致。
例如 Target User、Problem、Goal 變更後,現在程式會把 tech stack 也標成需要重新檢查。
規則表對 Tech Stack 寫的卻是「不影響」。
今天沒有處理這件事。
因為 Day 27 處理的是「Status 和完成條件對齊」。
needs-review 規則要不要改,是另一個產品決策。
它會影響 Day 24 那條 browser 斷言,也可能影響 generators.test.js 裡對 markScopeRelatedDataNeedsReview() 的期待。
所以留給 Day 28 或之後獨立處理比較乾淨。
今天先把一個比較小、但很刺眼的不一致補起來。
按鈕說不能產生,Status 就不能假裝已經完成。
Day 27 完成。
明天見。