create-cmp-cli 0.24.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 +10 -11
- 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 +271 -37
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- 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 +34 -2
- 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 +434 -53
- 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 +175 -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} +229 -27
- 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/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +80 -24
- 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 +175 -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 +3 -0
- package/template/qa/approve.mjs +10 -11
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +271 -37
- 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/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 +34 -2
- 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 +434 -53
- 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 +175 -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} +229 -27
- 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/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +80 -24
- 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
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
// and the caller reports which case it was in the step's output and the
|
|
15
15
|
// receipt, so a filtered run can never be mistaken for the full suite.
|
|
16
16
|
// - The BLAST-RADIUS ESCAPE HATCH is mandatory: some paths fan out too
|
|
17
|
-
// widely to subset safely
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// widely to subset safely. qa/ is the harness judging itself — that one is
|
|
18
|
+
// the core's, on every stack. WHICH OTHER paths fan out, and how a changed
|
|
19
|
+
// source maps to a test filter, are facts about one build tool and one
|
|
20
|
+
// source layout, so they come from the PROFILE (Stage 0 PR 6d;
|
|
21
|
+
// profiles/cmp/affected.mjs). Any one broad-impact change disables
|
|
22
|
+
// filtering for the run.
|
|
23
|
+
//
|
|
24
|
+
// A profile that supplies no mapping gets `mode: "all"` with that as the
|
|
25
|
+
// reason — fail open, said out loud. Vendored into a repo whose sources are
|
|
26
|
+
// not under composeApp/src, the old hardcoded rules did something worse than
|
|
27
|
+
// nothing: every path failed the layout test, so every fast run fell open to
|
|
28
|
+
// the full suite with the optimisation silently off.
|
|
22
29
|
//
|
|
23
30
|
// Pure functions over path lists — git access is injected/separate so the
|
|
24
31
|
// engine suite can test every branch with no repo state.
|
|
@@ -43,50 +50,37 @@ import path from "node:path";
|
|
|
43
50
|
// first fell open to the full suite, visible only in one parenthetical.
|
|
44
51
|
// Found by payment-blueprint's spine adoption (2026-09-03), where the same
|
|
45
52
|
// line also landed in their locked region.
|
|
46
|
-
|
|
53
|
+
// qa/.lane-in-progress is the lane's own marker (qa/lib/lane-markers.mjs) —
|
|
54
|
+
// present, untracked, for exactly the duration of the run that would read it.
|
|
55
|
+
export const LANE_OUTPUT_PREFIXES = ["qa/evidence", "qa-artifacts", "qa/flight-recorder.jsonl", "qa/.lane-in-progress"];
|
|
47
56
|
|
|
48
57
|
function isLaneOutput(p) {
|
|
49
58
|
return LANE_OUTPUT_PREFIXES.some((prefix) => p === prefix || p.startsWith(`${prefix}/`));
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
|
-
* The
|
|
54
|
-
*
|
|
55
|
-
* `p` is broad-impact, else null. Checked in order; the first match names the
|
|
56
|
-
* reason.
|
|
62
|
+
* The core's own blast-radius rule, on every stack: a change under qa/ is the
|
|
63
|
+
* harness judging itself, so nothing may be subsetted by it.
|
|
57
64
|
* @param {string} p POSIX relpath from the project root
|
|
58
65
|
* @returns {string|null}
|
|
59
66
|
*/
|
|
60
|
-
export function
|
|
61
|
-
if (p.endsWith(".gradle.kts") || p === "gradle.properties" || p === "gradle/libs.versions.toml") {
|
|
62
|
-
return "build files rewire compilation";
|
|
63
|
-
}
|
|
64
|
-
if (/(^|\/)di\//.test(p)) return "DI rewires the object graph";
|
|
65
|
-
if (/(^|\/)theme\//.test(p)) return "theme/tokens render into every screen";
|
|
66
|
-
if (p.includes("presentation/components/")) return "shared components render into every screen";
|
|
67
|
+
export function coreBroadImpactReason(p) {
|
|
67
68
|
if (p === "qa" || p.startsWith("qa/")) return "qa/ is the harness itself";
|
|
68
|
-
if (!p.startsWith("composeApp/src/")) return "outside composeApp/src";
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Derive the fast-mode unit-test filter from a list of changed paths.
|
|
74
74
|
*
|
|
75
|
-
* Mapping (deliberately simple and defensible): each changed `.kt` file under
|
|
76
|
-
* composeApp/src contributes its package's last segment — the parent
|
|
77
|
-
* directory name (`…/presentation/home/HomeViewModel.kt` → `home`, which the
|
|
78
|
-
* template's package-mirrors-path conformance makes a package segment) — and
|
|
79
|
-
* the union becomes Gradle `--tests "*<seg>*"` patterns matched against test
|
|
80
|
-
* class FQNs. Coarse on purpose: `*home*` runs every test whose FQN mentions
|
|
81
|
-
* the feature, which over-selects a little and under-maintains nothing.
|
|
82
|
-
*
|
|
83
75
|
* @param {string[]} changedPaths relpaths (either separator style) — tracked
|
|
84
76
|
* diffs plus untracked files, as from changedWorkingTreePaths()
|
|
77
|
+
* @param {{broadImpact: (p: string) => (string|null), patternsFor: (paths: string[]) => {patterns: string[], sourcePaths: string[]}}} [mapping]
|
|
78
|
+
* the profile's rules (profiles/<id>/affected.mjs). Absent = no subsetting.
|
|
85
79
|
* @returns {{mode: "filtered", patterns: string[], sourcePaths: string[]} |
|
|
86
80
|
* {mode: "all", reason: string, patterns: [], sourcePaths: string[]}}
|
|
87
81
|
* mode "all" ALWAYS carries the honest reason to report.
|
|
88
82
|
*/
|
|
89
|
-
export function deriveAffectedFilter(changedPaths) {
|
|
83
|
+
export function deriveAffectedFilter(changedPaths, mapping = null) {
|
|
90
84
|
const paths = [...new Set((changedPaths ?? [])
|
|
91
85
|
.filter((p) => typeof p === "string" && p.length > 0)
|
|
92
86
|
.map((p) => p.split(path.sep).join("/")))]
|
|
@@ -97,32 +91,93 @@ export function deriveAffectedFilter(changedPaths) {
|
|
|
97
91
|
return { mode: "all", reason: "no working-tree changes to scope by", patterns: [], sourcePaths: [] };
|
|
98
92
|
}
|
|
99
93
|
|
|
94
|
+
// No mapping, no subsetting — and the reason says which half is missing, so
|
|
95
|
+
// a profile author sees the optimisation is off rather than wondering why
|
|
96
|
+
// the fast lane costs what the full one does.
|
|
97
|
+
if (!mapping || typeof mapping.broadImpact !== "function" || typeof mapping.patternsFor !== "function") {
|
|
98
|
+
return { mode: "all", reason: "this profile declares no affected-test mapping — every fast run tests everything", patterns: [], sourcePaths: paths };
|
|
99
|
+
}
|
|
100
|
+
|
|
100
101
|
for (const p of paths) {
|
|
101
|
-
const broad =
|
|
102
|
+
const broad = coreBroadImpactReason(p) ?? mapping.broadImpact(p);
|
|
102
103
|
if (broad) {
|
|
103
104
|
return { mode: "all", reason: `broad-impact change — ${broad} (${p})`, patterns: [], sourcePaths: paths };
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
// Every remaining path is a scoped
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const segments = new Set();
|
|
112
|
-
for (const p of ktPaths) {
|
|
113
|
-
const seg = path.posix.basename(path.posix.dirname(p));
|
|
114
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)) segments.add(seg);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (segments.size === 0) {
|
|
108
|
+
// Every remaining path is a scoped source edit by the profile's own reckoning.
|
|
109
|
+
// A change that maps to no pattern (resources, manifests) falls open below.
|
|
110
|
+
const { patterns, sourcePaths } = mapping.patternsFor(paths);
|
|
111
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
118
112
|
return { mode: "all", reason: "changed files map to no test filter", patterns: [], sourcePaths: paths };
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
return { mode: "filtered", patterns, sourcePaths: Array.isArray(sourcePaths) ? sourcePaths : paths };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Must the expensive tier run for this change?
|
|
120
|
+
*
|
|
121
|
+
* The most costly thing an agent does is run a device or journey tier — minutes
|
|
122
|
+
* of emulator, build and flow — to prove something about a change that tier
|
|
123
|
+
* cannot see. Running it anyway is not rigour, it is latency, and latency is
|
|
124
|
+
* how an agent ends up CLAIMING instead of deriving (G1, G2).
|
|
125
|
+
*
|
|
126
|
+
* THE DECLARATION IS OF IRRELEVANCE, NOT OF RELEVANCE, and getting that
|
|
127
|
+
* backwards turns this into the defect it prevents. The first version of this
|
|
128
|
+
* function asked "is any changed path under a root that FEEDS the tier?" and
|
|
129
|
+
* answered "not required" when none matched — an allowlist, so anything nobody
|
|
130
|
+
* thought to list was silently deferred. `cmp` declares
|
|
131
|
+
* `sourceRoots: ["composeApp/src"]`, which excludes `build.gradle.kts` and
|
|
132
|
+
* `gradle/libs.versions.toml` — both of which absolutely change what runs on a
|
|
133
|
+
* device. A dependency bump would have skipped the device tier and said so
|
|
134
|
+
* confidently (ADR-0009, "what would make this wrong").
|
|
135
|
+
*
|
|
136
|
+
* So: the tier RUNS unless every changed path is under something explicitly
|
|
137
|
+
* declared unable to affect it. A path nobody classified obliges the tier,
|
|
138
|
+
* which is the safe direction — being wrong toward running costs minutes, being
|
|
139
|
+
* wrong toward deferring costs a regression nobody saw.
|
|
140
|
+
*
|
|
141
|
+
* This does not skip anything; it answers a question and always carries the
|
|
142
|
+
* reason, so a caller can record it and a reader can disagree with it. An
|
|
143
|
+
* unrecorded skip is a lie; a recorded one is evidence.
|
|
144
|
+
*
|
|
145
|
+
* @param {string[]|null} changedPaths relpaths, or null when git could not say
|
|
146
|
+
* @param {{irrelevantRoots?: string[], tierName?: string}} [decl] paths declared
|
|
147
|
+
* unable to affect the tier: a `dir/` prefix, or a `*.ext` suffix
|
|
148
|
+
* @returns {{required: boolean, reason: string, obliging: string[]}}
|
|
149
|
+
*/
|
|
150
|
+
export function deriveTierNeed(changedPaths, { irrelevantRoots = [], tierName = "the device tier" } = {}) {
|
|
151
|
+
if (!Array.isArray(changedPaths)) {
|
|
152
|
+
return { required: true, reason: `cannot tell what changed — ${tierName} runs`, obliging: [] };
|
|
153
|
+
}
|
|
154
|
+
const paths = changedPaths
|
|
155
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
156
|
+
.map((p) => p.split(path.sep).join("/"))
|
|
157
|
+
.filter((p) => !isLaneOutput(p));
|
|
158
|
+
if (paths.length === 0) {
|
|
159
|
+
return { required: true, reason: `no change to reason about — ${tierName} runs`, obliging: [] };
|
|
160
|
+
}
|
|
161
|
+
// The harness judging itself is always broad impact — the same rule the
|
|
162
|
+
// fast-lane filter uses, so the two cannot disagree about the same edit.
|
|
163
|
+
const core = paths.find((p) => coreBroadImpactReason(p));
|
|
164
|
+
if (core) {
|
|
165
|
+
return { required: true, reason: `broad-impact change — ${coreBroadImpactReason(core)} (${core})`, obliging: [core] };
|
|
166
|
+
}
|
|
167
|
+
const declared = (irrelevantRoots ?? []).filter((r) => typeof r === "string" && r.length > 0);
|
|
168
|
+
if (declared.length === 0) {
|
|
169
|
+
return { required: true, reason: `nothing is declared unable to affect ${tierName} — it runs`, obliging: paths };
|
|
170
|
+
}
|
|
171
|
+
const isIrrelevant = (p) =>
|
|
172
|
+
declared.some((r) => (r.startsWith("*.") ? p.endsWith(r.slice(1)) : p === r.replace(/\/$/, "") || p.startsWith(r.endsWith("/") ? r : `${r}/`)));
|
|
173
|
+
const obliging = paths.filter((p) => !isIrrelevant(p));
|
|
174
|
+
return obliging.length > 0
|
|
175
|
+
? {
|
|
176
|
+
required: true,
|
|
177
|
+
reason: `${obliging.length} changed path(s) are not declared irrelevant to ${tierName}: ${obliging.slice(0, 3).join(", ")}${obliging.length > 3 ? ", …" : ""}`,
|
|
178
|
+
obliging,
|
|
179
|
+
}
|
|
180
|
+
: { required: false, reason: `every changed path is declared unable to affect ${tierName} (${declared.join(", ")})`, obliging: [] };
|
|
126
181
|
}
|
|
127
182
|
|
|
128
183
|
function defaultRunGit(args, root) {
|