create-cmp-cli 0.18.0 → 0.20.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 (42) hide show
  1. package/README.md +3 -3
  2. package/llms.txt +1 -1
  3. package/package.json +1 -1
  4. package/packages/harness/src/approve.mjs +30 -2
  5. package/packages/harness/src/lib/approvals.mjs +74 -10
  6. package/packages/harness/src/lib/evidence-level.mjs +3 -1
  7. package/packages/harness/src/lib/flight-recorder.mjs +47 -2
  8. package/packages/harness/src/lib/inputs-hash.mjs +71 -3
  9. package/packages/harness/src/lib/lane-narrator.mjs +97 -0
  10. package/packages/harness/src/lib/lane-runner.mjs +173 -0
  11. package/packages/harness/src/lib/plan.mjs +286 -20
  12. package/packages/harness/src/lib/receipt-validate.mjs +56 -1
  13. package/packages/harness/src/lib/spec-coverage.mjs +111 -3
  14. package/packages/harness/src/lib/step-cache.mjs +1 -1
  15. package/packages/harness/src/lib/step-outcomes.mjs +123 -0
  16. package/packages/harness/src/lib/steps-cmp.mjs +1284 -0
  17. package/packages/harness/src/lib/walk.mjs +67 -17
  18. package/packages/harness/src/receipt-check.mjs +80 -4
  19. package/packages/harness/src/verify.mjs +119 -1197
  20. package/packages/receipts/src/index.mjs +1 -0
  21. package/packages/receipts/src/inputs-hash.mjs +71 -3
  22. package/packages/receipts/src/receipt-validate.mjs +56 -1
  23. package/template/CLAUDE.md +52 -6
  24. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +1 -1
  25. package/template/gitignore +3 -0
  26. package/template/qa/approve.mjs +30 -2
  27. package/template/qa/lib/approvals.mjs +74 -10
  28. package/template/qa/lib/evidence-level.mjs +3 -1
  29. package/template/qa/lib/flight-recorder.mjs +47 -2
  30. package/template/qa/lib/inputs-hash.mjs +71 -3
  31. package/template/qa/lib/lane-narrator.mjs +97 -0
  32. package/template/qa/lib/lane-runner.mjs +173 -0
  33. package/template/qa/lib/plan.mjs +286 -20
  34. package/template/qa/lib/receipt-validate.mjs +56 -1
  35. package/template/qa/lib/spec-coverage.mjs +111 -3
  36. package/template/qa/lib/step-cache.mjs +1 -1
  37. package/template/qa/lib/step-outcomes.mjs +123 -0
  38. package/template/qa/lib/steps-cmp.mjs +1284 -0
  39. package/template/qa/lib/walk.mjs +67 -17
  40. package/template/qa/receipt-check.mjs +80 -4
  41. package/template/qa/verify.mjs +119 -1197
  42. package/template/specs/README.md +26 -0
