create-cmp-cli 0.14.0 → 0.14.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cmp-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Create production mobile apps (Android + iOS, one Kotlin codebase) with AI \u2014 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": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-cmp-harness",
3
- "version": "0.14.0",
4
- "description": "The create-cmp verify lane the machine-owned harness code every stamped app carries byte-identical: evidence receipts, spec coverage, approvals, conformance reporting, golden trees, a11y, and the preview/inspector libs. Dependency-free ESM, vendored into each generated project so the lane runs offline with no install step, and content-hashed so a receipt can name the exact lane that issued it.",
3
+ "version": "0.14.1",
4
+ "description": "The create-cmp verify lane \u2014 the machine-owned harness code every stamped app carries byte-identical: evidence receipts, spec coverage, approvals, conformance reporting, golden trees, a11y, and the preview/inspector libs. Dependency-free ESM, vendored into each generated project so the lane runs offline with no install step, and content-hashed so a receipt can name the exact lane that issued it.",
5
5
  "type": "module",
6
6
  "main": "src/verify.mjs",
7
7
  "exports": {
@@ -93,6 +93,14 @@ Flags:
93
93
  with --profile ci (or release) it runs
94
94
  inside the lane and lands on the receipt.
95
95
  Never combinable with --fast
96
+ --no-journal skip the qa/flight-recorder.jsonl append.
97
+ qa/watch.mjs passes this on every
98
+ save-triggered run: journalling each save
99
+ would add hundreds of committed lines a day
100
+ and leave a permanently dirty tree inside
101
+ the loop the recorder exists to observe. The
102
+ gap is disclosed in the retrospective's own
103
+ output
96
104
  --json print the receipt as JSON instead of the
97
105
  human-readable step-by-step log
98
106
  --help, -h print this usage and exit 0 without
@@ -116,7 +124,13 @@ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
116
124
  process.exit(0);
117
125
  }
118
126
 
