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.
- package/README.md +3 -3
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/packages/harness/src/approve.mjs +30 -2
- package/packages/harness/src/lib/approvals.mjs +74 -10
- package/packages/harness/src/lib/evidence-level.mjs +3 -1
- package/packages/harness/src/lib/flight-recorder.mjs +47 -2
- package/packages/harness/src/lib/inputs-hash.mjs +71 -3
- package/packages/harness/src/lib/lane-narrator.mjs +97 -0
- package/packages/harness/src/lib/lane-runner.mjs +173 -0
- package/packages/harness/src/lib/plan.mjs +286 -20
- package/packages/harness/src/lib/receipt-validate.mjs +56 -1
- package/packages/harness/src/lib/spec-coverage.mjs +111 -3
- package/packages/harness/src/lib/step-cache.mjs +1 -1
- package/packages/harness/src/lib/step-outcomes.mjs +123 -0
- package/packages/harness/src/lib/steps-cmp.mjs +1284 -0
- package/packages/harness/src/lib/walk.mjs +67 -17
- package/packages/harness/src/receipt-check.mjs +80 -4
- package/packages/harness/src/verify.mjs +119 -1197
- package/packages/receipts/src/index.mjs +1 -0
- package/packages/receipts/src/inputs-hash.mjs +71 -3
- package/packages/receipts/src/receipt-validate.mjs +56 -1
- package/template/CLAUDE.md +52 -6
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +1 -1
- package/template/gitignore +3 -0
- package/template/qa/approve.mjs +30 -2
- package/template/qa/lib/approvals.mjs +74 -10
- package/template/qa/lib/evidence-level.mjs +3 -1
- package/template/qa/lib/flight-recorder.mjs +47 -2
- package/template/qa/lib/inputs-hash.mjs +71 -3
- package/template/qa/lib/lane-narrator.mjs +97 -0
- package/template/qa/lib/lane-runner.mjs +173 -0
- package/template/qa/lib/plan.mjs +286 -20
- package/template/qa/lib/receipt-validate.mjs +56 -1
- package/template/qa/lib/spec-coverage.mjs +111 -3
- package/template/qa/lib/step-cache.mjs +1 -1
- package/template/qa/lib/step-outcomes.mjs +123 -0
- package/template/qa/lib/steps-cmp.mjs +1284 -0
- package/template/qa/lib/walk.mjs +67 -17
- package/template/qa/receipt-check.mjs +80 -4
- package/template/qa/verify.mjs +119 -1197
- 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
|
+
}
|