create-cmp-cli 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +51 -353
  2. package/bin/create-cmp.mjs +19 -3
  3. package/llms.txt +3 -3
  4. package/options.schema.json +4 -0
  5. package/package.json +2 -2
  6. package/packages/harness/package.json +11 -3
  7. package/packages/harness/src/lib/harness-lock.mjs +2 -2
  8. package/packages/harness/src/lib/inputs-hash.mjs +1 -1
  9. package/packages/harness/src/lib/receipt-validate.mjs +1 -1
  10. package/packages/harness/src/receipt-check.mjs +1 -1
  11. package/packages/harness/src/verify.mjs +15 -1
  12. package/packages/receipts/package.json +11 -3
  13. package/packages/receipts/src/index.mjs +1 -1
  14. package/packages/receipts/src/inputs-hash.mjs +1 -1
  15. package/packages/receipts/src/receipt-validate.mjs +1 -1
  16. package/src/commands/attach.mjs +250 -0
  17. package/src/commands/create.mjs +13 -2
  18. package/src/commands/harden.mjs +263 -0
  19. package/src/commands/upgrade.mjs +20 -2
  20. package/src/lib/adr-seed.mjs +27 -0
  21. package/src/lib/harness-upgrade.mjs +40 -5
  22. package/src/lib/hooks.mjs +140 -0
  23. package/src/lib/minimal.mjs +130 -0
  24. package/src/lib/toggle.mjs +4 -1
  25. package/src/lib/verify.mjs +6 -1
  26. package/src/scaffold.mjs +18 -1
  27. package/template/.github/workflows/verify.yml +15 -0
  28. package/template/AGENTS.md +57 -8
  29. package/template/CLAUDE.md +49 -0
  30. package/template/CONTRIBUTING.md +13 -0
  31. package/template/README.md +33 -0
  32. package/template/docs/ARCHITECTURE.md +18 -1
  33. package/template/docs/TESTING.md +10 -0
  34. package/template/manifest.json +13 -0
  35. package/template/qa/lib/harness-lock.mjs +2 -2
  36. package/template/qa/lib/inputs-hash.mjs +1 -1
  37. package/template/qa/lib/receipt-validate.mjs +1 -1
  38. package/template/qa/receipt-check.mjs +1 -1
  39. package/template/qa/verify.mjs +15 -1
