create-cmp-cli 0.11.0 → 0.13.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 (51) hide show
  1. package/README.md +11 -9
  2. package/bin/create-cmp.mjs +3 -0
  3. package/package.json +1 -1
  4. package/src/commands/upgrade.mjs +287 -0
  5. package/src/lib/harness-upgrade.mjs +364 -0
  6. package/src/lib/package-name.mjs +72 -0
  7. package/src/scaffold.mjs +7 -2
  8. package/template/.claude/settings.json +30 -0
  9. package/template/CLAUDE.md +51 -6
  10. package/template/README.md +4 -0
  11. package/template/composeApp/build.gradle.kts +44 -0
  12. package/template/composeApp/src/androidDebug/AndroidManifest.xml +9 -0
  13. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/PlatformBehaviorSeamTest.kt +277 -0
  14. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/RuntimeStateSeamTest.kt +308 -0
  15. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/AlarmAsserts.kt +152 -0
  16. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ConfigControl.kt +124 -0
  17. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/DozeControl.kt +113 -0
  18. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NetworkControl.kt +137 -0
  19. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/NotificationAsserts.kt +163 -0
  20. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/PermissionControl.kt +132 -0
  21. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/ProcessControl.kt +217 -0
  22. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/Shell.kt +79 -0
  23. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/SystemState.kt +113 -0
  24. package/template/composeApp/src/androidInstrumentedTest/kotlin/com/example/app/testing/TimeWarp.kt +114 -0
  25. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +5 -2
  26. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppBottomBar.kt +1 -1
  27. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppButton.kt +1 -1
  28. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/AppIconButton.kt +1 -1
  29. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +58 -0
  30. package/template/docs/ARCHITECTURE.md +41 -2
  31. package/template/docs/TESTING.md +165 -0
  32. package/template/gradle/libs.versions.toml +15 -0
  33. package/template/manifest.json +1 -0
  34. package/template/qa/evidence/schema.json +20 -2
  35. package/template/qa/lib/affected-tests.mjs +147 -0
  36. package/template/qa/lib/audit-cadence.mjs +290 -0
  37. package/template/qa/lib/determinism.mjs +179 -0
  38. package/template/qa/lib/device-lease.mjs +249 -0
  39. package/template/qa/lib/evidence-badge.mjs +158 -0
  40. package/template/qa/lib/evidence-level.mjs +117 -0
  41. package/template/qa/lib/flight-recorder.mjs +332 -0
  42. package/template/qa/lib/inputs-hash.mjs +16 -1
  43. package/template/qa/lib/spec-coverage.mjs +54 -3
  44. package/template/qa/lib/step-cache.mjs +221 -0
  45. package/template/qa/receipt-check.mjs +22 -2
  46. package/template/qa/record-audit.mjs +83 -0
  47. package/template/qa/retrospective.mjs +51 -0
  48. package/template/qa/scaffold-feature.mjs +20 -1
  49. package/template/qa/verify.mjs +934 -57
  50. package/template/qa/watch.mjs +622 -0
  51. package/template/specs/app-base.spec.md +11 -0
@@ -46,7 +46,25 @@ function evaluate() {
46
46
  if (receipt === null) {
47
47
  return { valid: false, reason: "no receipt — run `node qa/verify.mjs`", profile: undefined };
48
48
  }
49
- return evaluateReceipt(receipt, () => computeInputsHash(ROOT));
49
+ // A fast-mode receipt (verify --fast) is an inner-loop signal, never done
50
+ // evidence — refused here before the hash is even recomputed, so a session
51
+ // can never end on "done" while its evidence trail's last run was --fast.
52
+ if (receipt.mode === "fast") {
53
+ return {
54
+ valid: false,
55
+ reason: "the last verify run was --fast (inner-loop only); run the full lane (`node qa/verify.mjs`) before finishing",
56
+ profile: receipt.profile,
57
+ };
58
+ }
59
+ const result = evaluateReceipt(receipt, () => computeInputsHash(ROOT));
60
+ // Surface the receipt's evidence rung (the ladder — qa/lib/evidence-level.mjs)
61
+ // alongside the verdict: the rung is the receipt's own derived field, read
62
+ // verbatim, never recomputed here. Older receipts without it stay valid.
63
+ const level = receipt.evidenceLevel;
64
+ if (level && typeof level === "object" && typeof level.rung === "string") {
65
+ result.evidenceLevel = level;
66
+ }
67
+ return result;
50
68
  }
51
69
 
52
70
  const result = evaluate();
@@ -65,10 +83,12 @@ if (asHook) {
65
83
  process.exit(0);
66
84
  }
67
85
 
86
+ const rungSuffix = result.evidenceLevel ? ` — evidence ${result.evidenceLevel.rung} · ${result.evidenceLevel.name}` : "";
87
+
68
88
  if (asJson) {
69
89
  console.log(JSON.stringify(result, null, 2));
70
90
  } else if (result.valid) {
71
- console.log(`VALID — ${result.reason}`);
91
+ console.log(`VALID — ${result.reason}${rungSuffix}`);
72
92
  } else {
73
93
  console.error(`INVALID — ${result.reason}`);
74
94
  }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // Record that a cmp-audit of one androidMain subsystem happened — the
