create-cmp-cli 0.8.0 → 0.9.0

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 (74) hide show
  1. package/README.md +67 -9
  2. package/llms.txt +5 -1
  3. package/package.json +1 -1
  4. package/src/lib/adr-seed.mjs +178 -0
  5. package/src/lib/tabs.mjs +91 -4
  6. package/src/scaffold.mjs +52 -1
  7. package/template/.claude/skills/add-feature/SKILL.md +35 -10
  8. package/template/.claude/skills/add-repository/SKILL.md +1 -1
  9. package/template/.claude/skills/add-screen/SKILL.md +13 -7
  10. package/template/.githooks/pre-push +24 -0
  11. package/template/CLAUDE.md +196 -48
  12. package/template/README.md +23 -27
  13. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/CrashRecorder.kt +99 -0
  14. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/DbInspector.kt +144 -0
  15. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +69 -2
  16. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +8 -4
  17. package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/NavInspector.kt +31 -0
  18. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/AppResultCatching.kt +32 -0
  19. package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +9 -2
  20. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/DomainError.kt +21 -0
  21. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +4 -1
  22. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/result/AppResult.kt +23 -0
  23. package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +4 -1
  24. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +138 -0
  25. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +56 -0
  26. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppHeader.kt +54 -0
  27. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +16 -8
  28. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentStateContainer.kt +105 -0
  29. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentUiState.kt +18 -0
  30. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/EmptyState.kt +58 -0
  31. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ErrorState.kt +52 -0
  32. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ListItemCard.kt +77 -0
  33. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ScreenColumn.kt +47 -0
  34. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/Shimmer.kt +90 -0
  35. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +9 -9
  36. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +5 -27
  37. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +14 -70
  38. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +33 -13
  39. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +13 -0
  40. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +7 -109
  41. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/NavInspectionHook.kt +21 -0
  42. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +4 -27
  43. package/template/composeApp/src/commonTest/kotlin/com/example/app/data/AppResultCatchingTest.kt +52 -0
  44. package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +29 -4
  45. package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +8 -6
  46. package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +39 -27
  47. package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +10 -6
  48. package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/ComponentStories.kt +269 -0
  49. package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt +37 -1
  50. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +207 -15
  51. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ComponentConformanceTest.kt +84 -0
  52. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +36 -4
  53. package/template/docs/ARCHITECTURE.md +317 -34
  54. package/template/docs/TESTING.md +6 -5
  55. package/template/docs/adr/0002-maestro-over-appium-for-e2e.md +39 -0
  56. package/template/docs/adr/0003-jvm-desktop-target-is-harness-infrastructure.md +39 -0
  57. package/template/docs/adr/0004-fakes-not-mocks-for-unit-tests.md +48 -0
  58. package/template/qa/approvals.json +42 -0
  59. package/template/qa/approve.mjs +139 -0
  60. package/template/qa/arch-doc.mjs +69 -0
  61. package/template/qa/comment.mjs +76 -0
  62. package/template/qa/comments.json +4 -0
  63. package/template/qa/golden/home.json +3 -3
  64. package/template/qa/lib/approvals.mjs +806 -0
  65. package/template/qa/lib/arch-doc.mjs +451 -0
  66. package/template/qa/lib/comments.mjs +252 -0
  67. package/template/qa/lib/component-stories.mjs +183 -0
  68. package/template/qa/lib/inputs-hash.mjs +5 -1
  69. package/template/qa/scaffold-feature.mjs +184 -67
  70. package/template/qa/setup-hooks.mjs +33 -0
  71. package/template/qa/verify.mjs +118 -9
  72. package/template/specs/app-base.spec.md +44 -7
  73. package/template/specs/home.spec.md +7 -4
  74. package/template/specs/intent.md +50 -0
@@ -3,7 +3,8 @@ name: add-feature
3
3
  description: >-
4
4
  Add a new conforming vertical-slice feature (Screen + ViewModel + UseCase + Repository + spec
5
5
  + tests + golden tree + nav route + DI wiring) to this Compose Multiplatform app, cloned
6
- deterministically from the `home` exemplar. Use this when the user wants to "add a feature",
6
+ deterministically from the project's configured exemplar feature (qa/approvals.json's
7
+ exemplarFeature — `home` by default). Use this when the user wants to "add a feature",
7
8
  "add a screen with data", "scaffold a feature", "create a new screen backed by a repository",