@@ -0,0 +1,130 @@
1
+ // minimal.mjs — the `--minimal` mode subtraction (LADDER §R3): stamp the app
2
+ // without its verification harness, keeping the eyes.
3
+ //
4
+ // The mode is a FILTER over the one template, never a fork (design invariant:
5
+ // no second artifact to keep green). Three mechanisms, each already owned by
6
+ // the engine, do the whole job:
7
+ //
8
+ // 1. content variance — `cmp:feature harness` / `!harness` marker blocks
9
+ // (CLAUDE.md, AGENTS.md, README, CI workflow, docs),
10
+ // stripped by the standard toggle machinery;
11
+ // 2. path subtraction — manifest `features.harness.paths` for the
12
+ // app-owned governance surfaces (specs/, approvals,
13
+ // skills, evidence, hooks);
14
+ // 3. lane subtraction — THIS module, for the machine-owned region: delete
15
+ // every machine-owned .mjs EXCEPT the preview
16
+ // entry points and their transitive imports.
17
+ //
18
+ // The keep-set is DERIVED by walking import statements from the entry points,
19
+ // never transcribed as a list — a hand-maintained enumeration of qa/lib files
20
+ // is exactly the kind of claim that rots (design invariant 5). What survives
21
+ // in a minimal scaffold is precisely what its own kept scripts can reach.
22
+ //
23
+ // `create-cmp harden` is the inverse: a three-way stamp-merge (base = the
24
+ // minimal stamp, new = the full stamp) that installs the subtraction back —
25
+ // additive, idempotent, never clobbering. See src/commands/harden.mjs.
26
+
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+
30
+ import { listHarnessFiles } from "../../packages/harness/src/lib/harness-region.mjs";
31
+ import { minimalHookSettings } from "./hooks.mjs";
32
+
33
+ /**
34
+ * Machine-owned entry points a minimal scaffold keeps: the preview gallery is
35
+ * the eyes' no-plugin surface (LADDER §R3 keeps previews; the manifest's own
36
+ * inspector notes already treat it as inspector-owned, not lane-owned).
37
+ * Entries missing from the tree (e.g. --no-inspector) are skipped.
38
+ */
39
+ export const MINIMAL_LANE_ENTRY_POINTS = ["qa/preview-gallery.mjs"];
40
+
41
+ /**
42
+ * SessionStart context for a minimal scaffold — what is true HERE, and the
43
+ * one command that adds the rest. No apostrophes: the hook command is
44
+ * single-quoted for the shell (hooks.mjs enforces this).
45
+ */
46
+ export const MINIMAL_SESSION_CONTEXT =
47
+ "This is a create-cmp MINIMAL scaffold: full app architecture with tests, " +
48
+ "no verification harness. AGENTS.md maps symptoms to commands. Fast signal: " +
49
+ "./gradlew :composeApp:desktopTest. Headless screen previews: ./gradlew " +
50
+ ":composeApp:renderScreens then node qa/preview-gallery.mjs. One idempotent " +
51
+ "command installs the full harness (verify lane, evidence receipts, " +
52
+ "machine-checked done): npx create-cmp-cli harden.";
53
+
54
+ // Matches the project's two import forms in lane code: static
55
+ // `from "./lib/x.mjs"` and dynamic `import(new URL("./lib/x.mjs", ...))`.
56
+ // Only ./-relative .mjs specifiers matter — node: and package imports are not
57
+ // files we ship.
58
+ const IMPORT_SPECIFIER_RE = /(?:from\s+|new URL\(\s*)["'](\.\.?\/[^"']+\.mjs)["']/g;
59
+
60
+ /**
61
+ * The machine-owned files a minimal scaffold keeps: the entry points plus
62
+ * their transitive ./-relative imports, resolved against the tree as stamped.
63
+ * @param {string} projectDir
64
+ * @returns {Set<string>} project-relative posix paths
65
+ */
66
+ export function laneKeepSet(projectDir) {
67
+ const keep = new Set();
68
+ const queue = MINIMAL_LANE_ENTRY_POINTS.filter((rel) =>
69
+ fs.existsSync(path.join(projectDir, rel))
70
+ );
71
+ while (queue.length > 0) {
72
+ const rel = queue.pop();
73
+ if (keep.has(rel)) continue;
74
+ keep.add(rel);
75
+ let source;
76
+ try {
77
+ source = fs.readFileSync(path.join(projectDir, rel), "utf8");
78
+ } catch {
79
+ continue;
80
+ }
81
+ for (const m of source.matchAll(IMPORT_SPECIFIER_RE)) {
82
+ const resolved = path.posix.join(path.posix.dirname(rel), m[1]);
83
+ if (!keep.has(resolved) && fs.existsSync(path.join(projectDir, resolved))) {
84
+ queue.push(resolved);
85
+ }
86
+ }
87
+ }
88
+ return keep;
89
+ }
90
+
91
+ /**
92
+ * Delete every machine-owned lane file outside the keep-set.
93
+ * @param {string} projectDir
94
+ * @param {(msg:string)=>void} [log]
95
+ * @returns {string[]} deleted relative paths
96
+ */
97
+ export function subtractLane(projectDir, log = () => {}) {
98
+ const keep = laneKeepSet(projectDir);
99
+ const deleted = [];
100
+ for (const rel of listHarnessFiles(projectDir)) {
101
+ if (keep.has(rel)) continue;
102
+ fs.rmSync(path.join(projectDir, rel));
103
+ deleted.push(rel);
104
+ }
105
+ if (deleted.length > 0) {
106
+ log(` removed ${deleted.length} lane file(s) (minimal mode keeps ${[...keep].sort().join(", ") || "none"})`);
107
+ }
108
+ return deleted;
109
+ }
110
+
111
+ /**
112
+ * Apply minimal mode to a stamped tree: subtract the lane, then rewrite
113
+ * .claude/settings.json to the derived minimal hook set (enforcement and
114
+ * lane-advisory hooks gone, SessionStart telling the truth about this
115
+ * scaffold). Marker stripping and manifest path deletion have already
116
+ * happened via the standard feature machinery by the time this runs.
117
+ * @param {string} projectDir
118
+ * @param {(msg:string)=>void} [log]
119
+ */
120
+ export function applyMinimalMode(projectDir, log = () => {}) {
121
+ subtractLane(projectDir, log);
122
+
123
+ const settingsPath = path.join(projectDir, ".claude", "settings.json");
124
+ if (fs.existsSync(settingsPath)) {
125
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
126
+ const minimal = minimalHookSettings(settings, { sessionContext: MINIMAL_SESSION_CONTEXT });
127
+ fs.writeFileSync(settingsPath, JSON.stringify(minimal, null, 2) + "\n");
128
+ log(" rewrote .claude/settings.json to the advisory-only hook set");
129
+ }
130
+ }
@@ -86,12 +86,15 @@ export function stripFeatureBlocks(content, disabledFeatures) {
86
86
 
87
87
  /**
88
88
  * Map an engine config object to the set of DISABLED feature names that the
89
- * manifest understands: ios, firebase, room, e2e, inspector, dev-client.
89
+ * manifest understands: harness, ios, firebase, room, e2e, inspector,
90
+ * dev-client. `harness` is the mode split (LADDER §R3): absent means full —
91
+ * only an explicit `harness: false` (`--minimal`) subtracts it.
90
92
  * @param {object} config
91
93
  * @returns {Set<string>}
92
94
  */
93
95
  export function disabledFeaturesFromConfig(config) {
94
96
  const disabled = new Set();
97
+ if (config.harness === false) disabled.add("harness");
95
98
  if (!config.platforms?.ios) disabled.add("ios");
96
99
  if (!config.firebase?.enabled) disabled.add("firebase");
97
100
  if (!config.room) disabled.add("room");
@@ -41,8 +41,13 @@ export async function runVerify({ projectDir, manifest, config, dryRun = false }
41
41
  const verify = (manifest && manifest.verify) || {};
42
42
  const results = [];
43
43
 
44
+ // Minimal mode has no in-app lane to run — its gate is the Gradle tier the
45
+ // scaffold DOES ship (unit + conformance + golden tests, debug build).
46
+ const minimal = config?.harness === false;
47
+ const androidCommand = minimal && verify.androidMinimal ? verify.androidMinimal : verify.android;
48
+
44
49
  const plan = [];
45
- if (verify.android) plan.push({ platform: "android", command: verify.android, eligible: true });
50
+ if (androidCommand) plan.push({ platform: "android", command: androidCommand, eligible: true });
46
51
  if (verify.ios) {
47
52
  const eligible = isMacOS() && !!config?.platforms?.ios;
48
53
  plan.push({ platform: "ios", command: verify.ios, eligible });
package/src/scaffold.mjs CHANGED
@@ -324,6 +324,7 @@ function writeSpecOfRecord(projectDir, config) {
324
324
  bundleId: config.iosBundleId,
325
325
  themePrefix: config.themePrefix,
326
326
  region: config.region,
327
+ harness: config.harness !== false,
327
328
  platforms: config.platforms,
328
329
  firebase: config.firebase,
329
330
  room: config.room,
@@ -464,9 +465,25 @@ export async function scaffold(config, opts = {}) {
464
465
  if (seeded.length === 0) process.stdout.write(" no configuration deviated from the interview default — nothing to seed\n");
465
466
 
466
467
  // (e.2) regenerate the architecture doc's derived sections for the tree as
467
- // stamped — see regenerateArchDoc above.
468
+ // stamped — see regenerateArchDoc above. MUST precede the minimal-mode lane
469
+ // subtraction below: the walker it imports (qa/lib/arch-doc.mjs) is lane
470
+ // code a minimal scaffold does not keep, and the doc's derived sections
471
+ // describe composeApp/ (which minimal mode never touches), so regenerating
472
+ // first is both necessary and correct.
468
473
  await regenerateArchDoc(projectDir);
469
474
 
475
+ // (e.3) minimal mode — subtract the machine-owned lane (keeping the preview
476
+ // entry points + their import closure) and rewrite the hook set to
477
+ // advisory-only. Marker blocks and manifest paths were already handled by
478
+ // the standard feature machinery above; this is the part only the engine
479
+ // can derive. Runs BEFORE writeLaneLock so the lock hashes exactly the
480
+ // region this app ships.
481
+ if (config.harness === false) {
482
+ step("Applying minimal mode (no verification harness)…");
483
+ const { applyMinimalMode } = await import("./lib/minimal.mjs");
484
+ applyMinimalMode(projectDir, (m) => process.stdout.write(`${m}\n`));
485
+ }
486
+
470
487
  // Write local.properties (sdk.dir) so the Gradle build can find the Android
471
488
  // SDK even when ANDROID_HOME/ANDROID_SDK_ROOT aren't exported (manifest
472
489
  // stampPipeline step 7). Skip silently if no SDK is found and env vars are
@@ -1,9 +1,17 @@
1
1
  # CI for your Compose Multiplatform app — stamped in by create-cmp.
2
2
  #
3
+ # >>> cmp:feature harness
3
4
  # What runs on every push/PR: a toolchain report (advisory) and the VERIFY LANE
4
5
  # (qa/verify.mjs) — build + unit tests + every other gate this project carries,
5
6
  # producing the evidence receipt. Green here = your frozen version set still
6
7
  # builds AND the harness's checks pass.
8
+ # <<< cmp:feature harness
9
+ # >>> cmp:feature !harness
10
+ # What runs on every push/PR: a toolchain report (advisory), the JVM test tier
11
+ # (unit + conformance + golden trees), and the Android debug build. This is a
12
+ # minimal scaffold — `npx create-cmp-cli harden` upgrades this workflow to the
13
+ # full verify lane with evidence receipts.
14
+ # <<< cmp:feature !harness
7
15
  #
8
16
  # iOS: a ready-to-enable macOS job is included (commented out) at the bottom.
9
17
  # macOS runners cost ~10x Linux minutes, so it's opt-in.
@@ -36,6 +44,12 @@ jobs:
36
44
  continue-on-error: true
37
45
  run: npx --yes create-cmp-cli@latest doctor --yes --no-install --no-ios
38
46
 
47
+ # >>> cmp:feature !harness
48
+ - name: Tests (JVM tier) + debug build
49
+ run: ./gradlew :composeApp:desktopTest :composeApp:assembleDebug
50
+ # <<< cmp:feature !harness
51
+
52
+ # >>> cmp:feature harness
39
53
  # Receipt attests HEAD: the committed evidence receipt (qa/evidence/latest.json)
40
54
  # must validly attest the checked-out tree — verdict PASS and the inputs-hash
41
55
  # still matching the verified surface — BEFORE we spend a runner re-running the
@@ -62,6 +76,7 @@ jobs:
62
76
  with:
63
77
  name: verify-evidence
64
78
  path: qa/evidence/latest.json
79
+ # <<< cmp:feature harness
65
80
 
66
81
  # ── iOS (opt-in) ──────────────────────────────────────────────────────────
67
82
  # Uncomment to build the iOS app on every push to main. Uses the exact
@@ -1,13 +1,62 @@
1
1
  # Agent instructions
2
2
 
3
- This repository is agent-first. The full working contract — the definition of done
4
- (`node qa/verify.mjs` must PASS), the architecture gates, the testing pyramid, and the
5
- device-free **UI feedback loop** (render every real screen headlessly and see exactly
6
- what your edit changed) — lives in [CLAUDE.md](./CLAUDE.md).
3
+ <!-- >>> cmp:feature harness -->
4
+ This repository is agent-first, with a verification harness. The working contract the
5
+ definition of done (`node qa/verify.mjs` must PASS, receipt committed), the architecture
6
+ gates, and the device-free **UI feedback loop** — lives in [CLAUDE.md](./CLAUDE.md).
7
+ <!-- <<< cmp:feature harness -->
8
+ <!-- >>> cmp:feature !harness -->
9
+ This repository is agent-first. The working guide — the architecture, the commands that
10
+ build and test it, and the device-free **UI feedback loop** — lives in
11
+ [CLAUDE.md](./CLAUDE.md).
12
+ <!-- <<< cmp:feature !harness -->
7
13
 
8
14
  Read CLAUDE.md before making changes. It applies to every coding agent, not only Claude.
9
15
 
10
- One rule worth knowing before you touch anything: the `.mjs` files directly under `qa/`
11
- and `qa/lib/` are **machine-owned harness code**, byte-identical in every create-cmp app
12
- and hash-locked by `qa/harness.lock.json`. Editing them fails the lane's first step. Fix
13
- the engine upstream instead see "The lane is not yours to edit" in CLAUDE.md.
16
+ ## Stuck? Symptom command
17
+
18
+ Every command below runs from the repo root with **nothing to install**: Gradle is
19
+ wrapped, the scripts ship inside this project, and `npx` fetches on demand. The
20
+ create-cmp Claude Code plugin layers better ergonomics over the same capabilities
21
+ (skills, the `cmp-inspector` MCP's structured tools) — an accelerator, never a
22
+ prerequisite.
23
+
24
+ | Symptom | Run |
25
+ |---|---|
26
+ <!-- >>> cmp:feature harness -->
27
+ | "Did my edit break anything?" | `node qa/verify.mjs --fast` — the inner loop: seconds of JVM-tier signal; never the done-gate |
28
+ | Want that answer on every save | `node qa/watch.mjs` — resident watcher; re-runs the fast tier on save, debounced |
29
+ <!-- <<< cmp:feature harness -->
30
+ <!-- >>> cmp:feature !harness -->
31
+ | "Did my edit break anything?" | `./gradlew :composeApp:desktopTest` — unit + conformance + golden-tree tests in seconds, no device |
32
+ <!-- <<< cmp:feature !harness -->
33
+ <!-- >>> cmp:feature inspector -->
34
+ | Can't see the UI (no device attached) | `./gradlew :composeApp:renderScreens && node qa/preview-gallery.mjs` — every real screen headless: `tree.json` for you to assert on, one gallery page for the human |
35
+ | Need the RUNNING app's real state | `adb forward tcp:9500 tcp:9500`, then `http://127.0.0.1:9500/inspect/remote` — debug builds serve the live semantics tree on loopback |
36
+ <!-- <<< cmp:feature inspector -->
37
+ <!-- >>> cmp:feature harness -->
38
+ | Adding a feature / screen / repository | `node qa/scaffold-feature.mjs <Name>` — clones the tested exemplar through every layer; never freehand the pattern (skills: `add-feature`, `add-screen`, `add-repository`) |
39
+ | A gate failed and looks arbitrary | `node qa/refusal-demo.mjs` — stages canonical violations so each gate names the clause it protects |
40
+ <!-- <<< cmp:feature harness -->
41
+ | Build broken, toolchain suspect | `npx create-cmp-cli doctor --fix` — diagnoses machine AND project (kotlin↔ksp lockstep, catalog drift); asks before any repair |
42
+ | Dependency versions stale or mismatched | `npx create-cmp-cli upgrade --dry-run` — diff against the next proven-green set before touching anything |
43
+ <!-- >>> cmp:feature harness -->
44
+ | Ready to claim done | `node qa/verify.mjs` — the full lane, once, deliberately; commit the receipt it writes |
45
+ <!-- <<< cmp:feature harness -->
46
+
47
+ Famous build failures (kotlin↔KSP mismatch, the KSP2/iOS catch-22, `SDK location not
48
+ found`, `No space left on device`): `doctor` diagnoses all of them offline; the worked
49
+ write-ups live upstream at
50
+ <https://github.com/kvdm-co-pilot/create-cmp/tree/main/docs/errors>.
51
+
52
+ <!-- >>> cmp:feature harness -->
53
+ One rule before you edit anything: the `.mjs` files directly under `qa/` and `qa/lib/`
54
+ are **machine-owned** harness code — byte-identical in every create-cmp app and
55
+ hash-locked by `qa/harness.lock.json`. Editing them fails the lane's first step. If the
56
+ lane is wrong, the fix is upstream — see "The lane is not yours to edit" in CLAUDE.md.
57
+ <!-- <<< cmp:feature harness -->
58
+ <!-- >>> cmp:feature !harness -->
59
+ This is a **minimal scaffold** — no verify lane, receipts, specs, or generators are
60
+ installed. One idempotent command installs the full harness and its machine-checked
61
+ definition of done: `npx create-cmp-cli harden`.
62
+ <!-- <<< cmp:feature !harness -->
@@ -1,3 +1,4 @@
1
+ <!-- >>> cmp:feature harness -->
1
2
  # __APP_NAME__ — AI delivery contract
2
3
 
3
4
  Generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a verification
@@ -422,3 +423,51 @@ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional C
422
423
  | `node qa/verify.mjs --determinism` | Timezone determinism probe, alone: runs the JVM test tier twice under UTC-12 and UTC+14 and FAILs naming any test whose outcome differs — the dynamic net behind ARCH-13's static one. Opt-in inside a lane via `--profile ci --determinism`; never with `--fast`; writes no receipt on its own |
423
424
  | `node qa/record-audit.mjs <subsystem>` | Record that a `cmp-audit` of an androidMain subsystem happened (appends subsystem + HEAD sha + timestamp to `qa/audits.jsonl`; refuses dirty/unknown targets). `--list` shows every derived subsystem and its audit status |
424
425
  | `node qa/retrospective.mjs` | How this project actually uses its harness, from `qa/flight-recorder.jsonl` (appended by every lane run): fast vs full ratio, verbatim SKIP reasons grouped, whether the device tier is ever reached, longest stretch with no full lane. States only what the journal recorded |
426
+ <!-- <<< cmp:feature harness -->
427
+ <!-- >>> cmp:feature !harness -->
428
+ # __APP_NAME__ — working guide
429
+
430
+ Generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) as a **minimal
431
+ scaffold**: the full app architecture and its tests, without the verification harness.
432
+
433
+ ## Build & test
434
+
435
+ | Command | What |
436
+ |---|---|
437
+ | `./gradlew :composeApp:desktopTest` | Unit + conformance + golden-tree tests (JVM, seconds) |
438
+ | `./gradlew :composeApp:assembleDebug` | Android debug build |
439
+ | `./gradlew :composeApp:installDebug` | Install on the attached device/emulator |
440
+
441
+ Run `desktopTest` after every change — it carries the architecture gates that keep this
442
+ codebase coherent. Never delete or weaken a failing test to reach green.
443
+
444
+ ## Architecture
445
+
446
+ [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) is the working guide: `presentation` →
447
+ `domain` ← `data`; typed errors (`AppResult`, no cross-layer throws); design tokens from
448
+ `presentation/theme/` (no hardcoded values); every screen a `*Screen` composable with a
449
+ tested ViewModel, mirroring the `home` exemplar through every layer.
450
+ <!-- >>> cmp:feature inspector -->
451
+
452
+ ## UI feedback loop — see what you build, without a device
453
+
454
+ `./gradlew :composeApp:renderScreens` renders every screen in
455
+ `inspector/PreviewRegistry.kt` headlessly (real DI, real theme) to
456
+ `composeApp/build/previews/<id>/{screen.png, tree.json}`; `node qa/preview-gallery.mjs`
457
+ builds one self-contained gallery page. Assert on `tree.json` structure — pixels are for
458
+ humans. Register new screens in the PreviewRegistry.
459
+
460
+ With a debug build running: `adb forward tcp:9500 tcp:9500`, then
461
+ `http://127.0.0.1:9500/inspect/remote` mirrors the live app with click-to-tap.
462
+ <!-- <<< cmp:feature inspector -->
463
+
464
+ ## What full mode adds
465
+
466
+ The verification harness: a verify lane (`qa/verify.mjs`) with evidence receipts,
467
+ behavior specs, approval gates, feature generators, and a Stop hook that makes "done"
468
+ machine-checked instead of honor-system. One idempotent command installs it all:
469
+
470
+ ```bash
471
+ npx create-cmp-cli harden
472
+ ```
473
+ <!-- <<< cmp:feature !harness -->
@@ -6,14 +6,27 @@
6
6
  2. Make the change — new features mirror the `home` exemplar
7
7
  (see [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)), with tests at every layer
8
8
  (see [`docs/TESTING.md`](./docs/TESTING.md)).
9
+ <!-- >>> cmp:feature harness -->
9
10
  3. Run the verify lane: `node qa/verify.mjs`.
10
11
  4. Commit **including the updated receipt** (`qa/evidence/latest.json`). A change without a
11
12
  PASS receipt is not done — CI re-runs the same lane and will say so.
13
+ <!-- <<< cmp:feature harness -->
14
+ <!-- >>> cmp:feature !harness -->
15
+ 3. Run the tests: `./gradlew :composeApp:desktopTest` must be green (CI re-runs it on
16
+ every push).
17
+ 4. Commit.
18
+ <!-- <<< cmp:feature !harness -->
12
19
  5. Open a PR. Keep it one concern; note any intended golden/baseline changes explicitly.
13
20
 
14
21
  ## Definition of done
15
22
 
23
+ <!-- >>> cmp:feature harness -->
16
24
  - `node qa/verify.mjs` → **PASS**, receipt committed.
25
+ <!-- <<< cmp:feature harness -->
26
+ <!-- >>> cmp:feature !harness -->
27
+ - `./gradlew :composeApp:desktopTest` green. (This is a minimal scaffold — the
28
+ machine-checked definition of done arrives with `npx create-cmp-cli harden`.)
29
+ <!-- <<< cmp:feature !harness -->
17
30
  - New behavior has tests; existing tests untouched unless the behavior intentionally changed
18
31
  (say so in the PR).
19
32
  - No hardcoded design values; testTags on anything E2E needs to reach.
@@ -1,5 +1,6 @@
1
1
  # __APP_NAME__
2
2
 
3
+ <!-- >>> cmp:feature harness -->
3
4
  <!-- cmp:generated evidence -->
4
5
  [![No evidence receipt](https://img.shields.io/badge/evidence-none_yet-9E9E9E)](https://github.com/kvdm-co-pilot/create-cmp) — no verify receipt yet. Run `node qa/verify.mjs`.
5
6
  <!-- /cmp:generated -->
@@ -9,6 +10,14 @@ A Kotlin / Compose Multiplatform app, generated by
9
10
  the architecture, testing conventions, and definition of done are enforced mechanically, not
10
11
  by convention. Start with [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md); AI collaborators
11
12
  follow the contract in [`CLAUDE.md`](./CLAUDE.md).
13
+ <!-- <<< cmp:feature harness -->
14
+ <!-- >>> cmp:feature !harness -->
15
+ A Kotlin / Compose Multiplatform app, generated by
16
+ [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) as a **minimal scaffold**: the
17
+ full app architecture with its tests, without the verification harness (one command adds
18
+ it — see "Full mode" below). Start with [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md);
19
+ AI collaborators follow the guide in [`CLAUDE.md`](./CLAUDE.md).
20
+ <!-- <<< cmp:feature !harness -->
12
21
 
13
22
  ## Quick start
14
23
 
@@ -29,8 +38,10 @@ follow the contract in [`CLAUDE.md`](./CLAUDE.md).
29
38
  # Unit tests (fast inner loop)
30
39
  ./gradlew :composeApp:desktopTest
31
40
 
41
+ <!-- >>> cmp:feature harness -->
32
42
  # The verify lane — build + tests + every shipped gate, with an evidence receipt
33
43
  node qa/verify.mjs
44
+ <!-- <<< cmp:feature harness -->
34
45
  ```
35
46
 
36
47
  <!-- >>> cmp:feature inspector -->
@@ -55,7 +66,9 @@ before shipping — from Claude Code, the `cmp-firebase-connect` skill drives it
55
66
  ## Project structure
56
67
 
57
68
  ```
69
+ <!-- >>> cmp:feature harness -->
58
70
  specs/ behavior specifications — new behavior starts here
71
+ <!-- <<< cmp:feature harness -->
59
72
  composeApp/src/commonMain/ shared UI + logic (presentation / domain / data / di)
60
73
  composeApp/src/commonTest/ unit tests — exemplar pattern in presentation/home
61
74
  composeApp/src/desktopTest/ conformance gates + Compose UI tests + golden trees (JVM tier)
@@ -66,8 +79,10 @@ composeApp/src/iosMain/ iOS actuals · iosApp/ is the Xcode shell
66
79
  <!-- >>> cmp:feature dev-client -->
67
80
  composeApp/src/desktopMain/ desktop dev-client (see docs/dev-client.md)
68
81
  <!-- <<< cmp:feature dev-client -->
82
+ <!-- >>> cmp:feature harness -->
69
83
  qa/verify.mjs the verify lane — this repo's definition of done
70
84
  qa/evidence/latest.json the committed evidence receipt (see CONTRIBUTING.md)
85
+ <!-- <<< cmp:feature harness -->
71
86
  <!-- >>> cmp:feature e2e -->
72
87
  qa/e2e/ Maestro E2E flows (device smoke)
73
88
  <!-- <<< cmp:feature e2e -->
@@ -82,9 +97,26 @@ docs/ architecture, testing, ADRs
82
97
  | [`docs/TESTING.md`](./docs/TESTING.md) | The test pyramid, conventions, how to run everything |
83
98
  | [`docs/adr/`](./docs/adr/) | Architecture decision records |
84
99
  | [`CONTRIBUTING.md`](./CONTRIBUTING.md) | Workflow, definition of done, commit style |
100
+ <!-- >>> cmp:feature harness -->
85
101
  | [`CLAUDE.md`](./CLAUDE.md) | The AI delivery contract |
102
+ <!-- <<< cmp:feature harness -->
103
+ <!-- >>> cmp:feature !harness -->
104
+ | [`CLAUDE.md`](./CLAUDE.md) | The AI working guide |
105
+ <!-- <<< cmp:feature !harness -->
86
106
  | [`CHANGELOG.md`](./CHANGELOG.md) | Notable changes (Keep a Changelog) |
87
107
 
108
+ <!-- >>> cmp:feature !harness -->
109
+ ## Full mode
110
+
111
+ This scaffold is deliberately light: no verify lane, evidence receipts, behavior specs,
112
+ approval gates, or generators. The full harness — a machine-checked definition of done,
113
+ with an audit trail — installs in place, idempotently, with one command:
114
+
115
+ ```bash
116
+ npx create-cmp-cli harden
117
+ ```
118
+ <!-- <<< cmp:feature !harness -->
119
+ <!-- >>> cmp:feature harness -->
88
120
  ## Verification
89
121
 
90
122
  Every change must pass the verify lane (`node qa/verify.mjs`) and commit its updated receipt
@@ -115,6 +147,7 @@ in [`.claude/settings.json`](./.claude/settings.json) — nothing else depends o
115
147
  CI independently enforces the same "receipt attests HEAD" check on every push
116
148
  (`.github/workflows/verify.yml`), so disabling the local hook only trades an immediate
117
149
  signal for a later one.
150
+ <!-- <<< cmp:feature harness -->
118
151
 
119
152
  ---
120
153
 
@@ -1,17 +1,29 @@
1
1
  # Architecture
2
2
 
3
+ <!-- >>> cmp:feature harness -->
3
4
  > **Reading this document.** Every normative sentence below carries a tier tag.
4
5
  > `[enforced: CLAUSE-ID]`: a named gate in `node qa/verify.mjs` fails the lane on violation.
5
6
  > `[governed]`: the sentence lives inside a hash-bound human approval (`qa/approvals.json`);
6
7
  > changing it without re-approval fails the `approvals` gate. `[advisory]`: a documented
7
8
  > convention with no mechanical check yet. Every sentence is law, signed intent, or advice —
8
9
  > and says which.
10
+ <!-- <<< cmp:feature harness -->
11
+ <!-- >>> cmp:feature !harness -->
12
+ > **Reading this document.** Every normative sentence below carries a tier tag.
13
+ > `[enforced: CLAUSE-ID]`: a named source-scanning gate in the desktopTest conformance
14
+ > suite fails the tests on violation. `[governed]` / `[advisory]`: conventions this
15
+ > minimal scaffold checks by review, not by machine — the full harness
16
+ > (`npx create-cmp-cli harden`) adds the mechanical gates.
17
+ <!-- <<< cmp:feature !harness -->
9
18
 
10
19
  ## 1. Purpose & quality goals
11
20
 
21
+ <!-- >>> cmp:feature harness -->
12
22
  This app's purpose, audience, and shape are recorded in [`specs/intent.md`](../specs/intent.md)
13
23
  — the root brief this document, the component registry, and the exemplar feature all trace
14
- back to. The table below is the default quality-goal set a fresh scaffold ships with. The
24
+ back to.
25
+ <!-- <<< cmp:feature harness -->
26
+ The table below is the default quality-goal set a fresh scaffold ships with. The
15
27
  genesis walk's architecture conversation is where a human promotes, demotes, or replaces
16
28
  them for this app's actual priorities ("offline matters more than a11y for a field-work
17
29
  app").
@@ -423,6 +435,11 @@ demotes to a regular feature. To add a feature, mirror the exemplar exactly:
423
435
  empty/content split) (+ test using a fake from `testing/fakes/`).
424
436
  4. DI: register in `di/AppModule.kt`.
425
437
  5. Navigation: add the route in `presentation/navigation/`.
438
+ <!-- >>> cmp:feature harness -->
426
439
  6. Run `node qa/verify.mjs` — done means PASS + committed receipt.
440
+ <!-- <<< cmp:feature harness -->
441
+ <!-- >>> cmp:feature !harness -->
442
+ 6. Run `./gradlew :composeApp:desktopTest` — green is the bar this scaffold can check.
443
+ <!-- <<< cmp:feature !harness -->
427
444
 
428
445
  Significant decisions get an ADR in [`docs/adr/`](./adr/) — see the template there.
@@ -13,12 +13,20 @@ copy their shape.
13
13
  <!-- >>> cmp:feature e2e -->
14
14
  | E2E smoke (few) | `qa/e2e/*.yaml` (Maestro) | `maestro test qa/e2e/smoke.yaml` |
15
15
  <!-- <<< cmp:feature e2e -->
16
+ <!-- >>> cmp:feature harness -->
16
17
  | The lane (all of it) | `qa/verify.mjs` | `node qa/verify.mjs` |
17
18
 
18
19
  Every durable test cites the spec clause it verifies (`// SPEC: HOME-02` — see
19
20
  [`specs/`](../specs/README.md)); **new behavior begins as a spec clause.** The lane's
20
21
  `specCoverage` step enforces this: it fails on orphan clauses (no citing test) and orphan tags
21
22
  (no matching clause, or one citing a withdrawn clause).
23
+ <!-- <<< cmp:feature harness -->
24
+ <!-- >>> cmp:feature !harness -->
25
+
26
+ Durable tests may cite a spec clause id in a comment (`// SPEC: HOME-02`) — the shipped
27
+ tests do. This minimal scaffold carries no `specs/` directory or coverage gate; both
28
+ arrive with `npx create-cmp-cli harden`.
29
+ <!-- <<< cmp:feature !harness -->
22
30
 
23
31
  ## Unit conventions
24
32
 
@@ -140,6 +148,7 @@ one: a search assert that passed standalone failed in-lane behind a 33s type gap
140
148
  asserts are for static post-navigation elements only.
141
149
  <!-- <<< cmp:feature e2e -->
142
150
 
151
+ <!-- >>> cmp:feature harness -->
143
152
  ## The verify lane
144
153
 
145
154
  `node qa/verify.mjs` is the definition of done: spec coverage → build → unit tests →
@@ -235,3 +244,4 @@ rung is the coarse grade; the per-step list stays the fine print. A FAILed lane
235
244
  | **L1 desktop** | Full static + JVM evidence: build, unit tests, conformance, golden trees, a11y, release COMPILE, and the pure-Node gates. | That the app runs on a device at all — no APK was installed or driven; platform behavior (alarms, notifications) is invisible from this rung. |
236
245
  | **L2 device** | L1 plus executed on-device evidence: the debug APK installed and driven (`e2eSmoke`), instrumented platform assertions (`androidChecks`), and/or live token drift. | That the release variant behaves (R8 differs from debug — that is L3's job), nor that alarms/notifications actually land unless an instrumented behavior test asserts them. |
237
246
  | **L3 release** | L2 plus `releaseSmoke` PASSed: the signed release APK installed and driven on a device. | Real-backend behavior (the emulator/dev backend is a documented tier boundary — see the instrumented-tier section) and store-review compliance. |
247
+ <!-- <<< cmp:feature harness -->
@@ -57,6 +57,18 @@
57
57
  "whenDisabled": "Remove the marker lines AND the body between them."
58
58
  },
59
59
  "features": {
60
+ "harness": {
61
+ "enabledByDefault": true,
62
+ "paths": [
63
+ "specs",
64
+ ".claude/skills",
65
+ ".githooks",
66
+ "qa/approvals.json",
67
+ "qa/comments.json",
68
+ "qa/evidence"
69
+ ],
70
+ "notes": "The verification harness — the mode split (`--minimal` disables it; `create-cmp harden` installs it back). Three mechanisms share the subtraction, each already owned by the engine: (1) the paths above delete the app-owned governance surfaces (specs/, approvals+comments ledgers, evidence, generator skills, the pre-push receipt hook); (2) `cmp:feature harness`/`!harness` marker blocks give CLAUDE.md, AGENTS.md, README.md, CONTRIBUTING.md, docs/TESTING.md and .github/workflows/verify.yml their two honest renderings from one file; (3) the machine-owned lane region is subtracted by the ENGINE (src/lib/minimal.mjs), which keeps qa/preview-gallery.mjs plus its transitive qa/lib imports — derived from import statements, never listed here, so it cannot rot. .claude/settings.json is rewritten to the advisory-only hook set via the src/lib/hooks.mjs classifier (Stop hook and qa/-referencing nudges are full-mode only). qa/golden/ stays in BOTH modes — the desktopTest golden-tree tests read it and minimal keeps the full JVM test tier. qa/e2e is its own feature and stays orthogonal (Maestro flows run standalone)."
71
+ },
60
72
  "ios": {
61
73
  "enabledByDefault": true,
62
74
  "paths": [
@@ -113,6 +125,7 @@
113
125
  },
114
126
  "verify": {
115
127
  "android": "node qa/verify.mjs --profile scaffold",
128
+ "androidMinimal": "./gradlew :composeApp:desktopTest :composeApp:assembleDebug",
116
129
  "androidBuildOnly": "./gradlew :composeApp:assembleDebug",
117
130
  "androidSmoke": "./gradlew :composeApp:installDebug && maestro test qa/e2e/smoke.yaml",
118
131
  "iosLink": "./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64",
@@ -8,7 +8,7 @@
8
8
  // Answered LOCALLY, offline, on every lane run. Needs nothing
9
9
  // but the tree and this file.
10
10
  //
11
- // AUTHENTICITY "is my lane the real published create-cmp-harness@X?"
11
+ // AUTHENTICITY "is my lane the real published @create-cmp/harness@X?"
12
12
  // Answered REMOTELY, on request, by comparing this file's
13
13
  // `sha256` against the published version's — `create-cmp
14
14
  // upgrade --harness` does it, and so can any third party
@@ -61,7 +61,7 @@ export function readHarnessLock(root) {
61
61
  * @param {{name?: string, version: string}} harness identity to record
62
62
  * @returns {{sha256: string, fileCount: number}}
63
63
  */
64
- export function writeHarnessLock(root, { name = "create-cmp-harness", version }) {
64
+ export function writeHarnessLock(root, { name = "@create-cmp/harness", version }) {
65
65
  if (typeof version !== "string" || version.length === 0) {
66
66
  throw new Error("writeHarnessLock: a harness version is required");
67
67
  }
@@ -5,7 +5,7 @@
5
5
  // there is exactly one definition of the surface and the algorithm.
6
6
  //
7
7
  // SINGLE SOURCE OF TRUTH: packages/receipts/src/inputs-hash.mjs in the
8
- // create-cmp repo (the `cmp-receipts` package). The copy in a generated
8
+ // create-cmp repo (the `@create-cmp/receipts` package). The copy in a generated
9
9
  // project's qa/lib/ is vendored byte-identical at scaffold time and pinned by
10
10
  // test/receipts-parity.test.mjs — edit the package source, then run
11
11
  // `node scripts/sync-harness.mjs`.
@@ -6,7 +6,7 @@
6
6
  // tarball rather than the working tree.
7
7
  //
8
8
  // SINGLE SOURCE OF TRUTH: packages/receipts/src/receipt-validate.mjs in the
9
- // create-cmp repo (the `cmp-receipts` package). The copy in a generated
9
+ // create-cmp repo (the `@create-cmp/receipts` package). The copy in a generated
10
10
  // project's qa/lib/ is vendored byte-identical at scaffold time and pinned by
11
11
  // test/receipts-parity.test.mjs — edit the package source, then run
12
12
  // `node scripts/sync-harness.mjs`.
@@ -39,7 +39,7 @@ function readStdinJson() {
39
39
  }
40
40
 
41
41
  // The predicate itself lives in qa/lib/receipt-validate.mjs (vendored from the
42
- // cmp-receipts package — one definition everywhere a receipt is judged); this
42
+ // @create-cmp/receipts package — one definition everywhere a receipt is judged); this
43
43
  // CLI only reads the receipt and frames the exit codes.
44
44
  function evaluate() {
45
45
  const receipt = readReceipt(ROOT);