create-cmp-cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +65 -27
  2. package/bin/create-cmp.mjs +2 -1
  3. package/options.schema.json +5 -2
  4. package/package.json +2 -2
  5. package/src/commands/create.mjs +26 -3
  6. package/src/lib/toggle.mjs +2 -2
  7. package/template/.claude/settings.json +12 -0
  8. package/template/.claude/skills/add-feature/SKILL.md +131 -0
  9. package/template/.claude/skills/add-repository/SKILL.md +106 -0
  10. package/template/.claude/skills/add-screen/SKILL.md +130 -0
  11. package/template/.github/workflows/verify.yml +30 -8
  12. package/template/.gradle/8.11.1/checksums/checksums.lock +0 -0
  13. package/template/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
  14. package/template/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
  15. package/template/.gradle/8.11.1/gc.properties +0 -0
  16. package/template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  17. package/template/.gradle/buildOutputCleanup/cache.properties +2 -0
  18. package/template/.gradle/vcs-1/gc.properties +0 -0
  19. package/template/CHANGELOG.md +14 -0
  20. package/template/CLAUDE.md +70 -0
  21. package/template/CONTRIBUTING.md +39 -0
  22. package/template/README.md +103 -0
  23. package/template/composeApp/build.gradle.kts +15 -8
  24. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +4 -0
  25. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +10 -2
  26. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +10 -0
  27. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +6 -2
  28. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +1 -0
  29. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +2 -1
  30. package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +26 -0
  31. package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +37 -0
  32. package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +104 -0
  33. package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +29 -0
  34. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/A11yConformanceTest.kt +64 -0
  35. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +143 -0
  36. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeGoldenTreeTest.kt +78 -0
  37. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +77 -0
  38. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/navigation/AppShellTest.kt +79 -0
  39. package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/ComposeTestExt.kt +16 -0
  40. package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/StructuralTree.kt +63 -0
  41. package/template/docs/ARCHITECTURE.md +51 -0
  42. package/template/docs/TESTING.md +64 -0
  43. package/template/docs/adr/0001-adopt-the-create-cmp-harness-conventions.md +32 -0
  44. package/template/docs/adr/template.md +17 -0
  45. package/template/gitignore +8 -1
  46. package/template/gradle/libs.versions.toml +2 -0
  47. package/template/manifest.json +11 -8
  48. package/template/qa/e2e/README.md +29 -0
  49. package/template/qa/e2e/smoke.yaml +35 -0
  50. package/template/qa/evidence/schema.json +56 -0
  51. package/template/qa/golden/home.json +15 -0
  52. package/template/qa/lib/inputs-hash.mjs +112 -0
  53. package/template/qa/lib/token-drift.mjs +94 -0
  54. package/template/qa/receipt-check.mjs +113 -0
  55. package/template/qa/refusal-demo.mjs +491 -0
  56. package/template/qa/scaffold-feature.mjs +488 -0
  57. package/template/qa/verify.mjs +467 -0
  58. package/template/specs/README.md +33 -0
  59. package/template/specs/app-base.spec.md +30 -0
  60. package/template/specs/home.spec.md +18 -0
  61. package/template/qa/appium/README.md +0 -23
  62. package/template/qa/appium/lib/appium-client.mjs +0 -225
  63. package/template/qa/appium/package.json +0 -8
  64. package/template/qa/appium/run-android-smoke.mjs +0 -39
  65. package/template/tests/appium/cmp/conftest.py +0 -96
  66. package/template/tests/appium/cmp/test_smoke.py +0 -17
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: add-screen
3
+ description: >-
4
+ Add a presentation-only slice — Screen + ViewModel + tests + golden tree + spec + nav route —
5
+ for an EXISTING entity/repository in this Compose Multiplatform app, cloned deterministically
6
+ from the `home` exemplar's presentation layer. Requires the entity's data layer (model,
7
+ repository, use case, fake) to already exist. Use this when the user wants to "add a screen",
8
+ "add a screen for an existing entity", "add a screen backed by a repository I already have",
9
+ "put a UI on this data", or names an existing domain noun they want a screen for (e.g. "add a
10
+ screen for Tag", "I need a Bookmarks screen — the repository's already there"). Works with NO
11
+ create-cmp plugin installed — the stamper (`qa/scaffold-feature.mjs --preset screen`) and this
12
+ skill both ship inside the generated project.
13
+ ---
14
+
15
+ # add-screen — stamp a presentation slice over an existing entity
16
+
17
+ > Deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs --preset screen`) does
18
+ > the mechanical work — copy the `home` exemplar's presentation files, whole-word identifier
19
+ > rename, DI injection for the ViewModel only, nav route + import. You (the AI) only refine spec
20
+ > wording and adapt the screen to the entity's real shape. You are not done until
21
+ > `node qa/verify.mjs` PASSes and the receipt is committed — see this project's `CLAUDE.md`.
22
+
23
+ This is the `screen` subset of `add-feature` — same stamper, same rename mechanic, filtered to
24
+ the 6 presentation+tests+spec files, **composed on top of an entity that already exists** (via
25
+ `add-repository` or a prior `add-feature` run). If the entity doesn't exist yet, this will refuse
26
+ to stamp anything — see step 2.
27
+
28
+ ## Why a stamper and not hand-written files
29
+
30
+ Every hand-written file is a drift chance in this project's architecture. `qa/scaffold-feature.mjs
31
+ --preset screen` produces a **conforming skeleton by construction**: a Screen composable with a
32
+ testTagged root, a ViewModel (+ test), a Compose UI test, a golden-tree test, and a six-clause
33
+ spec — wired into Koin and the nav graph. Your job is to make it *behave* like the real screen,
34
+ not to make it *structurally correct*.
35
+
36
+ ## The flow
37
+
38
+ ### 1. Name the feature and confirm the entity
39
+
40
+ Ask the human for the feature/screen name (PascalCase, plural-ish noun — e.g. `Tags`,
41
+ `Bookmarks`) and the entity it's backed by (e.g. `Tag`, `Bookmark`). If they don't give an
42
+ entity, the script defaults it by stripping a trailing `s`/`ies` from the feature name — naive
43
+ de-pluralization is unreliable for irregular nouns, so **always show the proposed entity and let
44
+ the human confirm or override it** before proceeding (`--entity <EntityName>`).
45
+
46
+ The entity must already exist — its use case, fake, and model must be present
47
+ (`domain/usecase/Get<Entity>sUseCase.kt`, `testing/fakes/Fake<Entity>Repository.kt`,
48
+ `domain/model/<Entity>.kt`). If it doesn't, the stamper will refuse before writing anything and
49
+ tell you to run `add-repository` first (or use plain `add-feature` instead, which generates both
50
+ layers together).
51
+
52
+ ### 2. Dry-run
53
+
54
+ ```
55
+ node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName> --preset screen --dry-run
56
+ ```
57
+
58
+ Show the human the file plan (Screen, ViewModel, ViewModel test, Compose UI test, golden-tree
59
+ test, spec) and the injection diffs (ViewModel DI binding, nav route + import — no repository or
60
+ use case wiring, since those already exist). Confirm before stamping for real — this is the last
61
+ chance to catch a wrong entity name.
62
+
63
+ If the entity doesn't exist, this step (and the real stamp) exits non-zero with an actionable
64
+ message rather than half-stamping a screen that won't compile.
65
+
66
+ ### 3. Stamp
67
+
68
+ ```
69
+ node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName> --preset screen
70
+ ```
71
+
72
+ This writes the Screen/ViewModel/tests/golden-tree-test, wires the ViewModel into
73
+ `di/AppModule.kt` and a route into `presentation/navigation/Screen.kt` +
74
+ `presentation/navigation/AppNavHost.kt` at their `// cmp:anchor` markers, and writes
75
+ `specs/<feature>.spec.md` with a default six-clause set (`<FEATURE>-01..06`) cited by the three
76
+ generated test files.
77
+
78
+ If it exits non-zero, read the message — it is actionable. Do not hand-edit around a stamper
79
+ failure.
80
+
81
+ ### 4. Refine the spec, then the behavior
82
+
83
+ Rewrite the clause prose in `specs/<feature>.spec.md` for the entity's real behavior — the six
84
+ clause **ids stay fixed** (specCoverage binds tests to ids, not prose). Propose the rewritten
85
+ clauses to the human; get them confirmed before moving on.
86
+
87
+ Then adapt the generated code to match the entity's actual shape (which may differ from the
88
+ `home` exemplar's `{id, title, subtitle}` list): update the screen's rendering in
89
+ `presentation/<feature>/<Feature>Screen.kt` and the copied tests
90
+ (`<Feature>ViewModelTest.kt`, `<Feature>ScreenTest.kt`) together, consistent with whatever
91
+ `<Entity>.kt` already looks like. The gate (step 6) will name exactly what you missed.
92
+
93
+ ### 5. Capture the golden tree
94
+
95
+ The golden baseline is **not** copied (a copied one would silently mismatch the adapted screen).
96
+ Generate it fresh once the screen renders the real behavior:
97
+
98
+ ```
99
+ UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Feature>GoldenTree*"
100
+ ```
101
+
102
+ Review `qa/golden/<feature>.json` briefly — it should reflect the structure you intended. Commit
103
+ it alongside the feature.
104
+
105
+ ### 6. Gate
106
+
107
+ ```
108
+ node qa/verify.mjs
109
+ ```
110
+
111
+ This must PASS. It proves: the spec's six clauses are all bound to a citing test
112
+ (specCoverage — `<FEATURE>-01..06` newly bound), the build compiles, unit tests pass, architecture
113
+ conformance holds, the golden tree matches, and accessibility holds. **Not done until this is
114
+ PASS and the evidence receipt (`qa/evidence/latest.json`) is committed with your change** — this
115
+ project's standing definition of done (see `CLAUDE.md`).
116
+
117
+ If it fails: read the failing step's reason, fix the actual behavior or spec/test binding, and
118
+ re-run. Do not delete or weaken a test to reach green.
119
+
120
+ ## Guardrails
121
+
122
+ - Works identically with or without the create-cmp Claude Code plugin installed — everything it
123
+ needs (`qa/scaffold-feature.mjs`, this file) ships inside the generated project.
124
+ - Respect whichever toggles this project was stamped with (e.g. if Room or Appium/Maestro were
125
+ disabled at scaffold time, don't reintroduce them for the new screen).
126
+ - `add-screen` generates a pushed-nav-route screen, not a bottom-nav tab, and does not generate a
127
+ "tap → detail" destination — same MVP scope as `add-feature`.
128
+ - If the entity doesn't exist yet, don't try to work around the stamper's refusal by hand-writing
129
+ the missing use case/fake/model — run `add-repository` first (or use plain `add-feature` for
130
+ both layers together), then retry.
@@ -1,8 +1,9 @@
1
1
  # CI for your Compose Multiplatform app — stamped in by create-cmp.
2
2
  #
3
- # What runs on every push/PR: a toolchain report (advisory) and the Android
4
- # debug build the same "verify gate" create-cmp ran when it generated this
5
- # project. Green here = your frozen version set still builds.
3
+ # What runs on every push/PR: a toolchain report (advisory) and the VERIFY LANE
4
+ # (qa/verify.mjs) build + unit tests + every other gate this project carries,
5
+ # producing the evidence receipt. Green here = your frozen version set still
6
+ # builds AND the harness's checks pass.
6
7
  #
7
8
  # iOS: a ready-to-enable macOS job is included (commented out) at the bottom.
8
9
  # macOS runners cost ~10x Linux minutes, so it's opt-in.
@@ -35,11 +36,32 @@ jobs:
35
36
  continue-on-error: true
36
37
  run: npx --yes create-cmp-cli@latest doctor --yes --no-install --no-ios
37
38
 
38
- # The verify gate. If this fails after a dependency bump, remember:
39
- # Kotlin / KSP / Compose / Room / AGP move as ONE set (see the comments
40
- # in gradle/libs.versions.toml).
41
- - name: Build Android (debug)
42
- run: ./gradlew :composeApp:assembleDebug --stacktrace
39
+ # Receipt attests HEAD: the committed evidence receipt (qa/evidence/latest.json)
40
+ # must validly attest the checked-out tree verdict PASS and the inputs-hash
41
+ # still matching the verified surface — BEFORE we spend a runner re-running the
42
+ # lane. Robust to rebase/merge/squash: validity is a content hash of the inputs,
43
+ # not a parent SHA (docs/adr/0005). This is the same predicate the local Stop
44
+ # hook runs (qa/receipt-check.mjs) — one check, two locations — so a stale or
45
+ # hand-edited receipt fails here even if the author bypassed the local hook.
46
+ - name: Receipt attests HEAD
47
+ run: node qa/receipt-check.mjs
48
+
49
+ # The verify lane: build + unit tests + all shipped gates, with a typed
50
+ # verdict and an evidence receipt. If the build step fails after a
51
+ # dependency bump, remember: Kotlin / KSP / Compose / Room / AGP move as
52
+ # ONE set (see the comments in gradle/libs.versions.toml).
53
+ - name: Verify lane
54
+ run: node qa/verify.mjs --profile ci
55
+
56
+ # The receipt this run produced, kept as a build artifact. The receipt
57
+ # committed by the author (qa/evidence/latest.json) lives in the repo —
58
+ # this artifact is CI's independent re-verification of the same change.
59
+ - name: Upload evidence receipt
60
+ if: always()
61
+ uses: actions/upload-artifact@v4
62
+ with:
63
+ name: verify-evidence
64
+ path: qa/evidence/latest.json
43
65
 
44
66
  # ── iOS (opt-in) ──────────────────────────────────────────────────────────
45
67
  # Uncomment to build the iOS app on every push to main. Uses the exact
File without changes
@@ -0,0 +1,2 @@
1
+ #Mon Jul 06 18:20:40 WAT 2026
2
+ gradle.version=8.11.1
File without changes
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to __APP_NAME__. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
+ versioning: [SemVer](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] — scaffold
9
+
10
+ ### Added
11
+ - Initial scaffold generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp):
12
+ Compose Multiplatform app with Clean Architecture, the `home` exemplar feature (with tests),
13
+ theme token catalog, and the verification harness (`qa/verify.mjs` + evidence receipts +
14
+ [`CLAUDE.md`](./CLAUDE.md) contract).
@@ -0,0 +1,70 @@
1
+ # __APP_NAME__ — AI delivery contract
2
+
3
+ This project was generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a
4
+ verification harness. Any AI session working in this repo follows this contract.
5
+
6
+ ## Definition of done
7
+
8
+ You are **not done until `node qa/verify.mjs` reports PASS** and the receipt it writes
9
+ (`qa/evidence/latest.json`) is included in your commit. Claiming completion without a PASS
10
+ receipt is a failure. SKIPped steps are recorded in the receipt — never present green-with-gaps
11
+ as fully verified.
12
+
13
+ ## Specifications — behavior starts here
14
+
15
+ **New behavior begins as a spec clause** in `specs/<feature>.spec.md` (Given/When/Then with a
16
+ stable id — see [`specs/README.md`](./specs/README.md)). Propose the clause, get it confirmed,
17
+ then implement. Durable tests cite their clause (`// SPEC: HOME-02`).
18
+ [`specs/app-base.spec.md`](./specs/app-base.spec.md) states the architecture and shell
19
+ invariants the conformance gates enforce.
20
+
21
+ ## Architecture (violations will be named by the conformance gates)
22
+
23
+ - **Layers:** `presentation` → `domain` ← `data`. `domain` imports nothing app-internal;
24
+ `presentation` never imports `data`. Koin wires implementations in `di/`.
25
+ - **Every screen:** a `*Screen` composable with a `testTag`ged root, a ViewModel with a test.
26
+ - **Design values** (colors / spacing / typography / radii) come from the theme's token catalog
27
+ (`presentation/theme/`). Never hardcode literals in screens.
28
+ - The `home` feature is the **exemplar** — it shows the full pattern through every layer,
29
+ including the tests. When you add a feature, mirror it exactly: Screen → ViewModel (+ test) →
30
+ UseCase (+ test) → Repository interface in `domain` + impl in `data` (+ test) → DI module
31
+ entry → navigation route.
32
+
33
+ ## Testing (the pyramid this project uses)
34
+
35
+ - **Unit** (`composeApp/src/commonTest`, run via `./gradlew :composeApp:desktopTest`):
36
+ kotlin-test + coroutines-test + Turbine. **Hand-written fakes** in `testing/fakes/` — never
37
+ mocking frameworks. Every new ViewModel/UseCase/Repository gets a test in the exemplar's
38
+ style: Arrange-Act-Assert, behavior-named backtick tests, one behavior per test.
39
+ - **Conformance + screen tests** (`composeApp/src/desktopTest`): dependency-free
40
+ source-scanning architecture gates (they enforce `specs/app-base.spec.md`'s ARCH clauses) + Compose UI Tests (durable,
41
+ spec-cited, testTag selectors) + the golden-tree structural baseline (`qa/golden/`). Golden
42
+ drift you did not intend = fix your change; intended drift = regenerate explicitly
43
+ (`UPDATE_GOLDEN=1`) and declare it.
44
+ - **E2E** (`qa/e2e/*.yaml`): Maestro flows; smoke covers boot + bottom nav. Selectors by
45
+ testTag — never by display text.
46
+ - Do not delete, weaken, or `@Ignore` a failing test to get to green. Fix the behavior, or if
47
+ the test itself is wrong, say so explicitly in your summary and justify the change.
48
+
49
+ ## Evidence
50
+
51
+ `node qa/verify.mjs` writes `qa/evidence/latest.json` (schema: `qa/evidence/schema.json`).
52
+ Commit it with your change — git history is the audit ledger. Binary artifacts under
53
+ `qa-artifacts/` are hashed into the receipt; never commit them.
54
+
55
+ ## Docs
56
+
57
+ [`README.md`](./README.md) (front page) · [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)
58
+ (layers + how to add a feature) · [`docs/TESTING.md`](./docs/TESTING.md) (pyramid +
59
+ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional Commits) ·
60
+ [`docs/adr/`](./docs/adr/) (decision records — significant decisions get one). Keep
61
+ [`CHANGELOG.md`](./CHANGELOG.md) current under `[Unreleased]` as part of your change.
62
+
63
+ ## Commands
64
+
65
+ | Command | What |
66
+ |---|---|
67
+ | `node qa/verify.mjs` | The verify lane (profile `local`) — your definition of done |
68
+ | `./gradlew :composeApp:desktopTest` | Unit tests only (fast inner loop) |
69
+ | `./gradlew :composeApp:assembleDebug` | Android debug build |
70
+ | `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
@@ -0,0 +1,39 @@
1
+ # Contributing
2
+
3
+ ## The workflow
4
+
5
+ 1. Branch from `main` (`feat/<name>`, `fix/<name>`).
6
+ 2. Make the change — new features mirror the `home` exemplar
7
+ (see [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)), with tests at every layer
8
+ (see [`docs/TESTING.md`](./docs/TESTING.md)).
9
+ 3. Run the verify lane: `node qa/verify.mjs`.
10
+ 4. Commit **including the updated receipt** (`qa/evidence/latest.json`). A change without a
11
+ PASS receipt is not done — CI re-runs the same lane and will say so.
12
+ 5. Open a PR. Keep it one concern; note any intended golden/baseline changes explicitly.
13
+
14
+ ## Definition of done
15
+
16
+ - `node qa/verify.mjs` → **PASS**, receipt committed.
17
+ - New behavior has tests; existing tests untouched unless the behavior intentionally changed
18
+ (say so in the PR).
19
+ - No hardcoded design values; testTags on anything E2E needs to reach.
20
+ - Significant decisions recorded as an ADR (`docs/adr/`).
21
+
22
+ ## Commit style — Conventional Commits
23
+
24
+ ```
25
+ <type>(<scope>): <imperative summary>
26
+
27
+ feat(home): add pull-to-refresh
28
+ fix(nav): keep state on tab re-selection
29
+ test(profile): cover error retry path
30
+ docs(adr): record image-loading choice
31
+ ```
32
+
33
+ Types: `feat` `fix` `test` `refactor` `docs` `build` `ci` `chore`. Scope = feature/module.
34
+
35
+ ## Code style
36
+
37
+ - Kotlin official style; match the surrounding file's idiom.
38
+ - Comments explain constraints the code can't (never narrate what the next line does).
39
+ - Public surface of `domain` stays framework-free.
@@ -0,0 +1,103 @@
1
+ # __APP_NAME__
2
+
3
+ A Kotlin / Compose Multiplatform app — generated by
4
+ [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a **verification harness**:
5
+ the architecture, testing conventions, and definition of done are enforced mechanically, not
6
+ by convention. Start with [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); AI collaborators
7
+ follow the contract in [`CLAUDE.md`](./CLAUDE.md).
8
+
9
+ ## Quick start
10
+
11
+ ```bash
12
+ # Android (device/emulator attached)
13
+ ./gradlew :composeApp:installDebug
14
+
15
+ <!-- >>> cmp:feature dev-client -->
16
+ # Desktop dev-client with Compose Hot Reload — the daily dev loop
17
+ ./gradlew :composeApp:hotRunDesktop --auto
18
+ <!-- <<< cmp:feature dev-client -->
19
+
20
+ # Unit tests (fast inner loop)
21
+ ./gradlew :composeApp:desktopTest
22
+
23
+ # The verify lane — build + tests + every shipped gate, with an evidence receipt
24
+ node qa/verify.mjs
25
+ ```
26
+
27
+ <!-- >>> cmp:feature ios -->
28
+ **iOS:** `cd iosApp && xcodegen generate && pod install`, then open
29
+ `iosApp.xcworkspace` in Xcode and run. (First time: `brew install xcodegen`.)
30
+ <!-- <<< cmp:feature ios -->
31
+
32
+ <!-- >>> cmp:feature firebase -->
33
+ **Firebase:** the scaffold ships a placeholder `google-services.json`. Wire your real project
34
+ before shipping — from Claude Code, the `cmp-firebase-connect` skill drives it end to end.
35
+ <!-- <<< cmp:feature firebase -->
36
+
37
+ ## Project structure
38
+
39
+ ```
40
+ specs/ behavior specifications — new behavior starts here
41
+ composeApp/src/commonMain/ shared UI + logic (presentation / domain / data / di)
42
+ composeApp/src/commonTest/ unit tests — exemplar pattern in presentation/home
43
+ composeApp/src/desktopTest/ conformance gates + Compose UI tests + golden trees (JVM tier)
44
+ composeApp/src/androidMain/ Android entry point + platform actuals
45
+ <!-- >>> cmp:feature ios -->
46
+ composeApp/src/iosMain/ iOS actuals · iosApp/ is the Xcode shell
47
+ <!-- <<< cmp:feature ios -->
48
+ <!-- >>> cmp:feature dev-client -->
49
+ composeApp/src/desktopMain/ desktop dev-client (see docs/dev-client.md)
50
+ <!-- <<< cmp:feature dev-client -->
51
+ qa/verify.mjs the verify lane — this repo's definition of done
52
+ qa/evidence/latest.json the committed evidence receipt (see CONTRIBUTING.md)
53
+ <!-- >>> cmp:feature e2e -->
54
+ qa/e2e/ Maestro E2E flows (device smoke)
55
+ <!-- <<< cmp:feature e2e -->
56
+ docs/ architecture, testing, ADRs
57
+ ```
58
+
59
+ ## Documentation
60
+
61
+ | Doc | What |
62
+ |---|---|
63
+ | [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) | Layers, data flow, how to add a feature |
64
+ | [`docs/TESTING.md`](./docs/TESTING.md) | The test pyramid, conventions, how to run everything |
65
+ | [`docs/adr/`](./docs/adr/) | Architecture decision records |
66
+ | [`CONTRIBUTING.md`](./CONTRIBUTING.md) | Workflow, definition of done, commit style |
67
+ | [`CLAUDE.md`](./CLAUDE.md) | The AI delivery contract |
68
+ | [`CHANGELOG.md`](./CHANGELOG.md) | Notable changes (Keep a Changelog) |
69
+
70
+ ## Verification
71
+
72
+ Every change must pass the verify lane (`node qa/verify.mjs`) and commit its updated receipt
73
+ (`qa/evidence/latest.json`). CI re-runs the same lane on every push — see
74
+ [`.github/workflows/verify.yml`](./.github/workflows/verify.yml).
75
+
76
+ ## Verification enforcement
77
+
78
+ This project ships a **Stop hook** (`.claude/settings.json`) that makes `CLAUDE.md`'s definition
79
+ of done mechanical instead of honor-system, for AI sessions using Claude Code.
80
+
81
+ **What it does:** when a session tries to end, the hook runs `node qa/receipt-check.mjs --hook`.
82
+ That script recomputes a sha256 hash over this project's "verified surface" (`composeApp/`,
83
+ `specs/`, `qa/`, and the Gradle build files — see `qa/lib/inputs-hash.mjs`) and compares it to the
84
+ `inputs.hash` recorded in the committed `qa/evidence/latest.json`. If the receipt is a `PASS` and
85
+ its hash matches the current tree, the session ends silently. If source changed without a fresh
86
+ `PASS` receipt — or the receipt is missing, a `FAIL`, or predates this mechanism — the hook blocks
87
+ with the specific reason and asks you to run `node qa/verify.mjs` and commit the receipt. **It runs
88
+ no build and no tests — only file hashing —** so it costs milliseconds, and it never fires twice in
89
+ a row for the same stop.
90
+
91
+ Doc-only edits (`*.md`, `README`, `.github/`, `.claude/`) are deliberately **outside** the verified
92
+ surface, so editing docs never invalidates a good receipt or forces a needless re-run — the intent
93
+ is transparent enforcement, not a hostile one.
94
+
95
+ **Why:** `CLAUDE.md` already says a change is "not done" without a `PASS` receipt committed. The
96
+ Stop hook is what makes that check happen automatically instead of relying on the AI session to
97
+ remember to run it.
98
+
99
+ **Escape hatch:** this is your project. If you don't want the hook, delete or comment out the
100
+ `Stop` block in [`.claude/settings.json`](./.claude/settings.json) — nothing else depends on it
101
+ locally. Note that CI independently enforces the same "receipt attests HEAD" check on every push
102
+ (see `.github/workflows/verify.yml`), so disabling the local hook only trades an immediate local
103
+ signal for a later one in CI.
@@ -51,16 +51,16 @@ kotlin {
51
51
  }
52
52
  // <<< cmp:feature ios
53
53
 
54
- // >>> cmp:feature dev-client
55
- // Desktop dev-client: runs the shared commonMain UI in a live JVM window — the daily dev
56
- // loop. Compose Hot Reload attaches to this target (task: hotRunDesktop). See docs/dev-client.md.
54
+ // The JVM tier — harness infrastructure, NOT feature-gated. This target hosts the fast
55
+ // verification loop: unit tests, conformance gates, golden-tree renders, and Compose UI
56
+ // Tests all run here (`:composeApp:desktopTest`), device-free. The dev-client window
57
+ // feature merely reuses it.
57
58
  jvm("desktop") {
58
59
  @OptIn(ExperimentalKotlinGradlePluginApi::class)
59
60
  compilerOptions {
60
61
  jvmTarget.set(JvmTarget.JVM_17)
61
62
  }
62
63
  }
63
- // <<< cmp:feature dev-client
64
64
 
65
65
  sourceSets {
66
66
  commonMain.dependencies {
@@ -130,7 +130,7 @@ kotlin {
130
130
  }
131
131
  // <<< cmp:feature ios
132
132
 
133
- // >>> cmp:feature dev-client
133
+ // JVM tier deps (harness infrastructure — see the jvm("desktop") target note).
134
134
  val desktopMain by getting {
135
135
  dependencies {
136
136
  implementation(compose.desktop.currentOs)
@@ -138,12 +138,21 @@ kotlin {
138
138
  implementation(libs.ktor.client.cio)
139
139
  }
140
140
  }
141
- // <<< cmp:feature dev-client
142
141
 
143
142
  commonTest.dependencies {
144
143
  implementation(libs.kotlin.test)
145
144
  implementation(libs.koin.test)
146
145
  implementation(libs.kotlinx.coroutines.test)
146
+ implementation(libs.turbine)
147
+ }
148
+
149
+ // JVM-tier test deps: Compose UI Tests, the golden-tree serializer, and the
150
+ // conformance gates all run here (the verify lane's fast, device-free steps).
151
+ val desktopTest by getting {
152
+ dependencies {
153
+ implementation(compose.desktop.uiTestJUnit4)
154
+ implementation(compose.desktop.currentOs)
155
+ }
147
156
  }
148
157
  }
149
158
  }
@@ -225,9 +234,7 @@ dependencies {
225
234
  add("kspIosX64", libs.room.compiler)
226
235
  add("kspIosArm64", libs.room.compiler)
227
236
  // <<< cmp:feature ios
228
- // >>> cmp:feature dev-client
229
237
  add("kspDesktop", libs.room.compiler)
230
- // <<< cmp:feature dev-client
231
238
  // <<< cmp:feature room
232
239
  add("coreLibraryDesugaring", libs.android.desugar.jdk)
233
240
  }
@@ -4,19 +4,23 @@ import __PACKAGE__.data.remote.ItemRepositoryImpl
4
4
  import __PACKAGE__.domain.repository.ItemRepository
5
5
  import __PACKAGE__.domain.usecase.GetItemsUseCase
6
6
  import __PACKAGE__.presentation.home.HomeViewModel
7
+ // cmp:anchor di-imports
7
8
  import org.koin.core.module.dsl.viewModelOf
8
9
  import org.koin.dsl.module
9
10
 
10
11
  val repositoryModule = module {
11
12
  single<ItemRepository> { ItemRepositoryImpl() }
13
+ // cmp:anchor di-repositories
12
14
  }
13
15
 
14
16
  val useCaseModule = module {
15
17
  factory { GetItemsUseCase(get()) }
18
+ // cmp:anchor di-usecases
16
19
  }
17
20
 
18
21
  val viewModelModule = module {
19
22
  viewModelOf(::HomeViewModel)
23
+ // cmp:anchor di-viewmodels
20
24
  }
21
25
 
22
26
  // Aggregated common modules, started from AppApplication (Android) and KoinHelper (iOS).
@@ -8,6 +8,8 @@ import androidx.compose.material3.Text
8
8
  import androidx.compose.material3.TextButton
9
9
  import androidx.compose.runtime.Composable
10
10
  import androidx.compose.ui.Modifier
11
+ import androidx.compose.ui.semantics.semantics
12
+ import androidx.compose.ui.semantics.testTag
11
13
  import __PACKAGE__.presentation.components.BaseScreen
12
14
  import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
13
15
  import __PACKAGE__.presentation.theme.designToken
@@ -27,8 +29,14 @@ fun DetailScreen(
27
29
  )
28
30
  .padding(__THEME_PREFIX__Tokens.PaddingPage),
29
31
  ) {
30
- TextButton(onClick = onBack) { Text(" Back") }
31
- Text("Detail", style = MaterialTheme.typography.headlineMedium)
32
+ TextButton(onClick = onBack, modifier = Modifier.semantics { testTag = "detail_back" }) {
33
+ Text("← Back")
34
+ }
35
+ Text(
36
+ "Detail",
37
+ style = MaterialTheme.typography.headlineMedium,
38
+ modifier = Modifier.semantics { testTag = "detail_title" },
39
+ )
32
40
  Text(
33
41
  "Item id: $itemId",
34
42
  style = MaterialTheme.typography.bodyLarge,
@@ -47,10 +47,20 @@ fun HomeScreen(
47
47
  modifier = Modifier.semantics { testTag = "home_title" }.padding(bottom = 12.dp),
48
48
  )
49
49
 
50
+ val errorMessage = state.errorMessage
50
51
  if (state.isLoading) {
51
52
  Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
52
53
  CircularProgressIndicator()
53
54
  }
55
+ } else if (errorMessage != null) {
56
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
57
+ Text(
58
+ text = errorMessage,
59
+ style = MaterialTheme.typography.bodyLarge,
60
+ color = MaterialTheme.colorScheme.error,
61
+ modifier = Modifier.semantics { testTag = "home_error" },
62
+ )
63
+ }
54
64
  } else {
55
65
  LazyColumn(verticalArrangement = Arrangement.spacedBy(__THEME_PREFIX__Tokens.GapCard)) {
56
66
  items(state.items, key = { it.id }) { item ->
@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
12
12
  data class HomeUiState(
13
13
  val isLoading: Boolean = true,
14
14
  val items: List<Item> = emptyList(),
15
+ val errorMessage: String? = null,
15
16
  )
16
17
 
17
18
  class HomeViewModel(
@@ -28,8 +29,11 @@ class HomeViewModel(
28
29
  fun load() {
29
30
  viewModelScope.launch {
30
31
  _state.value = HomeUiState(isLoading = true)
31
- val items = getItems()
32
- _state.value = HomeUiState(isLoading = false, items = items)
32
+ _state.value = try {
33
+ HomeUiState(isLoading = false, items = getItems())
34
+ } catch (e: Exception) {
35
+ HomeUiState(isLoading = false, errorMessage = e.message ?: "Something went wrong")
36
+ }
33
37
  }
34
38
  }
35
39
  }
@@ -39,5 +39,6 @@ fun AppNavHost() {
39
39
  onBack = { navController.popBackStack() },
40
40
  )
41
41
  }
42
+ // cmp:anchor nav-destinations
42
43
  }
43
44
  }
@@ -5,11 +5,12 @@ sealed class Screen(val route: String) {
5
5
  data object Shell : Screen(Routes.SHELL)
6
6
  // Example detail destination reachable from a tab. Add your own below.
7
7
  data object Detail : Screen(Routes.DETAIL)
8
+ // cmp:anchor screen-objects
8
9
  }
9
10
 
10
11
  object Routes {
11
12
  const val SHELL = "shell"
12
13
  const val DETAIL = "detail/{itemId}"
13
-
14
+ // cmp:anchor route-consts
14
15
  fun detail(itemId: String) = "detail/$itemId"
15
16
  }
@@ -0,0 +1,26 @@
1
+ package __PACKAGE__.data.remote
2
+
3
+ import kotlin.test.Test
4
+ import kotlin.test.assertEquals
5
+ import kotlin.test.assertTrue
6
+ import kotlinx.coroutines.test.runTest
7
+
8
+ /**
9
+ * The exemplar data-layer test. [ItemRepositoryImpl] is the template's dependency-light
10
+ * example source; when you swap it for a real Firestore/Ktor + Room implementation, keep
11
+ * this shape: test the repository through its DOMAIN contract, under `runTest` virtual
12
+ * time (the simulated I/O delay costs nothing here — delays are skipped, not slept).
13
+ */
14
+ class ItemRepositoryImplTest {
15
+
16
+ private val repository = ItemRepositoryImpl()
17
+
18
+ @Test
19
+ fun `returns the seeded example items`() = runTest {
20
+ val items = repository.getItems()
21
+
22
+ assertTrue(items.isNotEmpty(), "example source should seed items")
23
+ assertEquals(items.size, items.map { it.id }.toSet().size, "item ids must be unique")
24
+ assertTrue(items.all { it.title.isNotBlank() }, "every item needs a title")
25
+ }
26
+ }