119
- const RECOGNIZED_FLAGS = new Set(["--profile", "--json", "--fast", "--determinism"]);
127
+ // Every flag this file CONSUMES must be listed here, or the strict check below
128
+ // rejects it. `--no-journal` was consumed but unlisted in 0.13.0, which meant
129
+ // qa/watch.mjs — whose spawn passes exactly these flags — exited 2 on every
130
+ // save without ever running the lane. test/verify-flags.test.mjs now pins
131
+ // consumed ⊆ recognized and watch's spawn ⊆ recognized so the class cannot
132
+ // recur.
133
+ const RECOGNIZED_FLAGS = new Set(["--profile", "--json", "--fast", "--determinism", "--no-journal"]);
120
134
  for (let i = 0; i < rawArgs.length; i += 1) {
121
135
  const arg = rawArgs[i];
122
136
  if (arg === "--profile") {
@@ -43,7 +43,8 @@ import { consent } from "../bootstrap/exec.mjs";
43
43
  import { loadRegistry, latestSet, getSet } from "../lib/registry.mjs";
44
44
  import { planUpgrade, BACKUP_SUFFIX } from "../lib/upgrade.mjs";
45
45
  import { writeHarnessLock, checkHarnessIntegrity, describeIntegrity } from "../../packages/harness/src/lib/harness-lock.mjs";
46
- import { LOCAL_PATCH_PATH } from "../lib/harness-upgrade.mjs";
46
+ import { LOCAL_PATCH_PATH, stampBaseWith } from "../lib/harness-upgrade.mjs";
47
+ import { buildTokenMap } from "../lib/tokens.mjs";
47
48
  import {
48
49
  planHarnessUpgrade,
49
50
  applyHarnessPlan,
@@ -341,11 +342,28 @@ async function harnessPlanAndApply({ flags, record, projectDir, targetDir, tmpRo
341
342
  templateDir: baseTemplateDir,
342
343
  verify: false,
343
344
  });
344
- const plan = planHarnessUpgrade({ baseDir, newDir, projectDir });
345
+ // Engines before 0.14.0 ran lane code through the token stamper, so an app
346
+ // stamped by one carries `Fuelled` where the base template says
347
+ // `__APP_NAME__`. Without this the migration would report the engine's own
348
+ // substitution as if the app had forked the lane.
349
+ const plan = planHarnessUpgrade({
350
+ baseDir,
351
+ newDir,
352
+ projectDir,
353
+ stampBase: stampBaseWith(buildTokenMap(configFromSpecRecord(record, projectDir))),
354
+ });
345
355
 
346
356
  const anythingToDo = printHarnessReport(plan);
347
357
  if (!anythingToDo) {
348
358
  ok("Engine-owned files are fully up to date — nothing to apply.");
359
+ // Still advance the record. An app that is ALREADY at the new engine is as
360
+ // upgraded as it can be, and leaving engineVersion stale here would keep
361
+ // the very defect this write-back exists to fix: the next run would fetch
362
+ // an obsolete merge base and re-litigate changes that already landed. Not
363
+ // on a dry run — that promises to write nothing.
364
+ if (flags["dry-run"] !== true && writeBackEngineVersion(projectDir, currentVersion)) {
365
+ ok(`create-cmp.json engineVersion → ${colors.bold(currentVersion)}`);
366
+ }
349
367
  return 0;
350
368
  }
351
369
 
@@ -40,7 +40,7 @@ import path from "node:path";
40
40
  import { spawnSync } from "node:child_process";
41
41
 
42
42
  import { listFiles } from "./fsutil.mjs";
43
- import { isBinaryPath } from "./tokens.mjs";
43
+ import { isBinaryPath, replaceTokens } from "./tokens.mjs";
44
44
  import { BACKUP_SUFFIX } from "./upgrade.mjs";
45
45
  import { isHarnessFile } from "../../packages/harness/src/lib/harness-region.mjs";
46
46
 
@@ -178,6 +178,23 @@ export function mergeThreeWay(theirs, base, next) {
178
178
  * null when the path is in neither base nor new (app-authored — invisible)
179
179
  */
180
180
 
181
+
182
+ /**
183
+ * A `stampBase` function for an app's recorded config — reproduces what a
184
+ * pre-0.14.0 engine would have written into that app's lane, so a file the
185
+ * stamper touched is not mistaken for a local edit. Binary-safe: content that
186
+ * does not round-trip through UTF-8 is returned unchanged.
187
+ * @param {Array<[string,string]>} tokenMap from buildTokenMap(config)
188
+ * @returns {(base: Buffer) => Buffer}
189
+ */
190
+ export function stampBaseWith(tokenMap) {
191
+ return (base) => {
192
+ const text = base.toString("utf8");
193
+ if (!Buffer.from(text, "utf8").equals(base)) return base;
194
+ return Buffer.from(replaceTokens(text, tokenMap), "utf8");
195
+ };
196
+ }
197
+
181
198
  /** Where a preserved local patch to machine-owned lane code is written. */
182
199
  export const LOCAL_PATCH_PATH = "qa/harness-local.patch";
183
200
 
@@ -253,7 +270,7 @@ export function diffPatch(base, theirs, relPath) {
253
270
  * @param {Function} [params.merge] injectable three-way merge (classifier only)
254
271
  * @returns {{bucket:string, write:Buffer|null, sidecar:null, remove:boolean, patch?:string}|null}
255
272
  */
256
- export function decideRegionFile({ relPath, base, next, theirs, merge = mergeThreeWay }) {
273
+ export function decideRegionFile({ relPath, base, next, theirs, merge = mergeThreeWay, stampBase = null }) {
257
274
  const eq = (a, b) => a !== null && b !== null && a.equals(b);
258
275
  const replace = (bucket, extra = {}) => ({
259
276
  bucket,
@@ -274,6 +291,19 @@ export function decideRegionFile({ relPath, base, next, theirs, merge = mergeThr
274
291
  if (base === null) return replace("region-patched", { patch: "" }); // no base to diff against
275
292
  if (eq(theirs, base)) return replace("region-clean");
276
293
 
294
+ // The app's copy may be what its OWN engine produced rather than what the
295
+ // base tree holds, because engines before 0.14.0 ran lane code through the
296
+ // token stamper. Such an app carries `Fuelled` where the base template says
297
+ // `__APP_NAME__` — a difference the engine created, not the app. Asking
298
+ // "does this match the base as THIS app would have had it stamped?" is the
299
+ // accurate form of "did the app never touch it", and it needs no version
300
+ // comparison: for a 0.14.0+ base there is nothing left to stamp, so the
301
+ // check simply never fires.
302
+ if (stampBase !== null) {
303
+ const asStamped = stampBase(base);
304
+ if (eq(theirs, asStamped)) return replace("region-clean");
305
+ }
306
+
277
307
  // The app edited lane code. Is that edit already carried by the new engine?
278
308
  // A clean three-way merge landing exactly on `next` means the local change
279
309
  // contributed nothing beyond it — the hand-mirror case.
@@ -283,10 +313,10 @@ export function decideRegionFile({ relPath, base, next, theirs, merge = mergeThr
283
313
  return replace("region-patched", { patch: diffPatch(base, theirs, relPath) });
284
314
  }
285
315
 
286
- export function decideFile({ relPath, base, next, theirs, merge = mergeThreeWay }) {
316
+ export function decideFile({ relPath, base, next, theirs, merge = mergeThreeWay, stampBase = null }) {
287
317
  // Machine-owned lane files answer a different question — see decideRegionFile.
288
318
  if (isHarnessFile(relPath) && !(base === null && next === null)) {
289
- return decideRegionFile({ relPath, base, next, theirs, merge });
319
+ return decideRegionFile({ relPath, base, next, theirs, merge, stampBase });
290
320
  }
291
321
  const none = (bucket) => ({ bucket, write: null, sidecar: null, remove: false });
292
322
  const write = (bucket, content) => ({ bucket, write: content, sidecar: null, remove: false });
@@ -355,7 +385,7 @@ function toRel(root, abs) {
355
385
  * sidecar?:Buffer|null, remove?:boolean}>,
356
386
  * counts: Record<string, number>}}
357
387
  */
358
- export function planHarnessUpgrade({ baseDir, newDir, projectDir, merge = mergeThreeWay }) {
388
+ export function planHarnessUpgrade({ baseDir, newDir, projectDir, merge = mergeThreeWay, stampBase = null }) {
359
389
  const rels = new Set();
360
390
  for (const f of listFiles(baseDir)) rels.add(toRel(baseDir, f));
361
391
  for (const f of listFiles(newDir)) rels.add(toRel(newDir, f));
@@ -390,6 +420,7 @@ export function planHarnessUpgrade({ baseDir, newDir, projectDir, merge = mergeT
390
420
  next: readIfPresent(newDir, relPath),
391
421
  theirs: readIfPresent(projectDir, relPath),
392
422
  merge,
423
+ stampBase,
393
424
  });
394
425
  if (decision === null) continue;
395
426
  counts[decision.bucket] = (counts[decision.bucket] ?? 0) + 1;
@@ -93,6 +93,14 @@ Flags:
93
93
  with --profile ci (or release) it runs
94
94
  inside the lane and lands on the receipt.
95
95
  Never combinable with --fast
96
+ --no-journal skip the qa/flight-recorder.jsonl append.
97
+ qa/watch.mjs passes this on every
98
+ save-triggered run: journalling each save
99
+ would add hundreds of committed lines a day
100
+ and leave a permanently dirty tree inside
101
+ the loop the recorder exists to observe. The
102
+ gap is disclosed in the retrospective's own
103
+ output
96
104
  --json print the receipt as JSON instead of the
97
105
  human-readable step-by-step log
98
106
  --help, -h print this usage and exit 0 without
@@ -116,7 +124,13 @@ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
116
124
  process.exit(0);
117
125
  }
118
126
 
119
- const RECOGNIZED_FLAGS = new Set(["--profile", "--json", "--fast", "--determinism"]);
127
+ // Every flag this file CONSUMES must be listed here, or the strict check below
128
+ // rejects it. `--no-journal` was consumed but unlisted in 0.13.0, which meant
129
+ // qa/watch.mjs — whose spawn passes exactly these flags — exited 2 on every
130
+ // save without ever running the lane. test/verify-flags.test.mjs now pins
131
+ // consumed ⊆ recognized and watch's spawn ⊆ recognized so the class cannot
132
+ // recur.
133
+ const RECOGNIZED_FLAGS = new Set(["--profile", "--json", "--fast", "--determinism", "--no-journal"]);
120
134
  for (let i = 0; i < rawArgs.length; i += 1) {
121
135
  const arg = rawArgs[i];
122
136
  if (arg === "--profile") {
File without changes
@@ -1,2 +0,0 @@
1
- #Mon Jul 06 18:20:40 WAT 2026
2
- gradle.version=8.11.1
File without changes