create-cmp-cli 0.13.0 → 0.14.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/package.json +6 -2
- package/packages/harness/package.json +38 -0
- package/packages/harness/src/approve.mjs +247 -0
- package/packages/harness/src/arch-doc.mjs +69 -0
- package/packages/harness/src/comment.mjs +76 -0
- package/packages/harness/src/lib/a11y.mjs +113 -0
- package/packages/harness/src/lib/affected-tests.mjs +147 -0
- package/packages/harness/src/lib/approvals.mjs +1403 -0
- package/packages/harness/src/lib/arch-doc.mjs +451 -0
- package/packages/harness/src/lib/audit-cadence.mjs +290 -0
- package/packages/harness/src/lib/comments.mjs +252 -0
- package/packages/harness/src/lib/component-stories.mjs +183 -0
- package/packages/harness/src/lib/determinism.mjs +179 -0
- package/packages/harness/src/lib/device-lease.mjs +249 -0
- package/packages/harness/src/lib/evidence-badge.mjs +158 -0
- package/packages/harness/src/lib/evidence-level.mjs +117 -0
- package/packages/harness/src/lib/feature-brief.mjs +324 -0
- package/packages/harness/src/lib/flight-recorder.mjs +332 -0
- package/packages/harness/src/lib/harness-lock.mjs +147 -0
- package/packages/harness/src/lib/harness-region.mjs +159 -0
- package/packages/harness/src/lib/inputs-hash.mjs +194 -0
- package/packages/harness/src/lib/reachability.mjs +211 -0
- package/packages/harness/src/lib/receipt-validate.mjs +234 -0
- package/packages/harness/src/lib/render.mjs +254 -0
- package/packages/harness/src/lib/spec-coverage.mjs +131 -0
- package/packages/harness/src/lib/step-cache.mjs +221 -0
- package/packages/harness/src/lib/token-drift.mjs +94 -0
- package/packages/harness/src/lib/tree.mjs +108 -0
- package/packages/harness/src/preview-gallery.mjs +122 -0
- package/packages/harness/src/receipt-check.mjs +96 -0
- package/packages/harness/src/record-audit.mjs +83 -0
- package/packages/harness/src/refusal-demo.mjs +498 -0
- package/packages/harness/src/retrospective.mjs +51 -0
- package/packages/harness/src/scaffold-feature.mjs +723 -0
- package/packages/harness/src/setup-hooks.mjs +33 -0
- package/packages/harness/src/verify.mjs +1709 -0
- package/packages/harness/src/walkthrough.mjs +499 -0
- package/packages/harness/src/watch.mjs +622 -0
- package/packages/receipts/package.json +36 -0
- package/packages/receipts/src/index.mjs +16 -0
- package/packages/receipts/src/inputs-hash.mjs +194 -0
- package/packages/receipts/src/receipt-validate.mjs +234 -0
- package/src/commands/upgrade.mjs +96 -0
- package/src/lib/harness-upgrade.mjs +159 -2
- package/src/scaffold.mjs +60 -1
- package/template/AGENTS.md +5 -0
- package/template/CLAUDE.md +30 -0
- package/template/gitignore +8 -0
- package/template/qa/lib/harness-lock.mjs +147 -0
- package/template/qa/lib/harness-region.mjs +159 -0
- package/template/qa/lib/inputs-hash.mjs +1 -1
- package/template/qa/lib/receipt-validate.mjs +1 -1
- package/template/qa/preview-gallery.mjs +17 -2
- package/template/qa/verify.mjs +95 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// affected-tests.mjs — FAST-MODE-ONLY scoping of the unit-test suite to the
|
|
2
|
+
// tests plausibly affected by the working-tree change.
|
|
3
|
+
//
|
|
4
|
+
// The full lane always runs the whole suite; this module exists so the inner
|
|
5
|
+
// loop (`verify --fast`) doesn't pay for every test on a one-file edit. Its
|
|
6
|
+
// honesty contract:
|
|
7
|
+
//
|
|
8
|
+
// - FALSE NEGATIVES ARE ACCEPTABLE HERE — AND ONLY HERE. A filtered fast
|
|
9
|
+
// run can miss a cross-feature regression; that is tolerable purely
|
|
10
|
+
// because the full, unfiltered suite runs at the checkpoint (the full
|
|
11
|
+
// lane), where done is actually decided. No other gate gets this license.
|
|
12
|
+
// - FAIL OPEN, NEVER FAIL SILENT. No git, a failed git command, an unmapped
|
|
13
|
+
// change, a broad-impact change — every uncertain case runs EVERYTHING,
|
|
14
|
+
// and the caller reports which case it was in the step's output and the
|
|
15
|
+
// receipt, so a filtered run can never be mistaken for the full suite.
|
|
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.
|
|
22
|
+
//
|
|
23
|
+
// Pure functions over path lists — git access is injected/separate so the
|
|
24
|
+
// engine suite can test every branch with no repo state.
|
|
25
|
+
|
|
26
|
+
import { execSync } from "node:child_process";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lane OUTPUTS, excluded from the changed-set before any classification.
|
|
31
|
+
* The receipt (qa/evidence/) and hashed artifacts (qa-artifacts/) change on
|
|
32
|
+
* every lane run by design; counting them as "changes" would make the qa/**
|
|
33
|
+
* escape hatch self-triggering forever — run N's receipt forcing run N+1 to
|
|
34
|
+
* the full suite, permanently. They cannot affect a test outcome (the same
|
|
35
|
+
* principle as inputs-hash.mjs's EXCLUDED_PREFIXES: lane outputs are not
|
|
36
|
+
* verdict inputs).
|
|
37
|
+
*/
|
|
38
|
+
export const LANE_OUTPUT_PREFIXES = ["qa/evidence", "qa-artifacts"];
|
|
39
|
+
|
|
40
|
+
function isLaneOutput(p) {
|
|
41
|
+
return LANE_OUTPUT_PREFIXES.some((prefix) => p === prefix || p.startsWith(`${prefix}/`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The mandatory blast-radius escape hatch: paths whose change fans out too
|
|
46
|
+
* widely to subset the suite safely. Returns the human-readable category when
|
|
47
|
+
* `p` is broad-impact, else null. Checked in order; the first match names the
|
|
48
|
+
* reason.
|
|
49
|
+
* @param {string} p POSIX relpath from the project root
|
|
50
|
+
* @returns {string|null}
|
|
51
|
+
*/
|
|
52
|
+
export function broadImpactReason(p) {
|
|
53
|
+
if (p.endsWith(".gradle.kts") || p === "gradle.properties" || p === "gradle/libs.versions.toml") {
|
|
54
|
+
return "build files rewire compilation";
|
|
55
|
+
}
|
|
56
|
+
if (/(^|\/)di\//.test(p)) return "DI rewires the object graph";
|
|
57
|
+
if (/(^|\/)theme\//.test(p)) return "theme/tokens render into every screen";
|
|
58
|
+
if (p.includes("presentation/components/")) return "shared components render into every screen";
|
|
59
|
+
if (p === "qa" || p.startsWith("qa/")) return "qa/ is the harness itself";
|
|
60
|
+
if (!p.startsWith("composeApp/src/")) return "outside composeApp/src";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Derive the fast-mode unit-test filter from a list of changed paths.
|
|
66
|
+
*
|
|
67
|
+
* Mapping (deliberately simple and defensible): each changed `.kt` file under
|
|
68
|
+
* composeApp/src contributes its package's last segment — the parent
|
|
69
|
+
* directory name (`…/presentation/home/HomeViewModel.kt` → `home`, which the
|
|
70
|
+
* template's package-mirrors-path conformance makes a package segment) — and
|
|
71
|
+
* the union becomes Gradle `--tests "*<seg>*"` patterns matched against test
|
|
72
|
+
* class FQNs. Coarse on purpose: `*home*` runs every test whose FQN mentions
|
|
73
|
+
* the feature, which over-selects a little and under-maintains nothing.
|
|
74
|
+
*
|
|
75
|
+
* @param {string[]} changedPaths relpaths (either separator style) — tracked
|
|
76
|
+
* diffs plus untracked files, as from changedWorkingTreePaths()
|
|
77
|
+
* @returns {{mode: "filtered", patterns: string[], sourcePaths: string[]} |
|
|
78
|
+
* {mode: "all", reason: string, patterns: [], sourcePaths: string[]}}
|
|
79
|
+
* mode "all" ALWAYS carries the honest reason to report.
|
|
80
|
+
*/
|
|
81
|
+
export function deriveAffectedFilter(changedPaths) {
|
|
82
|
+
const paths = [...new Set((changedPaths ?? [])
|
|
83
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
84
|
+
.map((p) => p.split(path.sep).join("/")))]
|
|
85
|
+
.filter((p) => !isLaneOutput(p))
|
|
86
|
+
.sort();
|
|
87
|
+
|
|
88
|
+
if (paths.length === 0) {
|
|
89
|
+
return { mode: "all", reason: "no working-tree changes to scope by", patterns: [], sourcePaths: [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const p of paths) {
|
|
93
|
+
const broad = broadImpactReason(p);
|
|
94
|
+
if (broad) {
|
|
95
|
+
return { mode: "all", reason: `broad-impact change — ${broad} (${p})`, patterns: [], sourcePaths: paths };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Every remaining path is a scoped file under composeApp/src. Only .kt
|
|
100
|
+
// files map to test patterns; a change that maps to nothing (resources,
|
|
101
|
+
// manifests) falls open to the full suite below.
|
|
102
|
+
const ktPaths = paths.filter((p) => p.endsWith(".kt"));
|
|
103
|
+
const segments = new Set();
|
|
104
|
+
for (const p of ktPaths) {
|
|
105
|
+
const seg = path.posix.basename(path.posix.dirname(p));
|
|
106
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)) segments.add(seg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (segments.size === 0) {
|
|
110
|
+
return { mode: "all", reason: "changed files map to no test filter", patterns: [], sourcePaths: paths };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
mode: "filtered",
|
|
115
|
+
patterns: [...segments].sort().map((s) => `*${s}*`),
|
|
116
|
+
sourcePaths: ktPaths,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function defaultRunGit(args, root) {
|
|
121
|
+
try {
|
|
122
|
+
return execSync(`git ${args}`, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The working-tree change: tracked files differing from HEAD (staged or not)
|
|
130
|
+
* plus untracked-but-not-ignored files — the same "what will this commit
|
|
131
|
+
* touch" surface inputs-hash.mjs hashes.
|
|
132
|
+
*
|
|
133
|
+
* Returns null when git is unavailable or either command fails — the caller
|
|
134
|
+
* MUST treat null as "run everything" (fail open) and say so (never fail
|
|
135
|
+
* silent).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} root project root
|
|
138
|
+
* @param {(args: string, root: string) => string|null} [runGit] injectable for tests
|
|
139
|
+
* @returns {string[]|null}
|
|
140
|
+
*/
|
|
141
|
+
export function changedWorkingTreePaths(root, runGit = defaultRunGit) {
|
|
142
|
+
const diff = runGit("diff --name-only HEAD", root);
|
|
143
|
+
const untracked = runGit("ls-files --others --exclude-standard", root);
|
|
144
|
+
if (diff === null || untracked === null) return null;
|
|
145
|
+
const lines = (out) => out.replace(/\n+$/, "").split("\n").filter(Boolean);
|
|
146
|
+
return [...new Set([...lines(diff), ...lines(untracked)])];
|
|
147
|
+
}
|