iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

Part 4|第 15/30 篇
今日要做的事: 定義 budget/time 的 Hard 邊界,並讓使用者自行宣告的 16:8 只改排程。
今天要解決的目的: 讓 DishFlow 對「花多少、多久吃得到」有一致判定,同時不把 LifeFlow 變成斷食療效工具。

Day 12 已經處理過敏與設備,今天不再重講蝦、花枝條與鍋具清洗。

我只補兩個常被寫成「大約」的上限:預算與時間。上限若可以被推薦文案說服,就不叫 Hard。


今日任務卡

項目 內容
產出 validateBudget()、validateTime()、16:8 排程 context、選配 feature flag
工具 自寫 Rule Engine(Node 純函式);本篇未啟用 Remote Config
不使用 過敏規則重跑、體重/體脂推論、療效宣稱、運動或營養功能擴張
驗收 剛好等於上限 PASS、超過 1 單位 BLOCK;缺價追問;窗外不推正餐

程式放在 material/code/day14,沒有呼叫 Gemini。


1. 問題:先定義怎麼算,才談有沒有超過

「100 元內」看似清楚,但我很快遇到三個洞:

家中已有的豆腐要不要再算一次成本?
外食的外帶費算不算?
缺一個價格時,能不能先當成 0?

時間也一樣。單口電磁爐上的兩個步驟不能同時跑;買缺料、備料與等待,也不能在估時時消失。

若 A、B1、B2、C 各用不同算法,Day 24 的「預算符合」與「時間可執行」就沒有可比性。今天不是多加兩個 if,而是先把量測口徑鎖死。

https://ithelp.ithome.com.tw/upload/images/20260929/20121052zWmdmrxpnp.png

左邊是推薦文字把「多 1 元」說過去。右邊是今天要的:剛好等於上限 PASS,超過 1 單位 BLOCK。16:8 只改排程,不推論該不該斷食。


今天走的路

先鎖預算口徑:只算這次還要付的錢,冰箱裡已有的食材不重複計價;缺價格就追問,不當成 0。再鎖時間口徑:從取得食材、備料、單口爐序列化烹煮,到可以吃為止。兩邊都測剛好等於上限,以及超過 1 單位。接著測 16:8:未宣告不限制;已宣告且在窗外,不顯示正餐推薦,只顯示下一個可排程時間。最後用 node test.js 把邊界與窗外案例跑完。

https://ithelp.ithome.com.tw/upload/images/20260929/20121052Y1N5ye4r2A.png


2. 設計:Hard 數字與排程 Context 分開

2.1 Budget:比較本次還要付的錢

v1 統一使用 TWD,採「這次決策的新增支出」:

home
  → needs_purchase 的實付小計
  → pantry 既有食材視為已購,不重複計價

convenience_store / supermarket / eat_out
  → 本次餐點實付總額
  → 已知外帶、平台或必要費用要納入

缺任何必要價格時,不可以先填 0:

cost_complete=false → needs_confirmation=true → 不判定 budget_ok

邊界採包含上限:

estimated_payable_twd <= max_budget_twd → PASS
estimated_payable_twd >  max_budget_twd → BLOCK

這個定義回答的是「現在付不付得起」,不是歷史食材成本分析。

2.2 Time:從開始處理到可以吃

時間上限比較:

acquire_minutes
+ prep_minutes
+ executable_cook_schedule_minutes
+ required_wait_minutes
= estimated_total_minutes

若完全使用 pantry,acquire_minutes=0。
若需要外出購買或等外送,就不能把取得食物的時間藏掉。

設備仍固定:

induction_cooktop + deep_wok + tamagoyaki_pan + microwave
burners = 1
max_pots_willing = 2

burners=1 會把兩段爐火工作序列化後再算總時間。
max_pots_willing=2 仍是 Day 13 的 soft,不加入 time Hard。

時間邊界同樣包含上限:

estimated_total_minutes <= max_minutes → PASS
estimated_total_minutes >  max_minutes → BLOCK

估時欄位缺失就追問,不用「看起來很快」代替數字。

2.3 16:8:只接受使用者自己的窗

16:8 的資料責任是:

type = context_module
user_declared = true
timezone 必填
now_inside 由程式算

窗外時,不顯示「現在吃」的正餐 options,只顯示「目前不在你設定的進食時段」和下一個可排程時間。

時段是使用者自己設的,系統只照表排程。不寫減重或代謝,也不依體重、體脂或餐點 tags 自動打開 16:8;不想用了,設定裡關掉就好。


