create-cmp-cli 0.7.1 → 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.
- package/README.md +72 -11
- package/llms.txt +6 -2
- package/package.json +1 -1
- package/src/commands/upgrade.mjs +8 -1
- package/src/lib/adr-seed.mjs +178 -0
- package/src/lib/registry.mjs +15 -2
- package/src/lib/tabs.mjs +91 -4
- package/src/lib/upgrade.mjs +49 -5
- package/src/scaffold.mjs +52 -1
- package/src/versions/candidates.json +4 -0
- package/src/versions/registry.json +88 -0
- 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 +196 -48
- package/template/README.md +23 -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/InspectorHttpServer.kt +69 -2
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +8 -4
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/NavInspector.kt +31 -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/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 +54 -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 +13 -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 +21 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +4 -27
- 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 +269 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt +37 -1
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +207 -15
- 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 +317 -34
- package/template/docs/TESTING.md +6 -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/golden/home.json +3 -3
- package/template/qa/lib/approvals.mjs +806 -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 +118 -9
- package/template/specs/app-base.spec.md +44 -7
- package/template/specs/home.spec.md +7 -4
- package/template/specs/intent.md +50 -0
package/src/lib/upgrade.mjs
CHANGED
|
@@ -73,18 +73,49 @@ export function lockstepViolation(versions) {
|
|
|
73
73
|
const kotlin = versions.kotlin;
|
|
74
74
|
const ksp = versions.ksp;
|
|
75
75
|
if (!kotlin || !ksp) return null;
|
|
76
|
-
|
|
76
|
+
// Two valid schemes: the classic KSP1 form "<kotlin>-<kspVersion>" (e.g.
|
|
77
|
+
// 2.2.20-2.0.4), and the KSP2 aligned form where the KSP version EQUALS the
|
|
78
|
+
// Kotlin version (e.g. kotlin 2.3.10 ↔ ksp 2.3.10 — KSP dropped the -<ksp> suffix).
|
|
79
|
+
if (ksp !== kotlin && !ksp.startsWith(`${kotlin}-`)) {
|
|
77
80
|
return (
|
|
78
|
-
`kotlin ${kotlin} and ksp ${ksp} are OUT OF LOCKSTEP — ksp must be ` +
|
|
79
|
-
`"${kotlin}-<kspVersion>" (e.g. "${kotlin}-2.0.4").
|
|
81
|
+
`kotlin ${kotlin} and ksp ${ksp} are OUT OF LOCKSTEP — ksp must be either ` +
|
|
82
|
+
`"${kotlin}" (KSP2 aligned) or "${kotlin}-<kspVersion>" (e.g. "${kotlin}-2.0.4"). ` +
|
|
83
|
+
`Refusing to write a broken pairing.`
|
|
80
84
|
);
|
|
81
85
|
}
|
|
82
86
|
return null;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Apply a set's `androidSdk` levels (compileSdk / targetSdk) to the text of
|
|
91
|
+
* composeApp/build.gradle.kts. These live in build.gradle.kts, NOT the version
|
|
92
|
+
* catalog — but they are coupled to the set (a dependency built against a newer
|
|
93
|
+
* Android API forces a higher compileSdk, which in turn needs a newer AGP), so
|
|
94
|
+
* the version set manages them too. Surgical, line-preserving replacement of the
|
|
95
|
+
* `compileSdk = N` / `targetSdk = N` assignments.
|
|
96
|
+
* @param {string} content build.gradle.kts text
|
|
97
|
+
* @param {{compileSdk?:number,targetSdk?:number}} androidSdk
|
|
98
|
+
* @returns {{content:string, changes:Array<{key,from,to}>}}
|
|
99
|
+
*/
|
|
100
|
+
export function applyAndroidSdk(content, androidSdk) {
|
|
101
|
+
if (!androidSdk || content == null) return { content, changes: [] };
|
|
102
|
+
const changes = [];
|
|
103
|
+
let out = content;
|
|
104
|
+
for (const key of ["compileSdk", "targetSdk"]) {
|
|
105
|
+
if (androidSdk[key] == null) continue;
|
|
106
|
+
const re = new RegExp(`(\\b${key}\\s*=\\s*)(\\d+)`);
|
|
107
|
+
const m = out.match(re);
|
|
108
|
+
if (m && Number(m[2]) !== Number(androidSdk[key])) {
|
|
109
|
+
changes.push({ key, from: m[2], to: String(androidSdk[key]) });
|
|
110
|
+
out = out.replace(re, `$1${androidSdk[key]}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { content: changes.length ? out : content, changes };
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
/**
|
|
86
117
|
* Compute the full upgrade plan for one catalog + optional gradle.properties +
|
|
87
|
-
* optional wrapper properties. Pure — no filesystem.
|
|
118
|
+
* optional wrapper properties + optional composeApp/build.gradle.kts. Pure — no filesystem.
|
|
88
119
|
* @param {object} params
|
|
89
120
|
* @param {string} params.tomlContent gradle/libs.versions.toml text
|
|
90
121
|
* @param {string|null} params.gradlePropertiesContent gradle.properties text (null = absent)
|
|
@@ -101,7 +132,7 @@ export function lockstepViolation(versions) {
|
|
|
101
132
|
* fromOurTemplate: boolean
|
|
102
133
|
* }}
|
|
103
134
|
*/
|
|
104
|
-
export function planUpgrade({ tomlContent, gradlePropertiesContent, wrapperPropertiesContent, set }) {
|
|
135
|
+
export function planUpgrade({ tomlContent, gradlePropertiesContent, wrapperPropertiesContent, buildGradleContent = null, set }) {
|
|
105
136
|
const projectVersions = parseVersions(tomlContent);
|
|
106
137
|
const diff = diffAgainstSet(projectVersions, set);
|
|
107
138
|
const resulting = resultingVersions(projectVersions, diff.changes);
|
|
@@ -146,6 +177,17 @@ export function planUpgrade({ tomlContent, gradlePropertiesContent, wrapperPrope
|
|
|
146
177
|
}
|
|
147
178
|
}
|
|
148
179
|
|
|
180
|
+
// Android compileSdk / targetSdk (composeApp/build.gradle.kts), when the set pins them.
|
|
181
|
+
let sdkChanges = [];
|
|
182
|
+
let newBuildGradleContent = null;
|
|
183
|
+
if (!lockstepError && set.androidSdk && buildGradleContent !== null) {
|
|
184
|
+
const r = applyAndroidSdk(buildGradleContent, set.androidSdk);
|
|
185
|
+
if (r.changes.length > 0) {
|
|
186
|
+
sdkChanges = r.changes;
|
|
187
|
+
newBuildGradleContent = r.content;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
return {
|
|
150
192
|
diff,
|
|
151
193
|
lockstepError,
|
|
@@ -154,6 +196,8 @@ export function planUpgrade({ tomlContent, gradlePropertiesContent, wrapperPrope
|
|
|
154
196
|
newGradlePropertiesContent,
|
|
155
197
|
wrapperChange,
|
|
156
198
|
newWrapperPropertiesContent,
|
|
199
|
+
sdkChanges,
|
|
200
|
+
newBuildGradleContent,
|
|
157
201
|
fromOurTemplate: looksLikeOurTemplate(tomlContent),
|
|
158
202
|
};
|
|
159
203
|
}
|
package/src/scaffold.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
|
|
20
20
|
import { validate, formatErrors } from "./lib/schema.mjs";
|
|
21
21
|
import { buildTokenMap, replaceTokens, replacePathTokens, isBinaryPath, slugifyAppName } from "./lib/tokens.mjs";
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
deleteDisabledFeaturePaths,
|
|
27
27
|
} from "./lib/toggle.mjs";
|
|
28
28
|
import { rewriteTabSurfaces } from "./lib/tabs.mjs";
|
|
29
|
+
import { seedConfigAdrs } from "./lib/adr-seed.mjs";
|
|
29
30
|
import { copyDir, listFiles, listDirsDeepestFirst } from "./lib/fsutil.mjs";
|
|
30
31
|
import { runVerify, printVerifyVerdict } from "./lib/verify.mjs";
|
|
31
32
|
import { colors, step, ok, warn } from "./lib/log.mjs";
|
|
@@ -155,6 +156,42 @@ function stripDisabledBlocks(projectDir, disabled) {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
// Regenerate docs/ARCHITECTURE.md's `cmp:generated` sections against the tree
|
|
160
|
+
// AS STAMPED — after disabled-feature paths are deleted, tab surfaces are
|
|
161
|
+
// rewritten, the package is renamed, and marker blocks are stripped. The
|
|
162
|
+
// template ships the doc true for the RAW template's shape (all features on);
|
|
163
|
+
// any config that changes the tree (--no-ios removes iosMain and its actuals,
|
|
164
|
+
// custom tabs add PlaceholderScreen.kt, …) would otherwise fail the verify
|
|
165
|
+
// lane's archDoc freshness gate on a fresh, untouched app. Same philosophy as
|
|
166
|
+
// rewriteTabSurfaces: derived surfaces are regenerated from what was actually
|
|
167
|
+
// stamped, never hand-corrected afterward.
|
|
168
|
+
//
|
|
169
|
+
// The generator itself is the project's OWN vendored walker
|
|
170
|
+
// (qa/lib/arch-doc.mjs) — imported from the stamped output so the engine can
|
|
171
|
+
// never drift from what the app's `node qa/arch-doc.mjs --check` will verify
|
|
172
|
+
// later. Tolerant by design: a template without the walker or the doc (the
|
|
173
|
+
// synthetic test templates; a --no-e2e config that strips qa/) skips quietly,
|
|
174
|
+
// and a walker failure warns but never blocks the stamp.
|
|
175
|
+
async function regenerateArchDoc(projectDir) {
|
|
176
|
+
const walkerPath = path.join(projectDir, "qa", "lib", "arch-doc.mjs");
|
|
177
|
+
const docPath = path.join(projectDir, "docs", "ARCHITECTURE.md");
|
|
178
|
+
if (!fs.existsSync(walkerPath) || !fs.existsSync(docPath)) return;
|
|
179
|
+
step("Regenerating docs/ARCHITECTURE.md generated sections for the stamped tree…");
|
|
180
|
+
try {
|
|
181
|
+
const { writeArchDoc } = await import(pathToFileURL(walkerPath).href);
|
|
182
|
+
const result = writeArchDoc(projectDir);
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
warn(`ARCHITECTURE.md regeneration skipped: ${result.reason}`);
|
|
185
|
+
} else if (result.wrote) {
|
|
186
|
+
process.stdout.write(` arch-doc → updated section(s): ${result.changedSections.join(", ")}\n`);
|
|
187
|
+
} else {
|
|
188
|
+
process.stdout.write(" arch-doc → already fresh for this configuration\n");
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
warn(`ARCHITECTURE.md regeneration failed (stamp continues): ${err?.message ?? err}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
158
195
|
/**
|
|
159
196
|
* Resolve the Android SDK location from the environment or the conventional
|
|
160
197
|
* install path, then write `local.properties` (sdk.dir) into the project so
|
|
@@ -358,6 +395,20 @@ export async function scaffold(config, opts = {}) {
|
|
|
358
395
|
);
|
|
359
396
|
stripDisabledBlocks(projectDir, disabled);
|
|
360
397
|
|
|
398
|
+
// (e.1) Seed one project ADR per configuration decision that deviates from
|
|
399
|
+
// the interview default (--no-room, --no-ios, a non-"both" auth choice —
|
|
400
|
+
// see src/lib/adr-seed.mjs for why this is an engine hook, not a SKILL.md
|
|
401
|
+
// instruction). MUST run before regenerateArchDoc below so the stamped
|
|
402
|
+
// project's own adr-index walker (qa/lib/arch-doc.mjs, which scans
|
|
403
|
+
// docs/adr/*.md) picks the seeded files up in the same pass.
|
|
404
|
+
step("Seeding project ADRs for configuration decisions…");
|
|
405
|
+
const { seeded } = seedConfigAdrs(projectDir, config, (m) => process.stdout.write(`${m}\n`));
|
|
406
|
+
if (seeded.length === 0) process.stdout.write(" no configuration deviated from the interview default — nothing to seed\n");
|
|
407
|
+
|
|
408
|
+
// (e.2) regenerate the architecture doc's derived sections for the tree as
|
|
409
|
+
// stamped — see regenerateArchDoc above.
|
|
410
|
+
await regenerateArchDoc(projectDir);
|
|
411
|
+
|
|
361
412
|
// Write local.properties (sdk.dir) so the Gradle build can find the Android
|
|
362
413
|
// SDK even when ANDROID_HOME/ANDROID_SDK_ROOT aren't exported (manifest
|
|
363
414
|
// stampPipeline step 7). Skip silently if no SDK is found and env vars are
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Staging area for CANDIDATE version sets awaiting canary certification. `scripts/promote-set.mjs` applies one candidate to a fresh full-featured scaffold (catalog + gradle.properties + wrapper + composeApp/build.gradle.kts compileSdk/targetSdk), builds it for real (Android assembleDebug + the device-free lane gates + the iOS framework link), and ONLY on all-green promotes it into registry.json as a new proven-green set (the new default `upgrade` target). Red leaves registry.json untouched. Same shape as a registry set: `versions` mirrors the [versions] table, `androidSdk` pins compileSdk/targetSdk, plus `status: candidate` and `baseline`.",
|
|
3
|
+
"candidates": []
|
|
4
|
+
}
|
|
@@ -31,12 +31,100 @@
|
|
|
31
31
|
"version": "8.11.1",
|
|
32
32
|
"distributionUrl": "https://services.gradle.org/distributions/gradle-8.11.1-bin.zip"
|
|
33
33
|
},
|
|
34
|
+
"androidSdk": {
|
|
35
|
+
"compileSdk": 35,
|
|
36
|
+
"targetSdk": 35
|
|
37
|
+
},
|
|
34
38
|
"notes": [
|
|
35
39
|
"KSP must be `<kotlin>-<kspVersion>`: kotlin 2.2.20 pairs with ksp 2.2.20-2.0.4. Never bump one without the other — Kotlin / KSP / Compose / Room / AGP move as ONE set or the build dies.",
|
|
36
40
|
"ksp.useKSP2=true is REQUIRED when Room's KSP processor runs on Kotlin/Native (iOS): KSP1 fails with `ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt` (the KSP2/iOS catch-22).",
|
|
37
41
|
"Room 2.8.4 + sqlite-bundled 2.6.2 is the KMP-native pairing; Room < 2.7 has no Kotlin/Native support at all.",
|
|
38
42
|
"Gradle wrapper 8.11.1 matches AGP 8.7.3 (AGP 8.7 requires Gradle >= 8.9)."
|
|
39
43
|
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "2026.07c",
|
|
47
|
+
"label": "Conservative bump — coil + kotlinx-serialization to latest; lockstep held at 2026.06 (ktor 3.2.0 excluded: needs DEX 040 / newer AGP R8)",
|
|
48
|
+
"status": "proven-green",
|
|
49
|
+
"versions": {
|
|
50
|
+
"kotlin": "2.2.20",
|
|
51
|
+
"agp": "8.7.3",
|
|
52
|
+
"google-services": "4.4.2",
|
|
53
|
+
"compose-multiplatform": "1.10.3",
|
|
54
|
+
"koin": "4.1.1",
|
|
55
|
+
"ktor": "3.1.0",
|
|
56
|
+
"room": "2.8.4",
|
|
57
|
+
"sqlite": "2.6.2",
|
|
58
|
+
"ksp": "2.2.20-2.0.4",
|
|
59
|
+
"kotlinx-serialization": "1.9.0",
|
|
60
|
+
"kotlinx-datetime": "0.7.0",
|
|
61
|
+
"lifecycle": "2.10.0",
|
|
62
|
+
"navigation": "2.9.2",
|
|
63
|
+
"firebase-gitlive": "2.1.0",
|
|
64
|
+
"coil": "3.2.0",
|
|
65
|
+
"androidx-core": "1.15.0",
|
|
66
|
+
"androidx-activity": "1.10.1"
|
|
67
|
+
},
|
|
68
|
+
"gradleProperties": {
|
|
69
|
+
"ksp.useKSP2": "true"
|
|
70
|
+
},
|
|
71
|
+
"gradleWrapper": {
|
|
72
|
+
"version": "8.11.1",
|
|
73
|
+
"distributionUrl": "https://services.gradle.org/distributions/gradle-8.11.1-bin.zip"
|
|
74
|
+
},
|
|
75
|
+
"androidSdk": {
|
|
76
|
+
"compileSdk": 35,
|
|
77
|
+
"targetSdk": 35
|
|
78
|
+
},
|
|
79
|
+
"notes": [
|
|
80
|
+
"Delta vs 2026.06: coil 3.1.0→3.2.0, kotlinx-serialization 1.7.3→1.9.0. Everything else, including the entire Kotlin/KSP/Compose/Room/AGP lockstep, is held identical — the conservative-first strategy: prove the canary + upgrade loop before a risky compiler-version jump.",
|
|
81
|
+
"ktor 3.2.0 was EXCLUDED after the canary caught it: ktor-client-core-jvm 3.2.0 carries a backtick identifier with a space (`use streaming syntax`) that requires DEX version 040, which AGP 8.7.3's bundled R8 (8.7.18) rejects at :composeApp:mergeExtDexDebug. Bumping ktor needs a newer AGP/R8 — deferred to a bold set.",
|
|
82
|
+
"Bumps verified latest via Maven Central 2026-07-15.",
|
|
83
|
+
"Promoted by promote-set.mjs 2026-07-15: green on :composeApp:assembleDebug, :composeApp:desktopTest, :composeApp:linkDebugFrameworkIosSimulatorArm64 (456s). Evidence: qa-artifacts/canary/2026.07c.json"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "2026.07r",
|
|
88
|
+
"label": "July 2026 recommended standard — Kotlin 2.3.10 / KSP 2.3.10 / Compose 1.11.1 / AGP 8.13.2 / Gradle 8.14.3, compileSdk 36. ktor held at 3.1.0 (3.2.0 not dexable at minSdk 24) and androidx UI libs held at SDK-36-safe versions.",
|
|
89
|
+
"status": "proven-green",
|
|
90
|
+
"versions": {
|
|
91
|
+
"kotlin": "2.3.10",
|
|
92
|
+
"agp": "8.13.2",
|
|
93
|
+
"google-services": "4.5.0",
|
|
94
|
+
"compose-multiplatform": "1.11.1",
|
|
95
|
+
"koin": "4.2.2",
|
|
96
|
+
"ktor": "3.1.0",
|
|
97
|
+
"room": "2.8.4",
|
|
98
|
+
"sqlite": "2.7.0",
|
|
99
|
+
"ksp": "2.3.10",
|
|
100
|
+
"kotlinx-serialization": "1.9.0",
|
|
101
|
+
"kotlinx-datetime": "0.7.0",
|
|
102
|
+
"lifecycle": "2.10.0",
|
|
103
|
+
"navigation": "2.9.2",
|
|
104
|
+
"firebase-gitlive": "2.4.0",
|
|
105
|
+
"coil": "3.2.0",
|
|
106
|
+
"androidx-core": "1.15.0",
|
|
107
|
+
"androidx-activity": "1.10.1"
|
|
108
|
+
},
|
|
109
|
+
"gradleProperties": {
|
|
110
|
+
"ksp.useKSP2": "true"
|
|
111
|
+
},
|
|
112
|
+
"gradleWrapper": {
|
|
113
|
+
"version": "8.14.3",
|
|
114
|
+
"distributionUrl": "https://services.gradle.org/distributions/gradle-8.14.3-bin.zip"
|
|
115
|
+
},
|
|
116
|
+
"androidSdk": {
|
|
117
|
+
"compileSdk": 36,
|
|
118
|
+
"targetSdk": 35
|
|
119
|
+
},
|
|
120
|
+
"notes": [
|
|
121
|
+
"Sourced, mutually-compatible July 2026 set (verified 2026-07-15): Kotlin 2.3.10 is the newest Kotlin with a matching KSP (KSP tops out at 2.3.10; no KSP for Kotlin 2.4.10). KSP2 aligned scheme (ksp == kotlin). Compose Multiplatform 1.11.1 (latest stable) is compatible with Kotlin 2.3.10.",
|
|
122
|
+
"AGP 8.13.2 is the top of the Kotlin-2.3-supported AGP range (per developer.android.com/build/kotlin-support). Gradle 8.14.3 satisfies AGP 8.13's minimum. compileSdk moves to 36 (Android 16, the latest STABLE API) — now a managed field applied to composeApp/build.gradle.kts.",
|
|
123
|
+
"ktor HELD at 3.1.0: even AGP 8.13.2's newer R8 (8.13.19) cannot dex ktor-client-core 3.2.0 — its backtick identifier `use streaming syntax` needs DEX version 040, which D8 only emits at a much higher minSdk than 24. Not an R8-version issue; ktor 3.2.0 is not adoptable at a normal minSdk yet.",
|
|
124
|
+
"HELD ON PURPOSE: androidx-core (1.15.0) and lifecycle (2.10.0) are kept at their SDK-36-safe versions. Their latest (core 1.19.0, lifecycle 2.11.0) require compileSdk 37 — an unreleased-stable API — so 'latest-of-each' is NOT the recommended-stable choice; revisit when a compileSdk-37 set is warranted. targetSdk held at 35 for runtime-behavior stability.",
|
|
125
|
+
"Verified by the canary: Kotlin 2.3.10 + Compose 1.11.1 + firebase-gitlive 2.4.0 compile the template source and dex cleanly at compileSdk 36.",
|
|
126
|
+
"Promoted by promote-set.mjs 2026-07-15: green on :composeApp:assembleDebug, :composeApp:desktopTest, :composeApp:linkDebugFrameworkIosSimulatorArm64 (425s). Evidence: qa-artifacts/canary/2026.07r.json"
|
|
127
|
+
]
|
|
40
128
|
}
|
|
41
129
|
]
|
|
42
130
|
}
|
|
@@ -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
|