create-cmp-cli 0.10.1 → 0.12.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 +11 -9
- package/package.json +1 -1
- package/src/lib/package-name.mjs +72 -0
- package/src/lib/tabs.mjs +26 -0
- package/src/scaffold.mjs +7 -2
- package/template/.claude/settings.json +30 -0
- package/template/.claude/skills/add-feature/SKILL.md +20 -0
- package/template/.claude/skills/add-repository/SKILL.md +6 -0
- package/template/.claude/skills/add-screen/SKILL.md +6 -0
- package/template/CLAUDE.md +129 -8
- package/template/composeApp/build.gradle.kts +69 -0
- package/template/composeApp/proguard-rules.pro +12 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +9 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/PlatformBehaviorSeamTest.kt +277 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/RuntimeStateSeamTest.kt +308 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/AlarmAsserts.kt +152 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ConfigControl.kt +124 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/DozeControl.kt +113 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NetworkControl.kt +137 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NotificationAsserts.kt +163 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/PermissionControl.kt +132 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ProcessControl.kt +217 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/Shell.kt +79 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/SystemState.kt +113 -0
- package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/TimeWarp.kt +114 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +5 -2
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +1 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +1 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppIconButton.kt +1 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +16 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +2 -5
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +58 -0
- package/template/docs/ARCHITECTURE.md +41 -2
- package/template/docs/TESTING.md +165 -0
- package/template/gradle/libs.versions.toml +15 -0
- package/template/manifest.json +1 -0
- package/template/qa/approve.mjs +119 -11
- package/template/qa/evidence/schema.json +20 -2
- package/template/qa/lib/affected-tests.mjs +147 -0
- package/template/qa/lib/approvals.mjs +602 -21
- package/template/qa/lib/device-lease.mjs +249 -0
- package/template/qa/lib/evidence-level.mjs +117 -0
- package/template/qa/lib/feature-brief.mjs +324 -0
- package/template/qa/lib/inputs-hash.mjs +43 -6
- package/template/qa/lib/reachability.mjs +211 -0
- package/template/qa/lib/spec-coverage.mjs +131 -0
- package/template/qa/lib/step-cache.mjs +221 -0
- package/template/qa/receipt-check.mjs +22 -2
- package/template/qa/scaffold-feature.mjs +20 -1
- package/template/qa/verify.mjs +776 -95
- package/template/qa/watch.mjs +622 -0
- package/template/specs/app-base.spec.md +11 -0
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ http://127.0.0.1:9500/inspect/remote
|
|
|
76
76
|
A self-contained page mirrors the running app (~700ms refresh) and **click-to-tap drives the
|
|
77
77
|
real thing** — clicks scale to device pixels and dispatch as taps. This is the "two audiences,
|
|
78
78
|
one app" split at its purest: the human watches and drives real pixels in a browser while the
|
|
79
|
-
agent asserts on the semantics tree (`navigate_and_inspect`, `
|
|
79
|
+
agent asserts on the semantics tree (`navigate_and_inspect`, `inspect_tree`, `db_query`). Use
|
|
80
80
|
it to watch an e2e run, demo a feature, or poke at the app without touching the device. No
|
|
81
81
|
install, no CORS, debug builds only — release builds contain none of this code.
|
|
82
82
|
|
|
@@ -161,11 +161,11 @@ Same engine as the CLI, conversational front door. Each skill is a guided flow,
|
|
|
161
161
|
| `cmp-test` | "Write tests for my app." *Observes* the running app's semantics tree — what's actually on screen, what's tappable, where navigation goes — and derives the regression suite from that. Tests come from rendered reality, not guesses. |
|
|
162
162
|
| `cmp-qa-prep` | "Get my test environment up." Emulator + app install + E2E smoke run, with the gotchas handled. |
|
|
163
163
|
|
|
164
|
-
Plus the **`cmp-inspector` MCP server** (
|
|
164
|
+
Plus the **`cmp-inspector` MCP server** (15 tools — deliberately few; two production apps proved a lean surface gets used and a wide one gets ignored) — the machine-readable window into a running
|
|
165
165
|
Compose UI that `cmp-inspect`, `cmp-test`, and the verified dev loop are built on. One tree
|
|
166
166
|
contract, three sources: render a screen headlessly, connect to the live app, or read a device
|
|
167
167
|
via UIAutomator. It also carries the runtime half of the agent's eyes (crashes, logs, DB state —
|
|
168
|
-
`runtime_crashes`, `runtime_logs`, `
|
|
168
|
+
`runtime_crashes`, `runtime_logs`, `db_query`), the human-approval console
|
|
169
169
|
(`approval_status`, §8 below), the console's talk-back channel (`review_comments`,
|
|
170
170
|
`resolve_comment`, §9 below), and the genesis walk's design-language workbench
|
|
171
171
|
(`snapshot_variant`, §8 below).
|
|
@@ -313,7 +313,7 @@ scaffolded app carries the whole loop):
|
|
|
313
313
|
`./gradlew :composeApp:renderScreens && node qa/preview-gallery.mjs`.
|
|
314
314
|
|
|
315
315
|
**The verified dev loop (the flagship).** For any UI change: snapshot the live tree → make the
|
|
316
|
-
edit → reload → `
|
|
316
|
+
edit → reload → `preview_diff` compares before/after structure, token drift, and a11y, and returns
|
|
317
317
|
a verdict. The agent doesn't say "I centered the title" — it shows *"title bounds moved, tokens
|
|
318
318
|
unchanged, no a11y regressions: proven clean."*
|
|
319
319
|
|
|
@@ -338,11 +338,13 @@ standalone gate. All of it works on any KMP project.
|
|
|
338
338
|
generation and mechanical work to sub-agents with self-contained briefs, then **gates every
|
|
339
339
|
hand-off through the verify lane** before accepting it. Nothing is reported done on prose —
|
|
340
340
|
only on a receipt.
|
|
341
|
-
- **The MCP tools** are how any agent *sees*: `inspect_tree
|
|
342
|
-
`
|
|
343
|
-
|
|
344
|
-
`
|
|
345
|
-
|
|
341
|
+
- **The MCP tools** are how any agent *sees*: `inspect_tree` (one tree contract — subtree by
|
|
342
|
+
`testTag`, wireframe rendering, layout-gap reporting as options), `connect_live`
|
|
343
|
+
(self-healing: device → forward → health → launch → transport reset), `navigate_and_inspect`,
|
|
344
|
+
`render_screen`, and the preview loop (`preview`, `preview_status`, `preview_diff`).
|
|
345
|
+
Structure in, structure out — never pixels in model context. Token drift, a11y, and golden
|
|
346
|
+
regressions are the verify lane's job, not interactive tools. The same eyes
|
|
347
|
+
extend to runtime behavior (`runtime_crashes`, `runtime_logs`, `db_query`), to
|
|
346
348
|
the human side of the loop (`approval_status`, blocking on a console decision the same way
|
|
347
349
|
`preview_status` blocks on a render), and to the console's talk-back channel
|
|
348
350
|
(`review_comments`, `resolve_comment` — the agent observes feedback and closes the loop with a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cmp-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Create production mobile apps (Android + iOS, one Kotlin codebase) with AI — the delivery harness for Compose Multiplatform, the current generation of cross-platform (Google-backed KMP, iOS stable since May 2025). A deterministic, non-interactive generator that scaffolds a green-building app in minutes, then holds AI-driven changes to a machine-enforced verify lane with a committed evidence receipt. Every app carries a device-free UI preview loop (real screens rendered headlessly on save; changed-screen attribution and compile-error surfacing for coding agents, a live gallery for humans) plus agent-first docs (CLAUDE.md + AGENTS.md). Installs the `create-cmp` command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Semantic validation for the `package` option, beyond what the JSON-schema
|
|
2
|
+
// pattern in options.schema.json can express.
|
|
3
|
+
//
|
|
4
|
+
// The pattern already enforces the SHAPE of a reverse-DNS id (lowercase start,
|
|
5
|
+
// at least two segments, no digit-leading segments). What it cannot encode is
|
|
6
|
+
// that every segment also has to be a legal *Java identifier*: the value
|
|
7
|
+
// becomes the Android namespace and the Kotlin package, and AGP rejects a
|
|
8
|
+
// reserved word outright —
|
|
9
|
+
//
|
|
10
|
+
// Namespace 'com.final.proof' is not a valid Java package name as 'final'
|
|
11
|
+
// is a Java keyword
|
|
12
|
+
//
|
|
13
|
+
// Observed for real: `--package com.final.proof` was accepted, stamped a full
|
|
14
|
+
// project, and only failed at the first Gradle configure — late, and as a raw
|
|
15
|
+
// Gradle stack rather than an input error. Refuse it at the door instead, and
|
|
16
|
+
// name the offending segment.
|
|
17
|
+
|
|
18
|
+
// Java SE reserved words (JLS §3.9), plus the three reserved literals and the
|
|
19
|
+
// lone underscore (reserved since Java 9). `var`/`yield`/`record`/`sealed` and
|
|
20
|
+
// friends are contextual, not reserved — they are legal package segments, so
|
|
21
|
+
// they are deliberately absent.
|
|
22
|
+
export const JAVA_KEYWORDS = new Set([
|
|
23
|
+
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char",
|
|
24
|
+
"class", "const", "continue", "default", "do", "double", "else", "enum",
|
|
25
|
+
"extends", "final", "finally", "float", "for", "goto", "if", "implements",
|
|
26
|
+
"import", "instanceof", "int", "interface", "long", "native", "new",
|
|
27
|
+
"package", "private", "protected", "public", "return", "short", "static",
|
|
28
|
+
"strictfp", "super", "switch", "synchronized", "this", "throw", "throws",
|
|
29
|
+
"transient", "try", "void", "volatile", "while",
|
|
30
|
+
// reserved literals
|
|
31
|
+
"true", "false", "null",
|
|
32
|
+
// reserved identifier (Java 9+)
|
|
33
|
+
"_",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Segments of `pkg` that cannot be Java identifiers.
|
|
38
|
+
* @param {string} pkg
|
|
39
|
+
* @returns {string[]} offending segments, in source order (may repeat)
|
|
40
|
+
*/
|
|
41
|
+
export function reservedSegments(pkg) {
|
|
42
|
+
if (typeof pkg !== "string" || pkg.length === 0) return [];
|
|
43
|
+
return pkg.split(".").filter((seg) => JAVA_KEYWORDS.has(seg));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate the package id's segments. Same error shape as schema.mjs's
|
|
48
|
+
* validate(), so callers can merge the two lists and format them together.
|
|
49
|
+
* @param {string} pkg
|
|
50
|
+
* @param {string} [path] error path label
|
|
51
|
+
* @returns {{ valid: boolean, errors: Array<{path: string, message: string}> }}
|
|
52
|
+
*/
|
|
53
|
+
export function validatePackageName(pkg, path = "package") {
|
|
54
|
+
const bad = reservedSegments(pkg);
|
|
55
|
+
if (bad.length === 0) return { valid: true, errors: [] };
|
|
56
|
+
const uniq = [...new Set(bad)];
|
|
57
|
+
const which = uniq.map((s) => `'${s}'`).join(", ");
|
|
58
|
+
const lead = uniq.length > 1
|
|
59
|
+
? `segments ${which} are Java keywords and cannot be package segments`
|
|
60
|
+
: `segment ${which} is a Java keyword and cannot be a package segment`;
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
errors: [
|
|
64
|
+
{
|
|
65
|
+
path,
|
|
66
|
+
message:
|
|
67
|
+
`${lead} — Gradle will refuse the namespace. Rename it ` +
|
|
68
|
+
`(e.g. com.final.proof \u2192 com.finalproof).`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/lib/tabs.mjs
CHANGED
|
@@ -527,6 +527,8 @@ const PLACEHOLDER_REL =
|
|
|
527
527
|
const PREVIEW_REGISTRY_REL =
|
|
528
528
|
"composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt";
|
|
529
529
|
const SMOKE_REL = "qa/e2e/smoke.yaml";
|
|
530
|
+
const PROFILE_FEATURE_REL =
|
|
531
|
+
"composeApp/src/commonMain/kotlin/com/example/app/presentation/profile";
|
|
530
532
|
|
|
531
533
|
/**
|
|
532
534
|
* Rewrite every tab-driven surface present in the copied project dir. Runs
|
|
@@ -576,4 +578,28 @@ export function rewriteTabSurfaces(projectDir, tabs, log = () => {}) {
|
|
|
576
578
|
fs.writeFileSync(smokePath, renderSmokeYaml(infos));
|
|
577
579
|
log(` tabs → ${SMOKE_REL}`);
|
|
578
580
|
}
|
|
581
|
+
|
|
582
|
+
// A shipped default feature the config did NOT ask for must not be left on
|
|
583
|
+
// disk: rewriteNavHost drops its import and its appTabs() entry, and
|
|
584
|
+
// renderPreviewRegistryKt drops its preview, so the files would sit there
|
|
585
|
+
// wired to nothing — dead code the reachability gate (qa/lib/reachability.mjs)
|
|
586
|
+
// correctly FAILs at `--verify` time. Don't ship what you don't wire.
|
|
587
|
+
//
|
|
588
|
+
// `profile` is the only one: it is a self-contained stub screen (one file, no
|
|
589
|
+
// ViewModel, no spec clauses, no DI entry, no tests), so removing it leaves
|
|
590
|
+
// nothing dangling.
|
|
591
|
+
//
|
|
592
|
+
// `home` is deliberately NEVER stripped, even when it is not a configured tab.
|
|
593
|
+
// It is the governed EXEMPLAR (`qa/approvals.json`'s exemplarFeature — the file
|
|
594
|
+
// set `qa/scaffold-feature.mjs` clones and the genesis walk approves), and it
|
|
595
|
+
// owns `DetailScreen`, which AppNavHost registers as a destination
|
|
596
|
+
// unconditionally. So it stays both governed and reachable with no tab of its
|
|
597
|
+
// own — removing it would break the genesis walk and dangle the Detail route.
|
|
598
|
+
if (!infos.some((t) => t.slug === "profile")) {
|
|
599
|
+
const profilePath = path.join(projectDir, PROFILE_FEATURE_REL);
|
|
600
|
+
if (fs.existsSync(profilePath)) {
|
|
601
|
+
fs.rmSync(profilePath, { recursive: true, force: true });
|
|
602
|
+
log(` tabs → removed ${PROFILE_FEATURE_REL} (no profile tab configured)`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
579
605
|
}
|
package/src/scaffold.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import path from "node:path";
|
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
|
|
20
20
|
import { validate, formatErrors } from "./lib/schema.mjs";
|
|
21
|
+
import { validatePackageName } from "./lib/package-name.mjs";
|
|
21
22
|
import { buildTokenMap, replaceTokens, replacePathTokens, isBinaryPath, slugifyAppName } from "./lib/tokens.mjs";
|
|
22
23
|
import { renamePackageDirs } from "./lib/rename.mjs";
|
|
23
24
|
import {
|
|
@@ -51,8 +52,12 @@ export function loadSchema(opts = {}) {
|
|
|
51
52
|
*/
|
|
52
53
|
export function validateConfig(config, opts = {}) {
|
|
53
54
|
const schema = loadSchema(opts);
|
|
54
|
-
const {
|
|
55
|
-
|
|
55
|
+
const { errors } = validate(config, schema);
|
|
56
|
+
// The schema pattern proves the SHAPE of the package id; this proves its
|
|
57
|
+
// segments are legal Java identifiers. Merged into one list so a config with
|
|
58
|
+
// both problems reports both at once.
|
|
59
|
+
errors.push(...validatePackageName(config?.package).errors);
|
|
60
|
+
if (errors.length > 0) {
|
|
56
61
|
const err = new Error(`Invalid config:\n${formatErrors(errors)}`);
|
|
57
62
|
err.validationErrors = errors;
|
|
58
63
|
throw err;
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"This app is governed by its delivery contract (CLAUDE.md): behavior starts in specs/, done is `node qa/verify.mjs` with a committed receipt, approvals gate signed artifacts. The cmp-inspector MCP tools (preview loop, live tier) are the expected eyes — if they are absent from this session, that is a fault to diagnose (plugin disabled, session predates plugin enablement, or stale plugin copy; see cmp-doctor), not a cue to fall back to screenshots or blind adb.\"}}'"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "grep -qE 'screencap|uiautomator dump' && printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Reminder: raw pixels/blind taps lose structure. If cmp-inspector is connected, inspect_tree reads the semantic tree and navigate_and_inspect gives verified taps; if its tools are missing, diagnose first (cmp-doctor, Inspector MCP section).\"}}' || true"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "grep -qE 'connected[A-Za-z]*AndroidTest|maestro test|adb (-s [^ ]+ )?(install|uninstall)' && printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Reminder: device evidence is lane-owned and batched. node qa/verify.mjs sequences the device steps once, last, under a machine-global per-serial lease (qa/lib/device-lease.mjs) — the one device is scarce, slow, and fragile, so device proof is a checkpoint, never an inner loop. Driving it by hand mid-task risks colliding with a running lane (wedged adbd, device offline, false reds, crossed app state). Ad-hoc debugging stays allowed; batch the evidence into the lane.\"}}' || true"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "in=$(cat); printf '%s' \"$in\" | grep -qE 'node qa/verify\\.mjs' && ! printf '%s' \"$in\" | grep -q -- '--fast' && printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Reminder: for inner-loop iteration, node qa/verify.mjs --fast skips the device/release tier (releaseBuild, tokenDrift, e2eSmoke, androidChecks, releaseSmoke) and is much quicker. Run the full lane once, deliberately, before reporting work done — never speculatively, and never to re-confirm a result you already have. A --fast receipt never satisfies the done-gate.\"}}' || true"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
3
33
|
"Stop": [
|
|
4
34
|
{
|
|
5
35
|
"matcher": "",
|
|
@@ -19,6 +19,26 @@ description: >-
|
|
|
19
19
|
> feature to its real shape. You are not done until `node qa/verify.mjs` PASSes and the receipt
|
|
20
20
|
> is committed — see this project's `CLAUDE.md`.
|
|
21
21
|
|
|
22
|
+
## Step 0 — name the lane, out loud, before anything else
|
|
23
|
+
|
|
24
|
+
Every post-genesis change enters through one of two lanes (`CLAUDE.md` §"After genesis"),
|
|
25
|
+
and **the human must be told which one this request is taking, in your first reply** — one
|
|
26
|
+
or two plain sentences before any tool runs: what you understood the change to be, which
|
|
27
|
+
lane, and why. Never route silently; the human can overrule the triage in a word.
|
|
28
|
+
|
|
29
|
+
- **Brief lane** — the request carries decisions a future contributor could plausibly
|
|
30
|
+
"simplify" away, or blast radius into other governed artifacts. Say so, e.g.: *"This
|
|
31
|
+
carries real decisions (day-boundary rules, scheduling semantics) — I'll draft a feature
|
|
32
|
+
brief at `docs/features/<name>.md` with the open decisions for you to close and sign
|
|
33
|
+
BEFORE I stamp anything."* Only after the brief is signed does this skill's stamping
|
|
34
|
+
start — and if the feature has a UI surface, the **design gate comes next, before the
|
|
35
|
+
spec**: draft the screens on stub data, render them, and stop for the human's signature
|
|
36
|
+
on `feature-design:<name>` (brief → design → spec → build). A human signs rendered
|
|
37
|
+
screens, never a description of screens.
|
|
38
|
+
- **Direct lane** — an ordinary feature with no decisions worth recording. Say so, e.g.:
|
|
39
|
+
*"Straightforward slice, no decisions worth a brief — direct lane: spec clauses for your
|
|
40
|
+
confirmation, then I stamp and prove."* Then continue with Step 1 below.
|
|
41
|
+
|
|
22
42
|
## The clone source is configurable
|
|
23
43
|
|
|
24
44
|
The stamper clones from the project's **configured exemplar** — `qa/approvals.json`'s
|
|
@@ -24,6 +24,12 @@ to just the 5 data/domain files. Use it when you want the data layer to exist be
|
|
|
24
24
|
consumes it (e.g. you're modeling several entities up front, or a screen for this entity will
|
|
25
25
|
come later via `add-screen`).
|
|
26
26
|
|
|
27
|
+
**Name the lane first, out loud.** Before any tool runs, tell the human in a sentence which
|
|
28
|
+
lane this request takes (`CLAUDE.md` §"After genesis"): an entity carrying decisions worth
|
|
29
|
+
recording (data-model semantics, boundary rules) goes through a feature brief
|
|
30
|
+
(`docs/features/<name>.md`, signed before code); an ordinary entity is direct lane — say so,
|
|
31
|
+
then continue here. Never route silently.
|
|
32
|
+
|
|
27
33
|
## Why a stamper and not hand-written files
|
|
28
34
|
|
|
29
35
|
Every hand-written file is a drift chance in this project's architecture. `qa/scaffold-feature.mjs
|
|
@@ -25,6 +25,12 @@ the 6 presentation+tests+spec files, **composed on top of an entity that already
|
|
|
25
25
|
`add-repository` or a prior `add-feature` run). If the entity doesn't exist yet, this will refuse
|
|
26
26
|
to stamp anything — see step 2.
|
|
27
27
|
|
|
28
|
+
**Name the lane first, out loud.** Before any tool runs, tell the human in a sentence which
|
|
29
|
+
lane this request takes (`CLAUDE.md` §"After genesis"): a screen carrying decisions worth
|
|
30
|
+
recording or blast into signed artifacts goes through a feature brief
|
|
31
|
+
(`docs/features/<name>.md`, signed before code); an ordinary screen is direct lane — say
|
|
32
|
+
so, then continue here. Never route silently.
|
|
33
|
+
|
|
28
34
|
## Why a stamper and not hand-written files
|
|
29
35
|
|
|
30
36
|
Every hand-written file is a drift chance in this project's architecture. `qa/scaffold-feature.mjs
|
package/template/CLAUDE.md
CHANGED
|
@@ -15,7 +15,10 @@ tests, and gates the whole tree to produce the receipt, so it is slow by design;
|
|
|
15
15
|
after every edit wastes the minutes it takes. Iterate on the fast tier, and run the lane
|
|
16
16
|
once — when you believe the change is done.
|
|
17
17
|
|
|
18
|
-
- **Inner loop — run continuously (seconds):** the preview loop (below) for UI
|
|
18
|
+
- **Inner loop — run continuously (seconds):** the preview loop (below) for UI;
|
|
19
|
+
`node qa/watch.mjs` for verification — a resident watcher that re-runs the fast tier
|
|
20
|
+
(`node qa/verify.mjs --fast`) on every save and re-prints the step table, so the
|
|
21
|
+
did-I-break-anything signal is free the way an IDE's errors-on-save are free; and
|
|
19
22
|
`./gradlew :composeApp:desktopTest` for the unit tests your change touches. This is where
|
|
20
23
|
you catch your own mistakes.
|
|
21
24
|
- **Checkpoint — run once, at done:** `node qa/verify.mjs`. It writes the receipt; commit
|
|
@@ -85,9 +88,25 @@ the tree. The governed `architecture` artifact (below) hashes the document along
|
|
|
85
88
|
- Never delete, weaken, or `@Ignore` a failing test to reach green. Fix the behavior — or,
|
|
86
89
|
if the test itself is wrong, say so in your summary and justify the change.
|
|
87
90
|
|
|
91
|
+
**Platform behavior tests live in `composeApp/src/androidInstrumentedTest`** — when a
|
|
92
|
+
feature touches alarms, notifications, lock-screen intents, or audio routing, its behavior
|
|
93
|
+
test goes there, because no desktop tier can see those OS facts. Assertion helpers:
|
|
94
|
+
`NotificationAsserts`, `AlarmAsserts`, `SystemState`. **Runtime state control** — put the
|
|
95
|
+
device into the state your claim is about, instead of waiting for it: `TimeWarp` (clock,
|
|
96
|
+
timezone), `DozeControl` (forced idle), `PermissionControl`, `ProcessControl`,
|
|
97
|
+
`NetworkControl`, `ConfigControl` (dark mode, font scale, per-app locale). They compose —
|
|
98
|
+
the exemplar proves an `allowWhileIdle` alarm delivers from inside forced deep idle by
|
|
99
|
+
nesting a clock warp in a Doze bracket. Exemplars: `PlatformBehaviorSeamTest`,
|
|
100
|
+
`RuntimeStateSeamTest`. Each organ's header states what it does NOT reproduce; read it
|
|
101
|
+
before claiming more than it proves. The lane's `androidChecks` step runs them when a
|
|
102
|
+
device is attached; see `docs/TESTING.md`.
|
|
103
|
+
|
|
88
104
|
## Evidence
|
|
89
105
|
|
|
90
106
|
`node qa/verify.mjs` writes `qa/evidence/latest.json` (schema: `qa/evidence/schema.json`).
|
|
107
|
+
Each PASS receipt names its **evidence rung** (L0 scaffold / L1 desktop / L2 device /
|
|
108
|
+
L3 release), derived from which steps actually ran and passed — never declared, and a
|
|
109
|
+
SKIPped step never upgrades it (see `docs/TESTING.md` §"The evidence ladder").
|
|
91
110
|
Commit it with your change; git history is the audit ledger. Binary artifacts under
|
|
92
111
|
`qa-artifacts/` are hashed into the receipt, never committed. The studio console's Evidence
|
|
93
112
|
page reconstructs the full audit trail from the git log of `latest.json` — every commit is
|
|
@@ -111,6 +130,12 @@ a provisional palette carries the build until then).
|
|
|
111
130
|
the `cmp-new` interview; the seed's placeholder prose is marked unfilled. Its
|
|
112
131
|
`## Glossary` section is lifted verbatim into `docs/ARCHITECTURE.md` §8 — write it there
|
|
113
132
|
in the exact form you want published.
|
|
133
|
+
0.5. **First feature brief** — `docs/features/<exemplar>.md`, drafted from the interview's
|
|
134
|
+
"first screens" answer the moment intent is signed: the first feature's decisions and
|
|
135
|
+
their why, signed BEFORE its spec is written. Genesis runs the same decide → contract →
|
|
136
|
+
build loop as every later change; the app's first feature gets the same decide step
|
|
137
|
+
every later feature gets. (Express lane: skipped honestly, never fabricated;
|
|
138
|
+
placeholders earn a brief when they become real.)
|
|
114
139
|
1. **Architecture + structure** — `specs/app-base.spec.md` **and**
|
|
115
140
|
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) (`cmp:generated` sections stripped
|
|
116
141
|
before hashing, so a mechanical regeneration never invalidates the approval — only an
|
|
@@ -131,16 +156,88 @@ a provisional palette carries the build until then).
|
|
|
131
156
|
6. **Per-feature spec** — `specs/<feature>.spec.md`, one governed artifact per feature,
|
|
132
157
|
added as features land.
|
|
133
158
|
|
|
159
|
+
### After genesis — every change is the same loop
|
|
160
|
+
|
|
161
|
+
Genesis governs the app's birth. Everything after runs the SAME loop — decide →
|
|
162
|
+
contract → build → prove → sign — over a subset (`docs/CHANGE-FLOW-DESIGN.md` in the
|
|
163
|
+
create-cmp repo is the doc of record). Two lanes, one triage rule — and **the triage is
|
|
164
|
+
always visible**: your FIRST reply to any change request (new feature, edit, bug fix, copy
|
|
165
|
+
tweak, redesign — every entry point) states in one or two plain sentences what you
|
|
166
|
+
understood the change to be, which lane it takes, and why, before any tool runs. The human
|
|
167
|
+
can overrule the lane in a word; a silent route is a routing error even when the lane was
|
|
168
|
+
right.
|
|
169
|
+
|
|
170
|
+
**Brief lane** — when the change carries **decisions a future contributor could plausibly
|
|
171
|
+
"simplify" away** ("the day boundary is configurable, default 04:00 — not midnight") OR
|
|
172
|
+
**blast radius into other governed artifacts**. After naming the lane:
|
|
173
|
+
|
|
174
|
+
| Step | What |
|
|
175
|
+
|---|---|
|
|
176
|
+
| 1 | **Feature brief** — `docs/features/<name>.md`: the decisions with their why, research, rejected options, an **Open decisions** section until the human closes each. Signed BEFORE code. |
|
|
177
|
+
| 2 | **Design** — iff the feature has a UI surface (`"screens": true`, or screen files exist): draft the screens on STUB data, register them in the PreviewRegistry, render, and STOP. The human judges the rendered screens and signs `feature-design:<name>`. Never ask a human to approve a described UI. |
|
|
178
|
+
| 3 | **Contract** — reopen any signed spec the brief amends (`--reopen feature-spec:<surface> --reason "…"`); write the clauses where the behavior lives — about the form that now exists; the human signs |
|
|
179
|
+
| 4 | Build the slice (`add-feature` / preview loop). Declared blast lands "as declared"; re-approve touched visual artifacts on rendered output (wiring the signed screens from stub to real state drifts `feature-design:<name>` — its re-approval is that pass) |
|
|
180
|
+
| 5 | Prove — nothing to do: the lane's gates + receipt ARE the proof |
|
|
181
|
+
| 6 | The human's `--accept` — enabled only at provenDone AND a signed design |
|
|
182
|
+
|
|
183
|
+
**Direct lane** — everything else (bug fix, copy edit, tweak): confirm in chat, reopen →
|
|
184
|
+
amend clause → re-approve if a signed contract is touched, build, lane once at done.
|
|
185
|
+
Legacy features never get retro-briefs; spikes are ungoverned until they become real.
|
|
186
|
+
|
|
187
|
+
**A brief's LOCATION is the governance opt-in**: every `docs/features/*.md` is a governed
|
|
188
|
+
`feature-brief:<name>` artifact (hash-bound at signing; `<name>` pairs with
|
|
189
|
+
`specs/<name>.spec.md`). `docs/proposals/` stays ungoverned. The brief carries at most one
|
|
190
|
+
machine-read block, and it **declares — it never gates**:
|
|
191
|
+
|
|
192
|
+
```json cmp:feature
|
|
193
|
+
{ "touches": ["components", "design-system"], "screens": true }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`touches` is the declared blast radius — the artifact hashes already enforce; declaring
|
|
197
|
+
lets the console show "components re-approval, as planned" instead of an unexplained
|
|
198
|
+
failure, and surface **undeclared blast** when something drifted that no open brief
|
|
199
|
+
accounted for. `screens: true` declares a UI surface: it holds the design gate
|
|
200
|
+
(`feature-design:<name>` — the feature's own `presentation/<name>/*Screen.kt`, signed on
|
|
201
|
+
rendered output) before any screen file exists; once files exist, disk is ground truth
|
|
202
|
+
regardless. Both declare — neither gates.
|
|
203
|
+
|
|
204
|
+
**Doneness is DERIVED, never claimed.** There is no `--deliver` and no checks block —
|
|
205
|
+
deliberately (they existed and were removed as a weaker parallel truth). A feature is
|
|
206
|
+
`provenDone` when, mechanically: its spec has live clauses **and** every one is cited by a
|
|
207
|
+
test **and** the latest receipt is PASS **and** the receipt's `inputs.hash` attests the
|
|
208
|
+
tree as it stands. `--status` and the console print the same one-line `doneReason` either
|
|
209
|
+
way. What remains for humans is judgment: signing the brief (before code) and accepting
|
|
210
|
+
the feature (after proof — "the proven thing is what I wanted"); `--accept` is refused
|
|
211
|
+
until the derivation holds. Acceptance lives on the ledger row, never in the doc (the
|
|
212
|
+
signed bytes must not move when the human accepts).
|
|
213
|
+
|
|
214
|
+
| Command | What |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `node qa/approve.mjs feature-brief:<name>` | the human signs the brief (before code) |
|
|
217
|
+
| `node qa/approve.mjs --accept <name>` | the human's bookend; refused until provenDone |
|
|
218
|
+
|
|
219
|
+
Editing a feature is the same brief **reopened** — `--reopen-feature <name> --reason "…"`
|
|
220
|
+
reopens the brief + its spec + its design + the set it declared, as ONE recorded change;
|
|
221
|
+
re-approval is one walk back. Every reopen **requires a `--reason`** (it walks back a
|
|
222
|
+
signature; the signer reads why from the ledger itself) and is journaled with `via` — the
|
|
223
|
+
append-only journal (`qa/approvals.log.jsonl`, printed by `--log`, shown in the console
|
|
224
|
+
strip's History) is how "what happened while I was away" stays answerable.
|
|
225
|
+
|
|
134
226
|
| Command | What |
|
|
135
227
|
|---|---|
|
|
136
228
|
| `node qa/approve.mjs --status` | Every governed artifact with live state (`unreviewed` / `approved` / `changed-since-approval` / `reopened`), short hash, mode badge |
|
|
137
229
|
| `node qa/approve.mjs <artifact>` | Record approval — hashes the artifact's files now, stamps the time; also clears a `defaults-accepted` mode |
|
|
138
230
|
| `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
|
|
231
|
+
| `node qa/approve.mjs --reopen <artifact> --reason "…"` | Move an *approved* artifact (shaped or defaults-accepted) back to `reopened` for deliberate redesign. `--reason` is REQUIRED and recorded (`reopenedAt`, `via`, `reason` — on the row and in the journal). Refuses unknown ids and anything not currently approved. |
|
|
232
|
+
| `node qa/approve.mjs --reopen-feature <name> --reason "…"` | ONE recorded change: reopens the brief + `feature-spec:<name>` + `feature-design:<name>` + every declared `touches` artifact (each only if currently approved), all under one reason. |
|
|
233
|
+
| `node qa/approve.mjs --log` | The governance journal — every approve/reopen/accept with when, which surface (`via`), and why. |
|
|
140
234
|
|
|
141
235
|
With the create-cmp plugin, the same decisions can be made from the preview console
|
|
142
236
|
(`preview {projectDir}`'s URL) — it calls the same library, so the CLI and the console
|
|
143
|
-
never disagree.
|
|
237
|
+
never disagree. The console's rail is the human's work queue (colour = something waits
|
|
238
|
+
on them), and a drifted artifact shows, in its own section, exactly **what changed
|
|
239
|
+
against the signed bytes and which files are still exactly as signed**, with re-approval
|
|
240
|
+
in place. An agent blocks on a pending decision with
|
|
144
241
|
`approval_status {waitForDecision:true}`.
|
|
145
242
|
|
|
146
243
|
The verify lane's `approvals` gate (a step like any other, in every profile) resolves each
|
|
@@ -227,16 +324,35 @@ and tells you what your edit changed.
|
|
|
227
324
|
3. `preview_diff { screen }` proves the change in one call: `proven-clean` /
|
|
228
325
|
`changed-with-regressions` / `no-change`. No snapshot bookkeeping.
|
|
229
326
|
|
|
230
|
-
**
|
|
231
|
-
|
|
232
|
-
|
|
327
|
+
**If the tools are missing:** capability absence is a fault to diagnose and report — never
|
|
328
|
+
a silent fallback. If ToolSearch finds no `cmp-inspector` tools, STOP and tell the human
|
|
329
|
+
which it is: the plugin is disabled (`enabledPlugins` in `~/.claude/settings.json` or the
|
|
330
|
+
project settings); the session predates the plugin's enablement (MCP servers attach at
|
|
331
|
+
session start — restart the session; no in-session retry will surface them); or the plugin
|
|
332
|
+
copy is stale/broken (run cmp-doctor's inspector-MCP check group). Report before degrading.
|
|
333
|
+
|
|
334
|
+
**Degraded path** — for environments where the plugin is genuinely unavailable (CI, other
|
|
335
|
+
agents), and only after the fault is reported: `./gradlew :composeApp:renderScreens` renders
|
|
336
|
+
every screen to `composeApp/build/previews/<id>/{screen.png, tree.json}` (`-Pscreen=<id>`
|
|
337
|
+
for one); `node qa/preview-gallery.mjs` builds a self-contained gallery page from the
|
|
338
|
+
output. What this loses: on-save re-render, changed-screen attribution, compile errors
|
|
339
|
+
in-band, and the `preview_diff` change proof — structured feedback replaced by pixels.
|
|
233
340
|
|
|
234
341
|
**Live tier — the human's live device view (standing step).** Whenever `connect_live`
|
|
235
342
|
succeeds, OFFER the `remoteUrl` it returns (`http://127.0.0.1:9500/inspect/remote`) to the
|
|
236
343
|
human — every time, not as a maybe. It is a self-contained browser page that mirrors the
|
|
237
344
|
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`
|
|
239
|
-
`
|
|
345
|
+
the actual app while you assert on the tree (`navigate_and_inspect` — its before/after delta
|
|
346
|
+
is the change proof live — and `inspect_tree`). It is also the right way for a human to
|
|
347
|
+
*watch* an e2e run.
|
|
348
|
+
|
|
349
|
+
Asserting persisted state: `db_query` reads bounded rows from the running app's database;
|
|
350
|
+
use it when a flow's proof is a row existing (or not) after an action, instead of shelling
|
|
351
|
+
into sqlite or trusting the UI.
|
|
352
|
+
|
|
353
|
+
When the app crashes or misbehaves on device: `runtime_crashes` returns persisted crashes
|
|
354
|
+
with cause attribution and `runtime_logs` bounded structured logcat for the app's pid; use
|
|
355
|
+
these before hand-grepping `adb logcat`.
|
|
240
356
|
|
|
241
357
|
Screens come from `inspector/PreviewRegistry.kt` (desktopMain). The `add-feature` and
|
|
242
358
|
`add-screen` stampers auto-register stamped screens at the `// cmp:anchor preview-registry`
|
|
@@ -264,7 +380,12 @@ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional C
|
|
|
264
380
|
| Command | What |
|
|
265
381
|
|---|---|
|
|
266
382
|
| `node qa/verify.mjs` | The verify lane (profile `local`) — the done checkpoint, run once |
|
|
383
|
+
| `node qa/verify.mjs --fast` | **Inner loop — NOT the done-gate**: skips the device/release tier (`releaseBuild`, `tokenDrift`, `e2eSmoke`, `androidChecks`, `releaseSmoke`), reuses unchanged pure-Node step results (`CACHED`, content-hashed inputs), and scopes unit tests to the working-tree change (broad-impact changes — build files, DI, theme, shared components, `qa/` — run the full suite). Its receipt records `"mode": "fast"`, earns no evidence rung, and the Stop hook refuses it — run the full lane once at done |
|
|
384
|
+
| `node qa/watch.mjs` | **Resident inner loop — never a gate**: watches `composeApp/src`, `specs/`, `qa/` and re-runs `node qa/verify.mjs --fast` on save (debounced — a save storm is one run; defers while a verify lane or a preview render holds the project). `--once` for a single pass, `--json` for line-per-run output. The done-gate stays one deliberate full `node qa/verify.mjs` run |
|
|
267
385
|
| `./gradlew :composeApp:desktopTest` | Unit tests only (fast inner loop) |
|
|
268
386
|
| `node qa/setup-hooks.mjs` | Enable the pre-push receipt gate (one-time, after `git init`) |
|
|
269
387
|
| `./gradlew :composeApp:assembleDebug` | Android debug build |
|
|
388
|
+
| `./gradlew :composeApp:assembleRelease` | Android release build — R8 + `lintVital`, the variant the lane's `releaseBuild` step proves. Produces an **unsigned** APK; signing needs a keystore, which is yours to create and keep out of the repo. |
|
|
270
389
|
| `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
|
|
390
|
+
| `./gradlew :composeApp:connectedDebugAndroidTest` | Instrumented behavior tests on the attached device (the lane's `androidChecks` step) |
|
|
391
|
+
| `node qa/verify.mjs --profile release` | Ship-time lane: everything `ci` proves plus the release-APK Maestro smoke (`releaseSmoke`) |
|
|
@@ -161,6 +161,24 @@ kotlin {
|
|
|
161
161
|
implementation(compose.desktop.currentOs)
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// Android INSTRUMENTATION tier (composeApp/src/androidInstrumentedTest) — the one
|
|
166
|
+
// evidence tier that crosses the process boundary. Everything else in this build is
|
|
167
|
+
// JVM-side: desktopTest is a JVM, golden trees are structure, the conformance suite
|
|
168
|
+
// is static analysis, and the Maestro smoke taps UI without asserting anything about
|
|
169
|
+
// notifications or alarms. Alarms, notification channels, full-screen intents,
|
|
170
|
+
// PendingIntent identity, and audio routing are OS facts that only exist on a device
|
|
171
|
+
// — a fully green desktop lane is compatible with an alerting feature that never
|
|
172
|
+
// alerts. This source set runs via `:composeApp:connectedDebugAndroidTest` (the
|
|
173
|
+
// lane's `androidChecks` step; SKIPs honestly when no device is attached).
|
|
174
|
+
//
|
|
175
|
+
// Its dependencies are declared through AGP's androidTestImplementation
|
|
176
|
+
// configuration (the dependencies block at the bottom of this file), not here: the
|
|
177
|
+
// KMP source-set DSL compiles these sources but does not put the androidx.test
|
|
178
|
+
// artifacts on their classpath.
|
|
179
|
+
//
|
|
180
|
+
// No kotlin-test here: instrumented tests run under JUnit4 (AndroidJUnit4), so they
|
|
181
|
+
// assert with org.junit.Assert — a second assertion vocabulary buys nothing.
|
|
164
182
|
}
|
|
165
183
|
}
|
|
166
184
|
|
|
@@ -174,6 +192,9 @@ android {
|
|
|
174
192
|
targetSdk = 35
|
|
175
193
|
versionCode = 1
|
|
176
194
|
versionName = "1.0.0"
|
|
195
|
+
// Instrumentation entry point for the on-device behavior tier
|
|
196
|
+
// (composeApp/src/androidInstrumentedTest — see the source-set note above).
|
|
197
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
packaging {
|
|
@@ -182,6 +203,20 @@ android {
|
|
|
182
203
|
}
|
|
183
204
|
}
|
|
184
205
|
|
|
206
|
+
// AGP resolves BUILD-TYPE source sets at src/<buildType>/, while the Kotlin
|
|
207
|
+
// Multiplatform plugin only remaps the `main` one to src/androidMain/. Without this
|
|
208
|
+
// wiring, src/androidDebug/'s manifest and resources are silently never merged —
|
|
209
|
+
// dead files that look live: the debug network-security config never applied, and a
|
|
210
|
+
// permission declared there never reached the APK. Point the debug build type at them
|
|
211
|
+
// explicitly. Caught when an instrumented test asserted canScheduleExactAlarms() and
|
|
212
|
+
// found the grant it had declared was absent on the device.
|
|
213
|
+
sourceSets {
|
|
214
|
+
getByName("debug") {
|
|
215
|
+
manifest.srcFile("src/androidDebug/AndroidManifest.xml")
|
|
216
|
+
res.srcDirs("src/androidDebug/res")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
185
220
|
buildFeatures {
|
|
186
221
|
buildConfig = true
|
|
187
222
|
}
|
|
@@ -202,7 +237,19 @@ android {
|
|
|
202
237
|
getByName("release") {
|
|
203
238
|
isMinifyEnabled = true
|
|
204
239
|
isShrinkResources = true
|
|
240
|
+
// Every field debug declares, release must declare too. BuildConfig is generated
|
|
241
|
+
// PER BUILD TYPE, so a field only debug carries simply does not exist in release —
|
|
242
|
+
// and `if (!USE_FIREBASE_EMULATORS) return` is a RUNTIME guard that does nothing
|
|
243
|
+
// for a compile-time symbol. Declaring the flag alone made release the one build
|
|
244
|
+
// nobody could produce: the code reading the host and ports failed to resolve them.
|
|
245
|
+
// The values below are never used (the flag is false); they exist so the shape of
|
|
246
|
+
// BuildConfig is the same in both build types.
|
|
205
247
|
buildConfigField("boolean", "USE_FIREBASE_EMULATORS", "false")
|
|
248
|
+
buildConfigField("String", "FIREBASE_EMULATOR_HOST", "\"\"")
|
|
249
|
+
buildConfigField("int", "FIREBASE_AUTH_PORT", "0")
|
|
250
|
+
buildConfigField("int", "FIREBASE_FIRESTORE_PORT", "0")
|
|
251
|
+
buildConfigField("int", "FIREBASE_FUNCTIONS_PORT", "0")
|
|
252
|
+
buildConfigField("int", "FIREBASE_STORAGE_PORT", "0")
|
|
206
253
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
|
207
254
|
proguardFiles(
|
|
208
255
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
@@ -226,6 +273,19 @@ android {
|
|
|
226
273
|
// <<< cmp:feature !firebase
|
|
227
274
|
}
|
|
228
275
|
|
|
276
|
+
lint {
|
|
277
|
+
// ONE disabled check, not the gate. `lintVitalRelease` runs on every release build and
|
|
278
|
+
// it is worth keeping; what is not worth keeping is a detector that CRASHES on it.
|
|
279
|
+
// NullSafeMutableLiveData's detector throws IncompatibleClassChangeError against this
|
|
280
|
+
// Kotlin version (AGP's bug, not ours) and takes the whole release build with it.
|
|
281
|
+
//
|
|
282
|
+
// Disabling it costs nothing beyond the crash: an app on this template has no LiveData —
|
|
283
|
+
// state is Compose + Flow throughout — so the check has nothing to inspect. The blunt
|
|
284
|
+
// alternatives, `checkReleaseBuilds = false` or `abortOnError = false`, would switch off
|
|
285
|
+
// every real lint finding to dodge one broken one.
|
|
286
|
+
disable += "NullSafeMutableLiveData"
|
|
287
|
+
}
|
|
288
|
+
|
|
229
289
|
compileOptions {
|
|
230
290
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
231
291
|
targetCompatibility = JavaVersion.VERSION_17
|
|
@@ -244,6 +304,15 @@ dependencies {
|
|
|
244
304
|
add("kspDesktop", libs.room.compiler)
|
|
245
305
|
// <<< cmp:feature room
|
|
246
306
|
add("coreLibraryDesugaring", libs.android.desugar.jdk)
|
|
307
|
+
// The instrumentation tier's runner, JUnit4 harness, and device helpers. Declared
|
|
308
|
+
// through AGP's own configuration rather than the KMP androidInstrumentedTest
|
|
309
|
+
// source-set block — the latter compiles the sources but does not put these
|
|
310
|
+
// artifacts on their classpath.
|
|
311
|
+
add("androidTestImplementation", libs.androidx.test.runner)
|
|
312
|
+
add("androidTestImplementation", libs.androidx.test.core)
|
|
313
|
+
add("androidTestImplementation", libs.androidx.test.ext.junit)
|
|
314
|
+
add("androidTestImplementation", libs.androidx.uiautomator)
|
|
315
|
+
add("androidTestImplementation", libs.junit4)
|
|
247
316
|
}
|
|
248
317
|
|
|
249
318
|
// Pin the generated resources accessor package so `__PACKAGE__.generated.resources.Res`
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
-keep class dev.gitlive.firebase.** { *; }
|
|
21
21
|
-dontwarn com.google.firebase.**
|
|
22
22
|
|
|
23
|
+
# GitLive's RemoteConfig module is compiled against kotlinx-datetime's OWN Instant, which
|
|
24
|
+
# this version set no longer has: on Kotlin 2.2 Instant moved into the stdlib as
|
|
25
|
+
# kotlin.time.Instant. R8 hits an unresolvable reference and FAILS the release build.
|
|
26
|
+
#
|
|
27
|
+
# Suppressed rather than resolved, deliberately. The reference lives in
|
|
28
|
+
# FirebaseRemoteConfigInfo, and RemoteConfig is not a service this template wires up, so no
|
|
29
|
+
# code path reaches it. Adding kotlinx-datetime back purely to satisfy a class nobody calls
|
|
30
|
+
# would put two Instant types in the graph — the more expensive mistake. When GitLive ships
|
|
31
|
+
# a build against kotlin.time, delete these two lines; the release build will say if it is time.
|
|
32
|
+
-dontwarn kotlinx.datetime.Instant$Companion
|
|
33
|
+
-dontwarn kotlinx.datetime.Instant
|
|
34
|
+
|
|
23
35
|
# Coroutines
|
|
24
36
|
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
|
25
37
|
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
|