3. 實作/實跑:規則寫進程式,測試只是證明它有跑

程式在 material/code/day14。budget.js 算這次還要付多少,time.js 把單口爐步驟序列化後加總分鐘,window.js 處理 16:8 與本地 feature flag。test.js 不發明新規則,只餵邊界值。

3.1 預算:先加總,再跟上限比

export function estimatePayable(input) {
  const source = input.food_source ?? "home";
  if (source === "home") {
    const purchased = sumPrices(input.needs_purchase);
    return {
      food_source: source,
      estimated_payable_twd: purchased.total,
      cost_complete: purchased.complete,
      missing_prices: purchased.missing,
      note: "pantry 既有食材不重複計價",
    };
  }
  const meal = sumPrices(input.meal_charges);
  const fees = sumPrices(input.extra_fees);
  return {
    food_source: source,
    estimated_payable_twd: meal.total + fees.total,
    cost_complete: meal.complete && fees.complete,
    missing_prices: [...meal.missing, ...fees.missing],
  };
}

export function validateBudget(payableTwd, maxTwd, costComplete) {
  if (!costComplete) {
    return { ok: false, needs_confirmation: true, code: "BUDGET_PRICE_INCOMPLETE" };
  }
  const ok = payableTwd <= maxTwd;
  return { ok, needs_confirmation: false, code: ok ? "BUDGET_OK" : "BUDGET_EXCEEDED" };
}

home 只加 needs_purchase。冰箱裡已有的豆腐不進這次應付。外食把餐點實付和外帶費分開加。sumPrices 遇到缺價就標 incomplete,不會把缺的欄位當成 0 元過關。

3.2 時間:重疊的爐火先排開

export function serializeCookSteps(steps, burners = 1) {
  const heating = (steps ?? [])
    .filter((step) => step.uses_burner)
    .sort((a, b) => a.start_min - b.start_min || a.end_min - b.end_min);

  let cursor = 0;
  let hadOverlap = false;
  const serialized = [];

  for (const step of heating) {
    if (step.start_min < cursor) hadOverlap = true;
    const start = Math.max(step.start_min, cursor);
    const end = start + (step.end_min - step.start_min);
    serialized.push({ ...step, start_min: start, end_min: end });
    cursor = end;
  }
  return { had_overlap: hadOverlap, serialized, cook_minutes: cursor };
}

單口爐上,前一段還沒結束,後一段就不能開始。總時間是:

acquire + prep + cook_minutes + wait
export function validateTime(totalMinutes, maxMinutes, estimateComplete) {
  if (!estimateComplete) {
    return { ok: false, needs_confirmation: true, code: "TIME_ESTIMATE_INCOMPLETE" };
  }
  const ok = totalMinutes <= maxMinutes;
  return { ok, needs_confirmation: false, code: ok ? "TIME_OK" : "TIME_EXCEEDED" };
}

3.3 16:8:沒宣告就不限制;窗外就清空

export function evaluateEatingWindow({ featureFlag, sessionWindow, nowMinutes, mealOptions }) {
  if (!featureFlag) {
    return { feature_enabled: false, ui_entry_visible: false, options: mealOptions };
  }
  if (!sessionWindow || sessionWindow.user_declared !== true) {
    return { feature_enabled: true, ui_entry_visible: true, options: mealOptions };
  }
  const inside = isInsideWindow(nowMinutes, sessionWindow.window_start, sessionWindow.window_end);
  if (inside) return { now_inside: true, options: mealOptions };

  return {
    now_inside: false,
    options: [],
    next_available_at: nextWindowStart(nowMinutes, sessionWindow.window_start).label,
    message_key: "outside_user_declared_window",
    message: "目前不在你設定的進食時段",
  };
}

測試用的窗是 12:00–20:00,只是 fixture,不是系統建議的斷食時段。本篇 未啟用 Remote Config;本地 feature_eating_window = false,入口隱藏,也不遠端改使用者資料。

排程不改資料流。外食仍只寫 today_meals;自煮仍要等 Outflow 才扣 pantry。


4. 驗證:九項測試

在 material/code/day14 執行 node test.js。矩陣與結果如下:

案例 結果
預算上限 100,應付 100 PASS → BUDGET_OK
預算上限 100,應付 101 PASS → BUDGET_EXCEEDED
缺一項必要價格 PASS → needs_confirmation
時間上限 20,總時 20 PASS → TIME_OK
時間上限 20,總時 21 PASS → TIME_EXCEEDED
兩段爐火被錯排並行 PASS → 序列化後總時 15
未宣告 16:8 PASS → options 不限制
已宣告且在窗外 PASS → options 空,顯示明天 12:00
feature flag 關閉 PASS → 入口隱藏

