Project Context for AI Agents
Project Context for AI Agents
This file contains critical rules and patterns that AI agents must follow when implementing code in this project. Focus on unobvious details that agents might otherwise miss.
Brownfield refactor: This is an in-place architectural reset of whattomake.xcworkspace — not a greenfield project. Align existing code with the target SwiftUI-native patterns below.
Technology Stack & Versions
| Layer | Technology | Version / Notes |
|---|---|---|
| Platform | iOS | Deployment target 26.0 |
| Language | Swift | 6.0 (language mode; strict concurrency complete on both targets) |
| UI | SwiftUI + Observation | @Query, @Observable coordinators, @Bindable |
| Persistence | SwiftData | @Query reads; modelContext writes |
| Concurrency | Swift Concurrency | async/await only — Observation / @Observable, not @Published / @StateObject / @ObservedObject |
| Unit tests | Swift Testing | @Test, #expect; @testable import ForkPlan |
| Snapshot tests | swift-snapshot-testing | Point-Free; whattomakeTests target only |
| CI/CD | Fastlane + GitHub Actions | macos-26 runners |
| Build | Xcode 26+ | whattomake.xcworkspace, scheme whattomake |
| App product | ForkPlan.app |
Repo name what-to-make / whattomake |
Open: whattomake.xcworkspace (not .xcodeproj alone).
Deferred (not implemented yet): Imperial/metric unit display conversion. Additional Foundation Models features beyond paste→recipe and suggest-missing-ingredients.
Critical Implementation Rules
Language-Specific Rules
- Use Swift 6.0 with
SWIFT_STRICT_CONCURRENCY = completeon bothwhattomakeandwhattomakeTeststargets (Debug + Release). - Use Swift Concurrency only — do not introduce
@Published,@StateObject, or@ObservedObject(ObservableObject-style state). Prefer@Observable+@Bindable. - Mark UI-touching code with
@MainActor— views, coordinators, and persistence writes that touch the UI. - Pure helpers (
MenuGenerator,DaySelectionStorage) are non-isolated value types — do not put@MainActoronMenuGenerator. - No force unwraps (
!) unless strongly justified and documented. - Use verbose camelCase naming — no abbreviations like
elfor element. - Write DocC-style
///comments on public types and non-obvious methods; include- Parameters:blocks where helpful. - Validation errors: inline validation with user-visible messages in views/coordinators — not a separate domain error layer.
Framework-Specific Rules
Architecture (SwiftUI-native):
Views (@Query + @State) → Models ← SwiftData
Helpers/ (pure logic)
DesignSystem/
- Views (
Sources/Views/) — declarative UI;@Queryfor reads,@Environment(\.modelContext)for writes. - Models (
Sources/Models/) — SwiftData@Modeltypes (Recipe,Menu) and image helpers (ImageCodec,ImageStore). - Helpers (
Sources/Helpers/) — pure logic:MenuGenerator,DaySelectionStorage,DayDietConstraintStorage,AppStorageKey,MenuPersistence. - DesignSystem (
Sources/DesignSystem/) — shared styling; usefpAppTheme(),FpTypography,fpPrimary(), etc. Screen intent, flows, and token tables:ux-design.md. - Application (
Sources/Application/WeeklyMenuApp.swift) — sharedModelContainer, App Intent dependency registration; no use case or repository wiring. - Deleted after refactor:
Sources/UseCases/,Sources/Repositories/,Sources/ViewModels/.
Data flow (generate menu):
User action (view)
→ validate (≥ 7 recipes, ≥ 1 day, diet pools) via MenuGeneration.validationMessage
→ map recipes → RecipeSelectionInput [Sendable snapshot]
→ MenuGenerator.select(from:requests:) [cook-recency weighted; pure struct, no @MainActor]
→ compactMap selected inputs back to Recipe by id
→ MenuGeneration.run → MenuPersistence.replaceMenu (cook stats unchanged)
→ @Query auto-updates view
Day tweak / cook tracking:
Menu day swipe → MenuGeneration.rerollDay / assignRecipe / markCooked
→ markCooked bumps usageCount + lastCookedAt only
State management:
| Concern | Pattern |
|---|---|
| Recipe list | @Query(sort: \Recipe.name) |
| Latest menu | @Query via Menu.latestDescriptor() → menus.first |
| Day toggles | @AppStorage via DaySelectionStorage + AppStorageKey |
| Day diet filters | @AppStorage via DayDietConstraintStorage + AppStorageKey.dayDietConstraints |
| Transient UI | @State or thin @Observable coordinator (transitional until Epic 1 ViewModel deletion) |
| Writes | @Environment(\.modelContext) in views / MenuPersistence |
| Async UI | Task { @MainActor in ... } |
SwiftUI patterns:
@Queryauto-loads data — do not wire manual fetch/load paths.- Thin
@Observablecoordinators hold transient UI state only (validation messages, generation in-flight) — transitional until Epic 1 ViewModel deletion; do not add new ViewModels. - Menu list rows must render from snapshot value tuples
(day: String, name: String), not live@Modelobjects — prevents Form diffing crashes. - Map before
ForEach:Array(zip(menu.days, menu.recipes.map(\.name))). - Dynamic
@Queryfilters: use subviewinitwith_query = Query(...)— never inline predicate on changing@State. - Do not mutate persisted state from
.task(id:)while aFormis diffing.
SwiftData / persistence:
- Normal launches use a persistent
ModelContaineronly — no launch-argument store modes. - Menu lifecycle: delete-before-insert on regenerate —
MenuPersistence.replaceMenu(with:in:)deletes all existingMenurecords before inserting the new one. - Latest menu:
@QueryviaMenu.latestDescriptor()→ displaymenus.first. - Recipe fields:
name(required),notes(optional),usageCount(times marked cooked),lastCookedAt,thumbnailBase64,imageFilename,dietaryKindRaw/dietaryKind(standard|vegetarian|vegan),ingredients. RecipeIngredientfields:name(required),amount(optionalDecimal),unit(optional free text, stored as entered),sortOrder.- Ingredient units are stored as-entered — no imperial/metric conversion in v1.
- Menu fields:
generatedDate,days,recipes(snapshot of selected recipes). - Canonical day identifiers:
"Mon"…"Sun"(locale-independent). - Per-day diet constraints:
DayDietConstraint(any/vegetarian/vegan); vegan recipes satisfy vegetarian days; restricted days are hard filters (no fallback onto a veg/vegan day). - Menu generation validates diet pools before select (enough vegan recipes for vegan days; enough vegetarian∪remaining vegan for veg days).
- Menu selection weights recipes by cook recency (
lastCookedAt; never-cooked preferred). Planning a menu does not change cook stats. - Day actions on an existing menu: re-roll one day, choose a library recipe, or mark that day’s recipe cooked.
MenuGeneration/markCookedincrementsusageCountand setslastCookedAt.
Image storage (split design — do not blur):
- Thumbnails: Base64 JPEG string stored inline on
Recipe.thumbnailBase64(viaImageCodec). - Originals: Full-resolution files on disk via
ImageStoreatApplication Support/Images; referenced byimageFilename. - On delete, remove on-disk file best-effort; treat disk storage as local-only, not synced canonical data.
Product rules:
- Menu generation requires ≥ 7 recipes (
minRecipesRequired = 7). - User selects any subset of days Mon–Sun; each selected day may be Any, Veg, or Vegan.
- Generating or tweaking a menu does not increment cook stats; only Mark as cooked does.
- Recipe
nameis required;notesand photos are optional. - Recipe diet default is
RecipeDietaryKind/standard.
Folder structure (target):
Sources/
Application/ WeeklyMenuApp.swift
Views/ RecipesListView, AddRecipeView, GenerateMenuView
Models/ Recipe, RecipeDietaryKind, RecipeIngredient, Menu, ImageCodec (ImageStore)
Helpers/ MenuGenerator, MenuGeneration, DaySelectionStorage, DayDietConstraintStorage, AppStorageKey, MenuPersistence, MenuIntentSupport, ForkPlanModelContainer, AppleIntelligenceAvailability, RecipePasteExtraction, RecipeIngredientSuggestion, RecipeImagePlaygroundPrompt
Intents/ GetTodaysMealIntent, GetWeeklyMenuIntent, GenerateWeeklyMenuIntent, ForkPlanShortcuts
DesignSystem/ unchanged
Tests/
Fixtures/ makeTestContainer() (Story 0.3)
__Snapshots__/ iPhone17Pro-iOS26/ (see Tests/__Snapshots__/iPhone17Pro-iOS26/README.md)
Apple Intelligence — paste recipe / suggest ingredients:
AppleIntelligenceAvailability(Helpers) mapsSystemLanguageModelavailability intoavailable/notEnabled/unavailablefor UI gating.RecipePasteExtractor(Helpers) uses Foundation Models guided generation (@Generable) to turn pasted text intoRecipePasteDraft.RecipeIngredientSuggestorsuggests missing ingredients from recipe name (notes optional) plus existing lines; results are appended into ingredient drafts for review (user may delete unwanted rows). Empty results are a neutral status, not an error.- Paste extraction is Add Recipe only (hidden while editing). Overwrite confirmation appears when the form already has content.
AddRecipeCoordinatorcancels overlapping extract/suggest tasks and clears in-flight work on sheet dismiss so stale results cannot land.AddRecipeCoordinator/applyPasteDraft/extractRecipeFromPaste/suggestMissingIngredients/applyIngredientSuggestionsfill the form; user reviews then saves via existing SwiftData path. Save also commits any leftover pending suggestions so they cannot be dropped.- Amount parsing accepts ASCII and Unicode fractions (
1/2,½,1 1/2) so pasted amounts are not dropped on save. - Unavailable (ineligible / not ready / unknown): hide paste, suggest, and Image Playground UI. Not enabled: show controls disabled with Settings hint. Never auto-save generated drafts.
- Show
RecipeIngredientSuggestor/generatedContentDisclaimernear AI controls so users always verify generated content.
Apple Intelligence — Image Playground recipe photos:
RecipeImagePlaygroundPrompt(Helpers) builds version-agnostic concept text from name, ingredients, diet, and notes.RecipeImagePlaygroundSheetModifier(Views) presents iOS 26.imagePlaygroundSheet; swap that file for iOS 27 API changes without rewriting Add Recipe.- Do not use
ImageCreator(removed in iOS 27). Gate the button withEnvironmentValues/supportsImagePlaygroundandAppleIntelligenceAvailability(hide when unavailable; disable + Settings hint when not enabled). - Generated images reuse
AddRecipeCoordinator/handleLoadedImageData(thumbnail +ImageStore). User must still Save.
Testing Rules
Unit tests (Tests/):
- Framework: Swift Testing —
@Testfunctions,#expect,@MainActoron test structs when testing main-actor code. - Import:
@testable import ForkPlan(module name, not repo name). - Use
makeTestContainer()inTests/Fixtures/TestModelContainer.swift— in-memoryModelContainer, direct seed; no launch arguments. - Image disk tests may set
ImageStore/directoryOverrideto a temp directory and clear it indefer. - Test plan:
TestPlans/UnitTestsPlan.xctestplan→ targetwhattomakeTests. - Test target:
SWIFT_VERSION = 6.0,SWIFT_STRICT_CONCURRENCY = complete(same as app target).
Snapshot tests (Tests/):
- Library: Point-Free swift-snapshot-testing (
import SnapshotTesting); linked towhattomakeTestsonly. - Baselines:
Tests/__Snapshots__/iPhone17Pro-iOS26/— seeTests/__Snapshots__/iPhone17Pro-iOS26/README.mdfor slug convention and recording workflow. - Re-record baselines on a Mac via scheme env
RECORD_SNAPSHOTS=1, or use the manual GitHub Action Record Snapshot Baselines (workflow_dispatch+ALLOW_CI_SNAPSHOT_RECORD=1) — never enable recording in the normal PR compare workflow. - Shell
RECORD_SNAPSHOTS=1 xcodebuild …often does not reachTEST_HOST(ForkPlan.app); use Xcode scheme Test env vars, test-plan env, or the record-snapshots workflow (see snapshot README). - Snapshot tests seed data directly via
makeTestContainer()— no launch arguments. - CI runs snapshot compare on
macos-26viafastlane runUnitTests; compare uses documentedprecision: 0.98/perceptualPrecision: 0.98to tolerate dev-Mac vs runner drift until baselines are re-recorded onmacos-26(see snapshot README → CI compare mode).
Device slug mapping:
| Simulator destination | Device slug folder |
|---|---|
platform=iOS Simulator,name=iPhone 17 Pro |
Tests/__Snapshots__/iPhone17Pro-iOS26/ |
Baseline recording settings (Epic 2 snapshot tests must apply):
| Setting | Required value | Notes |
|---|---|---|
| Color scheme | Light | .preferredColorScheme(.light) or @Environment(\.colorScheme) override in test host |
| Locale | en_US |
Set via test Locale environment or view modifier — pick one approach and document for Epic 2 |
| Dynamic Type | Standard (.large / default) |
Do not use accessibility sizes in baseline snapshots |
| Simulator | iPhone 17 Pro | Must match CI Fastfile destination |
Removed (do not reintroduce):
UITests/target,UITestsPlan.xctestplanTests/Mocks/mock repositories-ui-tests-blank,-ui-tests-seeded,-debug-menulaunch arguments- Use case / view model unit tests tied to deleted layers
When behavior changes, update tests — especially validation rules, menu generation, persistence, image handling, and snapshot baselines.
Code Quality & Style Rules
- Keep business logic in Helpers/ — not in view
body. - Place new files in the flat
Sources/layout underViews/,Models/,Helpers/, orDesignSystem/; match neighboring file header comments. - Accessibility identifiers support VoiceOver continuity (UX-DR9) — preserve on interactive elements; not an XCUITest contract. Full UX principles:
ux-design.md.
Required accessibility identifiers:
| Area | Identifiers |
|---|---|
| Recipes | recipesList, emptyRecipesView, addRecipeButton |
| Add recipe | recipeNameField, notesField, choosePhotoButton, generateRecipeImageButton, saveRecipeButton, recipeDietaryKindPicker, ingredientNameField_<index>, ingredientAmountField_<index>, ingredientUnitField_<index>, addIngredientButton, pasteRecipeField, extractRecipeButton, suggestIngredientsButton |
| Menu | toggleDay_<Day>, dayDiet_<Day>, generateMenuButton, menuItem_<Day>, menuRecipesRequirementMessage, menuValidationMessage, rerollDay_<Day>, chooseRecipeDay_<Day>, markCookedDay_<Day> |
- Add accessibility identifiers to all user-interactive elements.
- Ignore naming inconsistencies (
whattomakevsForkPlan) unless they cause build, import, or test failures.
Development Workflow Rules
Local setup: macOS, Xcode 26+, iOS 26+ simulator (pinned: iPhone 17 Pro). Open whattomake.xcworkspace. Run bundle install for Fastlane.
Build & test commands:
# Unit + snapshot tests (pinned simulator)
xcodebuild -workspace whattomake.xcworkspace -scheme whattomake \
-testPlan UnitTestsPlan \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' test
# Re-record snapshot baselines (local only — never CI)
RECORD_SNAPSHOTS=1 xcodebuild -workspace whattomake.xcworkspace -scheme whattomake \
-testPlan UnitTestsPlan \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' test
# CI-equivalent via Fastlane (pinned iPhone 17 Pro via PINNED_TEST_DESTINATION in Fastfile)
WORKSPACE="$PWD" WORKSPACE_FILENAME="whattomake.xcworkspace" \
SCHEME="whattomake" TEST_PLAN="UnitTestsPlan" \
bundle exec fastlane runUnitTests
CI/release: PR checks in .github/workflows/pull-request.yml (Conventional Commit title validation + unit and snapshot tests via fastlane runUnitTests on macos-26, using the runner’s preinstalled Fastlane). Snapshot compare runs on the pinned iPhone 17 Pro simulator — not skipped on CI. The runUnitTests lane pins the test destination via PINNED_TEST_DESTINATION in fastlane/Fastfile. Merged-branch workflow in .github/workflows/merged.yml runs Oliver-Binns/Versioning to create GitHub releases/tags from commit semantics; TestFlight deploy runs only when Versioning produces a new release (skips chore/docs/ci/etc. merges to save CI minutes). Two version tracks: GitHub release semver is automated; App Store MARKETING_VERSION is set manually in Xcode before release; Fastlane reads marketing version from the project and increments CURRENT_PROJECT_VERSION from the latest TestFlight build for that marketing version.
Commits: Conventional Commits enforced by hooks/commit-msg.
<type>[optional scope][optional !]: <description>
Allowed types: build, ci, docs, fix, feat, chore, style, refactor, perf, test. Scope is optional; prefer unscoped messages unless scope adds clarity (e.g. fix(menu): handle empty state).
Architecture sensor (agent harness): ./scripts/check-architecture.sh — runs locally via hooks/pre-push (when core.hooksPath is set) and on every PR (including drafts) in the harness CI job. Full simulator tests still run only when the PR is ready for review. Boundaries and wrappers: architecture.md. Recovery: agent-playbook.md. Steering log: harness-log.md.
PR descriptions: Short prose only — 2–3 paragraphs summarizing what changed and why. Do not include a test plan, checklist, or ## Summary / ## Test plan sections; CI runs tests automatically.
PR review priorities:
- Functional regressions
- Data-loss / persistence risks
- SwiftUI state / concurrency defects
- Architecture boundary violations (reintroduced layers, logic in views)
- Snapshot test / accessibility regressions
- Missing tests for changed behavior
Refactor approach: Single batched release (NFR6) — no throwaway bug fixes in layers being deleted. Menu persistence fix: @Query + delete-before-insert via MenuPersistence.replaceMenu(with:in:).
Critical Don’t-Miss Rules
Do NOT:
- Reintroduce use cases, repositories, ViewModels (beyond thin transient coordinators), or
@Published/@StateObject/@ObservedObject - Wire manual menu load paths —
@Queryreplaces fetch wiring - Use session-only
generatedMenuwithout@Query - Create mock repositories for tests — use
makeTestContainer()instead - Add XCUITest or
-ui-tests-*launch arguments - Put business logic in view
body— extract toMenuGenerator, validation helpers - Iterate live
@ModelRecipein FormForEach— use snapshot tuples - Store full-resolution images inline in SwiftData — keep the thumbnail/original split
- Record snapshots in the normal PR/CI compare lane (
RECORD_SNAPSHOTS=1only via local scheme/test-plan or the manualrecord-snapshotsworkflow) - Put
@MainActoronMenuGenerator - Scatter raw
@AppStoragestring keys — useAppStorageKeyenum - Use
recipes.shuffle()without weighting (Phase 3 deferred)
Stability edge cases:
- Async UI work uses
Task { @MainActor in ... }— preserve main-actor isolation when extending. ImageStorefalls back to a tempImagesdirectory ifApplication Supportis unavailable — preserve this fallback.- On menu save failure:
do/catch→ user-visible error message (never silent).
Usage Guidelines
For AI Agents:
- Read this file before implementing any code.
- Follow ALL rules exactly as documented.
- When in doubt, prefer the more restrictive option.
- Update this file if new patterns emerge.
For Humans:
- Keep this file lean and focused on agent needs.
- Update when technology stack changes.
- Review quarterly for outdated rules.
- Remove rules that become obvious over time.
Last Updated: 2026-07-25