---
phase: 02.1
plan: 01
type: execute
wave: 0
depends_on: []
files_modified:
  - gradle/libs.versions.toml
  - composeApp/build.gradle.kts
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt
  - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt
autonomous: true
requirements: [UI-03, UI-04, UI-09, UI-10]
tags: [kotlin, kmp, compose-multiplatform, gradle, navigation, liquid, haze, compose-unstyled, wave-0]

must_haves:
  truths:
    - "navigation-compose 2.9.2, compose-unstyled 1.49.9, liquid 1.1.1, haze 1.6.10 resolve cleanly for iosArm64 and iosSimulatorArm64"
    - "Material Icons Outlined (CalendarMonth, MenuBook, Inventory2, ShoppingCart, Search) compile from accessible package"
    - "Wave 0 test stub files exist with @Test functions in @Ignore state for V-01..V-07 anchors"
  artifacts:
    - path: "gradle/libs.versions.toml"
      provides: "version catalog entries: navigation-compose, compose-unstyled, liquid, haze, compose-material-icons-extended (if needed)"
      contains: "navigation-compose = \"2.9.2\""
    - path: "composeApp/build.gradle.kts"
      provides: "commonMain dependencies wired"
      contains: "libs.navigation.compose"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt"
      provides: "V-01 test stub"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt"
      provides: "V-02 test stub"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt"
      provides: "V-03 test stub"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt"
      provides: "V-04 test stub"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt"
      provides: "V-05/V-06 test stubs"
    - path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt"
      provides: "V-07 test stub"
  key_links:
    - from: "composeApp/build.gradle.kts"
      to: "gradle/libs.versions.toml"
      via: "libs.navigation.compose / libs.compose.unstyled / libs.liquid / libs.haze references"
      pattern: "libs\\.(navigation\\.compose|compose\\.unstyled|liquid|haze)"
---

<objective>
Wave 0 — verify the three load-bearing assumptions (A1: Liquid iOS klibs resolve; A2: Material Icons Outlined available; A3: nav-compose 2.9.2 K/N back-stack save/restore), add the four new dependencies to the version catalog and `composeApp` build, and land the six commonTest stub files referenced by VALIDATION.md so V-01..V-07 anchors have target locations from day 1.

Purpose: De-risk the rest of the phase. If A1 fails, the GlassSurface backend default flips to Haze before any UI code is written; if A2 fails, `material-icons-extended` is added before screens reference icons.
Output: Updated `libs.versions.toml`, updated `composeApp/build.gradle.kts`, six failing-but-compiling test stubs.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md
@gradle/libs.versions.toml
@composeApp/build.gradle.kts

<interfaces>
Existing test analog (LoginViewModelTest.kt — pattern shape only):

```kotlin
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest

class XxxTest {
    @Test
    fun behaviorName() = runTest {
        // arrange
        // act
        // assert
    }
}
```

