Skip to content

Kitchen AI Phase 1a: tonight recipe recommendations with deterministic kitchen facts - #32

Draft
1395490241lhj-code wants to merge 4 commits into
mainfrom
claude/hopeful-volhard-9272e9
Draft

1395490241lhj-code wants to merge 4 commits into
mainfrom
claude/hopeful-volhard-9272e9

Conversation

@1395490241lhj-code

@1395490241lhj-code 1395490241lhj-code commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Kitchen AI Phase 1a, "今晚吃什么". Home dinner questions now give the model the real kitchen up front. The model only chooses and explains. Recipe cards show inventory facts the app computes itself.

This PR adds no new content-block type, no new proposal type and no dependency. Card-level 加入今晚 (Phase 1b) is not included. It needs a separate owner decision.

Commits:

What changes for the member (iOS, Home → Kitchen AI)

  • Home starters are now 今晚吃什么 / 用快过期的食材做饭 / 今晚想吃清淡一点 / 帮我补一道菜. 今晚吃什么 replaces 看看现在能做什么.

  • Meal questions preload the kitchen. A Home meal question loads inventory plus an app-ranked shortlist from the recipe library. The model can then reference library recipes by recipeID instead of guessing or writing out full recipes.

    • The context chips read 库存 · 今晚计划 · 菜谱.
    • Excluding 库存 also withholds the shortlist.
    • Planner is unchanged.
  • Recipe cards show deterministic kitchen rows:

    • 家里有 …
    • 还缺 … (up to 3 names, then 等 N 样; VoiceOver speaks every name)
    • 主要食材在库 · 用量与调料未核对, when nothing is missing
    • 可用掉临期 …

    These rows are presence-only. Quantities and seasonings are never checked, and the copy says so. The model's reason is shown only as the reason.

  • Adjacent recipe cards in one reply group into a vertical run. Each card is one accessibility element with a full spoken summary.

  • 继续调整 chips appear after a completed turn that has recipe cards: 换清淡一点的菜 / 换快手一点的菜 / 只用家里有的食材做菜 / 换一批菜. A tap only prefills the composer; it never sends.

  • VoiceOver announcements cover turn completion, failure, awaiting confirmation, offline, and action/undo outcomes. They only say what the screen already shows.

  • Offline guard. A send is blocked only when the network path is explicitly .unavailable; .unknown still sends. When blocked:

    • 当前没有网络连接,联网后再发送。 is shown.
    • The draft is kept and nothing is written.
    • Nothing is rerouted to a device model (D-044).
  • Server (src/server/services/ai-conversation.js):

    • propose_add_recipe_to_tonight and propose_replace_planned_meal keep an optional plannedServings (integer). Its description states the 1–12 range the client accepts. (bcca20a had removed it, because the old client refused it. PR iOS: fix bugs and logic issues found in the UX and functional reviews #31's interpreter now accepts it, so b089149 restores it.)
    • The add-to-tonight description now reads 低风险操作:客户端校验后直接执行,成员可撤销。 This is accurate: the coordinator treats this action as .low risk with no confirmation step.

Final implementation facts

Meal intent (ConversationContextAssembler.isMealQuestion). It applies to the Home entry only. If none of the rules below fires, nothing is preloaded.

  • English: a whole-word food cue (eat, eating, cook, cooking, dinner, meal(s), recipe(s)) plus a whole-word choice cue (what, suggest, recommend, idea(s)) or a Chinese choice cue.
  • Chinese, read from joined phrases rather than single characters:
    • (a) 吃[点些]?(什么|啥) asked as the question. It must be followed by 好/呢/吧/比较/合适/菜/饭, punctuation or the end, so 吃什么药 does not count.
    • (b) 想吃 followed by a choice word, a taste, or a dish noun within 4 characters.
    • (c) A dish verb (做/煮/炒/烧/炖/蒸/来/补/换/加/推荐), then 1–6 characters, then 菜. 菜 must end the phrase: end of text, punctuation, or 的/吧/呢/好/吗/了/啊/呀.
    • (d) 早/午/晚 + 饭/餐 joined directly to a cooking verb or a choice phrase, for example 晚饭做什么 or 晚餐有什么推荐.
    • (e) 菜谱/食谱/食材/做饭/做菜/下厨 together with a choice word.
    • (f) the family's own food plus 能/可以 + a cooking verb + 什么/啥, for example 家里有鸡蛋能做什么 or 冰箱里的东西能做点什么. Added in 5a6b15a.
  • Clause by clause (5a6b15a). The question is split at punctuation, and each clause is judged on its own:
    • A clause that names food someone else cooks (食堂, 餐厅, 餐馆, 饭店, 饭馆, 菜馆, 馆子, 外卖, 堂食, 出去吃, 外面吃, 老板, 服务员, 附近; English restaurant/takeout/takeaway/canteen/cafeteria) does not count. The exception is a clause that also names the family's own kitchen (家里, 在家, 家中, 自己做, 冰箱, 库存, 手头, 剩下).
    • A clause that names a medicine (药, but not 药膳) does not count.
    • The other clauses still count, so 「不想点外卖,在家做什么菜」 is a cooking question.
  • Inventory keywords (库存/食材/过期/临期/冰箱/…) still read inventory on any entry, as before, but without the shortlist. They take no account of where the food is; see the open risk below.

Shortlist (recipeCandidatesContext):

  • At most 8 candidates, each with recipeID, title, missingCount and usesExpiring.
  • Recipes already on tonight's plan are excluded, and so are recipes with no ingredient line the app can judge.
  • Order: fewest missing over recipeCreationInventory (D-024), then whether it uses something expiring, then library order.

Stock reading (KitchenAIStockReading, shared by the shortlist and the card):

  • 家里有 and 还缺 come from availableInventory. This is the same answer as the library's 还缺 N 样 and Recipe Detail.
  • 可用掉临期 comes from recipeCreationExpiringItems.
  • RecipeStockMatch only gains Index and summary(for:). The presence rule is unchanged.

Live context budget. The total is still 3000 characters. It is now filled smallest source first: a short source's unused share passes to longer ones, and no source gets less than an equal split.

Unchanged: the six AIContentBlock cases, the five AIActionProposal cases, ConversationActionCoordinator, ConversationResponseInterpreter, ConversationOrchestrator, persistence, confirmation and undo. The controller's confirm and undo paths only gained announcements.

Integration with main (b089149)

main @ 45b108d was merged in with a normal merge commit. It brings PR #31 (iOS review fixes) and PR #34 (Kitchen AI failure diagnostics).

  • The only text conflict was test/ai-conversation-stream.test.mjs. Both sides had appended tests at the end of the file.
  • The 9 other shared iOS files merged cleanly. Checked against main:
  • Servings protocol, end to end.
    • PR iOS: fix bugs and logic issues found in the UX and functional reviews #31 made the iOS interpreter accept plannedServings 1–12 on the two single-meal proposals and refuse anything else. The server schema now offers that field again (see Server above).
    • ConversationOrchestratorTests drive a provider tool call through the real interpreter, coordinator and store:
      • tonight for four writes 4;
      • no count, or null, keeps the existing rule;
      • 0, 13, 2.5 and "4" are refused with nothing written;
      • a replacement for four previews 「4 份」 and writes it on confirm.

Privacy fix (5a6b15a)

The false positives listed in the earlier review are fixed by the clause rule above.

  • Before the fix, ConversationContextAssemblerTests had 37 expected assertion failures in the two new cases:
    • eating-out and medicine questions read inventory and the shortlist;
    • 「家里有鸡蛋能做什么」 and 「冰箱里的东西能做点什么」 missed the shortlist.
  • After the fix: 56/56. The tests check the domain-tool reads and the final request context, not only the regex.
    • These read only tonightPlan and carry no inventory facts or missing counts: 早餐吃什么药, 今天食堂吃什么, 老板推荐什么菜, 推荐几家做菜好吃的餐厅, 附近有什么好吃的饭店, 今晚想出去吃饭, 今晚想出去吃什么, 外卖吃什么好, 午饭吃什么药好, 餐厅推荐什么菜.
    • These read inventory, tonightPlan and the shortlist: 今晚吃什么, 用快过期的食材做饭, 推荐一道家常菜, 家里有鸡蛋能做什么, 今晚四个人吃什么, 冰箱里的东西能做点什么, 不想点外卖,在家做什么菜, 不点外卖了家里有鸡蛋能做什么.
    • Excluding 库存 still withholds both inventory and the shortlist.
  • The Home tonightPlan read and the context budget are unchanged.

Test-only change from PR #33. AIConversationWorkspaceUITests.bringIntoView now scrolls back when the target sits above the visible band. At AX sizes the 继续调整 chips push the last card up. This is the same direction check as PR #33's 329cc50; nothing else from that PR is included. With it, testStructuredBlocks{Light,Dark}AccessibilityXXXL pass. Before it, both failed on the merged tree with the card scrolled above the band, and the Light case had already failed the same way on bcca20a.

Validation

Final tree 5a6b15a, local

All local iOS runs used xcodebuild through dev-sim-slot on the reserved simulator (iPhone 18 Pro, iOS 27.2) with parallel testing off. Xcode MCP was held by another worktree, so it was not used.

Suite Result
Full iOS unit target KitchenManagerTests 2660 run, 0 failures, 5 skipped (gated hosted smoke)
…of which ConversationOrchestrator / ContextAssembler / ActionCoordinator / DomainTools / ResponseInterpreter / AIConversationController 70 / 56 / 83 / 78 / 48 / 78, all passed
…of which RecipeStockMatch / AIRecipeStockPresentation / StatusStapleConsumption (PR #31) 8 / 5 / 7, all passed
…of which D-050 ConsumptionPersistence / KitchenStore, and de14ffd RecipePersistence / SwiftDataConsistency 20 / 49 / 13 / 6, all passed
npm test (full Node suite) 2014/2014
AIConversationAcceptanceUITests 4/4
AIConversationWorkspaceUITests 76/78 before the helper fix. The remaining 2 are the AXXXL StructuredBlocks cases, which then passed 2/2 with the helper fix

The full unit run was taken on the tree before the UI-test helper edit, and that edit is the only file changed since. server.js and every de14ffd file are identical to main. No SwiftData model or schema changes.

CI on 5a6b15a

Run Result
iOS tests, push 39708398951 (iPhone 17 Pro, iOS 26.5) success. Unit target: 2660 run, 0 failures, 5 skipped. Default UI set: 10 classes, 116/116. Includes ConversationOrchestratorTests 70/70, ConversationContextAssemblerTests 56/56, D-050 ConsumptionPersistenceTests 20/20 and KitchenStoreTests 49/49, de14ffd RecipePersistenceTests 13/13 and SwiftDataConsistencyTests 6/6, and StatusStapleConsumptionTests 7/7
Pages: Node 22 / 24 success
iOS release pipeline validation success
Targeted dispatch 39788704292: AIConversationWorkspaceUITests + AIConversationAcceptanceUITests (iOS 26.5) 82/82. testStructuredBlocks{Light,Dark}AccessibilityXXXL passed (58.8 s / 66.4 s). Unit target 2660/0 again

Screenshots from 39788704292. 20 Tonight and 20 AIBlocks captures, covering Light/Dark × Normal/AXXXL.

  • Dark mean luma is ≤47 and Light is ≥229, so the Dark captures really are dark.
  • At AXXXL, the recipe cards' 家里有 / 还缺 / 可用掉临期 rows wrap under their text column.
  • After bringIntoView, the 家常豆腐 draft card is fully in view with 查看菜谱 and 继续调整 below it.
  • The 继续调整 chips scroll horizontally at normal size and stack at AXXXL.

Since CI ran, main moved to 207ca09 (PR #36, native motion). It overlaps this PR only in ContentView.swift, in different DEBUG-fixture hunks, and merges cleanly. No CI has run on that combination.

Not run yet: real VoiceOver listening; a physical device.

Screenshot review guide (20 PNGs)

The images are local review evidence. They are not attached here. Location: ~/Library/Application Support/Claude/side-session-notes/local_b1036e8d-…/tonight-phase1a-screenshots/.

They are named Tonight-{Light|Dark}-{Normal|AXXXL}-{Starters|CardReady|CardShortfall|CardDraft|Adjustments}.png. At normal size, CardReady/CardShortfall and CardDraft/Adjustments are the same frame.

Check:

  1. Starters
    • 今晚吃什么 is first.
    • At AXXXL, the starters stay legible and their targets are ≥44pt.
  2. CardReady (番茄炒蛋)
    • 家里有 番茄、鸡蛋 / 主要食材在库 · 用量与调料未核对 / 可用掉临期 鸡蛋.
    • The fixture deliberately holds 1 tomato for a 2-tomato recipe, so the presence-only wording must not read as "enough".
    • At AXXXL, 「·」 never starts a line.
  3. CardShortfall (宫保鸡丁)
    • 还缺 花生米、大葱、生姜 等 4 样 wraps under its text column, not under the icon.
    • The reply text says the chicken is expiring, which matches 可用掉临期 鸡腿肉.
  4. CardDraft (家常豆腐)
    • The AI 推荐草稿 tag is visible and moves to its own line at AXXXL.
    • 还缺 lists both items.
  5. Adjustments
    • The chips row sits after the cards.
    • The chips only prefill the composer.
  6. Every capture asserts that the target lies between the task header and the context chips. For a card taller than that band, the assertion is on its bottom rows; at AXXXL the card's title may scroll off by design.
  7. Light/Dark contrast of the terracotta/amber 临期 icon and the secondary rows.

The iOS tests workflow runs on pushes to claude/**. It runs the whole KitchenManagerTests unit target plus its default UI selection, and its ios-screenshots artifact has the screenshot tour. The default UI selection does not include AIConversationWorkspaceUITests, so CI does not recapture the Tonight images. To have CI recapture them, dispatch the workflow with ui_tests: KitchenManagerUITests/AIConversationWorkspaceUITests.

Remaining open privacy risk (owner decision, not changed here)

The isMealQuestion false positives listed in the earlier review are fixed in 5a6b15a; see "Privacy fix".

One pre-existing contract remains, unchanged on main and on this branch:

  • inventoryKeywords (库存/食材/过期/临期/冰箱/…) read inventory on any entry whenever the word appears, regardless of setting. For example, 「餐厅的食材新鲜吗」 reads inventory, though not the shortlist.
  • Narrowing this changes an existing preload contract, so it needs an owner decision.

Known false negative: 「想吃宵夜」 does not preload. The model can still read the kitchen through its tools.

Effect on PR #33 (stacked on this branch)

PR #33 (claude/kitchen-ai-reliability @ 329cc50) is still based on bcca20a and was not touched. When it is integrated, merge this branch into it; do not rebase. Expect:

Vault reconciliation

  • Current Status.head_commit is 45b108d (PR iOS: fix bugs and logic issues found in the UX and functional reviews #31 merged). It stays there until this PR merges; then reconcile through the merge commit on main.
  • Notes to update after merge:
    • Architecture §12.13: the meal-intent rules (now clause-based, with rule (f)), shortlist, exclusion and budget above.
    • Architecture §11 / D-024 relation: KitchenAIStockReading.
    • UI Design System: AI recipe card stock rows and copy, starters, chips.
    • Next Actions:

Non-goals

No Phase 1b card actions. No OpenUI or other new dependency. No migration, sync, flag, credential or hosted-config change. No change to Inventory or Planner surfaces.

🤖 Generated with Claude Code

1395490241lhj-code and others added 2 commits October 20, 2026 13:59
Home meal questions now preload inventory and an app-ranked library
shortlist, so the model picks and explains instead of guessing or
re-emitting full recipes. Recipe cards show deterministic kitchen facts.

- Meal intent needs a food cue and a choice cue; English cues match
  whole words only. Home only; Planner unchanged.
- Shortlist (<= 8, recipeID/title/missingCount/usesExpiring) excludes
  tonight's planned recipes and is withheld with inventory.
- KitchenAIStockReading is shared by shortlist and card: in stock /
  missing from availableInventory, use-up hint from
  recipeCreationExpiringItems; ranking alone uses recipeCreationInventory
  (D-024). RecipeStockMatch gains Index and summary(for:), rule unchanged.
- Presence-only copy: "所需食材已在库 · 用量未核对" replaces "可直接做".
- VoiceOver announcements, offline send guard, adjustment chips that
  only prefill the composer, and the "今晚吃什么" Home starter.
- Server: drop plannedServings from add-to-tonight and replace-meal
  schemas the client rejects; describe add-to-tonight truthfully.

No change to the six content blocks, five proposals, coordinator,
interpreter, orchestrator, persistence, confirmation or undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home preloads inventory and the recipe shortlist only for questions that
clearly ask what to cook or eat. Ambiguous wording now preloads nothing;
the model can still read the kitchen through its tools.

- Chinese intent comes from joined cooking phrases, not single characters
  or words that merely co-occur: 吃什么 asked as the question, 想吃 with a
  choice, taste or dish, a dish phrase where 菜 ends it, a meal of the day
  joined to a cooking verb or choice, or a cooking noun with a choice word.
- Negatives such as 饭店推荐, 饭卡, 吃药, 菜馆, 菜单, 菜市场, 早餐店, 午餐会议,
  晚饭后吃什么药 and 我不想吃药 now read only tonight's plan; tests assert the
  real reads. Planner, exclusions and the context budget are unchanged.
- The all-present stock row reads "主要食材在库 · 用量与调料未核对", with a
  no-break space so the dot never opens a line at accessibility sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1395490241lhj-code and others added 2 commits October 21, 2026 21:53
Brings PR #31 (iOS review fixes) and PR #34 (Kitchen AI failure
diagnostics) under the Phase 1a work.

- test/ai-conversation-stream.test.mjs: both sides appended tests. Keep
  main's file whole (all 43 PR #34 tests, S1/S2 diagnostics included) and
  append this branch's servings test, rewritten for the protocol below.
- src/server/services/ai-conversation.js: keep plannedServings on
  propose_add_recipe_to_tonight and propose_replace_planned_meal. This
  branch had removed it because the old client refused it; PR #31's
  interpreter now accepts 1-12 and refuses anything else, so the schema
  offers the optional field with that range in its description. The
  low-risk wording for add-to-tonight is kept: the client executes it
  after validation without a confirmation step.
- The other shared iOS files merged without conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s end to end

- ConversationContextAssembler.isMealQuestion now reads a question clause
  by clause. A clause about food someone else cooks (食堂, 餐厅, 外卖, 老板,
  附近 ...) or about a medicine (药, not 药膳) does not read inventory or
  the shortlist, unless the same clause names the family's own kitchen
  (家里, 在家, 冰箱 ...). Other clauses still count, so 「不想点外卖,在家
  做什么菜」 stays a cooking question. New pattern (f) covers 家里有鸡蛋能
  做什么 / 冰箱里的东西能做点什么. The inventory exclusion, the Home
  tonightPlan read and the context budget are unchanged.
- Tests check the domain reads and the final request context: eating-out
  and medicine questions read only tonightPlan and carry no inventory facts
  or missing counts; home-cooking questions read inventory, tonightPlan and
  the shortlist; excluding inventory still withholds both.
- ConversationOrchestratorTests: four end-to-end cases from a provider tool
  call through the interpreter, coordinator and store: tonight for four,
  no or null count keeps the existing rule, 0/13/2.5/"4" is refused with
  nothing written, and a replacement for four previews "4 份" and writes
  it on confirm. The helper keeps the orchestrator alive for the whole
  stream, since its turn task holds it weakly.
- AIConversationWorkspaceUITests.bringIntoView: when the target sits above
  the visible band (at AX sizes 继续调整 pushes the last card up), scroll
  back in small steps; otherwise scroll down as before. Same direction
  check as PR #33's 329cc50; nothing else from that PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant