create-cmp-cli 0.2.0 → 0.3.1

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 (66) hide show
  1. package/README.md +65 -27
  2. package/bin/create-cmp.mjs +2 -1
  3. package/options.schema.json +5 -2
  4. package/package.json +2 -2
  5. package/src/commands/create.mjs +26 -3
  6. package/src/lib/toggle.mjs +2 -2
  7. package/template/.claude/settings.json +12 -0
  8. package/template/.claude/skills/add-feature/SKILL.md +131 -0
  9. package/template/.claude/skills/add-repository/SKILL.md +106 -0
  10. package/template/.claude/skills/add-screen/SKILL.md +130 -0
  11. package/template/.github/workflows/verify.yml +30 -8
  12. package/template/.gradle/8.11.1/checksums/checksums.lock +0 -0
  13. package/template/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
  14. package/template/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
  15. package/template/.gradle/8.11.1/gc.properties +0 -0
  16. package/template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  17. package/template/.gradle/buildOutputCleanup/cache.properties +2 -0
  18. package/template/.gradle/vcs-1/gc.properties +0 -0
  19. package/template/CHANGELOG.md +14 -0
  20. package/template/CLAUDE.md +70 -0
  21. package/template/CONTRIBUTING.md +39 -0
  22. package/template/README.md +103 -0
  23. package/template/composeApp/build.gradle.kts +15 -8
  24. package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +4 -0
  25. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +10 -2
  26. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +10 -0
  27. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +6 -2
  28. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +1 -0
  29. package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +2 -1
  30. package/template/composeApp/src/commonTest/kotlin/com/example/app/data/remote/ItemRepositoryImplTest.kt +26 -0
  31. package/template/composeApp/src/commonTest/kotlin/com/example/app/domain/usecase/GetItemsUseCaseTest.kt +37 -0
  32. package/template/composeApp/src/commonTest/kotlin/com/example/app/presentation/home/HomeViewModelTest.kt +104 -0
  33. package/template/composeApp/src/commonTest/kotlin/com/example/app/testing/fakes/FakeItemRepository.kt +29 -0
  34. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/A11yConformanceTest.kt +64 -0
  35. package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +143 -0
  36. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeGoldenTreeTest.kt +78 -0
  37. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/home/HomeScreenTest.kt +77 -0
  38. package/template/composeApp/src/desktopTest/kotlin/com/example/app/presentation/navigation/AppShellTest.kt +79 -0
  39. package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/ComposeTestExt.kt +16 -0
  40. package/template/composeApp/src/desktopTest/kotlin/com/example/app/testing/StructuralTree.kt +63 -0
  41. package/template/docs/ARCHITECTURE.md +51 -0
  42. package/template/docs/TESTING.md +64 -0
  43. package/template/docs/adr/0001-adopt-the-create-cmp-harness-conventions.md +32 -0
  44. package/template/docs/adr/template.md +17 -0
  45. package/template/gitignore +8 -1
  46. package/template/gradle/libs.versions.toml +2 -0
  47. package/template/manifest.json +11 -8
  48. package/template/qa/e2e/README.md +29 -0
  49. package/template/qa/e2e/smoke.yaml +35 -0
  50. package/template/qa/evidence/schema.json +56 -0
  51. package/template/qa/golden/home.json +15 -0
  52. package/template/qa/lib/inputs-hash.mjs +112 -0
  53. package/template/qa/lib/token-drift.mjs +94 -0
  54. package/template/qa/receipt-check.mjs +113 -0
  55. package/template/qa/refusal-demo.mjs +491 -0
  56. package/template/qa/scaffold-feature.mjs +488 -0
  57. package/template/qa/verify.mjs +467 -0
  58. package/template/specs/README.md +33 -0
  59. package/template/specs/app-base.spec.md +30 -0
  60. package/template/specs/home.spec.md +18 -0
  61. package/template/qa/appium/README.md +0 -23
  62. package/template/qa/appium/lib/appium-client.mjs +0 -225
  63. package/template/qa/appium/package.json +0 -8
  64. package/template/qa/appium/run-android-smoke.mjs +0 -39
  65. package/template/tests/appium/cmp/conftest.py +0 -96
  66. package/template/tests/appium/cmp/test_smoke.py +0 -17
@@ -0,0 +1,94 @@
1
+ // token-drift.mjs — pure, dependency-free comparison of a LIVE inspector tree's
2
+ // resolved design-token payloads against the declared design-system catalog.
3
+ //
4
+ // Mirrors the comparison semantics of diffAgainstDesignSystem() in the cmp-inspector
5
+ // MCP server (inspector/mcp/src/lib/drift.mjs): for each node in the tree that carries
6
+ // a designToken payload ({tokens:[names], resolved:{facet:value}}), and for each
7
+ // declared token name on that node, resolve the expected value from the catalog
8
+ // (dimens checked before colors — same lookup order as the MCP) and compare it
9
+ // against every one of the node's resolved facet values. If NONE of them match the
10
+ // declared value, the node has drifted from what it claims to use.
11
+ //
12
+ // Matching is case/whitespace-normalized string equality (trim + lowercase) — the
13
+ // exact normalization the MCP's `normalize()` applies (so "#0A2540" == "#0a2540",
14
+ // but "72dp" would NOT equal "72.0dp" — the MCP does not do numeric-format
15
+ // normalization, so neither do we, to keep the comparison rule identical).
16
+ //
17
+ // No imports, no Node built-ins beyond what the runtime provides for free — pure
18
+ // object-in/object-out, unit-testable with plain objects.
19
+
20
+ /**
21
+ * @param {{colors?:Record<string,string>, dimens?:Record<string,string>}} declaredCatalog
22
+ * @param {{root:object}|object} tree the parsed /inspect/tree document (or a bare node)
23
+ * @returns {{checked:number, drifted:Array<{node:string, token:string, facet:string, expected:string, actual:string}>}}
24
+ */
25
+ export function compareTokenDrift(declaredCatalog, tree) {
26
+ const colors = (declaredCatalog && declaredCatalog.colors) || {};
27
+ const dimens = (declaredCatalog && declaredCatalog.dimens) || {};
28
+
29
+ let checked = 0;
30
+ const drifted = [];
31
+
32
+ for (const { node, path } of walk(tree)) {
33
+ const dt = node && node.designToken;
34
+ if (!dt || !Array.isArray(dt.tokens) || dt.tokens.length === 0) continue;
35
+
36
+ const resolved = dt.resolved && typeof dt.resolved === "object" ? dt.resolved : {};
37
+ const resolvedEntries = Object.entries(resolved);
38
+
39
+ for (const token of dt.tokens) {
40
+ let declared;
41
+ if (Object.prototype.hasOwnProperty.call(dimens, token)) declared = dimens[token];
42
+ else if (Object.prototype.hasOwnProperty.call(colors, token)) declared = colors[token];
43
+ else continue; // token not in the declared catalog — nothing to diff against
44
+
45
+ checked += 1;
46
+
47
+ const declaredNorm = normalize(declared);
48
+ const matches = resolvedEntries.some(([, v]) => normalize(v) === declaredNorm);
49
+ if (matches) continue;
50
+
51
+ const [facet, actual] = pickFacetForReport(resolvedEntries);
52
+ drifted.push({
53
+ node: node.testTag || path,
54
+ token,
55
+ facet,
56
+ expected: declared,
57
+ actual,
58
+ });
59
+ }
60
+ }
61
+
62
+ return { checked, drifted };
63
+ }
64
+
65
+ // Depth-first walk yielding every node with a stable, dotted path. Accepts either a
66
+ // full tree ({schemaVersion, source, root}) or a bare node — same contract as the
67
+ // MCP's tree.mjs walk().
68
+ function* walk(tree) {
69
+ const root = tree && tree.root ? tree.root : tree;
70
+ if (!root || typeof root !== "object") return;
71
+ yield* walkNode(root, "root");
72
+ }
73
+
74
+ function* walkNode(node, path) {
75
+ yield { node, path };
76
+ const children = Array.isArray(node.children) ? node.children : [];
77
+ for (let i = 0; i < children.length; i++) {
78
+ yield* walkNode(children[i], `${path}.children[${i}]`);
79
+ }
80
+ }
81
+
82
+ // Case-insensitive, trimmed comparison — identical to the MCP's normalize().
83
+ function normalize(v) {
84
+ return String(v == null ? "" : v).trim().toLowerCase();
85
+ }
86
+
87
+ // Best-effort single (facet, value) to blame in the drift report. If the node
88
+ // resolved exactly one facet, name it directly; otherwise join every facet/value
89
+ // so the reader sees everything the node actually resolved.
90
+ function pickFacetForReport(entries) {
91
+ if (entries.length === 1) return [entries[0][0], String(entries[0][1])];
92
+ if (entries.length === 0) return ["(none)", "(no resolved values)"];
93
+ return [entries.map(([k]) => k).join(","), entries.map(([, v]) => String(v)).join(", ")];
94
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // The evidence-binding predicate — answers one question: does the committed
3
+ // receipt (qa/evidence/latest.json) validly attest THIS tree, right now?
4
+ //
5
+ // node qa/receipt-check.mjs [--hook] [--json]
6
+ //
7
+ // Both enforcement points reduce to this predicate: the local Stop hook
8
+ // (.claude/settings.json) calls it on every turn-end, and CI calls it before
9
+ // re-running the lane. See docs/adr/0005-evidence-binding-by-inputs-hash.md.
10
+ //
11
+ // VALID iff receipt.verdict === "PASS" && receipt.inputs.hash === recompute(tree)
12
+ // Exit codes (normal mode): VALID -> 0, INVALID -> 1.
13
+ // Exit codes (--hook mode, Claude Code Stop-hook protocol):
14
+ // stop_hook_active === true -> 0 (never block twice in a row)
15
+ // INVALID -> 2, reason on stderr (Claude Code's block-and-feed-back signal)
16
+ // VALID -> 0, silent
17
+
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ import { computeInputsHash } from "./lib/inputs-hash.mjs";
23
+
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
+ const RECEIPT_PATH = path.join(ROOT, "qa", "evidence", "latest.json");
26
+
27
+ const args = process.argv.slice(2);
28
+ const asHook = args.includes("--hook");
29
+ const asJson = args.includes("--json");
30
+
31
+ function readStdinJson() {
32
+ try {
33
+ const raw = fs.readFileSync(0, "utf8");
34
+ if (!raw.trim()) return {};
35
+ return JSON.parse(raw);
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+
41
+ function evaluate() {
42
+ let receipt;
43
+ try {
44
+ receipt = JSON.parse(fs.readFileSync(RECEIPT_PATH, "utf8"));
45
+ } catch {
46
+ return { valid: false, reason: "no receipt — run `node qa/verify.mjs`", profile: undefined };
47
+ }
48
+
49
+ const profile = receipt.profile;
50
+
51
+ if (!receipt.inputs || typeof receipt.inputs.hash !== "string") {
52
+ return {
53
+ valid: false,
54
+ reason: `receipt predates evidence binding — re-run the lane (attesting profile: ${profile ?? "unknown"})`,
55
+ profile,
56
+ };
57
+ }
58
+
59
+ if (receipt.verdict === "FAIL") {
60
+ return {
61
+ valid: false,
62
+ reason: `the committed receipt is a FAIL (attesting profile: ${profile ?? "unknown"})`,
63
+ profile,
64
+ };
65
+ }
66
+
67
+ const recomputed = computeInputsHash(ROOT);
68
+
69
+ if (receipt.inputs.hash !== recomputed.hash) {
70
+ return {
71
+ valid: false,
72
+ reason: `source changed since the receipt — re-run the lane (attesting profile: ${profile ?? "unknown"})`,
73
+ profile,
74
+ recomputed,
75
+ };
76
+ }
77
+
78
+ if (receipt.verdict !== "PASS") {
79
+ return {
80
+ valid: false,
81
+ reason: `receipt verdict is "${receipt.verdict}", not PASS (attesting profile: ${profile ?? "unknown"})`,
82
+ profile,
83
+ };
84
+ }
85
+
86
+ return { valid: true, reason: `receipt is valid — PASS, attesting profile: ${profile ?? "unknown"}`, profile, recomputed };
87
+ }
88
+
89
+ const result = evaluate();
90
+
91
+ if (asHook) {
92
+ const hookInput = readStdinJson();
93
+ if (hookInput.stop_hook_active === true) {
94
+ process.exit(0);
95
+ }
96
+ if (!result.valid) {
97
+ process.stderr.write(
98
+ `Not done: ${result.reason}. Run \`node qa/verify.mjs\` and commit the receipt, or see README §Verification enforcement to bypass.\n`,
99
+ );
100
+ process.exit(2);
101
+ }
102
+ process.exit(0);
103
+ }
104
+
105
+ if (asJson) {
106
+ console.log(JSON.stringify(result, null, 2));
107
+ } else if (result.valid) {
108
+ console.log(`VALID — ${result.reason}`);
109
+ } else {
110
+ console.error(`INVALID — ${result.reason}`);
111
+ }
112
+
113
+ process.exit(result.valid ? 0 : 1);