Existing libs.versions.toml relevant entries (already present):
- composeMultiplatform = "1.10.3"
- material3 = "1.10.0-alpha05"
- multiplatformSettings = "1.3.0"
- compose-components-resources, compose-foundation, compose-runtime, compose-ui already wired.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add nav-compose / compose-unstyled / liquid / haze (and material-icons-extended if needed) to version catalog and composeApp build</name>
  <files>gradle/libs.versions.toml, composeApp/build.gradle.kts</files>
  <read_first>
    - gradle/libs.versions.toml (current state — append-only edits; preserve all existing entries verbatim)
    - composeApp/build.gradle.kts (current state — locate the `commonMain.dependencies { ... }` block)
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Standard Stack (lines 117-178; locked coordinates and versions)
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Pitfall D (Material Icons availability — lines 461-465)
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § `gradle/libs.versions.toml` (modified)
  </read_first>
  <action>
    Edit `gradle/libs.versions.toml`:

    1. Append to `[versions]` block (after the existing `multiplatformSettings = "1.3.0"` line, preserving alphabetical/grouping conventions seen in the file):
       ```toml
       navigation-compose = "2.9.2"
       compose-unstyled = "1.49.9"
       liquid = "1.1.1"
       haze = "1.6.10"
       ```
    2. Append to `[libraries]` block (after the existing Phase 2 client block, separated by a comment header `# Phase 2.1 — App shell foundation (UI-03, UI-04, UI-09, UI-10)`):
       ```toml
       navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
       compose-unstyled = { module = "com.composables:composeunstyled", version.ref = "compose-unstyled" }
       liquid = { module = "io.github.fletchmckee.liquid:liquid", version.ref = "liquid" }
       haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
       ```

    Edit `composeApp/build.gradle.kts`:

    3. Inside the `commonMain.dependencies { ... }` block (locate by grep), append (after existing `implementation(libs.multiplatform.settings)` line, or after the last existing `implementation(...)` in commonMain):
       ```kotlin
       implementation(libs.navigation.compose)
       implementation(libs.compose.unstyled)
       implementation(libs.liquid)
       implementation(libs.haze)
       ```

    4. Verify A2 — Material Icons Outlined availability. Run a quick Gradle resolution probe:
       ```bash
       ./gradlew :composeApp:dependencies --configuration commonMainImplementation 2>&1 | grep -E "(material-icons-extended|material3)" | head -20
       ```
       The four icons referenced in UI-SPEC (`Icons.Outlined.CalendarMonth`, `MenuBook`, `Inventory2`, `ShoppingCart`) are NOT in the baseline icon set. They live in `material-icons-extended`. Add to catalog (per RESEARCH § Pitfall D):
       ```toml
       # in [versions]:
       compose-material-icons-extended = "1.7.3"
       # in [libraries]:
       compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "compose-material-icons-extended" }
       ```
       And in `composeApp/build.gradle.kts` `commonMain.dependencies`:
       ```kotlin
       implementation(libs.compose.material.icons.extended)
       ```

    Use the kebab-style alias-to-Kotlin-camel-case convention already in use (e.g. `multiplatform-settings` → `libs.multiplatform.settings`).

    Do NOT modify any existing entries. Preserve all comments. Append only.
  </action>
  <verify>
    <automated>count="$(./gradlew :composeApp:dependencies --configuration iosSimulatorArm64MainResolvableDependenciesMetadata 2>&1 | grep -E "(navigation-compose:2\\.9\\.2|composeunstyled:1\\.49\\.9|liquid:1\\.1\\.1|haze:1\\.6\\.10|material-icons-extended)" | wc -l | tr -d ' ')"; test "$count" -ge 5</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c '^navigation-compose = ' gradle/libs.versions.toml` returns 1 (the version entry)
    - `grep -c 'navigation-compose:navigation-compose' gradle/libs.versions.toml` returns 1
    - `grep -c 'composables:composeunstyled' gradle/libs.versions.toml` returns 1
    - `grep -c 'fletchmckee.liquid:liquid' gradle/libs.versions.toml` returns 1
    - `grep -c 'chrisbanes.haze:haze' gradle/libs.versions.toml` returns 1
    - `grep -c 'material-icons-extended' gradle/libs.versions.toml` returns at least 1 (version + library = 2)
    - `grep -c 'libs.navigation.compose' composeApp/build.gradle.kts` returns at least 1
    - `grep -c 'libs.compose.unstyled' composeApp/build.gradle.kts` returns at least 1
    - `grep -c 'libs.liquid' composeApp/build.gradle.kts` returns at least 1
    - `grep -c 'libs.haze' composeApp/build.gradle.kts` returns at least 1
    - `./gradlew :composeApp:help -q` exits 0 (catalog parses without error)
    - `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` exits 0 (A1 + A3 verified by successful K/N link with new dependencies on classpath)
    - All pre-existing `[versions]` and `[libraries]` keys are still present (`grep -c '^kotlin = ' gradle/libs.versions.toml` returns 1; `grep -c '^lokksmith-compose' gradle/libs.versions.toml` returns 1)
  </acceptance_criteria>
  <done>Version catalog declares the four new libraries (plus material-icons-extended) at the exact pinned versions; composeApp/build.gradle.kts wires them into commonMain; the iOS simulator framework links cleanly, proving A1 (Liquid iOS klibs resolve) and A3 (nav-compose 2.9.2 K/N classpath OK).</done>
</task>

<task type="auto">
  <name>Task 2: Land six failing test stubs for V-01..V-07 anchors</name>
  <files>
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt,
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt,
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt,
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt,
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt,
    composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt
  </files>
  <read_first>
    - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/LoginViewModelTest.kt (analog — `runTest`/`@Test`/`assertEquals` skeleton)
    - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt (analog — state-flow gate test shape)
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md § Wave 0 Requirements (locked file paths and anchor coverage)
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Validation Architecture lines 715-755
    - .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § Test files (new) lines 386-415
  </read_first>
  <action>
    Create six commonTest files. Each contains compiling test scaffolds that reference yet-to-be-created production types via `@Ignore`d test bodies (so the test compiles but does not yet pass — Wave 0 produces the targets, later waves implement and un-ignore). Use `kotlin.test` (`org.junit.*` is forbidden; `kotlin.test` only — matches Phase 2 convention).

    File 1 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.navigation

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-01 — UI-03 — `navigateToTab()` extension applies
     * popUpTo(graph.findStartDestination().id) { saveState = true }; launchSingleTop = true; restoreState = true.
     * Implemented in plan 02.1-04 (RootNavHost / Routes).
     */
    class NavigationTest {
        @Test
        @Ignore
        fun navigateToTab_appliesPopUpToWithSaveState() {
            // TODO(02.1-04): assert NavOptionsBuilder lambda flips popUpToId+saveState=true,
            // launchSingleTop=true, restoreState=true. Use TestNavHostController if available
            // in CMP commonTest; else capture a fake NavOptionsBuilder.
        }
    }
    ```

    File 2 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.ui.components.glass

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-02 — UI-04 — `resolveGlassBackend(...)` returns Liquid for iOS source-set defaults
     * with no debug override. Implemented in plan 02.1-03 (GlassSurface).
     */
    class GlassBackendTest {
        @Test
        @Ignore
        fun resolveGlassBackend_iosDefault_returnsLiquid() {
            // TODO(02.1-03): assert resolveGlassBackend(settings = MapSettings(), isDebug = false,
            //   default = GlassBackend.Liquid) == GlassBackend.Liquid
        }
    }
    ```

    File 3 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.ui.components.glass

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-03 — UI-04 — debug-build runtime override via multiplatform-settings honors
     * "debug.glass_backend" key with values "liquid" / "haze" / "flat".
     * Implemented in plan 02.1-03 (GlassSurface).
     */
    class GlassBackendOverrideTest {
        @Test
        @Ignore
        fun resolveGlassBackend_debugBuildHonorsSettingsOverride() {
            // TODO(02.1-03): use com.russhwolf.settings.MapSettings, set
            //   "debug.glass_backend" = "haze", isDebug = true, assert returns Haze.
        }

        @Test
        @Ignore
        fun resolveGlassBackend_productionBuildIgnoresSettingsOverride() {
            // TODO(02.1-03): same map but isDebug = false → returns the compile-time default.
        }
    }
    ```

    File 4 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.ui.screens.shell

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-04 — UI-09 — App.kt's Authenticated + currentUser != null branch resolves to AppShell,
     * not PostLoginPlaceholderScreen. Implemented in plan 02.1-08 (App.kt wire-up).
     *
     * Style: mirror AuthSessionTest.kt — runTest + state-flow assertion + Koin test container.
     */
    class AppShellGateTest {
        @Test
        @Ignore
        fun authenticatedWithUser_routesToAppShell_notPlaceholder() {
            // TODO(02.1-08): drive AuthSession through Authenticated state with a non-null currentUser
            //   and assert the App() composable selects the AppShell branch (via a probe-flag injected
            //   into the composition or via a refactored RootRouter pure function).
        }
    }
    ```

    File 5 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.ui.screens.recipes

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-05/V-06 — UI-10 — RecipesSearchViewModel state machine semantics.
     * Implemented in plan 02.1-07 (Search foundation).
     */
    class RecipesSearchViewModelTest {
        @Test
        @Ignore
        fun openThenQueryChangeThenClose_clearsQueryAndResetsIsOpen() {
            // V-05: TODO(02.1-07) — open() → onQueryChange("foo") → close() leaves
            //   state = SearchState(isOpen = false, query = "")
        }

        @Test
        @Ignore
        fun clear_resetsQueryButKeepsIsOpenTrue() {
            // V-06: TODO(02.1-07) — open() → onQueryChange("foo") → clear() leaves
            //   state = SearchState(isOpen = true, query = "")
        }
    }
    ```

    File 6 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt`:
    ```kotlin
    package dev.ulfrx.recipe.ui.screens.pantry

    import kotlin.test.Ignore
    import kotlin.test.Test

    /**
     * V-07 — UI-10 — PantrySearchViewModel parity with RecipesSearchViewModel
     * (open/close/clear semantics). Implemented in plan 02.1-07 (Search foundation).
     */
    class PantrySearchViewModelTest {
        @Test
        @Ignore
        fun openThenQueryChangeThenClose_clearsQueryAndResetsIsOpen() {
            // V-07: TODO(02.1-07) — same semantics as RecipesSearchViewModelTest.
        }

        @Test
        @Ignore
        fun clear_resetsQueryButKeepsIsOpenTrue() {
            // V-07: TODO(02.1-07).
        }
    }
    ```

    All six files use `kotlin.test.Test` + `kotlin.test.Ignore` only — no library types referenced (so they compile without depending on yet-to-be-created production code).
  </action>
  <verify>
    <automated>./gradlew :composeApp:compileTestKotlinIosSimulatorArm64 -q</automated>
  </verify>
  <acceptance_criteria>
    - `find composeApp/src/commonTest -name '*.kt' -path '*/navigation/NavigationTest.kt' -o -path '*/glass/GlassBackendTest.kt' -o -path '*/glass/GlassBackendOverrideTest.kt' -o -path '*/shell/AppShellGateTest.kt' -o -path '*/recipes/RecipesSearchViewModelTest.kt' -o -path '*/pantry/PantrySearchViewModelTest.kt' | wc -l` returns 6
    - `grep -l 'V-01' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt` matches
    - `grep -l 'V-02' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt` matches
    - `grep -l 'V-03' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt` matches
    - `grep -l 'V-04' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` matches
    - `grep -l 'V-05' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt` matches
    - `grep -l 'V-07' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt` matches
    - `grep -c '@Ignore' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt` returns 2
    - `./gradlew :composeApp:commonTest -q` exits 0 (no failures because all tests are `@Ignore`d)
    - No file imports `androidx.compose.material3` (Material 3 boundary): `grep -c 'material3' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 0
  </acceptance_criteria>
  <done>Six test files exist under `commonTest/`, each compiles, each contains @Ignore'd @Test functions referencing the V-anchor it covers, commonTest run is green (no real assertions yet — production code lands in subsequent waves).</done>