3
+ // write half of the audit-cadence report (qa/lib/audit-cadence.mjs).
4
+ //
5
+ // node qa/record-audit.mjs <subsystem> [--by <who-or-what>]
6
+ // node qa/record-audit.mjs --list
7
+ //
8
+ // Recording is a CLAIM — "this subsystem, as of this commit, was audited" —
9
+ // so the entry's sha is derived from HEAD by the library, never passed in,
10
+ // and the write is REFUSED when there is no git history, when the subsystem
11
+ // is not one this app actually has (derived from the tree, printed on
12
+ // refusal), or when the subsystem's files differ from HEAD (the record
13
+ // would name a commit the audited bytes did not match — commit first).
14
+ // Refusal over fabrication, the same stance as qa/approve.mjs.
15
+ //
16
+ // This CLI exists so the audit loop closes mechanically: the release
17
+ // profile's receipt nudges "changed since last audit → cmp-audit <name>",
18
+ // and the auditor's last act is this one command.
19
+
20
+ import path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ import { AUDITS_REL_PATH, ROOT_SUBSYSTEM, androidMainPackageRoot, evaluateAuditCadence, listSubsystems, recordAudit } from "./lib/audit-cadence.mjs";
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
26
+
27
+ const USAGE = `node qa/record-audit.mjs <subsystem> [--by <who-or-what>]
28
+
29
+ Appends one audit record (subsystem, HEAD sha, ISO timestamp, recorder) to
30
+ ${AUDITS_REL_PATH}. The verify lane's release profile reports which
31
+ subsystems changed since their last record. Subsystems are derived from the
32
+ tree: the immediate package directories under the androidMain Kotlin source
33
+ root ("${ROOT_SUBSYSTEM}" for files directly at the package root).
34
+
35
+ --list print the derived subsystems and their audit status
36
+ --by <name> who/what recorded this (default: git user.name)
37
+ --help, -h this usage
38
+ `;
39
+
40
+ const args = process.argv.slice(2);
41
+
42
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
43
+ console.log(USAGE);
44
+ process.exit(args.length === 0 ? 2 : 0);
45
+ }
46
+
47
+ if (args.includes("--list")) {
48
+ const report = evaluateAuditCadence(ROOT);
49
+ if (!report.ok) {
50
+ console.log(report.reason);
51
+ process.exit(0);
52
+ }
53
+ console.log(`androidMain subsystems under ${report.packageRoot} (${report.summary}):`);
54
+ for (const s of report.subsystems) {
55
+ const when = s.audit?.at ? ` — last audit ${s.audit.at.slice(0, 10)} (${s.audit.sha.slice(0, 7)}, by ${s.audit.by ?? "unknown"})` : "";
56
+ console.log(` ${s.name}: ${s.status}${when}`);
57
+ }
58
+ process.exit(0);
59
+ }
60
+
61
+ const byIdx = args.indexOf("--by");
62
+ const by = byIdx >= 0 ? args[byIdx + 1] : undefined;
63
+ if (byIdx >= 0 && !by) {
64
+ console.error("--by needs a value");
65
+ process.exit(2);
66
+ }
67
+ const positional = args.filter((a, i) => !(byIdx >= 0 && (i === byIdx || i === byIdx + 1)));
68
+ if (positional.length !== 1 || positional[0].startsWith("--")) {
69
+ console.error(`expected exactly one subsystem name — run node qa/record-audit.mjs --help`);
70
+ process.exit(2);
71
+ }
72
+
73
+ const res = recordAudit(ROOT, { subsystem: positional[0], by });
74
+ if (!res.ok) {
75
+ console.error(`refused: ${res.reason}`);
76
+ const pkgRoot = androidMainPackageRoot(ROOT);
77
+ if (pkgRoot.ok) {
78
+ console.error(`derived subsystems: ${listSubsystems(ROOT, pkgRoot.rel).join(", ")}`);
79
+ }
80
+ process.exit(1);
81
+ }
82
+ console.log(`recorded: audit of ${res.entry.subsystem} against ${res.sha.slice(0, 7)} (by ${res.entry.by}) → ${AUDITS_REL_PATH}`);
83
+ console.log("commit the ledger with your change — the release profile's receipt reads it.");
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // The flight recorder's reader — "did this project drift from its tooling?"
3
+ // answered mechanically, from qa/flight-recorder.jsonl alone.
4
+ //
5
+ // node qa/retrospective.mjs [--json]
6
+ //
7
+ // The verify lane appends one journal line per run (qa/lib/flight-recorder.mjs);
8
+ // this CLI turns those lines into the ten-second report a retrospective
9
+ // starts from: fast vs full ratio, which steps SKIP most and why (verbatim
10
+ // reasons, grouped), whether the device tier is ever actually reached, the
11
+ // longest recorded stretch with no full lane, and which degraded paths fired.
12
+ //
13
+ // HONESTY RULES — the reader is only as good as its refusals:
14
+ // - it states only what the journal recorded; a missing journal is
15
+ // "no flight data recorded yet" and exit 0, never an error, never a
16
+ // fabricated baseline;
17
+ // - a short journal says it is short instead of letting two entries read
18
+ // as a trend;
19
+ // - it never editorializes about the developer — every line is a count or
20
+ // a date about lane runs, and the human draws the conclusions.
21
+
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ import { readFlightJournal, renderFlightReport, summarizeFlightJournal } from "./lib/flight-recorder.mjs";
26
+
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
28
+ const asJson = process.argv.includes("--json");
29
+
30
+ const journal = readFlightJournal(ROOT);
31
+
32
+ if (!journal.exists) {
33
+ if (asJson) console.log(JSON.stringify({ recorded: false }));
34
+ else console.log("no flight data recorded yet — the journal (qa/flight-recorder.jsonl) appears after the first verify-lane run");
35
+ process.exit(0);
36
+ }
37
+ if (journal.error) {
38
+ console.error(`qa/flight-recorder.jsonl exists but could not be read: ${journal.error}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const summary = summarizeFlightJournal(journal.entries);
43
+
44
+ if (asJson) {
45
+ console.log(JSON.stringify({ recorded: true, malformed: journal.malformed, summary }, null, 2));
46
+ } else {
47
+ for (const line of renderFlightReport(summary, { malformed: journal.malformed })) {
48
+ console.log(line);
49
+ }
50
+ }
51
+ process.exit(0);
@@ -370,6 +370,9 @@ function defaultSpec() {
370
370
 
371
371
  > Generated by \`scaffold-feature.mjs\` from the \`${SOURCE_f}\` exemplar shape. Refine the clause
372
372
  > prose below for ${F}'s real behavior (ids stay fixed) before running the verify lane.
373
+ > Platform-behavior clauses (alarms, notifications, lock screen, audio routing) cannot be
374
+ > proven by any desktop tier — cite them from an instrumented test in
375
+ > \`composeApp/src/androidInstrumentedTest\` (see docs/TESTING.md, "The instrumented tier").
373
376
 
374
377
  - **${F_UPPER}-01** — Given the ${F} screen opens, When ${f} are being loaded, Then a loading
375
378
  indicator is shown and no ${f} are visible.
@@ -553,6 +556,22 @@ const APP_NAV_HOST = path.join(SRC("commonMain"), "presentation/navigation/AppNa
553
556
  // up with zero hand edits — the same reason we wire nav/DI automatically.
554
557
  const PREVIEW_REGISTRY = path.join(SRC("desktopMain"), "inspector/PreviewRegistry.kt");
555
558
 
559
+ // ARCH-14: ViewModels are registered with EXPLICIT `viewModel { … }` factories, never
560
+ // reflection-based `viewModelOf` (it silently ignores Kotlin constructor default
561
+ // parameter values — a compile-time wiring error becomes a runtime resolution crash).
562
+ // The stamped registration is cloned from the EXEMPLAR's own factory line in
563
+ // AppModule.kt (rename applied), so its get() arity always matches the cloned
564
+ // ViewModel's constructor whatever shape the configured exemplar has. The canonical
565
+ // single-use-case shape is the fallback when the exemplar's line isn't found.
566
+ function explicitViewModelRegistration(appModuleContent) {
567
+ const exemplarFactory = new RegExp(
568
+ `viewModel\\s*\\{\\s*${escapeRegExp(SOURCE_F)}ViewModel\\([^)]*\\)\\s*\\}`,
569
+ );
570
+ const m = appModuleContent.match(exemplarFactory);
571
+ if (m) return applyRename(m[0].replace(/\s+/g, " "));
572
+ return `viewModel { ${F}ViewModel(get()) }`;
573
+ }
574
+
556
575
  // Each step is tagged with the presets it belongs to, same mechanism as
557
576
  // FILES above: `repository` gets repo+usecase DI (+ imports) only; `screen`
558
577
  // gets viewModel DI (+ import) + nav route + import only; `feature` gets the
@@ -567,7 +586,7 @@ const ALL_INJECTION_PLANS = [
567
586
  { presets: ["feature", "screen"], apply: (c) => injectImport(c, APP_MODULE, `import ${PACKAGE}.presentation.${f}.${F}ViewModel`) },
568
587
  { presets: ["feature", "repository"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-repositories", `single<${E}Repository> { ${E}RepositoryImpl() }`) },
569
588
  { presets: ["feature", "repository"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-usecases", `factory { Get${E}sUseCase(get()) }`) },
570
- { presets: ["feature", "screen"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-viewmodels", `viewModelOf(::${F}ViewModel)`) },
589
+ { presets: ["feature", "screen"], apply: (c) => injectAtAnchor(c, APP_MODULE, "di-viewmodels", explicitViewModelRegistration(c)) },
571
590
  ],
572
591
  },
573
592
  {