@@ -0,0 +1,123 @@
1
+ // step-outcomes.mjs — a step's VERDICT, separated from its INVOCATION.
2
+ //
3
+ // A step that ran zero tests knows nothing about behaviour and must not speak
4
+ // as though it does. Observed 2026-09-02 (create-cmp-showcase): a concurrent
5
+ // adb session collided with androidChecks, Gradle exited non-zero having
6
+ // executed no tests, and the step reported "an on-device behavior claim is
7
+ // broken. Fix the behavior, not the test." The identical task passed 8 tests
8
+ // moments later. Believed, that sends the reader hunting a defect that does not
9
+ // exist; disbelieved once, it teaches them to discount every future red from
10
+ // the step — a gate that misattributes its own failures corrodes the gates that
11
+ // are right.
12
+ //
13
+ // Pure, so the wording and the rule are testable without Gradle or a device.
14
+ // (docs/proposals/evidence-economics.md C3, S4.)
15
+ //
16
+ // FOUR VERDICTS. PASS / FAIL / SKIP had no way to say "I could not run": a
17
+ // step whose infrastructure broke reported a behaviour failure. ERROR is that
18
+ // fourth word — zero tests executed, a deadline passed, a tool vanished, a
19
+ // step threw. An ERROR never accuses the change, never counts as evidence
20
+ // (evidence-level derives no rung over it; the plausibility check does not
21
+ // count it as executed), is visibly distinct from FAIL (⊘, not ✗), and is
22
+ // never silently retried. It still makes the lane FAIL — "could not check" is
23
+ // not green. This is JUnit's error-vs-failure, Bazel's FAILED_TO_BUILD /
24
+ // TIMEOUT vs FAILED, pytest's error vs failed — the distinction every mature
25
+ // runner makes and this one did not.
26
+
27
+ /**
28
+ * The androidChecks outcome from Gradle's exit and the JUnit summary.
29
+ *
30
+ * @param {{ok: boolean, out: string}} res the Gradle invocation
31
+ * @param {{tests: number, failures: number, errors: number}|null} summary parsed JUnit
32
+ * results, or null when none were written
33
+ * @param {{gradlew?: string}} [opts]
34
+ * @returns {{verdict: "PASS"|"FAIL"|"ERROR", executed: boolean, reason?: string}}
35
+ */
36
+ export function androidChecksOutcome(res, summary, { gradlew = "./gradlew" } = {}) {
37
+ const executed = Boolean(summary && summary.tests > 0);
38
+ if (res.ok) return { verdict: "PASS", executed };
39
+ const tail = String(res.out ?? "")
40
+ .split("\n")
41
+ .filter((l) => /FAILED|error:|failed/i.test(l))
42
+ .slice(0, 12)
43
+ .join("\n");
44
+ if (executed) {
45
+ return {
46
+ verdict: "FAIL",
47
+ executed,
48
+ reason:
49
+ `connectedDebugAndroidTest failed (${summary.failures + summary.errors} of ${summary.tests} tests) — ` +
50
+ `an on-device behavior claim is broken. Fix the behavior, not the test:\n${tail}`,
51
+ };
52
+ }
53
+ // ERROR, not FAIL: the step could not execute. A device tier that could not
54
+ // run is not evidence (the lane still FAILs), and going green would be the
55
+ // worse lie — but "your behaviour is broken" is withdrawn, and the receipt
56
+ // can tell a red that measured something from a red that measured nothing.
57
+ return {
58
+ verdict: "ERROR",
59
+ executed,
60
+ reason:
61
+ "connectedDebugAndroidTest DID NOT EXECUTE — the run reported no tests at all, so this step has observed " +
62
+ "nothing about your change and is not accusing it. Usual cause: another adb/Gradle session touching the same " +
63
+ "device (a manual `adb` command, a second lane, a running preview), or an install that never landed. " +
64
+ `Re-run this step alone with nothing else on the device before suspecting the code:\n ${gradlew} :composeApp:connectedDebugAndroidTest --rerun\n${tail}`,
65
+ };
66
+ }
67
+
68
+ /** Thrown by the lane's subprocess helper when a step's deadline passes. */
69
+ export class StepTimeout extends Error {
70
+ constructor(cmd, deadlineMs) {
71
+ super(`deadline of ${Math.round(deadlineMs / 60000)} min passed: ${cmd}`);
72
+ this.name = "StepTimeout";
73
+ this.cmd = cmd;
74
+ this.deadlineMs = deadlineMs;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Did a spawnSync result hit its deadline? Node reports ETIMEDOUT on
80
+ * `error.code` and the kill signal on `signal`; either alone is enough — an
81
+ * older Node sets only one of them.
82
+ * @param {{error?: {code?: string}, signal?: string|null}} res
83
+ * @returns {boolean}
84
+ */
85
+ export function spawnTimedOut(res) {
86
+ if (!res) return false;
87
+ if (res.error && res.error.code === "ETIMEDOUT") return true;
88
+ return res.signal === "SIGTERM" && (res.status === null || res.status === undefined);
89
+ }
90
+
91
+ /**
92
+ * A step's own deadline, from the journal's last measured duration for it:
93
+ * three times what it usually takes, never under five minutes (a cold Gradle
94
+ * daemon is slow, not wedged), never over thirty (past that it IS wedged).
95
+ * Unknown steps get the ceiling — a first run is never cut short.
96
+ * @param {number|null|undefined} expectedMs
97
+ * @returns {number}
98
+ */
99
+ export function stepDeadlineMs(expectedMs, { floorMs = 5 * 60_000, ceilingMs = 30 * 60_000 } = {}) {
100
+ if (!(expectedMs > 0)) return ceilingMs;
101
+ return Math.min(ceilingMs, Math.max(floorMs, Math.round(expectedMs * 3)));
102
+ }
103
+
104
+ /**
105
+ * The step result for a step that could not run — a deadline, or any throw
106
+ * out of the step's own body (which used to crash the whole lane; now it is
107
+ * one ERROR row and the lane keeps going, because the other steps' verdicts
108
+ * are still worth having).
109
+ * @param {string} name the step's display name
110
+ * @param {unknown} err
111
+ * @param {number} durationMs
112
+ * @returns {{name: string, verdict: "ERROR", reason: string, durationMs: number, details: {executed: false, kind: string}}}
113
+ */
114
+ export function stepErrorResult(name, err, durationMs) {
115
+ const timeout = err instanceof StepTimeout;
116
+ const reason = timeout
117
+ ? `DID NOT COMPLETE — no result within its deadline (${Math.round(err.deadlineMs / 60000)} min). This step has observed nothing about your change and is not accusing it. ` +
118
+ `A wedged Gradle daemon or a device that stopped answering are the usual causes; check \`./gradlew --status\` and \`adb devices\`, then re-run the step alone.
119
+ ${err.cmd}`
120
+ : `DID NOT RUN — the step threw before producing a verdict: ${err && err.message ? err.message : String(err)}. ` +
121
+ `Nothing here is a claim about your change.`;
122
+ return { name, verdict: "ERROR", reason, durationMs, details: { executed: false, kind: timeout ? "deadline" : "threw" } };
123
+ }