</task>

</tasks>

<verification>
- Catalog parses: `./gradlew :composeApp:help -q` exits 0
- iOS framework links: `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` exits 0 (proves A1 + A3)
- commonTest compiles + green: `./gradlew :composeApp:commonTest -q` exits 0
- All 6 test files exist at the exact paths listed in VALIDATION.md § Wave 0 Requirements
</verification>

<success_criteria>
1. nav-compose 2.9.2 + compose-unstyled 1.49.9 + liquid 1.1.1 + haze 1.6.10 + material-icons-extended 1.7.3 declared in `gradle/libs.versions.toml` and wired into `composeApp/build.gradle.kts` commonMain.
2. iOS simulator K/N framework links successfully (assumptions A1 and A3 confirmed).
3. Material Icons Outlined for the five icons used by this phase (CalendarMonth, MenuBook, Inventory2, ShoppingCart, Search) are reachable through the new `compose-material-icons-extended` artifact (assumption A2 resolved via preemptive add per RESEARCH § Open Question 2 recommendation).
4. Six commonTest stub files exist at the exact paths specified in VALIDATION.md § Wave 0 Requirements; all contain @Ignore'd @Test functions referencing their V-anchor IDs.
5. `./gradlew :composeApp:commonTest` exits green.
</success_criteria>

<output>
After completion, create `.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-01-SUMMARY.md` per `$HOME/.claude/get-shit-done/templates/summary.md`. Record the exact resolved versions of nav-compose, compose-unstyled, liquid, haze, and material-icons-extended (from `./gradlew dependencies` output) so subsequent plans can reference verified coordinates.
</output>
