create-cmp-cli 0.23.0 → 0.25.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/bin/create-cmp.mjs +33 -3
- package/package.json +10 -4
- package/packages/harness/package.json +18 -8
- package/packages/harness/src/approve.mjs +46 -18
- package/packages/harness/src/console/console-data.mjs +117 -0
- package/packages/harness/src/console/console-evidence.mjs +122 -0
- package/packages/harness/src/console/console-overview.mjs +642 -0
- package/packages/harness/src/console/console-shell.mjs +1139 -0
- package/packages/harness/src/console/console-tabs.mjs +2828 -0
- package/packages/harness/src/console/contrast.mjs +74 -0
- package/packages/harness/src/console/preview-service.mjs +1384 -0
- package/packages/harness/src/framework-check.mjs +747 -0
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- package/packages/harness/src/lib/approvals.mjs +177 -350
- package/packages/harness/src/lib/audit-cadence.mjs +1 -1
- package/packages/harness/src/lib/comments.mjs +18 -4
- package/packages/harness/src/lib/determinism.mjs +275 -14
- package/packages/harness/src/lib/evidence-badge.mjs +38 -6
- package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
- package/packages/harness/src/lib/evidence-level.mjs +144 -59
- package/packages/harness/src/lib/feature-brief.mjs +53 -16
- package/packages/harness/src/lib/flight-recorder.mjs +72 -8
- package/packages/harness/src/lib/framework-check.mjs +778 -0
- package/packages/harness/src/lib/harness-lock.mjs +87 -19
- package/packages/harness/src/lib/harness-manifest.mjs +208 -0
- package/packages/harness/src/lib/harness-region.mjs +106 -2
- package/packages/harness/src/lib/harness-source.mjs +142 -0
- package/packages/harness/src/lib/inputs-hash.mjs +180 -22
- package/packages/harness/src/lib/lane-markers.mjs +78 -0
- package/packages/harness/src/lib/lane-narrator.mjs +4 -2
- package/packages/harness/src/lib/lane-runner.mjs +72 -6
- package/packages/harness/src/lib/plan.mjs +44 -11
- package/packages/harness/src/lib/plant-calibration.mjs +135 -0
- package/packages/harness/src/lib/profile-loader.mjs +323 -0
- package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
- package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
- package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
- package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
- package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
- package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
- package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
- package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
- package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/packages/harness/src/lib/receipt-validate.mjs +84 -16
- package/packages/harness/src/lib/spec-coverage.mjs +179 -92
- package/packages/harness/src/lib/spec-model.mjs +262 -0
- package/packages/harness/src/lib/step-cache.mjs +11 -2
- package/packages/harness/src/lib/step-outcomes.mjs +84 -151
- package/packages/harness/src/lib/walk.mjs +1 -1
- package/packages/harness/src/plan.mjs +30 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +95 -26
- package/packages/harness/src/record-audit.mjs +1 -1
- package/packages/harness/src/scaffold-feature.mjs +10 -2
- package/packages/harness/src/verify.mjs +222 -142
- package/packages/harness/src/watch.mjs +146 -33
- package/packages/receipts/package.json +1 -1
- package/packages/receipts/src/index.mjs +2 -2
- package/packages/receipts/src/inputs-hash.mjs +180 -22
- package/packages/receipts/src/receipt-validate.mjs +84 -16
- package/src/commands/attach.mjs +128 -4
- package/src/commands/harden.mjs +7 -1
- package/src/commands/upgrade.mjs +7 -0
- package/src/lib/minimal.mjs +23 -1
- package/src/scaffold.mjs +8 -0
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/add-feature/SKILL.md +7 -3
- package/template/.claude/skills/add-repository/SKILL.md +2 -2
- package/template/.claude/skills/add-screen/SKILL.md +8 -4
- package/template/.githooks/pre-push +9 -0
- package/template/CLAUDE.md +55 -186
- package/template/docs/TESTING.md +1 -1
- package/template/gitignore +4 -0
- package/template/qa/approve.mjs +46 -18
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +747 -0
- package/template/qa/harness-manifest.json +10 -0
- package/template/qa/lib/a11y.mjs +1 -1
- package/template/qa/lib/affected-tests.mjs +99 -44
- package/template/qa/lib/agent-hold.mjs +234 -0
- package/template/qa/lib/approvals.mjs +177 -350
- package/template/qa/lib/audit-cadence.mjs +1 -1
- package/template/qa/lib/comments.mjs +18 -4
- package/template/qa/lib/determinism.mjs +275 -14
- package/template/qa/lib/evidence-badge.mjs +38 -6
- package/template/qa/lib/evidence-ladder.mjs +202 -0
- package/template/qa/lib/evidence-level.mjs +144 -59
- package/template/qa/lib/feature-brief.mjs +53 -16
- package/template/qa/lib/flight-recorder.mjs +72 -8
- package/template/qa/lib/framework-check.mjs +778 -0
- package/template/qa/lib/harness-lock.mjs +87 -19
- package/template/qa/lib/harness-manifest.mjs +208 -0
- package/template/qa/lib/harness-region.mjs +106 -2
- package/template/qa/lib/harness-source.mjs +142 -0
- package/template/qa/lib/inputs-hash.mjs +180 -22
- package/template/qa/lib/lane-markers.mjs +78 -0
- package/template/qa/lib/lane-narrator.mjs +4 -2
- package/template/qa/lib/lane-runner.mjs +72 -6
- package/template/qa/lib/plan.mjs +44 -11
- package/template/qa/lib/plant-calibration.mjs +135 -0
- package/template/qa/lib/profile-loader.mjs +323 -0
- package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
- package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
- package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
- package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
- package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/template/qa/lib/profiles/cmp/index.mjs +47 -0
- package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
- package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
- package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
- package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/template/qa/lib/receipt-validate.mjs +84 -16
- package/template/qa/lib/spec-coverage.mjs +179 -92
- package/template/qa/lib/spec-model.mjs +262 -0
- package/template/qa/lib/step-cache.mjs +11 -2
- package/template/qa/lib/step-outcomes.mjs +84 -151
- package/template/qa/lib/walk.mjs +1 -1
- package/template/qa/plan.mjs +30 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +95 -26
- package/template/qa/record-audit.mjs +1 -1
- package/template/qa/scaffold-feature.mjs +10 -2
- package/template/qa/verified-surface.json +17 -0
- package/template/qa/verify.mjs +222 -142
- package/template/qa/watch.mjs +146 -33
- /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
- /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
package/template/qa/verify.mjs
CHANGED
|
@@ -32,13 +32,17 @@ import path from "node:path";
|
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
|
|
34
34
|
import { computeInputsHash, undeclaredTopLevel } from "./lib/inputs-hash.mjs";
|
|
35
|
-
import {
|
|
35
|
+
import { gradeEvidence } from "./lib/evidence-level.mjs";
|
|
36
|
+
import { evidenceLadderFor } from "./lib/evidence-ladder.mjs";
|
|
36
37
|
import { updateReadmeBadge, README_REL_PATH } from "./lib/evidence-badge.mjs";
|
|
37
38
|
import { appendFlightRecord, buildFlightEntry, neverRunTiers, readFlightJournal } from "./lib/flight-recorder.mjs";
|
|
38
|
-
import { StepTimeout,
|
|
39
|
-
import { expectedDurations, runLane } from "./lib/lane-runner.mjs";
|
|
40
|
-
import {
|
|
39
|
+
import { StepTimeout, spawnTimedOut } from "./lib/step-outcomes.mjs";
|
|
40
|
+
import { expectedDurations, runLane, stepDisplayName } from "./lib/lane-runner.mjs";
|
|
41
|
+
import { resolveHarnessManifest } from "./lib/harness-manifest.mjs";
|
|
42
|
+
import { loadProfile, loadProfileSync } from "./lib/profile-loader.mjs";
|
|
43
|
+
import { laneMarkerPath } from "./lib/lane-markers.mjs";
|
|
41
44
|
import { checkHarnessIntegrity, describeIntegrity, LOCK_PATH } from "./lib/harness-lock.mjs";
|
|
45
|
+
import { readHarnessSource } from "./lib/harness-source.mjs";
|
|
42
46
|
|
|
43
47
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
44
48
|
const ROOT = path.resolve(HERE, "..");
|
|
@@ -62,22 +66,19 @@ Flags:
|
|
|
62
66
|
--profile <smoke|scaffold|local|ci|nightly|release>
|
|
63
67
|
which step set to run (default: local)
|
|
64
68
|
--fast INNER LOOP ONLY — run the resolved profile
|
|
65
|
-
minus
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
scopes unit tests to the working-tree change
|
|
73
|
-
(broad-impact changes run everything). The
|
|
69
|
+
minus this pack's expensive tier (named
|
|
70
|
+
under "This project" below), unconditionally,
|
|
71
|
+
device attached or not. Also reuses the
|
|
72
|
+
pure-Node steps' last PASS when their inputs
|
|
73
|
+
are unchanged (verdict CACHED), and lets the
|
|
74
|
+
pack scope its own build and test steps to
|
|
75
|
+
the working-tree change. The
|
|
74
76
|
receipt records mode "fast", derives no
|
|
75
77
|
evidence rung, and can NEVER satisfy the
|
|
76
78
|
done-gate — run the full lane once before
|
|
77
79
|
you call it done
|
|
78
|
-
--determinism run the timezone determinism probe: the
|
|
79
|
-
test tier
|
|
80
|
-
desktop suites) executes TWICE, under
|
|
80
|
+
--determinism run the timezone determinism probe: the
|
|
81
|
+
pack's host test tier executes TWICE, under
|
|
81
82
|
TZ=Etc/GMT+12 (UTC-12) and TZ=Etc/GMT-14
|
|
82
83
|
(UTC+14), and the probe FAILs naming every
|
|
83
84
|
test whose verdict or failure output
|
|
@@ -100,29 +101,66 @@ Flags:
|
|
|
100
101
|
--help, -h print this usage and exit 0 without
|
|
101
102
|
running anything
|
|
102
103
|
|
|
103
|
-
Profiles
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
local everything;
|
|
111
|
-
|
|
104
|
+
Profiles — WHAT EACH ONE IS FOR. Which steps each runs is your profile's, and
|
|
105
|
+
they are listed under "This project" below.
|
|
106
|
+
smoke the smallest end-to-end lane: the pack's pure-Node gates only,
|
|
107
|
+
through the real runner, receipt and journal. Seconds. Proves the
|
|
108
|
+
FRAMEWORK returns, both ways; never the change (its receipt is
|
|
109
|
+
refused as done-evidence). Driven by qa/framework-check.mjs.
|
|
110
|
+
scaffold what a stamp-time verify proves — enough to trust a fresh tree
|
|
111
|
+
local everything; steps needing a resource this host lacks SKIP, and the
|
|
112
|
+
reason is recorded rather than swallowed
|
|
112
113
|
ci everything; SKIPs are recorded so the pipeline stays honest
|
|
113
|
-
nightly everything ci proves with the determinism probe FORCED ON (it
|
|
114
|
-
the
|
|
115
|
-
does not). Proves the HARNESS, not a change: its
|
|
116
|
-
done-evidence by qa/receipt-check.mjs.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
per-change
|
|
114
|
+
nightly everything ci proves with the determinism probe FORCED ON (it
|
|
115
|
+
doubles the host test tier — the budget a scheduled run has and a
|
|
116
|
+
per-change run does not). Proves the HARNESS, not a change: its
|
|
117
|
+
receipt is refused as done-evidence by qa/receipt-check.mjs.
|
|
118
|
+
Schedule it; never wait on it.
|
|
119
|
+
release everything ci proves PLUS the pack's ship-time step; run it before
|
|
120
|
+
cutting a release, never per-change
|
|
120
121
|
`;
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* What THIS project's lane actually runs, appended to the neutral usage above.
|
|
125
|
+
*
|
|
126
|
+
* The help text used to enumerate a Compose app's step names — releaseBuild,
|
|
127
|
+
* tokenDrift, e2eSmoke, androidChecks — so `--help` in any other repo
|
|
128
|
+
* described a lane that repo does not have. The step names belong to the pack,
|
|
129
|
+
* so they are read from it. A project with no usable manifest still gets full
|
|
130
|
+
* help plus the one line that says why the rest is missing: `--help` must
|
|
131
|
+
* never refuse, and must never invent a lane either.
|
|
132
|
+
* @param {string} root
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
function projectSection(root) {
|
|
136
|
+
const manifest = resolveHarnessManifest(root);
|
|
137
|
+
if (!manifest.ok) return `\nThis project:\n ${manifest.reason}\n`;
|
|
138
|
+
const loaded = loadProfileSync(root, manifest.manifest.profile);
|
|
139
|
+
if (!loaded.ok) return `\nThis project:\n ${loaded.reason}\n`;
|
|
140
|
+
let pack;
|
|
141
|
+
try {
|
|
142
|
+
pack = loaded.profile.steps({
|
|
143
|
+
ROOT: root, HERE: path.join(root, "qa"), fast: false, determinism: false, profile: "local", mode: "full",
|
|
144
|
+
sh: () => ({ ok: true, out: "" }), tryGit: () => null, tryGitLines: () => [], DEGRADED_PATHS: [],
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return `\nThis project:\n profile "${loaded.profile.id}" could not describe its steps: ${err && err.message ? err.message : String(err)}\n`;
|
|
148
|
+
}
|
|
149
|
+
const names = (fns) => (Array.isArray(fns) ? fns.map((fn) => stepDisplayName(fn)).filter(Boolean) : []);
|
|
150
|
+
const lines = [`\nThis project (profile "${loaded.profile.id}"):`];
|
|
151
|
+
for (const [name, fns] of Object.entries(pack.stepsForProfile ?? {})) {
|
|
152
|
+
const list = names(fns);
|
|
153
|
+
if (list.length) lines.push(` ${name.padEnd(9)} ${list.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
const excluded = Array.isArray(pack.FAST_EXCLUDED_NAMES) ? pack.FAST_EXCLUDED_NAMES : [];
|
|
156
|
+
if (excluded.length) lines.push(` --fast omits: ${excluded.join(", ")}`);
|
|
157
|
+
return `${lines.join("\n")}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
122
160
|
const rawArgs = process.argv.slice(2);
|
|
123
161
|
|
|
124
162
|
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
125
|
-
console.log(USAGE);
|
|
163
|
+
console.log(USAGE + projectSection(ROOT));
|
|
126
164
|
process.exit(0);
|
|
127
165
|
}
|
|
128
166
|
|
|
@@ -180,19 +218,9 @@ if (determinism && profileExplicit && profile !== "ci" && profile !== "release")
|
|
|
180
218
|
process.exit(2);
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
// `--rerun` exists for evidence integrity (see stepUnitTests's comment): it
|
|
187
|
-
// stops Gradle's build cache replaying a PASS recorded against a different
|
|
188
|
-
// tree into a receipt that claims tests executed. That mechanism belongs to
|
|
189
|
-
// the runs that produce integrity-bearing artifacts — and a --fast run does
|
|
190
|
-
// not: its receipt already declares itself non-evidence (mode "fast", no
|
|
191
|
-
// evidence rung, refused by qa/receipt-check.mjs), so forcing execution there
|
|
192
|
-
// paid an integrity tax to protect an artifact with nothing to protect. Fast
|
|
193
|
-
// mode therefore omits the flag and lets Gradle's up-to-date/cache machinery
|
|
194
|
-
// do its job; full mode keeps it, byte-identical to before.
|
|
195
|
-
const RERUN = fast ? "" : " --rerun";
|
|
221
|
+
// `--rerun` (evidence integrity: no build cache replaying a PASS from a
|
|
222
|
+
// different tree) is the pack's to apply — it knows its build tool. The pack
|
|
223
|
+
// reads `fast` from ctx and scopes the flag to FULL mode itself.
|
|
196
224
|
|
|
197
225
|
// The running step's deadline (evidence-economics S4). Set by the step loop
|
|
198
226
|
// before each step from the journal's measured duration for it; every
|
|
@@ -204,8 +232,8 @@ let CURRENT_STEP_DEADLINE_MS = 30 * 60_000;
|
|
|
204
232
|
|
|
205
233
|
function sh(cmd, opts = {}) {
|
|
206
234
|
const started = Date.now();
|
|
207
|
-
// maxBuffer: first-run
|
|
208
|
-
// which would surface as a bogus FAIL (status null / ENOBUFS).
|
|
235
|
+
// maxBuffer: a build tool's first-run output easily exceeds spawnSync's 1MB
|
|
236
|
+
// default, which would surface as a bogus FAIL (status null / ENOBUFS).
|
|
209
237
|
const res = spawnSync(cmd, {
|
|
210
238
|
shell: true,
|
|
211
239
|
cwd: ROOT,
|
|
@@ -223,23 +251,13 @@ function sh(cmd, opts = {}) {
|
|
|
223
251
|
return { ok, status: res.status, error: res.error?.message, out: `${res.stdout ?? ""}${res.stderr ?? ""}`, durationMs: Date.now() - started };
|
|
224
252
|
}
|
|
225
253
|
|
|
226
|
-
// ──
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
// (mtime-bounded, so a crashed lane never wedges the eyes for long).
|
|
234
|
-
// 2. COORDINATE (the daemon -> this lane), the symmetric half: the daemon
|
|
235
|
-
// stamps its OWN marker for the duration of a render's Gradle build;
|
|
236
|
-
// shGradle waits for it to clear (or go stale) before launching this
|
|
237
|
-
// lane's own Gradle command — same mtime-bounded shape, so a crashed
|
|
238
|
-
// daemon never wedges the lane for long either.
|
|
239
|
-
// 3. SELF-HEAL: a Gradle step that still hits the collision clears kspCaches
|
|
240
|
-
// and retries once — the manual recovery that always worked, automated.
|
|
241
|
-
const LANE_MARKER = path.join(ROOT, "composeApp", "build", ".cmp-lane-in-progress");
|
|
242
|
-
const KSP_COLLISION_RE = /Storage for \[[^\]]*\] is already registered/;
|
|
254
|
+
// ── The lane marker ─────────────────────────────────────────────────────────
|
|
255
|
+
// Stamped for the run's duration and rewritten at every step start (the
|
|
256
|
+
// narration the narrator, the watcher, the Stop hook and the chain view read).
|
|
257
|
+
// Core state, under qa/, beside the agent hold — qa/lib/lane-markers.mjs.
|
|
258
|
+
// Coexistence with the eyes' own builds (the render marker, the KSP self-heal)
|
|
259
|
+
// is the pack's: it knows its build tool and its build directory.
|
|
260
|
+
const LANE_MARKER = laneMarkerPath(ROOT);
|
|
243
261
|
|
|
244
262
|
// Degraded-path activations observed during this run — self-heals and
|
|
245
263
|
// fallbacks that kept the lane moving without failing it. Collected for the
|
|
@@ -249,49 +267,6 @@ const KSP_COLLISION_RE = /Storage for \[[^\]]*\] is already registered/;
|
|
|
249
267
|
// two apart.
|
|
250
268
|
const DEGRADED_PATHS = [];
|
|
251
269
|
|
|
252
|
-
// The daemon's half of defense 2 above — pid + ISO timestamp, mirroring
|
|
253
|
-
// LANE_MARKER's own content shape (see where LANE_MARKER is stamped, below).
|
|
254
|
-
const RENDER_MARKER = path.join(ROOT, "composeApp", "build", ".cmp-render-in-progress");
|
|
255
|
-
const RENDER_MARKER_FRESH_MS = 5 * 60 * 1000; // older than this = a crashed daemon's stale marker, ignore it
|
|
256
|
-
const RENDER_WAIT_TIMEOUT_MS = 3 * 60 * 1000; // give up waiting after this long regardless
|
|
257
|
-
const RENDER_WAIT_POLL_MS = 2000;
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Defer this lane's next Gradle command while the preview daemon's render
|
|
261
|
-
* marker is present AND fresh (mtime younger than RENDER_MARKER_FRESH_MS).
|
|
262
|
-
* Polls every RENDER_WAIT_POLL_MS; gives up and proceeds anyway after
|
|
263
|
-
* RENDER_WAIT_TIMEOUT_MS, or the moment the marker disappears or goes stale —
|
|
264
|
-
* whichever comes first. A missing/unreadable marker returns immediately:
|
|
265
|
-
* this is a coexistence courtesy, never a hard dependency on the daemon.
|
|
266
|
-
*/
|
|
267
|
-
function waitForRenderMarker() {
|
|
268
|
-
const deadline = Date.now() + RENDER_WAIT_TIMEOUT_MS;
|
|
269
|
-
for (;;) {
|
|
270
|
-
let stat;
|
|
271
|
-
try {
|
|
272
|
-
stat = fs.statSync(RENDER_MARKER);
|
|
273
|
-
} catch {
|
|
274
|
-
return; // no render in flight
|
|
275
|
-
}
|
|
276
|
-
if (Date.now() - stat.mtimeMs >= RENDER_MARKER_FRESH_MS) return; // gone stale
|
|
277
|
-
if (Date.now() >= deadline) return; // waited long enough — proceed regardless
|
|
278
|
-
sh(`sleep ${RENDER_WAIT_POLL_MS / 1000}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function shGradle(cmd, opts = {}) {
|
|
283
|
-
waitForRenderMarker();
|
|
284
|
-
const first = sh(cmd, opts);
|
|
285
|
-
if (first.ok || !KSP_COLLISION_RE.test(first.out)) return first;
|
|
286
|
-
console.error("· KSP cache collision (concurrent Gradle — the preview daemon?) — clearing kspCaches, retrying once");
|
|
287
|
-
fs.rmSync(path.join(ROOT, "composeApp", "build", "kspCaches"), { recursive: true, force: true });
|
|
288
|
-
const retry = sh(cmd, opts);
|
|
289
|
-
retry.durationMs += first.durationMs;
|
|
290
|
-
retry.selfHealed = "ksp-cache-collision";
|
|
291
|
-
DEGRADED_PATHS.push("ksp-cache-collision: cleared kspCaches and retried the Gradle step");
|
|
292
|
-
return retry;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
270
|
function tryGit(cmd) {
|
|
296
271
|
try {
|
|
297
272
|
return execSync(`git ${cmd}`, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -316,13 +291,42 @@ function tryGitLines(cmd) {
|
|
|
316
291
|
}
|
|
317
292
|
}
|
|
318
293
|
|
|
319
|
-
// ── The
|
|
320
|
-
// Every step this lane runs
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
294
|
+
// ── The stack profile (qa/harness-manifest.json → qa/lib/profiles/<id>/) ─────
|
|
295
|
+
// Every step this lane runs comes from the profile the MANIFEST names, loaded
|
|
296
|
+
// by id — never imported by name. This runner does not know what Compose is;
|
|
297
|
+
// it knows the shape of a profile (qa/lib/profile-loader.mjs) and asks the
|
|
298
|
+
// project which one it uses. No manifest, no profile, no lane: there is no
|
|
299
|
+
// default, and each refusal names the command that fixes it. Refused before a
|
|
300
|
+
// single step runs, on the same exit code as an unknown argument — the lane
|
|
301
|
+
// was asked to do something it cannot honestly do.
|
|
302
|
+
const manifest = resolveHarnessManifest(ROOT);
|
|
303
|
+
if (!manifest.ok) {
|
|
304
|
+
console.error(manifest.reason);
|
|
305
|
+
process.exit(2);
|
|
306
|
+
}
|
|
307
|
+
const loaded = await loadProfile(ROOT, manifest.manifest.profile);
|
|
308
|
+
if (!loaded.ok) {
|
|
309
|
+
console.error(loaded.reason);
|
|
310
|
+
process.exit(2);
|
|
311
|
+
}
|
|
312
|
+
const pack = loaded.profile.steps({ ROOT, HERE, fast, determinism, profile, mode, sh, tryGit, tryGitLines, DEGRADED_PATHS });
|
|
324
313
|
const { stepsForProfile, DEVICE_STEPS, FAST_EXCLUDED_NAMES, STEP_FN_BY_NAME } = pack;
|
|
325
314
|
|
|
315
|
+
// ── The evidence ladder, resolved once, before a step runs ──────────────────
|
|
316
|
+
// The ladder has TWO spellings — the profile's top-level `ladder` and the
|
|
317
|
+
// pack's `evidenceLadder` — and this line used to read only the second one,
|
|
318
|
+
// while the Stop hook read only the first. Both spellings are legitimate and
|
|
319
|
+
// qa/lib/evidence-ladder.mjs carries the argument for the precedence; what was
|
|
320
|
+
// not legitimate is that a profile declaring only the spelling `harness init`
|
|
321
|
+
// SEEDS was graded at no rung with nothing said about it. Resolved here rather
|
|
322
|
+
// than at the grading call so the refusal — two declarations that disagree —
|
|
323
|
+
// lands before any work is done, and so the rung the receipt records and the
|
|
324
|
+
// rung a no-lane reader would compute come from the same bytes.
|
|
325
|
+
const resolvedLadder = evidenceLadderFor(loaded.profile, pack);
|
|
326
|
+
if (!resolvedLadder.ok) {
|
|
327
|
+
console.error(resolvedLadder.reason);
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
326
330
|
|
|
327
331
|
if (!stepsForProfile[profile]) {
|
|
328
332
|
console.error(`Unknown profile "${profile}" — use smoke | scaffold | local | ci | nightly | release.`);
|
|
@@ -334,8 +338,8 @@ if (!stepsForProfile[profile]) {
|
|
|
334
338
|
// qa/evidence/latest.json. The done-gate (qa/receipt-check.mjs) validates a
|
|
335
339
|
// receipt by verdict + content hash — a receipt whose steps are one probe
|
|
336
340
|
// would satisfy it while attesting almost nothing, so a probe-only run must
|
|
337
|
-
// never mint one. The lane marker IS still stamped: the probe runs
|
|
338
|
-
// and owes
|
|
341
|
+
// never mint one. The lane marker IS still stamped: the probe runs the pack's
|
|
342
|
+
// own build steps and owes every other watcher the same courtesy as the lane.
|
|
339
343
|
if (determinism && !profileExplicit) {
|
|
340
344
|
fs.mkdirSync(path.dirname(LANE_MARKER), { recursive: true });
|
|
341
345
|
fs.writeFileSync(LANE_MARKER, `${process.pid} ${new Date().toISOString()}\n`);
|
|
@@ -356,19 +360,17 @@ if (determinism && !profileExplicit) {
|
|
|
356
360
|
}
|
|
357
361
|
|
|
358
362
|
// ── --fast: the inner loop, mechanically unable to claim done ───────────────
|
|
359
|
-
// The genuinely slow tier is
|
|
360
|
-
// (
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// the
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
// recorded, no evidence rung is derived (qa/lib/evidence-level.mjs), and
|
|
371
|
-
// qa/receipt-check.mjs refuses a fast receipt as done evidence.
|
|
363
|
+
// The genuinely slow tier is the pack's own: whatever it names in
|
|
364
|
+
// FAST_EXCLUDED_NAMES (on mobile, the device and release steps). --fast
|
|
365
|
+
// filters that tier out of whatever profile resolved, UNCONDITIONALLY — so a
|
|
366
|
+
// small change gets its did-I-break-anything-obvious signal without paying
|
|
367
|
+
// for the expensive half. The rest of the profile still runs, but cheaply:
|
|
368
|
+
// the pure-Node steps reuse an unchanged PASS from the step cache (CACHED —
|
|
369
|
+
// see the memoization block above), and the pack scopes its own test steps to
|
|
370
|
+
// the working-tree change (it reads `fast` from ctx). The loophole is closed
|
|
371
|
+
// at the receipt, not by convention: mode "fast" is recorded, no evidence
|
|
372
|
+
// rung is derived (qa/lib/evidence-level.mjs), and qa/receipt-check.mjs
|
|
373
|
+
// refuses a fast receipt as done evidence.
|
|
372
374
|
const FAST_EXCLUDED_FNS = new Set(FAST_EXCLUDED_NAMES.map((name) => STEP_FN_BY_NAME[name]));
|
|
373
375
|
const laneSteps = fast
|
|
374
376
|
? stepsForProfile[profile].filter((fn) => !FAST_EXCLUDED_FNS.has(fn))
|
|
@@ -428,6 +430,19 @@ const lane = runLane({
|
|
|
428
430
|
// run — see the scope decision at leaseDeviceForStep. Release is idempotent
|
|
429
431
|
// and never deletes a foreign holder's lease.
|
|
430
432
|
onFinally: () => pack.releaseLease(),
|
|
433
|
+
// WHICH step's failure makes every verdict behind it meaningless is the
|
|
434
|
+
// PACK's to name, never the spine's to guess (lane-runner.mjs's
|
|
435
|
+
// compileShortCircuit). Passed unconditionally, so the KEY is always present:
|
|
436
|
+
// presence is what tells the runner "this caller knows about the
|
|
437
|
+
// declaration", and a pack that declares nothing then short-circuits on
|
|
438
|
+
// NOTHING rather than inheriting another stack's step name. Writing
|
|
439
|
+
// `pack.compileStepName ?? "build"` here would put the literal back and
|
|
440
|
+
// undo the fix.
|
|
441
|
+
compileStepName: pack.compileStepName,
|
|
442
|
+
// How long a step may take before it is wedged — the pack's judgement about
|
|
443
|
+
// its own toolchain, never the spine's about somebody else's. A pack that
|
|
444
|
+
// declares none gets the fallback and its ERROR rows say so.
|
|
445
|
+
stepDeadlines: pack.stepDeadlines,
|
|
431
446
|
});
|
|
432
447
|
const steps = lane.steps;
|
|
433
448
|
// CACHED counts as PASS for the lane verdict (it IS a prior PASS, reused only
|
|
@@ -436,13 +451,21 @@ const steps = lane.steps;
|
|
|
436
451
|
// not green; only the ACCUSATION is withheld. (laneVerdict, qa/lib/lane-runner.mjs)
|
|
437
452
|
const verdict = lane.verdict;
|
|
438
453
|
|
|
439
|
-
// Receipt STRENGTH — a
|
|
440
|
-
// claims, and the difference should never live only in the
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
454
|
+
// Receipt STRENGTH — a green that reached the harder tier and a green that did
|
|
455
|
+
// not are different claims, and the difference should never live only in the
|
|
456
|
+
// SKIP lines. Which steps needed a resource the host may not have is the pack's
|
|
457
|
+
// (DEVICE_STEPS, which also drives --fast); those that actually RAN are named on
|
|
458
|
+
// the receipt.
|
|
459
|
+
//
|
|
460
|
+
// THE LABEL IS THE PACK'S TOO. The core used to compose it here, and its
|
|
461
|
+
// negative case was the word "desktop-only" — printed on the verdict line of
|
|
462
|
+
// every lane in every repo, including a backend service with no desktop and no
|
|
463
|
+
// device. A profile could name its device steps and still could not name the
|
|
464
|
+
// CATEGORY, so the most-read string the lane emits asserted a stack fact the
|
|
465
|
+
// core has no way to know. Now the core prints what it is handed and prints
|
|
466
|
+
// NOTHING when it is handed nothing: silence is honest, a borrowed noun is not.
|
|
444
467
|
const onDeviceSteps = steps.filter((s) => DEVICE_STEPS.includes(s.name) && s.verdict === "PASS").map((s) => s.name);
|
|
445
|
-
const strengthLabel =
|
|
468
|
+
const strengthLabel = typeof pack.strengthLabel === "function" ? pack.strengthLabel(onDeviceSteps) : null;
|
|
446
469
|
|
|
447
470
|
// Receipt RUNG — the evidence ladder (qa/lib/evidence-level.mjs): the coarse,
|
|
448
471
|
// named grade (L0 scaffold / L1 desktop / L2 device / L3 release) DERIVED from
|
|
@@ -450,10 +473,20 @@ const strengthLabel = onDeviceSteps.length ? `on-device: ${onDeviceSteps.join("+
|
|
|
450
473
|
// fine print; the rung is added alongside, never in place of it. null on FAIL —
|
|
451
474
|
// a failed lane has no rung. null on a --fast run too: the inner loop is a
|
|
452
475
|
// signal, never evidence, so a fast receipt derives NO rung at all.
|
|
453
|
-
// The ladder is the
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
476
|
+
// The ladder is the PROFILE's, in either of the two places a profile may
|
|
477
|
+
// declare it (resolved above, qa/lib/evidence-ladder.mjs): a profile that
|
|
478
|
+
// declares none in either earns no rung (a backend graded by another stack's
|
|
479
|
+
// step names was L0 by construction — wrong, not conservative).
|
|
480
|
+
//
|
|
481
|
+
// AND THE BADGE FLOOR: the profile's `plants` declaration goes to the grader
|
|
482
|
+
// too, because a ladder is a vocabulary and plants are what prove the steps
|
|
483
|
+
// under it still bite. A profile shipping none earns NO rung however green its
|
|
484
|
+
// lane (NORTH-STAR §8.9, §6.7, §3's third *never*) — measured on two adopters
|
|
485
|
+
// differing in exactly one export, both of which earned L1 until 2026-09-08.
|
|
486
|
+
// `.why` is the sentence for a rung that is absent: derived by the same call,
|
|
487
|
+
// so the grade and its explanation can never disagree.
|
|
488
|
+
const grade = gradeEvidence(steps, profile, { mode, ladder: resolvedLadder.ladder, plants: loaded.profile.plants });
|
|
489
|
+
const level = grade.level;
|
|
457
490
|
|
|
458
491
|
// Artifacts: hash whatever the run left under qa-artifacts/ (never committed).
|
|
459
492
|
const artifacts = [];
|
|
@@ -488,6 +521,13 @@ function harnessForReceipt() {
|
|
|
488
521
|
status: r.status,
|
|
489
522
|
intact: r.status === "intact",
|
|
490
523
|
};
|
|
524
|
+
// PROVENANCE (ADR-0008): where these bytes came from, when the tree records
|
|
525
|
+
// it. Omitted entirely when unrecorded — absent means "not known", and every
|
|
526
|
+
// receipt minted before the record existed is in that state. Never defaulted
|
|
527
|
+
// to `local`, which would invent an origin for all of them. It is reported,
|
|
528
|
+
// never consulted: no verdict, gate or level reads this field.
|
|
529
|
+
const provenance = readHarnessSource(ROOT);
|
|
530
|
+
if (provenance?.source) summary.source = provenance.source;
|
|
491
531
|
if (r.status === "modified") {
|
|
492
532
|
summary.modified = r.modified;
|
|
493
533
|
summary.missing = r.missing;
|
|
@@ -516,8 +556,15 @@ if (undeclared.length) {
|
|
|
516
556
|
// ci → merge, nightly → nightly (proves the harness, never a change), release →
|
|
517
557
|
// release. Receipts predating this field are read as their profile's stage.
|
|
518
558
|
const STAGE_OF_PROFILE = { smoke: "smoke", scaffold: "scaffold", local: "change", ci: "merge", nightly: "nightly", release: "release" };
|
|
559
|
+
// Computed once: `harness` and `pack` both read it, and checkHarnessIntegrity
|
|
560
|
+
// hashes the whole region.
|
|
561
|
+
const harnessSummary = harnessForReceipt();
|
|
519
562
|
const receipt = {
|
|
520
|
-
|
|
563
|
+
// ADR-0007: the format name is ROUTING METADATA, not part of the claim, so
|
|
564
|
+
// this rename asserts nothing new and expires nothing old. Readers accept
|
|
565
|
+
// both names for the life of /1; `cmp-evidence/1` stopped being WRITTEN here
|
|
566
|
+
// and never stops being READ.
|
|
567
|
+
schema: "prooflane-evidence/1",
|
|
521
568
|
profile,
|
|
522
569
|
stage: STAGE_OF_PROFILE[profile] ?? profile,
|
|
523
570
|
// "full" is the done-gate; "fast" (--fast) excluded the device/release tier
|
|
@@ -547,7 +594,20 @@ const receipt = {
|
|
|
547
594
|
// checksum, not a signature, and someone who edits the lane can edit this
|
|
548
595
|
// too. What they cannot edit is what the registry published under that
|
|
549
596
|
// version, which is why `version` + `sha256` travel together.
|
|
550
|
-
harness:
|
|
597
|
+
harness: harnessSummary,
|
|
598
|
+
// WHICH PACK produced these rows. `harness` says which lane ran; `pack` says
|
|
599
|
+
// which step pack the lane loaded — and the two can differ once a profile is
|
|
600
|
+
// versioned on its own. Without this a cmp L2 and a backend pack's L2 are the
|
|
601
|
+
// same bytes on the wire and an auditor cannot tell "device e2e passed" from
|
|
602
|
+
// "integration tests passed" (AGNOSTIC-HARNESS-ARCHITECTURE.md §8). Named
|
|
603
|
+
// `pack` because `profile` is taken by the RUN profile (scaffold/local/ci/…);
|
|
604
|
+
// the collision is resolved at schema/2, not here. The pack declares its id;
|
|
605
|
+
// its version is the lock's until the profile loader gives it its own.
|
|
606
|
+
// ADR-0008 (accepted 2026-09-08): the pack's version is the PROFILE'S OWN —
|
|
607
|
+
// `export const version` — or null. Never the harness lock's number, which is
|
|
608
|
+
// a version of the wrong thing: a profile declaring 0.3.1 was minting receipts
|
|
609
|
+
// that said 0.20.0 (NORTH-STAR §9.2).
|
|
610
|
+
pack: { id: pack.id, version: typeof loaded.profile?.version === "string" ? loaded.profile.version : null },
|
|
551
611
|
strength: { onDeviceSteps },
|
|
552
612
|
evidenceLevel: level,
|
|
553
613
|
artifacts,
|
|
@@ -559,7 +619,13 @@ const receipt = {
|
|
|
559
619
|
};
|
|
560
620
|
|
|
561
621
|
fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
|
|
562
|
-
|
|
622
|
+
// A fast run writes its own file. Before 2026-09-08 it wrote latest.json, so a
|
|
623
|
+
// resident watcher's next pass overwrote the checkpoint's receipt with a fast
|
|
624
|
+
// one the Stop hook refuses — the agent had done everything right and was told
|
|
625
|
+
// it had not. latest-fast.json is never committed (template gitignore) and is
|
|
626
|
+
// read by nothing that grades; latest.json stays the single receipt-of-record.
|
|
627
|
+
const RECEIPT_FILE = fast ? "latest-fast.json" : "latest.json";
|
|
628
|
+
fs.writeFileSync(path.join(EVIDENCE_DIR, RECEIPT_FILE), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
563
629
|
// latest.json is the single receipt-of-record. Commit it with your change: the
|
|
564
630
|
// studio console's Evidence audit trail reconstructs the full history from the
|
|
565
631
|
// git log of this file — every commit is one verified, attributed state.
|
|
@@ -598,6 +664,9 @@ const flight = noJournal
|
|
|
598
664
|
mode,
|
|
599
665
|
verdict,
|
|
600
666
|
evidenceLevel: level,
|
|
667
|
+
// The grader travels with the grade — §8.9's comparability rule is
|
|
668
|
+
// unenforceable in a journal that records rungs without packs.
|
|
669
|
+
pack: { id: pack.id },
|
|
601
670
|
steps,
|
|
602
671
|
sha: receipt.commit.sha,
|
|
603
672
|
durationMs: Date.now() - laneStartedAt,
|
|
@@ -621,7 +690,18 @@ if (asJson) {
|
|
|
621
690
|
`\n${verdict === "PASS" ? "⚡⚡" : "❌"} verify lane [FAST — INNER LOOP ONLY, NOT DONE]: ${verdict} (skipped device/release tier: ${fastExcluded.join(", ") || "none"}) — this fast receipt satisfies no done-gate; run the full lane (node qa/verify.mjs) once before you finish`,
|
|
622
691
|
);
|
|
623
692
|
} else {
|
|
624
|
-
|
|
693
|
+
// The rung NEVER appears without the pack that defines it. §8.9 makes one
|
|
694
|
+
// pack's L2 and another's different claims that must be "shown as such", and
|
|
695
|
+
// this is the line an agent reads on every single run — the surface where a
|
|
696
|
+
// bare rung would do the most quiet damage. Only the pack's ID: `pack.version`
|
|
697
|
+
// is the profile's own or null (ADR-0008), and a null is not worth a column.
|
|
698
|
+
console.log(`\n${verdict === "PASS" ? "✅" : "❌"} verify lane: ${verdict}${level ? ` · ${level.rung} ${level.name} · pack ${pack.id}` : ""}${strengthLabel ? ` (${strengthLabel})` : ""} — receipt written to qa/evidence/latest.json${badge.changed ? ` and ${README_REL_PATH}'s evidence badge refreshed` : ""} (commit ${badge.changed ? "them" : "it"} with your change)`);
|
|
699
|
+
// A GREEN LANE THAT EARNED NO RUNG SAYS WHY, on the line a human is already
|
|
700
|
+
// reading. Silence here is the exact shape of the defect that cost a foreign
|
|
701
|
+
// author their grade — a lane that passed, a rung that never appeared, and
|
|
702
|
+
// nothing anywhere connecting the two (NORTH-STAR §9.2). On a FAILed lane the
|
|
703
|
+
// absence explains itself, so this stays quiet and lets the red row speak.
|
|
704
|
+
if (verdict === "PASS" && !level && grade.why) console.log(` ⓘ ${grade.why}`);
|
|
625
705
|
}
|
|
626
706
|
|
|
627
707
|
// A TIER THAT HAS NEVER RUN HERE. A SKIP is non-fatal by design — absence of a
|