8
9
  "add a list screen", or names a new domain noun they want a screen for (e.g. "add a Favorites
9
10
  feature", "I need a Bookmarks screen"). Works with NO create-cmp plugin installed — the
@@ -13,11 +14,28 @@ description: >-
13
14
  # add-feature — stamp a conforming vertical slice
14
15
 
15
16
  > Spec-first, deterministic-stamp, gate-proven. The script (`qa/scaffold-feature.mjs`) does the
16
- > mechanical work — copy the `home` exemplar file set, whole-word identifier rename, anchor
17
+ > mechanical work — copy the exemplar file set, whole-word identifier rename, anchor
17
18
  > injection into the three shared files. You (the AI) only refine spec wording and adapt the
18
19
  > feature to its real shape. You are not done until `node qa/verify.mjs` PASSes and the receipt
19
20
  > is committed — see this project's `CLAUDE.md`.
20
21
 
22
+ ## The clone source is configurable
23
+
24
+ The stamper clones from the project's **configured exemplar** — `qa/approvals.json`'s
25
+ top-level `"exemplarFeature"` key (absent ⇒ `home`, the shipped exemplar). This is the same
26
+ resolution the approvals registry uses for the governed `exemplar-feature` artifact, so what
27
+ gets stamped is always exactly what the human signed off on. After the genesis walk retargets
28
+ `exemplarFeature` to the user's own first feature, every stamp from then on clones *their*
29
+ pattern in *their* domain language — do not assume `home` still exists as the exemplar; read
30
+ the config (or just run the stamper: it resolves the source itself).
31
+
32
+ If the configured exemplar has grown files beyond the canonical 11-file shape (an extra
33
+ ViewModel, a helper, a second use case named for its entity), the stamper clones **only the
34
+ canonical set** and prints a `WARNING:` listing exactly what it skipped — never silently.
35
+ When you see that warning, tell the human: the extras are part of the exemplar's pattern in
36
+ spirit but not in mechanism, and porting them into the new feature (or slimming the exemplar
37
+ back to canon) is a deliberate follow-up, not something to ignore.
38
+
21
39
  ## Why a stamper and not hand-written files
22
40
 
23
41
  This project's whole thesis is that determinism beats freehand generation for anything
@@ -58,9 +76,9 @@ node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName>
58
76
 
59
77
  This writes the new Screen/ViewModel/UseCase/Repository(+impl)/tests/fake, wires them into
60
78
  `di/AppModule.kt`, `presentation/navigation/Screen.kt`, and `presentation/navigation/AppNavHost.kt`
61
- at their `// cmp:anchor` markers, and writes `specs/<feature>.spec.md` with a default six-clause
62
- set (`<FEATURE>-01..06`: loading, success, error, reload-after-failure, tap-navigates, golden
63
- tree) — copied verbatim from the `home` exemplar's shape.
79
+ at their `// cmp:anchor` markers, and writes `specs/<feature>.spec.md` with a default seven-clause
80
+ set (`<FEATURE>-01..07`: loading, success, error, reload-after-failure, tap-navigates, golden
81
+ tree, empty state) — copied verbatim from the configured exemplar's shape.
64
82
 
65
83
  The stamped screen arrives **already wrapped in `BaseScreen { … }`** (SHELL-05): it is a
66
84
  pushed NavHost destination, so unlike the tab exemplar it must handle its own insets — the
@@ -73,9 +91,9 @@ flagging, not something to route around by hand-splicing.
73
91
 
74
92
  ### 4. Refine the spec, then the behavior
75
93
 
76
- The default spec clauses are placeholders shaped like the `home` exemplar (a plain list of
94
+ The default spec clauses are placeholders shaped like the exemplar (by default a plain list of
77
95
  title/subtitle rows). **Rewrite the clause prose** in `specs/<feature>.spec.md` to describe the
78
- feature's real behavior — the six clause **ids stay fixed** (`specCoverage` binds tests to ids,
96
+ feature's real behavior — the seven clause **ids stay fixed** (`specCoverage` binds tests to ids,
79
97
  not prose), only the wording changes. Propose the rewritten clauses to the human; get them
80
98
  confirmed before moving on — this project's contract is spec-first.
81
99
 
@@ -84,7 +102,12 @@ Then adapt the generated code to match:
84
102
  - If the feature isn't shaped like "a list of `{id, title, subtitle}`", update the entity's
85
103
  fields in `domain/model/<Entity>.kt`, the sample data in `<Entity>RepositoryImpl.kt`, and the
86
104
  screen's rendering in `presentation/<feature>/<Feature>Screen.kt` together — keep them
87
- consistent with each other and with the tests.
105
+ consistent with each other and with the tests. The stamped screen already **composes the
106
+ registry vocabulary** (`ScreenColumn`/`AppHeader`/`ContentStateContainer`/`ListItemCard`,
107
+ `presentation/components/*.kt`) — adapt the content shape inside `ContentStateContainer`'s
108
+ trailing slot, don't hand-roll a new header/loading state/list row on top of it. If the
109
+ feature's data genuinely needs a component the nine don't cover, propose the addition to the
110
+ human explicitly (a new file is a registry change — it invalidates the `components` approval).
88
111
  - Update the copied tests (`<Feature>ViewModelTest.kt`, `<Feature>ScreenTest.kt`) to match
89
112
  whatever you changed. The gate (step 6) will tell you exactly what you missed — a compile
90
113
  error names the mismatch; a spec-coverage failure names an orphaned clause or tag.
@@ -113,10 +136,12 @@ copy-paste artifact. Commit it alongside the feature.
113
136
  node qa/verify.mjs
114
137
  ```
115
138
 
116
- This must PASS. It proves: the spec's six clauses are all bound to a citing test
139
+ This must PASS. It proves: the spec's seven clauses are all bound to a citing test
117
140
  (`specCoverage`), the build compiles, unit tests pass (ViewModel + UseCase + Repository +
118
141
  fakes), architecture conformance holds (`presentation` doesn't import `data`, the new
119
- `*Screen.kt` carries a `testTag`, the new `*ViewModel.kt` has a matching test), the golden tree
142
+ `*Screen.kt` is automation-reachable — a literal `testTag` or `screenTag =` wiring into a
143
+ registry component — the new `*ViewModel.kt` has a matching test, and it references no
144
+ `CircularProgressIndicator`/`LinearProgressIndicator` directly), the golden tree
120
145
  matches what you just captured, and accessibility holds. **Not done until this is PASS and the
121
146
  evidence receipt (`qa/evidence/latest.json`) is committed with your change** — this is this
122
147
  project's standing definition of done (see `CLAUDE.md`).
@@ -76,7 +76,7 @@ the real entity:
76
76
 
77
77
  **No spec clauses are added by this preset, and that's intentional** — a bare repository has no
78
78
  observable behavior to specify yet (no screen, no user-facing flow). Clauses attach once a
79
- screen consumes this entity; that's `add-screen`'s job (`FEATURE-01..06` bound to the repository
79
+ screen consumes this entity; that's `add-screen`'s job (`FEATURE-01..07` bound to the repository
80
80
  you just built).
81
81
 
82
82
  ### 5. Gate
@@ -29,7 +29,7 @@ to stamp anything — see step 2.
29
29
 
30
30
  Every hand-written file is a drift chance in this project's architecture. `qa/scaffold-feature.mjs
31
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
32
+ testTagged root, a ViewModel (+ test), a Compose UI test, a golden-tree test, and a seven-clause
33
33
  spec — wired into Koin and the nav graph. Your job is to make it *behave* like the real screen,
34
34
  not to make it *structurally correct*.
35
35
 
@@ -72,7 +72,7 @@ node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName> --preset screen
72
72
  This writes the Screen/ViewModel/tests/golden-tree-test, wires the ViewModel into
73
73
  `di/AppModule.kt` and a route into `presentation/navigation/Screen.kt` +
74
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
75
+ `specs/<feature>.spec.md` with a default seven-clause set (`<FEATURE>-01..07`) cited by the three
76
76
  generated test files.
77
77
 
78
78
  If it exits non-zero, read the message — it is actionable. Do not hand-edit around a stamper
@@ -88,7 +88,11 @@ Then adapt the generated code to match the entity's actual shape (which may diff
88
88
  `home` exemplar's `{id, title, subtitle}` list): update the screen's rendering in
89
89
  `presentation/<feature>/<Feature>Screen.kt` and the copied tests
90
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.
91
+ `<Entity>.kt` already looks like. The stamped screen already **composes the registry
92
+ vocabulary** (`ScreenColumn`/`AppHeader`/`ContentStateContainer`/`ListItemCard`,
93
+ `presentation/components/*.kt`) — adapt the content shape inside `ContentStateContainer`'s
94
+ trailing slot rather than hand-rolling a new header/loading state/list row. The gate (step 6)
95
+ will name exactly what you missed.
92
96
 
93
97
  ### 5. Capture the golden tree
94
98
 
@@ -109,10 +113,12 @@ node qa/verify.mjs
109
113
  ```
110
114
 
111
115
  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
+ (specCoverage — `<FEATURE>-01..07` newly bound), the build compiles, unit tests pass, architecture
117
+ conformance holds (the screen is automation-reachable via a literal `testTag` or `screenTag =`
118
+ wiring into a registry component, and references no `CircularProgressIndicator`/
119
+ `LinearProgressIndicator` directly), the golden tree matches, and accessibility holds. **Not done
120
+ until this is PASS and the evidence receipt (`qa/evidence/latest.json`) is committed with your
121
+ change** — this project's standing definition of done (see `CLAUDE.md`).
116
122
 
117
123
  If it fails: read the failing step's reason, fix the actual behavior or spec/test binding, and
118
124
  re-run. Do not delete or weaken a test to reach green.
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ # create-cmp pre-push gate — the human checkpoint before code leaves the machine.
3
+ #
4
+ # The committed evidence receipt (qa/evidence/latest.json) must attest HEAD.
5
+ # This is the SAME check CI runs (qa/receipt-check.mjs), moved earlier so an
6
+ # unverified push is caught locally. It is the CHEAP predicate — a hash
7
+ # comparison — not the full lane, so it never rebuilds anything.
8
+ #
9
+ # Enable once with `node qa/setup-hooks.mjs`. Bypass in a pinch with
10
+ # `git push --no-verify`; CI still enforces the same check.
11
+
12
+ if [ ! -f qa/receipt-check.mjs ]; then
13
+ exit 0 # not a create-cmp project layout — nothing to gate
14
+ fi
15
+
16
+ if node qa/receipt-check.mjs; then
17
+ exit 0
18
+ fi
19
+
20
+ echo ""
21
+ echo "✗ pre-push blocked — the committed receipt does not attest HEAD."
22
+ echo " Run: node qa/verify.mjs then commit qa/evidence/latest.json, and push again."
23
+ echo " Bypass this local gate with: git push --no-verify (CI still checks)."
24
+ exit 1
@@ -1,85 +1,232 @@
1
1
  # __APP_NAME__ — AI delivery contract
2
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.
3
+ Generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a verification
4
+ harness. Every AI session in this repo works under this contract.
5
5
 
6
6
  ## Definition of done
7
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.
8
+ Done means `node qa/verify.mjs` reports PASS and the receipt it writes
9
+ (`qa/evidence/latest.json`) is in your commit. Claiming completion without a PASS receipt is
10
+ a failure. SKIPped steps are recorded in the receipt; never present green-with-gaps as fully
11
+ verified.
12
+
13
+ **Verify in two tiers — the full lane is a checkpoint, not an inner loop.** It builds,
14
+ tests, and gates the whole tree to produce the receipt, so it is slow by design; running it
15
+ after every edit wastes the minutes it takes. Iterate on the fast tier, and run the lane
16
+ once — when you believe the change is done.
17
+
18
+ - **Inner loop — run continuously (seconds):** the preview loop (below) for UI, and
19
+ `./gradlew :composeApp:desktopTest` for the unit tests your change touches. This is where
20
+ you catch your own mistakes.
21
+ - **Checkpoint — run once, at done:** `node qa/verify.mjs`. It writes the receipt; commit
22
+ the receipt with your change. The Stop hook (`qa/receipt-check.mjs`) then confirms — with a
23
+ cheap hash check, not another lane run — that a valid receipt attests your commit, and CI
24
+ re-runs the full lane on push. After a green checkpoint, do not re-run the lane unless you
25
+ change the tree again.
26
+
27
+ Humans get the same gate at push time: run `node qa/setup-hooks.mjs` once (after `git init`)
28
+ to enable the shipped pre-push hook. It blocks a push whose committed receipt doesn't attest
29
+ HEAD — the same cheap check, before code leaves the machine (`git push --no-verify` bypasses
30
+ it; CI still enforces it).
12
31
 
13
32
  ## Specifications — behavior starts here
14
33
 
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,
34
+ New behavior begins as a spec clause in `specs/<feature>.spec.md`: Given/When/Then with a
35
+ stable id (see [`specs/README.md`](./specs/README.md)). Propose the clause, get it confirmed,
17
36
  then implement. Durable tests cite their clause (`// SPEC: HOME-02`).
18
37
  [`specs/app-base.spec.md`](./specs/app-base.spec.md) states the architecture and shell
19
38
  invariants the conformance gates enforce.
20
39
 
21
- ## Architecture (violations will be named by the conformance gates)
40
+ ## Architecture
41
+
42
+ [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) is the doc of record. Every normative
43
+ sentence in it carries a tier tag: `[enforced: CLAUSE-ID]`, `[governed]`, or `[advisory]`.
44
+ Its `cmp:generated` sections (expect/actual table, layer inventory, ADR index, glossary) are
45
+ derived from a tree walk, never hand-maintained — `node qa/arch-doc.mjs` regenerates them,
46
+ and `node qa/arch-doc.mjs --check`, a verify-lane step, fails the lane when they drift from
47
+ the tree. The governed `architecture` artifact (below) hashes the document alongside
48
+ `specs/app-base.spec.md`, so approving it is consent to this document. The summary:
22
49
 
23
50
  - **Layers:** `presentation` → `domain` ← `data`. `domain` imports nothing app-internal;
24
51
  `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)
52
+ - **Screens compose the registry vocabulary** (`presentation/components/*.kt`
53
+ `ScreenColumn`, `AppHeader`, `ContentStateContainer`, `ListItemCard`, ). Do not hand-roll
54
+ a header, loading state, or list row; the exemplar's `HomeScreen.kt` shows the pattern.
55
+ `ScreenColumn(screenTag = "<feature>")` tags the root, and ARCH-04 accepts that wiring as
56
+ tag provenance a literal `testTag` is only needed for content the registry does not
57
+ already tag (per-row ids, for example). Every screen is a `*Screen` composable with a
58
+ tested ViewModel.
59
+ - **Errors are typed, never thrown across layers.** Repositories return `AppResult`
60
+ (`Failure` carries a `DomainError` kind). The data layer's `suspendRunCatching` is the
61
+ only catch point and always rethrows `CancellationException`. ViewModels contain no
62
+ `try`/`catch`; they fold results into a sealed UiState, and presentation maps error kinds
63
+ to user copy.
64
+ - **Design values** (colors, spacing, typography, radii) come from the token catalog in
65
+ `presentation/theme/`. No hardcoded literals in screens.
66
+ - **One feature is the exemplar** — the full pattern through every layer, tests included.
67
+ It ships as `home`; `qa/approvals.json`'s `exemplarFeature` key retargets it to your own
68
+ first feature once shaped (see "Configurable exemplar"). New features mirror the exemplar
69
+ exactly: Screen → ViewModel (+ test) → UseCase (+ test) → Repository interface in `domain`
70
+ + impl in `data` (+ test) → DI entry → navigation route.
71
+
72
+ ## Testing
34
73
 
35
74
  - **Unit** (`composeApp/src/commonTest`, run via `./gradlew :composeApp:desktopTest`):
36
- kotlin-test + coroutines-test + Turbine. **Hand-written fakes** in `testing/fakes/` — never
75
+ kotlin-test + coroutines-test + Turbine. Hand-written fakes in `testing/fakes/`; no
37
76
  mocking frameworks. Every new ViewModel/UseCase/Repository gets a test in the exemplar's
38
77
  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.
78
+ - **Conformance + screen tests** (`composeApp/src/desktopTest`): source-scanning
79
+ architecture gates enforcing `specs/app-base.spec.md`'s ARCH clauses, Compose UI Tests
80
+ (spec-cited, testTag selectors), and the golden-tree baseline (`qa/golden/`). Unintended
81
+ golden drift means fix your change; intended drift is regenerated explicitly
82
+ (`UPDATE_GOLDEN=1`) and declared.
83
+ - **E2E** (`qa/e2e/*.yaml`): Maestro flows; smoke covers boot + bottom nav. Select by
84
+ testTag, never by display text.
85
+ - Never delete, weaken, or `@Ignore` a failing test to reach green. Fix the behavior or,
86
+ if the test itself is wrong, say so in your summary and justify the change.
48
87
 
49
88
  ## Evidence
50
89
 
51
90
  `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.
91
+ Commit it with your change; git history is the audit ledger. Binary artifacts under
92
+ `qa-artifacts/` are hashed into the receipt, never committed. The studio console's Evidence
93
+ page reconstructs the full audit trail from the git log of `latest.json` — every commit is
94
+ one verified, attributed state — so committing each receipt is what builds the record.
95
+
96
+ ## Approvals — governed artifacts need a human's sign-off
97
+
98
+ Some artifacts are **governed**: a human approves them, and the approval is bound to the
99
+ artifact's content by hash (`qa/approvals.json`) — the evidence-receipt idea, applied to a
100
+ human decision. The ordered walk is a **definition order**, not just an approval order:
101
+ each artifact is the vocabulary the next is written in, so on a fresh app each step is a
102
+ conversation that ends in an approval — the genesis walk, six conversations:
103
+
104
+ 0. **Intent** — `specs/intent.md`, the root brief everything else traces to (purpose,
105
+ audience, platforms, brand feel, reference apps, first screens, **glossary**). Filled by
106
+ the `cmp-new` interview; the seed's placeholder prose is marked unfilled. Its
107
+ `## Glossary` section is lifted verbatim into `docs/ARCHITECTURE.md` §8 — write it there
108
+ in the exact form you want published.
109
+ 1. **Design system** — `presentation/theme/Theme.kt`, `presentation/theme/Tokens.kt`.
110
+ 2. **Architecture + structure** — `specs/app-base.spec.md` **and**
111
+ [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) (`cmp:generated` sections stripped
112
+ before hashing, so a mechanical regeneration never invalidates the approval — only an
113
+ authored-prose edit does).
114
+ 3. **Components** — every `presentation/components/*.kt` (a dynamic, sorted glob). Once
115
+ approved, the registry is law: adding or changing a common component invalidates the
116
+ approval until a human re-approves.
117
+ 4. **Exemplar feature** — the **configured** exemplar's 11-file set the `add-feature`
118
+ stamper clones from (see "Configurable exemplar").
119
+ 5. **Exemplar spec** — `specs/<exemplar>.spec.md`.
120
+ 6. **Per-feature spec** — `specs/<feature>.spec.md`, one governed artifact per feature,
121
+ added as features land.
122
+
123
+ | Command | What |
124
+ |---|---|
125
+ | `node qa/approve.mjs --status` | Every governed artifact with live state (`unreviewed` / `approved` / `changed-since-approval` / `reopened`), short hash, mode badge |
126
+ | `node qa/approve.mjs <artifact>` | Record approval — hashes the artifact's files now, stamps the time; also clears a `defaults-accepted` mode |
127
+ | `node qa/approve.mjs --accept-defaults` | **Express lane**: approve every currently-resolvable artifact in one visible act, each stamped `"mode": "defaults-accepted"` — build now, walk the definition later. Unresolvable artifacts are skipped with the standard refusal printed. The ledger never pretends the defaults were designed. |
128
+ | `node qa/approve.mjs --reopen <artifact>` | Move an *approved* artifact (shaped or defaults-accepted) back to `reopened` for deliberate redesign — recorded (`reopenedAt`). Refuses unknown ids and anything not currently approved. |
129
+
130
+ With the create-cmp plugin, the same decisions can be made from the preview console
131
+ (`preview {projectDir}`'s URL) — it calls the same library, so the CLI and the console
132
+ never disagree. An agent blocks on a pending decision with
133
+ `approval_status {waitForDecision:true}`.
134
+
135
+ The verify lane's `approvals` gate (a step like any other, in every profile) resolves each
136
+ artifact's live status against `qa/approvals.json`:
137
+
138
+ - **`unreviewed`** → SKIP with a warning line. Nothing fails until a human opts in by
139
+ approving.
140
+ - **`reopened`** → SKIP with a warning line, exactly like `unreviewed`. Sanctioned redesign
141
+ never trips the gate; edits made while reopened are never drift. Re-approve when the
142
+ redesign lands.
143
+ - **`approved`, hash still matches** → PASS.
144
+ - **`approved`, hash no longer matches** → FAIL, naming the artifact and the re-approval
145
+ command. The artifact changed after sign-off — re-approve it or revert the change.
146
+ Invalidation is mechanical, like golden-tree drift, not a judgment call.
147
+
148
+ That asymmetry is the point: **redesign is a decision; drift is an accident** — the ledger
149
+ records which was which. A run with one reopened artifact and one drifted artifact FAILs
150
+ naming only the drifted one.
151
+
152
+ A gate FAIL fails the lane verdict, which fails `qa/receipt-check.mjs` (the Stop hook) by
153
+ the same mechanism as any other FAIL — no separate enforcement to maintain. `add-feature`
154
+ seeds each new feature's spec as `unreviewed` and prints the approval reminder; it never
155
+ refuses to stamp over this.
156
+
157
+ ### Configurable exemplar — the DNA features are cloned from
158
+
159
+ `qa/approvals.json` carries a top-level `"exemplarFeature"` key (absent means `"home"`, so
160
+ older ledgers keep meaning what they meant). It names the feature whose 11-file set is the
161
+ governed **exemplar-feature** artifact and the clone source `qa/scaffold-feature.mjs`
162
+ stamps new features from. The genesis walk's endgame is pointing it at *your* first real
163
+ feature: stamp it (`add-feature`), shape it, then set `exemplarFeature` — from then on the
164
+ stamper clones your pattern in your domain language, and `home` demotes to an ordinary
165
+ feature spec. If the configured exemplar has grown files beyond the canonical 11-file
166
+ shape, the stamper clones the canonical set and warns, listing exactly what it skipped.
167
+
168
+ ## Comments — review feedback flows back through the agent
169
+
170
+ Approvals are binding (they gate the verify lane); **comments are advisory** — a human's
171
+ running commentary, with a defined path back into your plan, spec, and code.
172
+ `qa/comments.json` is the ledger; `qa/lib/comments.mjs` is the library, mirroring
173
+ `qa/lib/approvals.mjs`'s shape: state, validation, transitions, nothing fabricated.
174
+
175
+ **The loop of record:**
176
+
177
+ 1. A human adds a comment from the preview console — on a screen, a spec clause, a
178
+ design-system token or component, or an architecture tree node.
179
+ 2. You observe it — `review_comments { waitForComment: true }` (plugin) blocks until a new
180
+ one lands; without the plugin, `node qa/comment.mjs --list --open`.
181
+ 3. You act on it — update the plan, the spec clause, or the code it points at.
182
+ 4. You resolve it **after** acting, with a note saying what you did —
183
+ `resolve_comment { id, note }` (plugin) or `node qa/comment.mjs --resolve <id> --note
184
+ "..."` (CLI, records author `agent-cli`). The console then shows `resolved` plus your
185
+ note. The console never edits code: humans comment, agents resolve.
186
+
187
+ | Command | What |
188
+ |---|---|
189
+ | `node qa/comment.mjs --list` | Every comment, open and resolved, with resolution notes |
190
+ | `node qa/comment.mjs --list --open` | Only open comments |
191
+ | `node qa/comment.mjs --resolve <id> --note "..."` | Resolve a comment, recording what changed |
192
+
193
+ A comment targets one of: a **screen**, an **element** (screen + testTag), a **spec-line**
194
+ (file + clause id), a **design-system** token, an **architecture** path, or **general**.
195
+ `addComment` refuses empty text and a target missing the fields its type requires — the
196
+ same refusal-over-fabrication stance as approvals. A ledger that exists but cannot be
197
+ parsed is never treated as empty (that would hide real feedback); reads and writes surface
198
+ the honest error instead.
54
199
 
55
200
  ## UI feedback loop — see what you build, without a device
56
201
 
57
202
  <!-- >>> cmp:feature inspector -->
58
- While building or changing ANY screen, use the preview loop instead of an emulator: it
59
- renders this app's REAL screens (real DI, real theme, seeded data) headlessly in seconds
60
- and tells you exactly what your edit changed.
203
+ While building or changing any screen, use the preview loop instead of an emulator. It
204
+ renders this app's real screens (real DI, real theme, seeded data) headlessly in seconds
205
+ and tells you what your edit changed.
61
206
 
62
207
  **With the create-cmp plugin (cmp-inspector MCP tools):**
63
208
 
64
- 1. `preview { projectDir }` — once per session. Returns a live gallery URL (give it to
65
- the human; it re-renders itself on every save) plus per-screen structural summaries
66
- for you. Sources are watched; you never run Gradle by hand.
67
- 2. After each edit: `preview_status { waitForRender: true }` blocks until the
68
- render/compile outcome. `changedLastRender` names the screens your edit touched
69
- (empty = the edit reached no screen); `lastErrorSource: "compile"` means the edit
70
- didn't even build (the compiler's `e:` lines are in `lastError`).
71
- 3. `preview_diff { screen }` one call proves the change: verdict `proven-clean` /
72
- `changed-with-regressions` / `no-change`. No snapshot bookkeeping needed.
209
+ 1. `preview { projectDir }` — once per session. Returns a live gallery URL for the human
210
+ (it re-renders on every save) and per-screen structural summaries for you. Sources are
211
+ watched; you never run Gradle by hand.
212
+ 2. After each edit: `preview_status { waitForRender: true }` blocks until the outcome.
213
+ `changedLastRender` names the screens your edit touched (empty = the edit reached no
214
+ screen); `lastErrorSource: "compile"` means the edit did not build — the compiler's `e:`
215
+ lines are in `lastError`.
216
+ 3. `preview_diff { screen }` proves the change in one call: `proven-clean` /
217
+ `changed-with-regressions` / `no-change`. No snapshot bookkeeping.
73
218
 
74
219
  **Without the plugin:** `./gradlew :composeApp:renderScreens` renders every screen to
75
220
  `composeApp/build/previews/<id>/{screen.png, tree.json}` (`-Pscreen=<id>` for one);
76
221
  `node qa/preview-gallery.mjs` builds a self-contained gallery page from the output.
77
222
 
78
- Screens come from `inspector/PreviewRegistry.kt` (desktopMain). The `add-feature`/`add-screen`
79
- stamper **auto-registers** a stamped screen there (at the `// cmp:anchor preview-registry`
80
- marker). **When you add a screen by hand, register it there** — a forced-state variant is just
81
- another entry (`"home@empty"`).
82
- Assert on the `tree.json` structure; never read PNG bytes (pixels are for humans).
223
+ Screens come from `inspector/PreviewRegistry.kt` (desktopMain). The `add-feature` and
224
+ `add-screen` stampers auto-register stamped screens at the `// cmp:anchor preview-registry`
225
+ marker; when you add a screen by hand, register it there — a forced-state variant is just
226
+ another entry (`"home@empty"`). Every common component also carries a story entry
227
+ (`"component.<kebab-name>"` in `inspector/ComponentStories.kt`); when you add a component,
228
+ add its story — the lane's `componentStories` step fails naming the missing id otherwise.
229
+ Assert on `tree.json` structure; never read PNG bytes. Pixels are for humans.
83
230
  <!-- <<< cmp:feature inspector -->
84
231
  <!-- >>> cmp:feature dev-client -->
85
232
  For one interactive window instead of stills of every screen:
@@ -98,7 +245,8 @@ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional C
98
245
 
99
246
  | Command | What |
100
247
  |---|---|
101
- | `node qa/verify.mjs` | The verify lane (profile `local`) — your definition of done |
248
+ | `node qa/verify.mjs` | The verify lane (profile `local`) — the done checkpoint, run once |
102
249
  | `./gradlew :composeApp:desktopTest` | Unit tests only (fast inner loop) |
250
+ | `node qa/setup-hooks.mjs` | Enable the pre-push receipt gate (one-time, after `git init`) |
103
251
  | `./gradlew :composeApp:assembleDebug` | Android debug build |
104
252
  | `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
@@ -1,6 +1,6 @@
1
1
  # __APP_NAME__
2
2
 
3
- A Kotlin / Compose Multiplatform app generated by
3
+ A Kotlin / Compose Multiplatform app, generated by
4
4
  [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a **verification harness**:
5
5
  the architecture, testing conventions, and definition of done are enforced mechanically, not
6
6
  by convention. Start with [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); AI collaborators
@@ -80,32 +80,28 @@ Every change must pass the verify lane (`node qa/verify.mjs`) and commit its upd
80
80
 
81
81
  ## Verification enforcement
82
82
 
83
- This project ships a **Stop hook** (`.claude/settings.json`) that makes `CLAUDE.md`'s definition
84
- of done mechanical instead of honor-system, for AI sessions using Claude Code.
85
-
86
- **What it does:** when a session tries to end, the hook runs `node qa/receipt-check.mjs --hook`.
87
- That script recomputes a sha256 hash over this project's "verified surface" (`composeApp/`,
88
- `specs/`, `qa/`, and the Gradle build files — see `qa/lib/inputs-hash.mjs`) and compares it to the
89
- `inputs.hash` recorded in the committed `qa/evidence/latest.json`. If the receipt is a `PASS` and
90
- its hash matches the current tree, the session ends silently. If source changed without a fresh
91
- `PASS` receipt — or the receipt is missing, a `FAIL`, or predates this mechanism — the hook blocks
92
- with the specific reason and asks you to run `node qa/verify.mjs` and commit the receipt. **It runs
93
- no build and no tests only file hashing —** so it costs milliseconds, and it never fires twice in
94
- a row for the same stop.
95
-
96
- Doc-only edits (`*.md`, `README`, `.github/`, `.claude/`) are deliberately **outside** the verified
97
- surface, so editing docs never invalidates a good receipt or forces a needless re-run — the intent
98
- is transparent enforcement, not a hostile one.
99
-
100
- **Why:** `CLAUDE.md` already says a change is "not done" without a `PASS` receipt committed. The
101
- Stop hook is what makes that check happen automatically instead of relying on the AI session to
102
- remember to run it.
103
-
104
- **Escape hatch:** this is your project. If you don't want the hook, delete or comment out the
105
- `Stop` block in [`.claude/settings.json`](./.claude/settings.json) — nothing else depends on it
106
- locally. Note that CI independently enforces the same "receipt attests HEAD" check on every push
107
- (see `.github/workflows/verify.yml`), so disabling the local hook only trades an immediate local
108
- signal for a later one in CI.
83
+ For AI sessions using Claude Code, a **Stop hook** (`.claude/settings.json`) makes
84
+ `CLAUDE.md`'s definition of done mechanical instead of honor-system.
85
+
86
+ **What it does:** when a session tries to end, the hook runs
87
+ `node qa/receipt-check.mjs --hook`. That script recomputes a sha256 hash over the project's
88
+ verified surface (`composeApp/`, `specs/`, `qa/`, and the Gradle build files — see
89
+ `qa/lib/inputs-hash.mjs`) and compares it to the `inputs.hash` in the committed
90
+ `qa/evidence/latest.json`. A `PASS` receipt whose hash matches the tree ends the session
91
+ silently. Source changed without a fresh `PASS` — or a missing, failed, or pre-mechanism
92
+ receipt — blocks with the specific reason and asks for `node qa/verify.mjs` plus a committed
93
+ receipt. It runs no build and no tests, only file hashing, so it costs milliseconds, and it
94
+ never fires twice in a row for the same stop.
95
+
96
+ Doc-only edits (`*.md`, `README`, `.github/`, `.claude/`) are deliberately outside the
97
+ verified surface: editing docs never invalidates a good receipt. The intent is transparent
98
+ enforcement, not a hostile one.
99
+
100
+ **Escape hatch:** this is your project. If you don't want the hook, delete the `Stop` block
101
+ in [`.claude/settings.json`](./.claude/settings.json) nothing else depends on it locally.
102
+ CI independently enforces the same "receipt attests HEAD" check on every push
103
+ (`.github/workflows/verify.yml`), so disabling the local hook only trades an immediate
104
+ signal for a later one.
109
105
 
110
106
  ---
111
107