iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0

Day 26 結尾,我留了一個問題:

這種 draft,要讓 Setup Status 照 isSpecificationSetupComplete() 的規則顯示,還是在還原、匯入的時候就擋下來?

我也說過,這一天可能會動到產品程式,不會是純測試。

今天 Day 27 先回答這個問題,再補測試。

要處理的不一致是:

按鈕已經鎖住了,但 Setup Status 還在說完成。

Day 23 我寫過,按鈕用 isSpecificationSetupComplete() 判斷 disabled,Setup Status 四行則是在 render.js 裡各自算出來。

那時候兩邊一致。Day 26 才看到有一種情況會對不上。

這種 bug 很煩。

因為它不是資料被寫壞,也不是使用者按得下去。

它是畫面同時給使用者兩個互相矛盾的訊號:

  • 產生 Project Specification 按鈕是 disabled。
  • 右側 Setup Status 卻顯示某些區塊已經完成。

對使用者來說,這會變成一個很直覺的疑問:

你不是說完成了嗎?那為什麼不能產生?

今天處理的就是這個不一致。

ClarifyBuild 是我這系列正在做的小工具:它會把一個模糊的產品 idea,透過幾站釐清流程,整理成 Project Specification,最後再組成 AI Coding Prompt。

今天改了 scopeEngine.js 和 render.js,新增 1 條 browser test。

npm test 從 32 條變成 33 條,全過。


正常表單路徑不太會出事

這個問題不是從正常表單操作發生的。

ClarifyBuild 在 Specification Setup 裡,本來就有幾個表單 guard。

例如功能規格要存起來時,會檢查:

  • actor 不能空白
  • user value 不能空白
  • 至少要有一個 behavior
  • 每個 behavior 都要有 trigger、system behavior、result

技術設定如果選「我指定技術」,也一定要填至少一個項目。

所以如果使用者是一步一步從畫面輸入,通常不會產生這些怪資料。

但 ClarifyBuild 還有 draft restore 和 draft import。

也就是說,資料不一定永遠來自現在這一版表單。

它可能來自:

  • 匯入進來的 draft,包含手動改過的 json。匯入後也會存進 localStorage,下次啟動再還原。
  • 以後規則改了,之前存的 draft 可能就不符合新規則。

這些資料有可能「結構合法」,但「產品語意不完整」。

例如:

techStack.mode = "specified"
techStack.items = []

再例如 feature specification:

actor = " "
userValue = "快速找到需要的論文"
behaviors = [...]

這兩份資料的型別都沒壞。

mode 是字串,items 是陣列。

actor 也是 string。

壞掉的是意思。

你說你要指定技術,卻沒有指定任何技術。你說有 actor,但 trim 之後其實是空白。

Day 27 的問題,就藏在這個差異裡:

還原與匯入的驗證接受的是「結構能不能載入」,setup 完成條件判斷的是「產品語意能不能產生 Spec」。

這兩件事不能混在一起。


我沒有讓 restore / import 變嚴格

看到這種 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,按鈕也解鎖。

使用者不用重填,只要補上缺的那一格。


原本的 Status 用了比較鬆的判斷

Generate button 其實已經是對的。

它用的是:

isSpecificationSetupComplete(state.draft)

只要 setup 不完整,按鈕就 disabled。

問題在右側 Setup Status。

原本功能規格的完成數,是用比較粗的條件算:

有 specification
沒有 needsReview
basedOnRevision 等於 feature.revision

這會漏掉幾件事:

  • actor 是不是空白
  • userValue 是不是空白
  • behaviors 是不是空陣列
  • behavior 的 trigger / systemBehavior / result 是不是完整

也就是說,一份 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 要修的地方。


把完成條件抽成 helper

今天沒有在 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 行為。


功能規格和技術設定,現在跟按鈕用同一組 helper

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 regression test

這次比較適合補 browser test。

因為問題不是 predicate 算錯。

Day 25 已經測過 isSpecificationSetupComplete() 的每個條件。

問題是 UI 顯示用了另一套比較鬆的邏輯。

所以今天新增的測試放在 test/browserFlow.test.js。

測試名稱是:

browser restored setup status follows strict completion rules

它先放一份完整 draft 進 localStorage,確認正向情境:

  • Generate button enabled
  • 功能規格顯示 1 / 1
  • 技術設定顯示完成
  • feature chip 顯示完成

然後它一個一個載入 hand-edited draft 形狀:

specified tech stack without items
blank feature-spec actor
blank feature-spec user value
empty behavior list
behavior missing result

每一種都確認:

  • Generate button disabled
  • Setup Status 不再誤報完成
  • feature spec 內容不完整時,chip 顯示未完成

這條測試測的是:

當一份結構合法但 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 27,其實守了兩件事

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 完成。

明天見。


上一篇
Day 26|不能只相信 disabled button
系列文
AI 寫不好,可能是我沒說清楚:30 天打造 ClarifyBuild,讓 Vibe Coding 從需求開始 共 27 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言