4.1 剛好等於上限 PASS,多 1 元就 BLOCK

https://ithelp.ithome.com.tw/upload/images/20260929/20121052Of835VdIUN.png

這張測的是邊界值,不是「大概一百」。家裡煮那組,needs_purchase 加總剛好 100,code 是 BUDGET_OK。外食那組便當 95 加外帶袋 6,合計 101,code 是 BUDGET_EXCEEDED。多 1 元也不能靠文案通融。note 寫著 pantry 既有食材不重複計價,對上第 2 節的口徑。

4.2 缺價不填 0

https://ithelp.ithome.com.tw/upload/images/20260929/20121052rjgiYBvrDr.png

雞胸沒有 price_twd,missing_prices 列出「雞胸」,cost_complete 是 false。estimated_payable_twd 雖然印出 0,但那不是把缺價當成免費;needs_confirmation 是 true,code 是 BUDGET_PRICE_INCOMPLETE,不會判定 budget_ok。

4.3 時間同樣卡在上限上

https://ithelp.ithome.com.tw/upload/images/20260929/20121052DEDuB0edLE.jpg

備料 5 分、烹煮 15 分,總時 20,剛好等於上限,TIME_OK。烹煮多 1 分變成 21,TIME_EXCEEDED。跟預算同一套規則:包含上限,超過 1 單位就擋。

4.4 單口爐:並行先拆開,再算總分鐘

https://ithelp.ithome.com.tw/upload/images/20260929/20121052qCblJnYIjh.png

測試資料寫的是「0–10 煮湯、3–8 同時煎蛋」,had_overlap 是 true。序列化後煮湯仍是 0–10,煎蛋被推到 10–15,總烹煮 15 分。上限 20,所以 TIME_OK。這道菜沒有被擋,只是估時不能照兩個爐口算;先重排,再跟上限比。

4.5 16:8 只改排程;Remote Config 未啟用

https://ithelp.ithome.com.tw/upload/images/20260929/20121052aBMD7xB32i.jpg

  1. 未宣告:晚上 22:00 也還有 P1、P2。沒自己開 16:8,就不限制 options。
  2. 已宣告且在窗外:現在 21:00,窗是 12:00–20:00。options 是空陣列,next_available_at 是「明天 12:00」,文案是「目前不在你設定的進食時段」。畫面上沒有正餐推薦卡。
  3. feature flag 關閉:ui_entry_visible 是 false。入口隱藏,options 仍在,沒有遠端改掉使用者資料。本篇沒接 Firebase Remote Config。

九項都過。沒有呼叫 Gemini。

Day 24 比較 A、B1、B2、C 時,四種方法要沿用同一個成本與時間口徑。今天不預告誰勝。


5. 今日結論

今天的結論是:Hard 上限先要有一致量測口徑;16:8 則只能是使用者宣告的排程 Context。

  • Budget 比本次新增實付,Time 比到可食用為止;缺值就追問,不當成 0。
  • 單口爐工作必須序列化,max_pots_willing=2 仍留在 soft。
  • 16:8 不讀體態做決定、不宣稱療效;本篇未啟用 Remote Config,只用本地 flag 關閉入口。

下一篇: 把 store、compile、Hard、Soft 與排程組成完整 Strategy Stack,再開始 Pilot。


附錄 A:完整 Rule 輸入

{
  "budget_rule": {
    "currency": "TWD",
    "max_budget_twd": 100,
    "comparison": "estimated_payable_twd_lte_cap",
    "missing_price": "needs_confirmation"
  },
  "time_rule": {
    "max_minutes": 20,
    "start": "begin_acquisition_or_prep",
    "end": "ready_to_eat",
    "include": [
      "acquire_minutes",
      "prep_minutes",
      "serialized_cook_minutes",
      "required_wait_minutes"
    ],
    "burners": 1,
    "missing_estimate": "needs_confirmation"
  },
  "eating_window": {
    "enabled": false,
    "user_declared_required": true,
    "timezone_required": true,
    "outside_behavior": "empty_options_and_schedule_next"
  }
}

上一篇
[Day 13] 先過 Hard,少洗鍋才有投票權
下一篇
[Day 15] 策略都接上了,會不會反而比貼 Prompt 更麻煩?
系列文
DishFlow AI Agent:用 Google AI 打造 Eat-Cost Balance 的下一餐決策系統 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言