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.
Files changed (141) hide show
  1. package/bin/create-cmp.mjs +33 -3
  2. package/package.json +10 -4
  3. package/packages/harness/package.json +18 -8
  4. package/packages/harness/src/approve.mjs +10 -11
  5. package/packages/harness/src/console/console-data.mjs +117 -0
  6. package/packages/harness/src/console/console-evidence.mjs +122 -0
  7. package/packages/harness/src/console/console-overview.mjs +642 -0
  8. package/packages/harness/src/console/console-shell.mjs +1139 -0
  9. package/packages/harness/src/console/console-tabs.mjs +2828 -0
  10. package/packages/harness/src/console/contrast.mjs +74 -0
  11. package/packages/harness/src/console/preview-service.mjs +1384 -0
  12. package/packages/harness/src/framework-check.mjs +271 -37
  13. package/packages/harness/src/lib/a11y.mjs +1 -1
  14. package/packages/harness/src/lib/affected-tests.mjs +99 -44
  15. package/packages/harness/src/lib/approvals.mjs +177 -350
  16. package/packages/harness/src/lib/audit-cadence.mjs +1 -1
  17. package/packages/harness/src/lib/comments.mjs +18 -4
  18. package/packages/harness/src/lib/determinism.mjs +275 -14
  19. package/packages/harness/src/lib/evidence-badge.mjs +34 -2
  20. package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
  21. package/packages/harness/src/lib/evidence-level.mjs +144 -59
  22. package/packages/harness/src/lib/feature-brief.mjs +53 -16
  23. package/packages/harness/src/lib/flight-recorder.mjs +72 -8
  24. package/packages/harness/src/lib/framework-check.mjs +434 -53
  25. package/packages/harness/src/lib/harness-lock.mjs +87 -19
  26. package/packages/harness/src/lib/harness-manifest.mjs +208 -0
  27. package/packages/harness/src/lib/harness-region.mjs +106 -2
  28. package/packages/harness/src/lib/harness-source.mjs +142 -0
  29. package/packages/harness/src/lib/inputs-hash.mjs +175 -22
  30. package/packages/harness/src/lib/lane-markers.mjs +78 -0
  31. package/packages/harness/src/lib/lane-narrator.mjs +4 -2
  32. package/packages/harness/src/lib/lane-runner.mjs +72 -6
  33. package/packages/harness/src/lib/plan.mjs +44 -11
  34. package/packages/harness/src/lib/plant-calibration.mjs +135 -0
  35. package/packages/harness/src/lib/profile-loader.mjs +323 -0
  36. package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
  37. package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
  38. package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
  39. package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  40. package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
  41. package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
  42. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  43. package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
  44. package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
  45. package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
  46. package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
  47. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
  48. package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
  49. package/packages/harness/src/lib/receipt-validate.mjs +84 -16
  50. package/packages/harness/src/lib/spec-coverage.mjs +179 -92
  51. package/packages/harness/src/lib/spec-model.mjs +262 -0
  52. package/packages/harness/src/lib/step-cache.mjs +11 -2
  53. package/packages/harness/src/lib/step-outcomes.mjs +84 -151
  54. package/packages/harness/src/lib/walk.mjs +1 -1
  55. package/packages/harness/src/preview-gallery.mjs +1 -1
  56. package/packages/harness/src/receipt-check.mjs +80 -24
  57. package/packages/harness/src/record-audit.mjs +1 -1
  58. package/packages/harness/src/scaffold-feature.mjs +10 -2
  59. package/packages/harness/src/verify.mjs +222 -142
  60. package/packages/harness/src/watch.mjs +146 -33
  61. package/packages/receipts/package.json +1 -1
  62. package/packages/receipts/src/index.mjs +2 -2
  63. package/packages/receipts/src/inputs-hash.mjs +175 -22
  64. package/packages/receipts/src/receipt-validate.mjs +84 -16
  65. package/src/commands/attach.mjs +128 -4
  66. package/src/commands/harden.mjs +7 -1
  67. package/src/commands/upgrade.mjs +7 -0
  68. package/src/lib/minimal.mjs +23 -1
  69. package/src/scaffold.mjs +8 -0
  70. package/template/.claude/settings.json +2 -2
  71. package/template/.claude/skills/add-feature/SKILL.md +7 -3
  72. package/template/.claude/skills/add-repository/SKILL.md +2 -2
  73. package/template/.claude/skills/add-screen/SKILL.md +8 -4
  74. package/template/.githooks/pre-push +9 -0
  75. package/template/CLAUDE.md +55 -186
  76. package/template/docs/TESTING.md +1 -1
  77. package/template/gitignore +3 -0
  78. package/template/qa/approve.mjs +10 -11
  79. package/template/qa/comments.json +1 -1
  80. package/template/qa/evidence/schema.json +258 -34
  81. package/template/qa/framework-check.mjs +271 -37
  82. package/template/qa/harness-manifest.json +10 -0
  83. package/template/qa/lib/a11y.mjs +1 -1
  84. package/template/qa/lib/affected-tests.mjs +99 -44
  85. package/template/qa/lib/approvals.mjs +177 -350
  86. package/template/qa/lib/audit-cadence.mjs +1 -1
  87. package/template/qa/lib/comments.mjs +18 -4
  88. package/template/qa/lib/determinism.mjs +275 -14
  89. package/template/qa/lib/evidence-badge.mjs +34 -2
  90. package/template/qa/lib/evidence-ladder.mjs +202 -0
  91. package/template/qa/lib/evidence-level.mjs +144 -59
  92. package/template/qa/lib/feature-brief.mjs +53 -16
  93. package/template/qa/lib/flight-recorder.mjs +72 -8
  94. package/template/qa/lib/framework-check.mjs +434 -53
  95. package/template/qa/lib/harness-lock.mjs +87 -19
  96. package/template/qa/lib/harness-manifest.mjs +208 -0
  97. package/template/qa/lib/harness-region.mjs +106 -2
  98. package/template/qa/lib/harness-source.mjs +142 -0
  99. package/template/qa/lib/inputs-hash.mjs +175 -22
  100. package/template/qa/lib/lane-markers.mjs +78 -0
  101. package/template/qa/lib/lane-narrator.mjs +4 -2
  102. package/template/qa/lib/lane-runner.mjs +72 -6
  103. package/template/qa/lib/plan.mjs +44 -11
  104. package/template/qa/lib/plant-calibration.mjs +135 -0
  105. package/template/qa/lib/profile-loader.mjs +323 -0
  106. package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
  107. package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
  108. package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
  109. package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  110. package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
  111. package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
  112. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  113. package/template/qa/lib/profiles/cmp/index.mjs +47 -0
  114. package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
  115. package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
  116. package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
  117. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
  118. package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +229 -27
  119. package/template/qa/lib/receipt-validate.mjs +84 -16
  120. package/template/qa/lib/spec-coverage.mjs +179 -92
  121. package/template/qa/lib/spec-model.mjs +262 -0
  122. package/template/qa/lib/step-cache.mjs +11 -2
  123. package/template/qa/lib/step-outcomes.mjs +84 -151
  124. package/template/qa/lib/walk.mjs +1 -1
  125. package/template/qa/preview-gallery.mjs +1 -1
  126. package/template/qa/receipt-check.mjs +80 -24
  127. package/template/qa/record-audit.mjs +1 -1
  128. package/template/qa/scaffold-feature.mjs +10 -2
  129. package/template/qa/verified-surface.json +17 -0
  130. package/template/qa/verify.mjs +222 -142
  131. package/template/qa/watch.mjs +146 -33
  132. /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  133. /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  134. /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  135. /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  136. /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
  137. /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  138. /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  139. /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  140. /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  141. /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 (build files rewire compilation, DI rewires
18
- // object graphs, theme/tokens and shared components render into every
19
- // screen, qa/ is the harness judging itself, and anything outside
20
- // composeApp/src is by definition not a scoped source edit). Any one such
21
- // change disables filtering for the run.
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
- export const LANE_OUTPUT_PREFIXES = ["qa/evidence", "qa-artifacts", "qa/flight-recorder.jsonl"];
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 mandatory blast-radius escape hatch: paths whose change fans out too
54
- * widely to subset the suite safely. Returns the human-readable category when
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 broadImpactReason(p) {
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 = broadImpactReason(p);
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 file under composeApp/src. Only .kt
108
- // files map to test patterns; a change that maps to nothing (resources,
109
- // manifests) falls open to the full suite below.
110
- const ktPaths = paths.filter((p) => p.endsWith(".kt"));
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
- mode: "filtered",
123
- patterns: [...segments].sort().map((s) => `*${s}*`),
124
- sourcePaths: ktPaths,
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) {