create-cmp-cli 0.8.0 → 0.10.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.
- package/README.md +83 -9
- package/llms.txt +5 -1
- package/package.json +1 -1
- package/src/lib/adr-seed.mjs +178 -0
- package/src/lib/tabs.mjs +97 -4
- package/src/scaffold.mjs +52 -1
- package/template/.claude/skills/add-feature/SKILL.md +35 -10
- package/template/.claude/skills/add-repository/SKILL.md +1 -1
- package/template/.claude/skills/add-screen/SKILL.md +13 -7
- package/template/.githooks/pre-push +24 -0
- package/template/CLAUDE.md +213 -47
- package/template/README.md +32 -27
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/CrashRecorder.kt +99 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/DbInspector.kt +144 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +19 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +177 -21
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +8 -4
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +10 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/NavInspector.kt +62 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/AppResultCatching.kt +32 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +9 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/DomainError.kt +21 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +4 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/result/AppResult.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +4 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/brand/BrandMark.kt +75 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +138 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +56 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppHeader.kt +63 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppIconButton.kt +48 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +16 -8
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentStateContainer.kt +105 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ContentUiState.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/EmptyState.kt +58 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ErrorState.kt +52 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ListItemCard.kt +77 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/ScreenColumn.kt +47 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/Shimmer.kt +90 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +9 -9
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +5 -27
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +14 -70
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +33 -13
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +22 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +7 -109
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/NavInspectionHook.kt +31 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +4 -27
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +70 -6
- package/template/composeApp/src/commonTest/kotlin/com/example/app/data/AppResultCatchingTest.kt +52 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +29 -4
- package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +8 -6
- package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +39 -27
- package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +10 -6
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/ComponentStories.kt +300 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewDaemon.kt +5 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewHarness.kt +91 -1
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt +37 -1
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewSemanticsJson.kt +14 -1
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +250 -16
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ComponentConformanceTest.kt +84 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +36 -4
- package/template/docs/ARCHITECTURE.md +372 -34
- package/template/docs/TESTING.md +13 -5
- package/template/docs/adr/0002-maestro-over-appium-for-e2e.md +39 -0
- package/template/docs/adr/0003-jvm-desktop-target-is-harness-infrastructure.md +39 -0
- package/template/docs/adr/0004-fakes-not-mocks-for-unit-tests.md +48 -0
- package/template/qa/approvals.json +42 -0
- package/template/qa/approve.mjs +139 -0
- package/template/qa/arch-doc.mjs +69 -0
- package/template/qa/comment.mjs +76 -0
- package/template/qa/comments.json +4 -0
- package/template/qa/e2e/smoke.yaml +6 -0
- package/template/qa/golden/home.json +3 -3
- package/template/qa/lib/a11y.mjs +17 -8
- package/template/qa/lib/approvals.mjs +822 -0
- package/template/qa/lib/arch-doc.mjs +451 -0
- package/template/qa/lib/comments.mjs +252 -0
- package/template/qa/lib/component-stories.mjs +183 -0
- package/template/qa/lib/inputs-hash.mjs +5 -1
- package/template/qa/scaffold-feature.mjs +184 -67
- package/template/qa/setup-hooks.mjs +33 -0
- package/template/qa/verify.mjs +181 -15
- package/template/qa/walkthrough.mjs +499 -0
- package/template/specs/app-base.spec.md +49 -7
- package/template/specs/home.spec.md +7 -4
- 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
|
|
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
|
|
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
|
|
62
|
-
set (`<FEATURE>-01..
|
|
63
|
-
tree) — copied verbatim from the
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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..
|
|
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
|
|
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
|
|
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
|
|
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..
|
|
113
|
-
conformance holds
|
|
114
|
-
|
|
115
|
-
|
|
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
|
package/template/CLAUDE.md
CHANGED
|
@@ -1,85 +1,250 @@
|
|
|
1
1
|
# __APP_NAME__ — AI delivery contract
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
(`qa/evidence/latest.json`) is
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
stable id
|
|
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
|
|
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
|
-
- **
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
32
71
|
|
|
33
|
-
## Testing
|
|
72
|
+
## Testing
|
|
34
73
|
|
|
35
74
|
- **Unit** (`composeApp/src/commonTest`, run via `./gradlew :composeApp:desktopTest`):
|
|
36
|
-
kotlin-test + coroutines-test + Turbine.
|
|
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`):
|
|
40
|
-
|
|
41
|
-
spec-cited, testTag selectors)
|
|
42
|
-
drift
|
|
43
|
-
(`UPDATE_GOLDEN=1`) and
|
|
44
|
-
- **E2E** (`qa/e2e/*.yaml`): Maestro flows; smoke covers boot + bottom nav.
|
|
45
|
-
testTag
|
|
46
|
-
-
|
|
47
|
-
the test itself is wrong, say so
|
|
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
|
|
53
|
-
`qa-artifacts/` are hashed into the receipt
|
|
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
|
+
The order encodes two disciplines: **behavior is spec-first** (the exemplar's clauses are
|
|
105
|
+
confirmed before the slice is built) and **visuals are UI-first** (the design system and
|
|
106
|
+
component vocabulary are distilled from the real screens, so they lock after the exemplar —
|
|
107
|
+
a provisional palette carries the build until then).
|
|
108
|
+
|
|
109
|
+
0. **Intent** — `specs/intent.md`, the root brief everything else traces to (purpose,
|
|
110
|
+
audience, platforms, brand feel, reference apps, first screens, **glossary**). Filled by
|
|
111
|
+
the `cmp-new` interview; the seed's placeholder prose is marked unfilled. Its
|
|
112
|
+
`## Glossary` section is lifted verbatim into `docs/ARCHITECTURE.md` §8 — write it there
|
|
113
|
+
in the exact form you want published.
|
|
114
|
+
1. **Architecture + structure** — `specs/app-base.spec.md` **and**
|
|
115
|
+
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) (`cmp:generated` sections stripped
|
|
116
|
+
before hashing, so a mechanical regeneration never invalidates the approval — only an
|
|
117
|
+
authored-prose edit does).
|
|
118
|
+
2. **Exemplar spec** — `specs/<exemplar>.spec.md`. Confirmed BEFORE the slice is built:
|
|
119
|
+
propose the clauses, get the human's yes, then implement to satisfy them (the same
|
|
120
|
+
discipline `add-feature` already enforces post-genesis).
|
|
121
|
+
3. **Exemplar feature** — the **configured** exemplar's file set the `add-feature`
|
|
122
|
+
stamper clones from (see "Configurable exemplar"), built to the confirmed spec.
|
|
123
|
+
4. **Design system** — `presentation/theme/Theme.kt`, `presentation/theme/Tokens.kt`.
|
|
124
|
+
Locked on the REAL exemplar: candidates render on real screens, never stubs. If the
|
|
125
|
+
lock changes the exemplar's look, reopen → re-approve it — that loop is the design,
|
|
126
|
+
not a failure.
|
|
127
|
+
5. **Components** — every `presentation/components/*.kt` (a dynamic, sorted glob),
|
|
128
|
+
distilled from the screens per the inclusion rubric (`docs/ARCHITECTURE.md` §7). Once
|
|
129
|
+
approved, the registry is law: adding or changing a common component invalidates the
|
|
130
|
+
approval until a human re-approves.
|
|
131
|
+
6. **Per-feature spec** — `specs/<feature>.spec.md`, one governed artifact per feature,
|
|
132
|
+
added as features land.
|
|
133
|
+
|
|
134
|
+
| Command | What |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `node qa/approve.mjs --status` | Every governed artifact with live state (`unreviewed` / `approved` / `changed-since-approval` / `reopened`), short hash, mode badge |
|
|
137
|
+
| `node qa/approve.mjs <artifact>` | Record approval — hashes the artifact's files now, stamps the time; also clears a `defaults-accepted` mode |
|
|
138
|
+
| `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. |
|
|
139
|
+
| `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. |
|
|
140
|
+
|
|
141
|
+
With the create-cmp plugin, the same decisions can be made from the preview console
|
|
142
|
+
(`preview {projectDir}`'s URL) — it calls the same library, so the CLI and the console
|
|
143
|
+
never disagree. An agent blocks on a pending decision with
|
|
144
|
+
`approval_status {waitForDecision:true}`.
|
|
145
|
+
|
|
146
|
+
The verify lane's `approvals` gate (a step like any other, in every profile) resolves each
|
|
147
|
+
artifact's live status against `qa/approvals.json`:
|
|
148
|
+
|
|
149
|
+
- **`unreviewed`** → SKIP with a warning line. Nothing fails until a human opts in by
|
|
150
|
+
approving.
|
|
151
|
+
- **`reopened`** → SKIP with a warning line, exactly like `unreviewed`. Sanctioned redesign
|
|
152
|
+
never trips the gate; edits made while reopened are never drift. Re-approve when the
|
|
153
|
+
redesign lands.
|
|
154
|
+
- **`approved`, hash still matches** → PASS.
|
|
155
|
+
- **`approved`, hash no longer matches** → FAIL, naming the artifact and the re-approval
|
|
156
|
+
command. The artifact changed after sign-off — re-approve it or revert the change.
|
|
157
|
+
Invalidation is mechanical, like golden-tree drift, not a judgment call.
|
|
158
|
+
|
|
159
|
+
That asymmetry is the point: **redesign is a decision; drift is an accident** — the ledger
|
|
160
|
+
records which was which. A run with one reopened artifact and one drifted artifact FAILs
|
|
161
|
+
naming only the drifted one.
|
|
162
|
+
|
|
163
|
+
A gate FAIL fails the lane verdict, which fails `qa/receipt-check.mjs` (the Stop hook) by
|
|
164
|
+
the same mechanism as any other FAIL — no separate enforcement to maintain. `add-feature`
|
|
165
|
+
seeds each new feature's spec as `unreviewed` and prints the approval reminder; it never
|
|
166
|
+
refuses to stamp over this.
|
|
167
|
+
|
|
168
|
+
### Configurable exemplar — the DNA features are cloned from
|
|
169
|
+
|
|
170
|
+
`qa/approvals.json` carries a top-level `"exemplarFeature"` key (absent means `"home"`, so
|
|
171
|
+
older ledgers keep meaning what they meant). It names the feature whose 11-file set is the
|
|
172
|
+
governed **exemplar-feature** artifact and the clone source `qa/scaffold-feature.mjs`
|
|
173
|
+
stamps new features from. The genesis walk's endgame is pointing it at *your* first real
|
|
174
|
+
feature: stamp it (`add-feature`), shape it, then set `exemplarFeature` — from then on the
|
|
175
|
+
stamper clones your pattern in your domain language, and `home` demotes to an ordinary
|
|
176
|
+
feature spec. If the configured exemplar has grown files beyond the canonical 11-file
|
|
177
|
+
shape, the stamper clones the canonical set and warns, listing exactly what it skipped.
|
|
178
|
+
|
|
179
|
+
## Comments — review feedback flows back through the agent
|
|
180
|
+
|
|
181
|
+
Approvals are binding (they gate the verify lane); **comments are advisory** — a human's
|
|
182
|
+
running commentary, with a defined path back into your plan, spec, and code.
|
|
183
|
+
`qa/comments.json` is the ledger; `qa/lib/comments.mjs` is the library, mirroring
|
|
184
|
+
`qa/lib/approvals.mjs`'s shape: state, validation, transitions, nothing fabricated.
|
|
185
|
+
|
|
186
|
+
**The loop of record:**
|
|
187
|
+
|
|
188
|
+
1. A human adds a comment from the preview console — on a screen, a spec clause, a
|
|
189
|
+
design-system token or component, or an architecture tree node.
|
|
190
|
+
2. You observe it — `review_comments { waitForComment: true }` (plugin) blocks until a new
|
|
191
|
+
one lands; without the plugin, `node qa/comment.mjs --list --open`.
|
|
192
|
+
3. You act on it — update the plan, the spec clause, or the code it points at.
|
|
193
|
+
4. You resolve it **after** acting, with a note saying what you did —
|
|
194
|
+
`resolve_comment { id, note }` (plugin) or `node qa/comment.mjs --resolve <id> --note
|
|
195
|
+
"..."` (CLI, records author `agent-cli`). The console then shows `resolved` plus your
|
|
196
|
+
note. The console never edits code: humans comment, agents resolve.
|
|
197
|
+
|
|
198
|
+
| Command | What |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `node qa/comment.mjs --list` | Every comment, open and resolved, with resolution notes |
|
|
201
|
+
| `node qa/comment.mjs --list --open` | Only open comments |
|
|
202
|
+
| `node qa/comment.mjs --resolve <id> --note "..."` | Resolve a comment, recording what changed |
|
|
203
|
+
|
|
204
|
+
A comment targets one of: a **screen**, an **element** (screen + testTag), a **spec-line**
|
|
205
|
+
(file + clause id), a **design-system** token, an **architecture** path, or **general**.
|
|
206
|
+
`addComment` refuses empty text and a target missing the fields its type requires — the
|
|
207
|
+
same refusal-over-fabrication stance as approvals. A ledger that exists but cannot be
|
|
208
|
+
parsed is never treated as empty (that would hide real feedback); reads and writes surface
|
|
209
|
+
the honest error instead.
|
|
54
210
|
|
|
55
211
|
## UI feedback loop — see what you build, without a device
|
|
56
212
|
|
|
57
213
|
<!-- >>> cmp:feature inspector -->
|
|
58
|
-
While building or changing
|
|
59
|
-
renders this app's
|
|
60
|
-
and tells you
|
|
214
|
+
While building or changing any screen, use the preview loop instead of an emulator. It
|
|
215
|
+
renders this app's real screens (real DI, real theme, seeded data) headlessly in seconds
|
|
216
|
+
and tells you what your edit changed.
|
|
61
217
|
|
|
62
218
|
**With the create-cmp plugin (cmp-inspector MCP tools):**
|
|
63
219
|
|
|
64
|
-
1. `preview { projectDir }` — once per session. Returns a live gallery URL
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
2. After each edit: `preview_status { waitForRender: true }`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
3. `preview_diff { screen }`
|
|
72
|
-
`changed-with-regressions` / `no-change`. No snapshot bookkeeping
|
|
220
|
+
1. `preview { projectDir }` — once per session. Returns a live gallery URL for the human
|
|
221
|
+
(it re-renders on every save) and per-screen structural summaries for you. Sources are
|
|
222
|
+
watched; you never run Gradle by hand.
|
|
223
|
+
2. After each edit: `preview_status { waitForRender: true }` blocks until the outcome.
|
|
224
|
+
`changedLastRender` names the screens your edit touched (empty = the edit reached no
|
|
225
|
+
screen); `lastErrorSource: "compile"` means the edit did not build — the compiler's `e:`
|
|
226
|
+
lines are in `lastError`.
|
|
227
|
+
3. `preview_diff { screen }` proves the change in one call: `proven-clean` /
|
|
228
|
+
`changed-with-regressions` / `no-change`. No snapshot bookkeeping.
|
|
73
229
|
|
|
74
230
|
**Without the plugin:** `./gradlew :composeApp:renderScreens` renders every screen to
|
|
75
231
|
`composeApp/build/previews/<id>/{screen.png, tree.json}` (`-Pscreen=<id>` for one);
|
|
76
232
|
`node qa/preview-gallery.mjs` builds a self-contained gallery page from the output.
|
|
77
233
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
234
|
+
**Live tier — the human's live device view (standing step).** Whenever `connect_live`
|
|
235
|
+
succeeds, OFFER the `remoteUrl` it returns (`http://127.0.0.1:9500/inspect/remote`) to the
|
|
236
|
+
human — every time, not as a maybe. It is a self-contained browser page that mirrors the
|
|
237
|
+
running app (~700ms refresh) with click-to-tap driving the real device: they watch and drive
|
|
238
|
+
the actual app while you assert on the tree (`navigate_and_inspect` / `prove_change` /
|
|
239
|
+
`db_query`). It is also the right way for a human to *watch* an e2e run.
|
|
240
|
+
|
|
241
|
+
Screens come from `inspector/PreviewRegistry.kt` (desktopMain). The `add-feature` and
|
|
242
|
+
`add-screen` stampers auto-register stamped screens at the `// cmp:anchor preview-registry`
|
|
243
|
+
marker; when you add a screen by hand, register it there — a forced-state variant is just
|
|
244
|
+
another entry (`"home@empty"`). Every common component also carries a story entry
|
|
245
|
+
(`"component.<kebab-name>"` in `inspector/ComponentStories.kt`); when you add a component,
|
|
246
|
+
add its story — the lane's `componentStories` step fails naming the missing id otherwise.
|
|
247
|
+
Assert on `tree.json` structure; never read PNG bytes. Pixels are for humans.
|
|
83
248
|
<!-- <<< cmp:feature inspector -->
|
|
84
249
|
<!-- >>> cmp:feature dev-client -->
|
|
85
250
|
For one interactive window instead of stills of every screen:
|
|
@@ -98,7 +263,8 @@ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional C
|
|
|
98
263
|
|
|
99
264
|
| Command | What |
|
|
100
265
|
|---|---|
|
|
101
|
-
| `node qa/verify.mjs` | The verify lane (profile `local`) —
|
|
266
|
+
| `node qa/verify.mjs` | The verify lane (profile `local`) — the done checkpoint, run once |
|
|
102
267
|
| `./gradlew :composeApp:desktopTest` | Unit tests only (fast inner loop) |
|
|
268
|
+
| `node qa/setup-hooks.mjs` | Enable the pre-push receipt gate (one-time, after `git init`) |
|
|
103
269
|
| `./gradlew :composeApp:assembleDebug` | Android debug build |
|
|
104
270
|
| `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
|
package/template/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
A Kotlin / Compose Multiplatform app
|
|
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
|
|
@@ -29,6 +29,15 @@ follow the contract in [`CLAUDE.md`](./CLAUDE.md).
|
|
|
29
29
|
node qa/verify.mjs
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
<!-- >>> cmp:feature inspector -->
|
|
33
|
+
**Watch and drive the app live from a browser:** with a debug build running on a
|
|
34
|
+
device/emulator, run `adb forward tcp:9500 tcp:9500` and open
|
|
35
|
+
`http://127.0.0.1:9500/inspect/remote` — a self-contained page that mirrors the running app
|
|
36
|
+
(~700ms refresh) with **click-to-tap driving the real device**. The human's window into the
|
|
37
|
+
same app the agent inspects structurally; also the way to watch an e2e run. Debug builds
|
|
38
|
+
only — release builds contain no inspector code.
|
|
39
|
+
<!-- <<< cmp:feature inspector -->
|
|
40
|
+
|
|
32
41
|
<!-- >>> cmp:feature ios -->
|
|
33
42
|
**iOS:** `cd iosApp && xcodegen generate && pod install`, then open
|
|
34
43
|
`iosApp.xcworkspace` in Xcode and run. (First time: `brew install xcodegen`.)
|
|
@@ -80,32 +89,28 @@ Every change must pass the verify lane (`node qa/verify.mjs`) and commit its upd
|
|
|
80
89
|
|
|
81
90
|
## Verification enforcement
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
of done mechanical instead of honor-system
|
|
85
|
-
|
|
86
|
-
**What it does:** when a session tries to end, the hook runs
|
|
87
|
-
That script recomputes a sha256 hash over
|
|
88
|
-
`specs/`, `qa/`, and the Gradle build files — see
|
|
89
|
-
`inputs.
|
|
90
|
-
|
|
91
|
-
`PASS`
|
|
92
|
-
with the specific reason and asks
|
|
93
|
-
no build and no tests
|
|
94
|
-
a row for the same stop.
|
|
95
|
-
|
|
96
|
-
Doc-only edits (`*.md`, `README`, `.github/`, `.claude/`) are deliberately
|
|
97
|
-
surface
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
92
|
+
For AI sessions using Claude Code, a **Stop hook** (`.claude/settings.json`) makes
|
|
93
|
+
`CLAUDE.md`'s definition of done mechanical instead of honor-system.
|
|
94
|
+
|
|
95
|
+
**What it does:** when a session tries to end, the hook runs
|
|
96
|
+
`node qa/receipt-check.mjs --hook`. That script recomputes a sha256 hash over the project's
|
|
97
|
+
verified surface (`composeApp/`, `specs/`, `qa/`, and the Gradle build files — see
|
|
98
|
+
`qa/lib/inputs-hash.mjs`) and compares it to the `inputs.hash` in the committed
|
|
99
|
+
`qa/evidence/latest.json`. A `PASS` receipt whose hash matches the tree ends the session
|
|
100
|
+
silently. Source changed without a fresh `PASS` — or a missing, failed, or pre-mechanism
|
|
101
|
+
receipt — blocks with the specific reason and asks for `node qa/verify.mjs` plus a committed
|
|
102
|
+
receipt. It runs no build and no tests, only file hashing, so it costs milliseconds, and it
|
|
103
|
+
never fires twice in a row for the same stop.
|
|
104
|
+
|
|
105
|
+
Doc-only edits (`*.md`, `README`, `.github/`, `.claude/`) are deliberately outside the
|
|
106
|
+
verified surface: editing docs never invalidates a good receipt. The intent is transparent
|
|
107
|
+
enforcement, not a hostile one.
|
|
108
|
+
|
|
109
|
+
**Escape hatch:** this is your project. If you don't want the hook, delete the `Stop` block
|
|
110
|
+
in [`.claude/settings.json`](./.claude/settings.json) — nothing else depends on it locally.
|
|
111
|
+
CI independently enforces the same "receipt attests HEAD" check on every push
|
|
112
|
+
(`.github/workflows/verify.yml`), so disabling the local hook only trades an immediate
|
|
113
|
+
signal for a later one.
|
|
109
114
|
|
|
110
115
|
---
|
|
111
116
|
|